演练:在 Win32 中承载 WPF 内容

Windows Presentation Foundation (WPF) 提供了用于创建应用程序的丰富环境。 但是,当你对 Win32 代码有大量投入时,向应用程序添加 WPF 功能(而不是重写原始代码)可能更有效。 WPF 提供了一个简单的机制,用于在 Win32 窗口中承载 WPF 内容。

本教程介绍如何编写示例应用程序,在 Win32 窗口示例中承载 WPF 内容,该应用程序可在 Win32 窗口中承载 WPF 内容。 你可以扩展此示例,使其可承载任何 Win32 窗口。 由于涉及混合托管代码和非托管代码,应用程序是使用 C++/CLI 编写的。

要求

本教程假定你已基本熟悉 WPF 和 Win32 编程。 有关 WPF 编程的基本介绍,请参阅入门。 有关 Win32 编程的简介,可参考有关该主题的众多书籍,尤其是 Charles Petzold 所著的 Programming Windows(《Windows 编程》)

由于此教程随附的示例是使用 C++/CLI 实现的,因而本教程假定你熟悉使用 C++ 进行 Windows API 编程,并且了解托管代码编程。 熟悉 C++/CLI 将有所帮助,但并非必备条件。

注意

本教程包括来自相关示例的一些代码示例。 但是,出于可读性考虑,不包括完整的示例代码。 有关完整的示例代码,请参阅在 Win32 窗口示例中承载 WPF 内容

基本过程

本节概括介绍了用于在 Win32 窗口中承载 WPF 内容的基本过程。 其余章节说明每个步骤的详细内容。

在 Win32 窗口中承载 WPF 内容的关键是 HwndSource 类。 此类将 WPF 内容包装在 Win32 窗口中,从而使其可以作为子窗口并入用户界面 (UI) 中。 以下方法在单个应用程序中合并 Win32 和 WPF。

  1. 将你的 WPF 内容作为托管类实现。

  2. 使用 C++/CLI 实现 Windows 应用程序。 如果从现有应用程序和非托管 C++ 代码开始,你通常可以通过更改项目设置以包括 /clr 编译器标志来使其可以调用托管代码。

  3. 将线程处理模型设置为单线程单元 (STA)。

  4. 在窗口过程中处理 WM_CREATE 通知,并执行以下任务:

    1. 创建一个新的 HwndSource 对象,将父窗口用作其 parent 参数。

    2. 创建 WPF 内容类的一个实例。

    3. HwndSourceRootVisual 属性分配对 WPF 内容对象的引用。

    4. 获取该内容的 HWND。 Handle 对象的 HwndSource 属性包含窗口句柄 (HWND)。 要获取可在应用程序的非托管部分中使用的 HWND,需将 Handle.ToPointer() 强制装换为 HWND。

  5. 实现一个托管类,该类包含一个用于保存对 WPF 内容的引用的静态字段。 该类使你可以从 Win32 代码获取对 WPF 内容的引用。

  6. 向静态字段分配 WPF 内容。

  7. 通过将处理程序附加到一个或多个 WPF 事件来从 WPF 内容接收通知。

  8. 通过使用存储在静态字段中用于设置属性等的引用来与 WPF 内容通信。

注意

你也可以使用 WPF 内容。 但是,你必须将其作为动态链接库 (DLL) 单独编译并从 Win32 应用程序引用 DLL。 该过程的其余部分与上述相似。

实现主机应用程序

本节介绍如何在基本的 Win32 应用程序中托管 WPF 内容。 该内容本身是在 C++/CLI 中作为托管类实现的。 大多数情况下,它是简单的 WPF 编程。 在实现 WPF 内容中对内容实现的主要方面进行了探讨。

基本应用程序

主机应用程序的起点是创建 Visual Studio 2005 模板。

  1. 打开 Visual Studio 2005,然后从“文件”菜单中选择“新建项目”

  2. 在 Visual C++ 项目类型列表中选择“Win32”。 如果默认语言不是 C++,你将在“其他语言”下找到这些项目类型

  3. 选择“Win32 项目”模板,为项目分配一个名称,然后单击“确定”以启动“Win32 应用程序向导”

  4. 接受向导的默认设置,然后单击“完成”以启动项目

该模板将创建一个基本的 Win32 应用程序,包括:

  • 应用程序的入口点。

  • 一个窗口和关联的窗口过程 (WndProc)。

  • 带有“文件”和“帮助”标题的菜单。 “文件”菜单具有用于关闭应用程序的“退出”项。 “帮助”菜单具有用于启动一个简单对话框的“关于”项

在开始编写用于承载 WPF 内容的代码之前,需要对基本模板做两项修改。

