Windows Presentation Foundation (WPF)提供了用于创建应用程序的丰富环境。 但是,当你对 Win32 代码进行大量投资时,向应用程序添加 WPF 功能而不是重写原始代码可能更有效。 WPF 提供了一种直接的机制,用于在 Win32 窗口中托管 WPF 内容。
本教程介绍如何编写在 Win32 窗口示例中托管 WPF 内容的示例应用程序、托管 Win32 窗口中的 WPF 内容。 可以扩展此示例以托管任何 Win32 窗口。 由于它涉及混合托管和非托管代码,因此应用程序以 C++/CLI 编写。
要求
本教程假定基本熟悉 WPF 和 Win32 编程。 有关 WPF 编程的基本简介,请参阅 入门。 关于 Win32 编程的介绍,可以参考众多相关书籍,特别是 Charles Petzold 的《Windows 编程》。
由于本教程附带的示例是在 C++/CLI 中实现的,因此本教程假定熟悉使用 C++ 来编程 Windows API 以及了解托管代码编程。 熟悉 C++/CLI 非常有用,但并不重要。
注释
本教程包含关联示例中的多个代码示例。 但是,为了提高可读性,它不包括完整的示例代码。 有关完整的示例代码,请参阅 Win32 窗口示例中的托管 WPF 内容。
基本过程
本部分概述了用于在 Win32 窗口中托管 WPF 内容的基本过程。 其余部分介绍每个步骤的详细信息。
在 Win32 窗口中托管 WPF 内容的关键是 HwndSource 类。 此类将 WPF 内容包装在 Win32 窗口中,以子窗口的形式将其嵌入到用户界面(UI)中。 以下方法将 Win32 和 WPF 合并到单个应用程序中。
将 WPF 内容实现为托管类。
使用 C++/CLI 实现 Windows 应用程序。 如果开始处理现有应用程序和非托管C++代码,通常可以通过更改项目设置以包含
/clr
编译器标志,使其能够调用托管代码。将线程模型设置为单线程单元(STA)。
在窗口过程中处理 WM_CREATE通知,并执行以下操作:
使用父窗口作为其
parent
参数创建新HwndSource对象。创建 WPF 内容类的实例。
将 WPF 内容对象的引用分配给 HwndSource 对象的 RootVisual 属性。
获取内容的 HWND。 HwndSource 对象的 Handle 属性包含窗口句柄(HWND)。 若要获取可在应用程序的非托管部分中使用的 HWND,请将
Handle.ToPointer()
强制转换为 HWND。
实现包含静态字段的托管类,用于保存对 WPF 内容的引用。 此类允许您从 Win32 代码中获取对 WPF 内容的引用。
将 WPF 内容分配给静态字段。
通过将处理程序附加到一个或多个 WPF 事件来接收来自 WPF 内容的通知。
使用存储在静态字段中的引用与 WPF 内容进行通信,例如设置属性或执行其他操作。
注释
还可以使用 WPF 内容。 但是,必须单独将其编译为动态链接库(DLL),并从 Win32 应用程序引用该 DLL。 该过程的其余部分类似于上述过程。
实现宿主应用程序
本部分介绍如何在基本的 Win32 应用程序中托管 WPF 内容。 内容本身作为托管类在 C++/CLI 中实现。 在大多数情况下,它是简单的 WPF 编程。 内容实现的关键方面在 实现 WPF 内容中进行了讨论。
基本应用程序
主机应用程序的起点是创建 Visual Studio 2005 模板。
打开 Visual Studio 2005,然后从“文件”菜单中选择“新建项目”。
从 Visual C++ 项目类型列表中选择 Win32 。 如果未C++默认语言,则会在 “其他语言”下找到这些项目类型。
选择 Win32 项目 模板,为项目分配名称,然后单击“ 确定 ”以启动 Win32 应用程序向导。
接受向导的默认设置,然后单击“ 完成 ”以启动项目。
该模板创建一个基本的 Win32 应用程序,包括:
应用程序的入口点。
具有关联窗口过程(WndProc)的窗口。
包含“文件和帮助”标题的菜单。 “文件”菜单中有一个名为“退出”的选项,用于关闭应用程序。 “ 帮助 ”菜单具有启动简单对话框的 “关于 ”项。
开始编写代码以托管 WPF 内容之前,需要对基本模板进行两次修改。
第一个是将项目编译为托管代码。 默认情况下,项目编译为非托管代码。 但是,由于 WPF 是在托管代码中实现的,因此必须相应地编译项目。
右键单击 解决方案资源管理器 中的项目名称,然后从上下文菜单中选择 “属性 ”以启动 “属性页 ”对话框。
从左窗格中的树视图中选择 “配置属性 ”。
从右窗格中的“项目默认值”列表中选择公共语言运行时支持。
从下拉列表框中选择 公共语言运行时支持(/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
。 当用户单击取消时,处理程序仅仅触发OnButtonClicked
。 事件参数对象 OnButtonClicked
包含一个布尔字段,指示单击了哪个按钮。
承载 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 内容的窗口。 通过将对象创建为作为应用程序一部分的 Win32 窗口的子级来托管 HwndSource 父窗口中的对象。 HwndSource构造函数参数包含与创建 Win32 子窗口时传递给 CreateWindow 的信息大致相同。
接下来创建 WPF 内容对象的实例。 在这种情况下,WPF 内容是使用 C++/CLI 作为单独的类 WPFPage
实现的。 还可以使用 XAML 实现 WPF 内容。 为此,你需要设置一个单独的项目,并将 WPF 内容编译成 DLL。 你可以向项目添加对该 DLL 的引用,并使用该引用创建 WPF 内容的实例。
通过将 WPF 内容的引用分配给HwndSource的RootVisual属性,可以在子窗口中显示 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 实现的简要演练。 本节包含以下子部分。
布局
WPF 内容中的 UI 元素由五 TextBox 个控件组成,其中包含关联的 Label 控件:名称、地址、城市、州和 Zip。 还有两 Button 个控件: “确定 ”和 “取消”
WPF 内容是在 WPFPage
类中实现的。 布局使用 Grid 布局元素进行处理。 类继承自 Grid,从而有效地使其成为 WPF 内容根元素。
WPF 内容构造函数采用所需的宽度和高度,并相应地调整大小 Grid 。 然后,它通过创建一组 ColumnDefinition 和 RowDefinition 对象并将其分别添加到 Grid 对象基 ColumnDefinitions 和 RowDefinitions 集合来定义基本布局。 这定义了五行和七列的网格,其维度由单元格的内容决定。
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 类静态 SetColumn 和 SetRow 方法以将控件放置在相应的单元格中。 创建控件后,示例在Grid的Children属性上调用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 控件获取数据。 该示例使用不太直接的方法。 它处理 Click WPF 内容中的内容,然后引发自定义事件 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;
}