Wie kann ich ein WPF-Steuerelement in meine C++/MFC-Anwendung einbinden?
Voraussetzungen:
- Plattform
- .NET Framework 3.5 SP1
- Sprachen
- C++
- Werkzeuge
- Visual C++ 2008
- Zu bearbeitende Projekte
- MFCApp
Einleitung
In diesem Tutorial wird beschrieben, wie das WPF-Steuerelement BizCardFlow in eine bestehende C++/MFC-Anwendung integriert werden kann. Dabei wird eine WPF-Page (ein Container-Objekt aus WPF) über ein verwaltetes HwndSource-Objekt in ein natives HWND-Objekt instanziiert. Wie dies im Einzelnen funktioniert, zeigt Ihnen dieses Dokument.
Einbinden von BizCardFlow in eine bestehende C++/MFC-Anwendung
Das Einbinden des Steuerelements erfordert die Existenz einer bestehenden Anwendung. Um dies exemplarisch darzustellen wird in diesem Tutorial das Projekt MFCApp verwendet.
Hinzufügen der Verweise
Damit Verweise auf .NET-Assemblies hinzugefügt werden können, muss in den Projekteigenschaften zunächst die Unterstützung für die .NET Common Language Runtime eingeschaltet werden:

Die Verweise können im Solution Explorer („Projektmappen-Explorer“ in der deutschen Version von Visual Studio) über einen Rechts-Klick auf die MFC-Anwendung und den Kontext-Menü-Eintrag Properties (Eigenschaften) in dem sich daraufhin öffnenden Dialog hinzugefügt werden.

Aus der Kategorie .NET werden Verweise auf die folgenden Assemblies (Baugruppen) benötigt:
- PresentationCore
- PresentationFramework
- WindowsBase
- System.Windows.Forms

