Freigeben über


Walkthrough: Hosting Windows Presentation Foundation Content in a Win32 Application

Windows Presentation Foundation (WPF) provides a rich environment for creating applications. However, when you have a substantial investment in Win32 code, it might be more effective to add WPF functionality to your application rather than rewriting your original code. WPF provides a straightforward mechanism for hosting WPF content in a Win32 window.

This tutorial describes how to write a sample application, Host a Windows Presentation Foundation Page in a Microsoft Win32 Window, that hosts WPF content in a Win32 window. You can extend this sample to host any Win32 window. Because it involves mixing managed and unmanaged code, the application is written in C++/CLI.

This topic contains the following sections.

  • Requirements
  • The Basic Procedure
  • Implementing the Host Application
  • Implementing the WPF Page
  • Related Topics

Requirements

This tutorial assumes a basic familiarity with both WPF and Win32 programming. For a basic introduction to WPF programming, see Getting Started. For an introduction to Win32 programming, you should reference any of the numerous books on the subject, in particular Programming Windows by Charles Petzold.

Because the sample that accompanies this tutorial is implemented in C++/CLI, this tutorial assumes familiarity with the use of C++ to program the Win32 API plus an understanding of managed code programming. Familiarity with C++/CLI is helpful but not essential.

NoteNote:

This tutorial includes a number of code examples from the associated sample. However, for readability, it does not include the complete sample code. For the complete sample code, see Hosting Windows Presentation Foundation Content in a Win32 Window Sample.

The Basic Procedure

This section outlines the basic procedure you use to host WPF content in a Win32 window. The remaining sections explain the details of each step.

The key to hosting WPF content on a Win32 window is the HwndSource class. This class wraps the WPF content in a Win32 window, allowing it to be incorporated into your user interface (UI) as a child window. The following approach combines the Win32 and WPF in a single application.

  1. Implement your WPF content as a managed class.

  2. Implement a Win32 application with C++/CLI. If you are starting with an existing application and unmanaged C++ code, you can usually enable it to call managed code by changing your project settings to include the /clr compiler flag.

  3. Set the threading model to single-threaded apartment (STA).

  4. Handle the WM_CREATE notification in your window procedure and do the following:

    1. Create a new HwndSource object with the parent window as its parent parameter.

    2. Create an instance of your WPF content class.

    3. Assign a reference to the WPF content object to the RootVisual property of the HwndSource.

    4. Get the HWND for the content. The Handle property of the HwndSource object contains the window handle (HWND). To get an HWND that you can use in the unmanaged part of your application, cast Handle.ToPointer() to an HWND.

  5. Implement a managed class that contains a static field to hold a reference to your WPF content. This class allows you to get a reference to the WPF content from your Win32 code.

  6. Assign the WPF content to the static field.

  7. Receive notifications from the WPF content by attaching a handler to one or more of the WPF events.

  8. Communicate with the WPF content by using the reference that you stored in the static field to set properties, and so on.

NoteNote:

You can also use Extensible Application Markup Language (XAML) to implement your WPF content. However, you will have to compile it separately as a dynamic-link library (DLL) and reference that DLL from your Win32 application. The remainder of the procedure is similar to that outlined above.

Implementing the Host Application

This section describes how to host WPF content in a basic Win32 application. The content itself is implemented in C++/CLI as a managed class. For the most part, it is straightforward WPF programming. The key aspects of the content implementation are discussed in Implementing the WPF Content.

  • The Basic Application
  • Hosting the WPF Content
  • Holding a Reference to the WPF Content
  • Communicating with the WPF Content

The Basic Application

The starting point for the host application was to create a Microsoft Visual Studio 2005 template.

  1. Open Visual Studio 2005, and select New Project from the File menu.

  2. Select Win32 from the list of Visual C++ project types. If your default language is not C++, you will find these project types under Other Languages.

  3. Select a Win32 Project template, assign a name to the project and click OK to launch the Win32 Application Wizard.

  4. Accept the wizard's default settings and click Finish to start the project.

The template creates a basic Win32 application, including:

  • An entry point for the application.

  • A window, with an associated window procedure (WndProc).

  • A menu with File and Help headings. The File menu has an Exit item that closes the application. The Help menu has an About item that launches a simple dialog box.

Before you start writing code to host the WPF content, you need to make two modifications to the basic template.