第一项是将项目作为托管代码进行编译。 默认情况下,项目将作为非托管代码进行编译。 但是,由于 WPF 是在托管代码中实现的,因此必须将项目进行相应编译。

  1. 在“解决方案资源管理器”中右键单击项目名,然后从上下文菜单中选择“属性”以启动“属性页”对话框

  2. 从左窗格中的树视图中选择“配置属性”

  3. 从右窗格中的“项目默认值”列表中选择“公共语言运行时”支持

  4. 从下拉列表框中选择“公共语言运行时支持(/clr)”

注意

此编译器标志使你能够在应用程序中使用托管代码,但非托管代码将仍将像以前一样进行编译。

WPF 使用单线程单元 (STA) 线程处理模型。 为了正常使用 WPF 内容代码,必须通过对入口点应用特性,从而将应用程序的线程模型设置为 STA。

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

承载 WPF 内容

WPF 内容是简单的地址条目应用程序。 它包含若干个 TextBox 控件,这些控件用于获得用户名称、地址等。 还有两个 Button 控件,即“确定”和“取消”。 用户单击“确定”时,该按钮的 Click 事件处理程序将收集来自 TextBox 控件的数据,将其分配给相应的属性,并引发自定义事件 OnButtonClicked。 当用户单击“取消”时,该处理程序只引发 OnButtonClickedOnButtonClicked 的事件参数对象包含布尔型字段,用于指示被单击的按钮。

用于托管 WPF 内容的代码在主机窗口上的 WM_CREATE 通知的处理程序中实现。

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;

GetHwnd 方法使用大小和位置信息以及父窗口句柄并返回托管的 WPF 内容的窗口句柄。

注意

无法对 #using 命名空间使用 System::Windows::Interop 指令。 如果进行此操作,将造成该命名空间中的 MSG 结构和在 winuser.h 中声明的 MSG 结构之间的名称冲突。 必须使用完全限定名来访问该命名空间的内容。

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();
}

无法直接在应用程序窗口中托管 WPF 内容。 从而,首先创建 HwndSource 对象以包装 WPF 内容。 此对象基本上是一个专门用于托管 WPF 内容的窗口。 通过将 HwndSource 对象创建为 Win32 窗口(应用程序的一部分)的子级而将其托管于父窗口中。 HwndSource 构造函数参数所包含的信息与创建 Win32 子窗口时要传递给 CreateWindow 的信息基本相同。

接下来,创建 WPF 内容对象的实例。 在此情况下,通过使用 C++/CLI,将 WPF 内容作为单独的类 WPFPage 实现。 还可以使用 XAML 实现 WPF 内容。 但为此,需要设置一个单独的项目,并生成 WPF 内容作为 DLL。 可以向项目添加对 DLL 的引用,并使用该引用创建 WPF 内容的实例。

通过向 HwndSourceRootVisual 属性分配一个对 WPF 内容的引用,在子窗口中显示 WPF 内容。

代码的下一行将一个事件处理程序 WPFButtonClicked 附加到 WPF 内容 OnButtonClicked 事件。 用户单击“确定”或“取消”按钮时,该处理程序将被调用。 有关对此事件处理程序的深入探讨,请参阅与 WPF 内容通信

代码显示的最后一行返回与 HwndSource 对象关联的窗口句柄 (HWND)。 可以从 Win32 代码使用此句柄,将消息发送到托管窗口,尽管该示例并未执行这一操作。 每当 HwndSource 对象收到一条消息时就会引发一个事件。 若要处理消息,请调用 AddHook 方法来附加消息处理程序,然后在此处理程序中处理消息。

保存对 WPF 内容的引用

对于许多应用程序,你将需要稍后与 WPF 内容进行通信。 例如,可能需要修改 WPF 内容属性,或可能让 HwndSource 对象托管不同的 WPF 内容。 执行此操作需要对 HwndSource 对象或 WPF 内容的引用。 HwndSource 对象及其关联 WPF 内容保留在内存中,直到销毁窗口句柄。 但是,一旦从窗口过程返回,分配给 HwndSource 对象的变量就将超出范围。 用来处理 Win32 应用程序的这一问题的惯用方法是使用静态或全局变量。 遗憾的是,你无法向这些类型的变量分配托管对象。 可以向全局或静态变量分配与 HwndSource 对象关联的窗口句柄,但这一操作不会提供对对象本身的访问。

针对这一问题最简单的解决方案是实现一个托管类,该类包含一组静态字段,这些字段保存对需要访问的任何托管对象的引用。 此示例使用 WPFPageHost 类来保存对 WPF 内容的引用,以及对该内容某些属性的初始值的引用(用户以后可能会对这些属性进行更改)。 这在标头中进行定义。

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;
};

GetHwnd 函数的后半部分将值分配给这些字段以供以后使用(在 myPage 仍处于范围内时)。

与 WPF 内容通信

与 WPF 内容的通信有两种类型。 用户单击“确定”或“取消”按钮时,应用程序收到来自 WPF 内容的信息。 该应用程序还具有 UI,使用户可以更改各种 WPF 内容属性,例如背景色或默认字体大小。

