WPF 和 Win32 互操作
本主题概述如何互操作 Windows Presentation Foundation (WPF) 和 Win32 代码。 WPF 提供了用于创建应用程序的丰富环境。 但是,当你对 Win32 代码进行大量投资时,重用其中一些代码可能更有效。
WPF 和 Win32 互操作基础知识
WPF 和 Win32 代码之间的互操作有两种基本技术。
在 Win32 窗口中托管 WPF 内容。 借助此方法,可以在标准 Win32 窗口和应用程序的框架中使用 WPF 的高级图形功能。
在 WPF 内容中托管 Win32 窗口。 借助这种技术,可以在其他 WPF 内容的环境中使用现有的自定义 Win32 控件,并跨界传递数据。
本主题从概念上介绍了上述每一种技术。 有关在 Win32 中托管 WPF 的更面向代码的插图,请参阅 演练:在 Win32 中托管 WPF 内容。 有关在 WPF 中托管 Win32 的更面向代码的插图,请参阅 演练:在 WPF中托管 Win32 控件。
WPF 互操作项目
WPF API 是托管代码,但大多数现有的 Win32 程序都是以非托管C++编写的。 无法从真正的非托管程序调用 WPF API。 但是,通过将 /clr
选项与 Microsoft Visual C++ 编译器结合使用,可以创建混合托管非托管程序,以便无缝混合托管和非托管 API 调用。
一个项目级的复杂性是,不能将可扩展应用程序标记语言(XAML)文件编译为C++项目。 有几个项目划分技术可以弥补这一点。
创建一个 C# DLL,其中包含所有 XAML 页面作为编译的程序集,然后将C++可执行文件包含该 DLL 作为引用。
为 WPF 内容创建 C# 可执行文件,并使其引用包含 Win32 内容的C++ DLL。
使用 Load 在运行时加载任意 XAML,而不是编译 XAML。
不要使用 XAML,所有 WPF 都在代码中编写,从 Application开始构建元素树。
使用任何最适合你的方法。
注意
如果以前没有使用过 C++/CLI,你可能会注意到一些“新”关键字,例如互操作代码示例中的 gcnew
和 nullptr
。 这些关键字取代了较旧的双下划线语法(__gc
),并为C++中的托管代码提供更自然的语法。 若要详细了解 C++/CLI 托管功能,请参阅 运行时平台的组件扩展。
WPF 如何使用 Hwnds
要充分利用 WPF 的“HWND 互操作”,需要了解 WPF 如何使用 HWND。 对于任何 HWND,不能将 WPF 呈现与 DirectX 呈现或 GDI/GDI+ 呈现混合使用。 这具有许多影响。 首先,为了混合这些渲染模型,您必须创建一个互操作解决方案,并为您选择使用的每个渲染模型指定特定的互操作部分。 此外,渲染行为为互操作解决方案能够完成的功能创建了一个“限制空间”。 “领空”概念在主题 技术区域概述中进行了更详细的解释。
屏幕上的所有 WPF 元素最终都由 HWND 提供支持。 创建 WPF Window时,WPF 会创建一个顶级 HWND,并使用 HwndSource 将 Window 及其 WPF 内容放入 HWND 中。 应用程序中其余的 WPF 内容共享相同的 HWND。 例外是菜单、组合框下拉列表和其他弹出窗口。 这些元素会创建它们自己的顶级窗口,这就是为什么 WPF 菜单可能会超出其所在窗口的 HWND 边缘。 使用 HwndHost 在 WPF 中放置 HWND 时,WPF 会通知 Win32 如何相对于 WPF Window HWND 定位新的子 HWND。
HWND 的相关概念包括每个 HWND 内部以及 HWND 之间的透明度。 本主题 技术区域概述中对此进行了讨论。
在 Microsoft Win32 窗口中托管 WPF 内容
在 Win32 窗口中托管 WPF 的键是 HwndSource 类。 此类将 WPF 内容包装在 Win32 窗口中,使 WPF 内容可以作为子窗口嵌入到您的 UI 中。 以下方法将 Win32 和 WPF 合并到单个应用程序中。
将 WPF 内容(内容根元素)实现为托管类。 通常,类继承自可包含多个子元素和/或用作根元素的类之一,例如 DockPanel 或 Page。 在后续步骤中,此类称为 WPF 内容类,类的实例称为 WPF 内容对象。
使用 C++/CLI 实现 Windows 应用程序。 如果您正在使用现有的非托管C++应用程序,通常可以通过更改项目设置以包含
/clr
编译器标志,来使其能够调用托管代码(本主题未描述支持/clr
编译所需的全部要求)。将线程模型设置为单线程单元(STA)。 WPF 使用此线程模型。
在窗口过程函数中处理WM_CREATE通知。
在处理程序(或处理程序调用的函数)中执行以下操作:
使用父窗口 HWND 作为其
parent
参数创建新的 HwndSource 对象。创建 WPF 内容类的实例。
将对 WPF 内容对象的引用分配给 HwndSource 对象 RootVisual 属性。
HwndSource 对象 Handle 属性包含窗口句柄(HWND)。 若要获取可以在应用程序的非托管部分使用的 HWND,请将
Handle.ToPointer()
转换为 HWND 类型。
实现一个托管的类,该类包含一个静态字段,该字段持有对你的 WPF 内容对象的引用。 此类允许你从 Win32 代码获取对 WPF 内容对象的引用,但更重要的是,它可以防止你的 HwndSource 对象被无意中垃圾回收。
通过将处理程序附加到一个或多个 WPF 内容对象事件来接收来自 WPF 内容对象的通知。
通过使用存储在静态字段中的引用来设置属性、调用方法等,与 WPF 内容对象通信。
注意
如果您生成一个单独的程序集并引用它,那么可以在 XAML 中为第 1 步使用内容类的默认分部类来定义部分或全部 WPF 内容类。 虽然通常会将 Application 对象作为 XAML 编译到程序集中的一部分,但最终不会在互操作中使用该 Application;相反,您只需使用应用程序引用的 XAML 文件中的一个或多个根类,并引用它们的分部类。 该过程的其余部分实质上类似于上述过程。
每个步骤都通过主题 演练:在 Win32 中托管 WPF 内容中的代码进行演示。
在 WPF 中托管 Microsoft Win32 窗口
在其他 WPF 内容中托管 Win32 窗口的关键是 HwndHost 类。 此类将窗口包装在一个可以添加到 WPF 元素树中的 WPF 元素内。 HwndHost 还支持 API,这些 API 允许你执行托管窗口的进程消息等任务。 基本过程为:
为 WPF 应用程序创建元素树(可以通过代码或标记)。 在元素树中查找适当且允许的点,可在其中将 HwndHost 实现添加为子元素。 在这些步骤的其余部分中,此元素称为保留元素。
派生自 HwndHost,以创建保存 Win32 内容的对象。
在该主机类中,重写 HwndHost 方法 BuildWindowCore。 返回托管窗口的 HWND。 你可以将实际控件包装为返回窗口的子窗口。将控件嵌入到一个宿主窗口中,为 WPF 内容提供了一种简单的方法,以便从这些控件接收通知。 此方法有助于更正有关托管控制边界的消息处理的某些 Win32 问题。
重写 HwndHost 方法 DestroyWindowCore 和 WndProc。 此处的意图是进行清理并删除对托管内容的引用,尤其是在你创建了对非托管对象的引用时。
在后台代码文件中,创建控件承载类的实例,并将其设为容器元素的子级。 通常使用事件处理程序(如 Loaded),或使用分部类构造函数。 但你也可以通过运行时行为添加互操作内容。
处理所选窗口消息,例如控制通知。 有两种方法。 两者都提供对消息流的相同访问权限,因此选择在很大程度上是编程便利性问题。
让宿主 WPF 元素通过处理 MessageHook 事件来处理消息。 每当消息发送到宿主窗口的主窗口过程时,都会引发此事件。
不能使用 WndProc处理来自进程外窗口的消息。
使用平台调用调用非托管
SendMessage
函数来与托管窗口通信。
按照以下步骤创建一个适用于鼠标输入的应用程序。 可以通过实现 IKeyboardInputSink 接口,为托管窗口添加制表符支持。
每个步骤都通过代码演示在主题 演练:在 WPF 中托管 Win32 控件。
WPF 中的 Hwnds
可以将 HwndHost 视为特殊控件。 (从技术上讲,HwndHost 是一个 FrameworkElement 派生类,而不是 Control 派生类,但它可以被视为用于互操作的控件。HwndHost 抽象化托管内容的基础 Win32 性质,以便 WPF 的其余部分将托管内容视为另一个类似控件的对象,该对象应呈现和处理输入。 HwndHost 的行为通常与任何其他 WPF FrameworkElement类似,但根据基础 HWND 支持的限制,输出(绘图和图形)和输入(鼠标和键盘)存在一些重要差异。
输出行为方面的显著差异
FrameworkElement(即 HwndHost 基类)具有相当多的属性,这些属性表示对 UI 的更改。 这些属性包括 FrameworkElement.FlowDirection等,其作用是更改作为父元素的元素内部的布局。 但是,这些属性中的大多数都未映射到可能的 Win32 等效项,即使此类等效项可能存在。 这些属性及其含义过于依赖于特定的渲染技术,因此难以实现有效的映射。 因此,在 HwndHost 上设置 FlowDirection 等属性不起作用。
HwndHost 不能旋转、缩放、倾斜或以其他方式受到转换影响。
HwndHost 不支持 Opacity 属性(Alpha 混合)。 如果 HwndHost 内的内容执行包含 alpha 信息的 System.Drawing 操作,这本身不是冲突,但整个 HwndHost 仅支持 Opacity = 1.0 (100%)。
HwndHost 将显示在同一顶级窗口中其他 WPF 元素之上。 但是,ToolTip 或 ContextMenu 生成的菜单是一个独立的顶级窗口,因此在 HwndHost时行为正确。
HwndHost 不尊重其父 UIElement的剪辑区域。 如果您尝试在滚动区域或 Canvas中放置 HwndHost 类,可能会出现问题。
输入行为中的显著差异
通常,虽然输入设备在由 HwndHost 托管的 Win32 区域范围内,但输入事件直接发送到 Win32。
当鼠标悬停在 HwndHost上时,应用程序不会接收到 WPF 鼠标事件,并且 WPF 属性 IsMouseOver 的值将是
false
。当 HwndHost 具有键盘焦点时,您的应用程序将不会收到 WPF 键盘事件,且 WPF 属性 IsKeyboardFocusWithin 的值将会是
false
。当焦点位于 HwndHost 内并更改为 HwndHost内另一个控件时,应用程序将不会接收 GotFocus 或 LostFocus的 WPF 事件。
相关触笔属性和事件类似,当触笔悬停在 HwndHost上方时,不会报告信息。
标签切换、助记符和快捷键
使用 IKeyboardInputSink 和 IKeyboardInputSite 接口可为混合 WPF 和 Win32 应用程序创建无缝键盘体验:
在 Win32 和 WPF 组件之间切换标签
当焦点位于 Win32 组件和 WPF 组件内时,助记键和加速器都起作用。
HwndHost 和 HwndSource 类都提供 IKeyboardInputSink的实现,但它们可能无法处理针对更高级方案所需的所有输入消息。 重写相关的方法以实现所需的键盘功能。
接口仅支持 WPF 和 Win32 区域之间的转换时发生的情况。 在 Win32 区域中,Tabbing 行为完全由 Win32 实现的逻辑控制(如果有)。
另请参阅
- HwndHost
- HwndSource
- System.Windows.Interop
- 演练:在 WPF 中托管 Win32 控件
- 演练:在 Win32 中托管 WPF 内容