The first is to compile the project as managed code. By default, the project compiles as unmanaged code. However, because WPF is implemented in managed code, the project must be compiled accordingly.

  1. Right-click the project name in Solution Explorer and select Properties from the context menu to launch the Property Pages dialog box.

  2. Select Configuration Properties from the treeview in the left pane.

  3. Select Common Language Runtime support from the Project Defaults list in the right pane.

  4. Select Common Language Runtime Support (/clr) from the drop-down list box.

NoteNote:

This compiler flag allows you to use managed code in your application, but your unmanaged code will still compile as before.

WPF uses the single-threaded apartment (STA) threading model. In order to work properly with the WPF content code, you must set the application's threading model to STA by applying an attribute to the entry point.

[System::STAThreadAttribute] //Needs to be an STA thread to play nicely with WPF
int APIENTRY _tWinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,
                     int       nCmdShow)
{

Hosting the WPF Content

The WPF content is a simple address entry application. It consists of several TextBox controls to take user name, address, and so on. There are also two Button controls, OK and Cancel. When the user clicks OK, the button's Click event handler collects the data from the TextBox controls, assigns it to corresponding properties, and raises a custom event, OnButtonClicked. When the user clicks Cancel, the handler simply raises OnButtonClicked. The event argument object for OnButtonClicked contains a Boolean field that indicates which button was clicked.

The code to host the WPF content is implemented in a handler for the WM_CREATE notification on the host window.

case WM_CREATE :
  GetClientRect(hWnd, &rect);
  wpfHwnd = GetHwnd(hWnd, rect.right-375, 0, 375, 250);
  CreateDataDisplay(hWnd, 275, rect.right-375, 375);
  CreateRadioButtons(hWnd);
break;

The GetHwnd method takes size and position information plus the parent window handle and returns the window handle of the hosted WPF content .

NoteNote:

You cannot use a #using directive for the System::Windows::Interop namespace. Doing so creates a name collision between the MSG structure in that namespace and the MSG structure declared in winuser.h. You must instead use fully-qualified names to access the contents of that namespace.

HWND GetHwnd(HWND parent, int x, int y, int width, int height)
{
    System::Windows::Interop::HwndSourceParameters^ sourceParams = gcnew System::Windows::Interop::HwndSourceParameters(
    "hi" // NAME
    );
    sourceParams->PositionX = x;
    sourceParams->PositionY = y;
    sourceParams->Height = height;
    sourceParams->Width = width;
    sourceParams->ParentWindow = IntPtr(parent);
    sourceParams->WindowStyle = WS_VISIBLE | WS_CHILD; // style
    System::Windows::Interop::HwndSource^ source = gcnew System::Windows::Interop::HwndSource(*sourceParams);
    WPFPage ^myPage = gcnew WPFPage(width, height);
    //Assign a reference to the WPF page and a set of UI properties to a set of static properties in a class
    //that is designed for that purpose.
    WPFPageHost::hostedPage = myPage;
    WPFPageHost::initBackBrush = myPage->Background;
    WPFPageHost::initFontFamily = myPage->DefaultFontFamily;
    WPFPageHost::initFontSize = myPage->DefaultFontSize;
    WPFPageHost::initFontStyle = myPage->DefaultFontStyle;
    WPFPageHost::initFontWeight = myPage->DefaultFontWeight;
    WPFPageHost::initForeBrush = myPage->DefaultForeBrush;
    myPage->OnButtonClicked += gcnew WPFPage::ButtonClickHandler(WPFButtonClicked);
    source->RootVisual = myPage;
    return (HWND) source->Handle.ToPointer();
}

You cannot host the WPF content directly in your application window. Instead, you first create an HwndSource object to wrap the WPF content. This object is basically a window that is designed to host a WPF content. You host the HwndSource object in the parent window by creating it as a child of a Win32 window that is part of your application. The HwndSource constructor parameters contain much the same information that you would pass to CreateWindow when you create a Win32 child window.

You next create an instance of the WPF content object. In this case, the WPF content is implemented as a separate class, WPFPage, using C++/CLI. You could also implement the WPF content with XAML. However, to do so you need to set up a separate project and build the WPF content as a DLL. You can add a reference to that DLL to your project, and use that reference to create an instance of the WPF content.

You display the WPF content in your child window by assigning a reference to the WPF content to the RootVisual property of the HwndSource.

The next line of code attaches an event handler, WPFButtonClicked, to the WPF content OnButtonClicked event. This handler is called when the user clicks the OK or Cancel button. See communicating_with_the_WPF content for further discussion of this event handler.

The final line of code shown returns the window handle (HWND) that is associated with the HwndSource object. You can use this handle from your Win32 code to send messages to the hosted window, although the sample does not do so. The HwndSource object raises an event every time it receives a message. To process the messages, call the AddHook method to attach a message handler and then process the messages in that handler.

Holding a Reference to the WPF Content

For many applications, you will want to communicate with the WPF content later. For example, you might want to modify the WPF content properties, or perhaps have the HwndSource object host different WPF content. To do this, you need a reference to the HwndSource object or the WPF content. The HwndSource object and its associated WPF content remain in memory until you destroy the window handle. However, the variable you assign to the HwndSource object will go out of scope as soon as you return from the window procedure. The customary way to handle this issue with Win32 applications is to use a static or global variable. Unfortunately, you cannot assign a managed object to those types of variables. You can assign the window handle associated with HwndSource object to a global or static variable, but that doe not provide access to the object itself.

The simplest solution to this issue is to implement a managed class that contains a set of static fields to hold references to any managed objects that you need access to. The sample uses the WPFPageHost class to hold a reference to the WPF content, plus the initial values of a number of its properties that might be changed later by the user. This is defined in the header.

public ref class WPFPageHost
{
public:
  WPFPageHost();
  static WPFPage^ hostedPage;
  //initial property settings
  static System::Windows::Media::Brush^ initBackBrush;
  static System::Windows::Media::Brush^ initForeBrush;
  static System::Windows::Media::FontFamily^ initFontFamily;
  static System::Windows::FontStyle initFontStyle;
  static System::Windows::FontWeight initFontWeight;
  static double initFontSize;
};

The latter part of the GetHwnd function assigns values to those fields for later use while myPage is still in scope.

Communicating with the WPF Content

There are two types of communication with the WPF content. The application receives information from the WPF content when the user clicks the OK or Cancel buttons. The application also has a UI that allows the user to change various WPF content properties, such as the background color or default font size.

As mentioned above, when the user clicks either button the WPF content raises an OnButtonClicked event. The application attaches a handler to this event to receive these notifications. If the OK button was clicked, the handler gets the user information from the WPF content and displays it in a set of static controls.

void WPFButtonClicked(Object ^sender, MyPageEventArgs ^args)
{
    if(args->IsOK) //display data if OK button was clicked
    {
        WPFPage ^myPage = WPFPageHost::hostedPage;
        LPCWSTR userName = (LPCWSTR) InteropServices::Marshal::StringToHGlobalAuto("Name: " + myPage->EnteredName).ToPointer();
        SetWindowText(nameLabel, userName);
        LPCWSTR userAddress = (LPCWSTR) InteropServices::Marshal::StringToHGlobalAuto("Address: " + myPage->EnteredAddress).ToPointer();
        SetWindowText(addressLabel, userAddress);
        LPCWSTR userCity = (LPCWSTR) InteropServices::Marshal::StringToHGlobalAuto("City: " + myPage->EnteredCity).ToPointer();
        SetWindowText(cityLabel, userCity);
        LPCWSTR userState = (LPCWSTR) InteropServices::Marshal::StringToHGlobalAuto("State: " + myPage->EnteredState).ToPointer();
        SetWindowText(stateLabel, userState);
        LPCWSTR userZip = (LPCWSTR) InteropServices::Marshal::StringToHGlobalAuto("Zip: " + myPage->EnteredZip).ToPointer();
        SetWindowText(zipLabel, userZip);
    }
    else
    {
        SetWindowText(nameLabel, L"Name: ");
        SetWindowText(addressLabel, L"Address: ");
        SetWindowText(cityLabel, L"City: ");
        SetWindowText(stateLabel, L"State: ");
        SetWindowText(zipLabel, L"Zip: ");
    }
}

The handler receives a custom event argument object from the WPF content, MyPageEventArgs. The object's IsOK property is set to true if the OK button was clicked, and false if the Cancel button was clicked.

If the OK button was clicked, the handler gets a reference to the WPF content from the container class. It then collects the user information that is held by the associated WPF content properties and uses the static controls to display the information on the parent window. Because the WPF content data is in the form of a managed string, it has to be marshaled for use by a Win32 control. If the Cancel button was clicked, the handler clears the data from the static controls.

The application UI provides a set of radio buttons that allow the user to modify the background color of the WPF content, and several font-related properties. The following example is an excerpt from the application's window procedure (WndProc) and its message handling that sets various properties on different messages, including the background color. The others are similar, and are not shown. See the complete sample for details and context.

case WM_COMMAND:
  wmId    = LOWORD(wParam);
  wmEvent = HIWORD(wParam);

  switch (wmId)
  {
  //Menu selections
    case IDM_ABOUT:
      DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
    break;
    case IDM_EXIT:
      DestroyWindow(hWnd);
    break;
    //RadioButtons
    case IDC_ORIGINALBACKGROUND :
      WPFPageHost::hostedPage->Background = WPFPageHost::initBackBrush;
    break;
    case IDC_LIGHTGREENBACKGROUND :
      WPFPageHost::hostedPage->Background = gcnew SolidColorBrush(Colors::LightGreen);
    break;
    case IDC_LIGHTSALMONBACKGROUND :
      WPFPageHost::hostedPage->Background = gcnew SolidColorBrush(Colors::LightSalmon);
    break;

To set the background color, get a reference to the WPF content (hostedPage) from WPFPageHost and set the background color property to the appropriate color. The sample uses three color options: the original color, light green, or light salmon. The original background color is stored as a static field in the WPFPageHost class. To set the other two, you create a new SolidColorBrush object and pass the constructor a static colors value from the Colors object.

Implementing the WPF Page

You can host and use the WPF content without any knowledge of the actual implementation. If the WPF content had been packaged in a separate DLL, it could have been built in any common language runtime (CLR) language. Following is a brief walkthrough of the C++/CLI implementation that is used in the sample. This section contains the following subsections.

  • Layout
  • Returning the Data to the Host Window
  • Setting the WPF Properties

Layout

The UI elements in the WPF content consist of five TextBox controls, with associated Label controls: Name, Address, City, State, and Zip. There are also two Button controls, OK and Cancel

The WPF content is implemented in the WPFPage class. Layout is handled with a Grid layout element. The class inherits from Grid, which effectively makes it the WPF content root element.

The WPF content constructor takes the required width and height, and sizes the Grid accordingly. It then defines the basic layout by creating a set of ColumnDefinition and RowDefinition objects and adding them to the Grid object base ColumnDefinitions and RowDefinitions collections, respectively. This defines a grid of five rows and seven columns, with the dimensions determined by the contents of the cells.

WPFPage::WPFPage(int allottedWidth, int allotedHeight)
{
  array<ColumnDefinition ^> ^ columnDef = gcnew array<ColumnDefinition ^> (4);
  array<RowDefinition ^> ^ rowDef = gcnew array<RowDefinition ^> (6);

  this->Height = allotedHeight;
  this->Width = allottedWidth;
  this->Background = gcnew SolidColorBrush(Colors::LightGray);
  
  //Set up the Grid's row and column definitions
  for(int i=0; i<4; i++)
  {
    columnDef[i] = gcnew ColumnDefinition();
    columnDef[i]->Width = GridLength(1, GridUnitType::Auto);
    this->ColumnDefinitions->Add(columnDef[i]);
  }
  for(int i=0; i<6; i++)
  {
    rowDef[i] = gcnew RowDefinition();
    rowDef[i]->Height = GridLength(1, GridUnitType::Auto);
    this->RowDefinitions->Add(rowDef[i]);
  }

Next, the constructor adds the UI elements to the Grid. The first element is the title text, which is a Label control that is centered in the first row of the grid.

//Add the title
titleText = gcnew Label();
titleText->Content = "Simple WPF Control";
titleText->HorizontalAlignment = System::Windows::HorizontalAlignment::Center;
titleText->Margin = Thickness(10, 5, 10, 0);
titleText->FontWeight = FontWeights::Bold;
titleText->FontSize = 14;
Grid::SetColumn(titleText, 0);
Grid::SetRow(titleText, 0);
Grid::SetColumnSpan(titleText, 4);
this->Children->Add(titleText);

The next row contains the Name Label control and its associated TextBox control. Because the same code is used for each label/textbox pair, it is placed in a pair of private methods and used for all five label/textbox pairs. The methods create the appropriate control, and call the Grid class static SetColumn and SetRow methods to place the controls in the appropriate cell. Once the control is created, the sample calls the Children.Add method on Grid to add the control to the grid. The code to add the remaining label/textbox pairs is similar. See the sample code for details.

//Add the Name Label and TextBox
nameLabel = CreateLabel(0, 1, "Name");
this->Children->Add(nameLabel);
nameTextBox = CreateTextBox(1, 1, 3);
this->Children->Add(nameTextBox);

The implementation of the two methods is as follows:

Label ^WPFPage::CreateLabel(int column, int row, String ^ text)
{
  Label ^ newLabel = gcnew Label();
  newLabel->Content = text;
  newLabel->Margin = Thickness(10, 5, 10, 0);
  newLabel->FontWeight = FontWeights::Normal;
  newLabel->FontSize = 12;
  Grid::SetColumn(newLabel, column);
  Grid::SetRow(newLabel, row);
  return newLabel;
}
TextBox ^WPFPage::CreateTextBox(int column, int row, int span)
{
  TextBox ^newTextBox = gcnew TextBox();
  newTextBox->Margin = Thickness(10, 5, 10, 0);
  Grid::SetColumn(newTextBox, column);
  Grid::SetRow(newTextBox, row);
  Grid::SetColumnSpan(newTextBox, span);
  return newTextBox;
}

Finally, the sample adds the OK and Cancel buttons and attaches an event handler to their Click events.

//Add the Buttons and atttach event handlers
okButton = CreateButton(0, 5, "OK");
cancelButton = CreateButton(1, 5, "Cancel");
this->Children->Add(okButton);
this->Children->Add(cancelButton);
okButton->Click += gcnew RoutedEventHandler(this, &WPFPage::ButtonClicked);
cancelButton->Click += gcnew RoutedEventHandler(this, &WPFPage::ButtonClicked);

Returning the Data to the Host Window

When either button is clicked, its Click event is raised. The host window could simply attach handlers to these events and get the data directly from the TextBox controls. The sample uses a somewhat less direct approach. It handles the Click within the WPF content , and then raises a custom event OnButtonClicked, to notify the WPF content . This allows the WPF content to do some parameter validation before notifying the host. The handler gets the text from the TextBox controls and assigns it to public properties, from which the host can retrieve the information.

The event declaration, in WPFPage.h:

public:
  delegate void ButtonClickHandler(Object ^, MyPageEventArgs ^);
  WPFPage();
  WPFPage(int height, int width);
  event ButtonClickHandler ^OnButtonClicked;

The Click event handler, in WPFPage.cpp:

void WPFPage::ButtonClicked(Object ^sender, RoutedEventArgs ^args)
{

  //TODO: validate input data
  bool okClicked = true;
  if(sender == cancelButton)
    okClicked = false;
  EnteredName = nameTextBox->Text;
  EnteredAddress = addressTextBox->Text;
  EnteredCity = cityTextBox->Text;
  EnteredState = stateTextBox->Text;
  EnteredZip = zipTextBox->Text;
  OnButtonClicked(this, gcnew MyPageEventArgs(okClicked));
}

Setting the WPF Properties

The Win32 host allows the user to change several WPF content properties. From the Win32 side, it is simply a matter of changing the properties. The implementation in the WPF content class is somewhat more complicated, because there is no single global property that controls the fonts for all controls. Instead, the appropriate property for each control is changed in the properties set accessors. The following example shows the code for the DefaultFontFamily property. Setting the property calls a private method that in turn sets the FontFamily properties for the various controls.

From WPFPage.h:

property FontFamily^ DefaultFontFamily
{
  FontFamily^ get() {return _defaultFontFamily;}
  void set(FontFamily^ value) {SetFontFamily(value);}
};

From WPFPage.cpp:

void WPFPage::SetFontFamily(FontFamily^ newFontFamily)
{
  _defaultFontFamily = newFontFamily;
  titleText->FontFamily = newFontFamily;
  nameLabel->FontFamily = newFontFamily;
  addressLabel->FontFamily = newFontFamily;
  cityLabel->FontFamily = newFontFamily;
  stateLabel->FontFamily = newFontFamily;
  zipLabel->FontFamily = newFontFamily;
}

See Also

Reference

HwndSource

Concepts

WPF and Win32 Interoperation Overview