Aus der Kategorie Browse (Durchsuchen) wird ein Verweis auf die folgende Assembly benötigt:
- Microsoft.WPF.Samples.BizCardFlow.dll aus dem Verzeichnis „Output“ des BizCardFlow-Projekts
In der Header-Datei des Dialogs, in das die WPF-Komponente hinzugefügt werden soll, müssen nun noch using-Anweisungen zum Importieren der benötigten Namensräume hinzugefügt werden.
using namespace System;
using namespace System::Collections::ObjectModel;
using namespace System::Windows;
using namespace System::Windows::Interop;
using namespace Microsoft::WPF::Samples;
using namespace System::Xml;
Hinzufügen des Steuerelements
Damit BizCardFlow in einem MFC-Dialog angezeigt werden kann, muss zunächst ein HWND-Objekt deklariert werden. Die Deklarationen der HwndSource und der WPF-Page, einem Oberflächen-Container-Objekt aus WPF, erfolgen in einer Klasse mit öffentlichen, statischen Feldern. Der folgende Quelltext sollte in die Implementierungs-Datei des Dialogs kopiert werden.
HWND hwndWPF; //The HWND associated with the hosted WPF page
ref class Globals
{
public:
// declare HwndSource to host a WPF Page
static HwndSource^ gHwndSource;
// declare specific WPF Page
static BizCardFlowPage^ gwcBizCardFlow;
};
Die Initialisierung erfolgt über den Aufruf der Methode GetHwnd in der OnCreate-Methode des Dialogs:
int CMFCAppDlg::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
...
// Define left and top border, width and heigth of the WPF control here ...
hwndWPF = GetHwnd(this->GetSafeHwnd(), -11, -11, 758, 400);
...
}
Die Methode GetHwnd initialisiert eine neue Instanz der Klasse HwndSourceParameters und besetzt die Eigenschaften mit den übergebenen Parametern für Position, Höhe und Breite sowie das übergeordnete Fenster. In das statische Feld gHwndSource der Klasse Globals wird mit den Parametern anschließend eine Instanz der Klasse HwndSource erstellt. Der Eigenschaft RootVisual kann nun eine Instanz der WPF-Page als FrameworkElement zugewiesen werden. Als Rückgabe-Parameter dient letztlich der Aufruf der Methode ToPointer auf der Eigenschaft Handle des HwndSource-Objekts.
Der folgende Quelltext sollte in die Implementierungs-Datei des Dialogs kopiert werden.
HWND GetHwnd(HWND parent, int x, int y, int width, int height)
{
// Initialize parameters for the gHwndSource
HwndSourceParameters^ sourceParams =
gcnew HwndSourceParameters ("MFCWPFApp");
sourceParams->PositionX = x;
sourceParams->PositionY = y;
sourceParams->Height = height;
sourceParams->Width = width;
sourceParams->ParentWindow = IntPtr(parent);
sourceParams->WindowStyle = WS_VISIBLE | WS_CHILD;
// Initialize the gHwndSource with parameters
Globals::gHwndSource =
gcnew HwndSource(*sourceParams);
// Initialize the specific WPF Page
Globals::gwcBizCardFlow = gcnew BizCardFlowPage();
// return specific WPF Page as HWND
FrameworkElement^ myPage = Globals::gwcBizCardFlow;
Globals::gHwndSource->RootVisual = myPage;
return (HWND) Globals::gHwndSource->Handle.ToPointer();
}
BizCardFlow wird nun nach dem Kompilieren an den definierten Koordinaten angezeigt.
Binden von Daten
Das Steuerelement visualisiert Daten – ohne Daten keine Visualisierung. Um jetzt Daten an das Steuerelement zu binden, kann eine ObservableCollection mit Person-Objekten gefüllt werden.
Die ObservableCollection entspringt dem Namensraum System.Collections.ObjectModel aus der Assembly WindowsBase.dll. Person dem Namensraum Microsoft.WPF.Samples und liegt in der Assembly des WPF-Steuerelements Microsoft.WPF.Samples.BizCardFlow.dll – dem hinzugefügten Projekt.
Das Zuweisen der Daten sollte zum richtigen Zeitpunkt (vor dem Rendern des Dialogs) erfolgen. So zum Beispiel beim Initialisieren des Dialogs.
BOOL CMFCAppDlg::OnInitDialog()
{
CDialog::OnInitDialog();
...
// Build up an observable collection ...
ObservableCollection<Person^>^ ppl =
gcnew ObservableCollection<Person^>;
// Build up a data entity ...
Person^ p = gcnew Person();
p->Gender = "male";
p->FirstName = "John";
p->LastName = "Doe";
//...
// Add data entity to collection ...
ppl->Add(p);
// Bind data to property of the WPF control ...
Globals::gwcBizCardFlow->ItemsSource = ppl;
return TRUE;
}
BizCardFlow visualisiert nun nach dem Kompilieren die zugewiesenen Daten. Wenn von außen, von der MFC-Anwendung, das Selektieren eines Datensatzes gewünscht ist, kann dies über die Eigenschaft SelectedIndex forciert werden.
Binden an die SelectedItem-Eigenschaft des Steuerelementes
WPF bringt eine völlig eigene Bindungs-Logik mit sich, die im Interoperabilitäts-Szenario leider nicht verwendet werden kann. Damit auf das jeweils aktuelle Element des BizCardFlow verwiesen werden kann (z.B. um die Daten in anderen Steuerelementen anzuzeigen) wirft BizCardFlow beim Wechsel des aktuellen Elements das Ereignis SelectedItemChanged. Auf dieses kann eine EventHandler-Methode registriert werden, die die Daten verarbeitet.
BOOL CMFCAppDlg::OnInitDialog()
{
CDialog::OnInitDialog();
...
// Use helper class to subscribe to event on WPF control
// and update CEdit controls on the MFC dialog
EventHelper^ ev =
gcnew EventHelper(
Globals::gwcBizCardFlow,
GetDlgItem(IDC_EDIT_Lastame),
...);
}
Damit eine Methode als EventHandler registriert werden kann, mss sie jedoch in einer verwalteten Klasse implementiert sein. In dieser muss jetzt nur noch auf die Konvertierung des verwalteten Strings zu einem CString geachtet werden.
public ref class EventHelper
{
...
public:
EventHelper(BizCardFlowPage^ bizCardFlow,
...)
{
...
// Register event handler method...
bizCardFlow->SelectedItemChanged +=
gcnew System::EventHandler(
this, &EventHelper::Item_Changed);
}
void Item_Changed(Object^ sender, System::EventArgs^ e)
{
Person^ p =
safe_cast<Person^>(
Globals::gwcBizCardFlow->SelectedItem);
CString lastname(p->LastName);
_lastName->SetWindowTextW(lastname);
CString firstName(p->FirstName);
_firstName->SetWindowTextW(firstName);
...
}
};