演练:在 Win32 中托管 WPF 内容

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

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

要求

本教程假定基本熟悉 WPF 和 Win32 编程。 有关 WPF 编程的基本简介,请参阅 入门。 要了解 Win32 编程的入门知识,你应该参考众多有关该主题的书籍,特别是查尔斯·佩茨罗德所著的 编程 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. 使用父窗口作为其 parent 参数创建新的 HwndSource 对象。

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

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

    4. 获取与内容相关的 HWND。 HwndSource 对象的 Handle 属性包含窗口句柄(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 内容的窗口句柄。

注意

不能对 System::Windows::Interop 命名空间使用 #using 指令。 这样做会在该命名空间中的 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 内容对象的实例。 在这种情况下,WPF 内容使用 C++/CLI 作为单独的类(WPFPage)实现。 还可以使用 XAML 实现 WPF 内容。 为了实现这一点,你需要搭建一个单独的项目,并将 WPF 内容生成为 DLL。 你可以向项目添加对该 DLL 的引用,并使用该引用创建 WPF 内容的实例。

通过将对 WPF 内容的引用分配给 HwndSourceRootVisual 属性,可以在子窗口中显示 WPF 内容。

下一行代码将事件处理程序 WPFButtonClicked附加到 WPF 内容 OnButtonClicked 事件。 当用户单击 “确定”“取消” 按钮时,将会调用该事件处理程序。 有关此事件处理程序的进一步讨论,请参阅 communicating_with_the_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 实现的简要演练。 本节包含以下子部分。

布局

WPF 内容中的 UI 元素由五个 TextBox 控件组成,与之相关的 Label 控件包括:名称、地址、城市、州和邮政编码。 还有两个 Button 控件,确定取消

WPF 内容在 WPFPage 类中实现。 布局使用 Grid 布局元素进行处理。 类继承自 Grid,从而有效地使其成为 WPF 内容根元素。

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

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

下一行包含 Name Label 控件及其关联的 TextBox 控件。 由于每个标签/文本框对使用相同的代码,因此它放置在一对专用方法中,并用于所有五个标签/文本框对。 这些方法创建适当的控件,并调用 Grid 类静态 SetColumnSetRow 方法将控件放置在相应的单元格中。 创建控件后,该示例对 GridChildren 属性调用 Add 方法,以将控件添加到网格中。 添加剩余标签/文本框对的代码也类似。 有关详细信息,请参阅示例代码。

//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 内容类中的实现稍微复杂一些,因为没有控制所有控件字体的单个全局属性。 而是在属性集访问器中更改每个控件的相应属性。 以下示例显示了 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;
}

另请参阅