如上所述,用户单击任一按钮时,WPF 内容将引发 OnButtonClicked 事件。 该应用程序将一个处理程序附加到此事件以接收这些通知。 如果单击“确定”按钮,该处理程序将从 WPF 内容获得用户信息,并将其显示在一组静态控件中

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: ");
    }
}

该处理程序从 WPF 内容接收到一个自定义事件参数对象 MyPageEventArgs。 如果单击“确定”按钮,对象的 IsOK 属性被设置为 true;如果单击“取消”按钮,该属性被设置为 false

如果单击“确定”按钮,该处理程序将从容器类获取对 WPF 内容的引用。 然后它会收集由关联的 WPF 内容属性保存的用户信息,并使用静态控件在父窗口上显示该信息。 由于 WPF 内容数据的形式为托管字符串,因此必须对其进行封送处理以供 Win32 控件使用。 如果单击“取消”按钮,则处理程序将清除静态控件中的数据

应用程序 UI 提供一组单选按钮,允许用户修改 WPF 内容的背景色和若干与字体相关的属性。 下面的示例是应用程序的窗口过程 (WndProc) 的一段摘录及其消息处理功能,通过该功能可设置不同消息上的各种属性,包括背景色。 其他内容与此类似,将不进行展示。 请查看完整示例了解详细信息和上下文。

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;

若要设置背景色,请从 WPFPageHost 获取对 WPF 内容 (hostedPage) 的引用并将背景色属性设置为适当的颜色。 此示例使用三种颜色选项:原始颜色、浅绿色或浅橙红色。 原始背景色作为静态字段存储在 WPFPageHost 类中。 若要设置其他两种颜色,请创建一个新的 SolidColorBrush 对象,并从 Colors 对象向构造函数传递一个静态颜色值。

实现 WPF 页

可以托管和使用 WPF 内容而无需任何有关实际实现的知识。 如果 WPF 内容已打包到一个单独的 DLL 中,那么它可能是用任何公共语言运行时 (CLR) 语言构建的。 以下是在该示例中使用的 C++/CLI 实现的简短演练。 本节包含下列子节。

Layout

WPF 内容中的 UI 元素由五个 TextBox 控件(以及关联的 Label 控件)构成:名称、地址、城市、州和邮编。 还有两个 Button 控件,即“确定”和“取消”

WPF 内容在 WPFPage 类中实现。 布局通过 Grid 布局元素进行处理。 该类继承自实际使其成为 WPF 内容根元素的 Grid

WPF 内容构造函数采用所需的宽度和高度,并相应地调整 Grid 的大小。 然后,它通过创建一组 ColumnDefinitionRowDefinition 对象并将它们分别添加到 Grid 对象基 ColumnDefinitionsRowDefinitions 集合来定义基本布局。 这将定义具有 5 行和 7 列的网格,该网格具有单元内容定义的维度。

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]);
  }

接下来,此构造函数将 UI 元素添加到 Grid。 第一个元素是标题文本,这是一个在网格首行居中的 Label 控件。

//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);

下一行包含名称 Label 控制及其关联的 TextBox 控件。 由于对每个标签/文本框对使用同一代码,因而该代码被置于一对专用方法内并用于所有五个标签/文本框对。 该方法创建相应的控件,并调用 Grid 类静态 SetColumnSetRow 方法,以将控件置于相应单元格内。 创建控件后,该示例对 AddChildren 属性调用 Grid 方法,以便将控件添加到网格。 用于添加剩余标签/文本框对的代码相似。 请参阅示例代码了解详细信息。

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

这两种方法的实现如下所示:

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;
}

最后,该示例添加“确定”和“取消”按钮,然后将一个事件处理程序附加到它们的 Click 事件

//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);

将数据返回到主机窗口

单击任一按钮后,将引发其 Click 事件。 主机窗口只需将处理程序附加到这些事件,然后直接从 TextBox 控件获取数据。 此示例使用的是不太直接的方法。 它处理 WPF 内容内的 Click,然后引发自定义事件 OnButtonClicked 来通知 WPF 内容。 这使 WPF 内容能够在通知主机之前执行某些参数验证。 该处理程序从 TextBox 控件获取文本,然后将其分配给公共属性(主机可从其中检索信息)。

WPFPage.h 中的事件声明:

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

WPFPage.cpp 中的 Click 事件处理程序:

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));
}

设置 WPF 属性

Win32 主机使用户可以更改若干 WPF 内容属性。 就 Win32 端而言,这只是更改属性的问题。 WPF 内容类中的实现从某种意义上来说更加复杂,因为不存在控制所有控件的字体的单个全局属性。 相反,每个控件的相应属性均是在属性的 set 访问器中进行更改的。 以下示例显示 DefaultFontFamily 属性的代码。 设置属性将调用专用方法,该方法依次又将为各控件设置 FontFamily 属性。

从 WPFPage.h:

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

从 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;
}

另请参阅