WPF 和 Win32 互操作
本主题概述如何对 WPF 和 Win32 代码进行互操作。 Windows Presentation Foundation (WPF) 提供用于创建应用程序的丰富环境。 但是,如果您对 Win32 代码的投入较大,那么更有效的办法是重用该代码的一部分。
本主题包括下列各节。
- WPF 和 Win32 互操作基础
- WPF 互操作项目
- WPF 如何使用 HWND
- 在 Microsoft Win32 窗口中承载 WPF 内容
- 在 WPF 中承载 Microsoft Win32 窗口
- Tab 键、助记键和快捷键
- 相关主题
WPF 和 Win32 互操作基础
WPF 和 Win32 代码互操作中有两项基本的技术。
在 Win32 窗口中承载 WPF 内容。 使用此技术,您可以在标准 Win32 窗口和应用程序的框架内使用 WPF 的高级图形功能。
在 WPF 内容中承载 Win32 窗口。 使用此技术,您可以在其他 WPF 内容的上下文中使用现有的自定义 Win32 控件,并跨边界传递数据。
本主题将概念性地介绍每个技术。 有关在 Win32 中承载 WPF 的更侧重代码的演示,请参见演练:在 Win32 中承载 WPF 内容。 有关在 WPF 中承载 Win32 的更侧重代码的演示,请参见演练:在 WPF 中承载 Win32 控件。
WPF 互操作项目
WPF APIs 是托管代码,但是大部分现有 Win32 程序以非托管 C++ 的形式编写。 您不能从真正的非托管程序中调用 WPF APIs。 但是,通过将 /clr 选项与 Microsoft Visual C++ 编译器配合使用,可以创建托管/非托管混合程序,其中可以无缝地混合托管和非托管 API 调用。
一个需要考虑的项目级问题是您不能将Extensible Application Markup Language (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 托管功能的更多信息,请参见Language Features for Targeting the CLR和 Hello, C++/CLI(欢迎使用 C++/CLI)。 |
WPF 如何使用 HWND
为了充分利用 WPF“HWND 互操作”,您需要了解 WPF 如何使用 HWND。 对于任何 HWND,您不能将 WPF 呈现与 DirectX 呈现或 GDI/GDI+ 呈现混合。 这其中有许多含义。 首先,为了完全混合这些呈现模型,您必须创建互操作解决方案,并为您选择使用的每个呈现模型使用指定的互操作段。 同时,呈现行为为您的互操作解决方案可完成的任务创建“空间”限制。 “空间”概念会在主题 技术区概述中详细介绍。
HWND 最终会支持屏幕上的所有 WPF 元素。 当您创建 WPF Window 时,WPF 会创建顶级 HWND,并使用 HwndSource 将 Window 及其 WPF 内容放入 HWND 中。 应用程序中其余的 WPF 内容共享此单个 HWND。 不过,菜单、组合框下拉列表和其他弹出窗口例外。 这些元素创建它们自己的顶级窗口,这正是 WPF 菜单能跳出包含它的窗口 HWND 之外的原因。 当您使用 HwndHost 将 HWND 放入 WPF 时,WPF 通知 Win32 如何相对于 WPF Window HWND 定位新的子 HWND。
HWND 的相关概念是在每个 HWND 中和每个 HWND 之间是透明的。 主题 技术区概述中也会对此进行讨论。
在 Microsoft Win32 窗口中承载 WPF 内容
在 Win32 窗口中承载 WPF 的关键是使用 HwndSource 类。 此类在 Win32 窗口中包装 WPF 内容,这样 WPF 内容可以作为子窗口并入到您的user interface (UI) 中。 以下方法在一个应用程序中同时使用了 Win32 和 WPF。
将您的 WPF 内容(内容根元素)实现为托管类。 通常,该类继承自可包含多个子元素和/或用作根元素的类之一,例如 DockPanel 或 Page。 在后面的步骤中,该类称为 WPF 内容类,而该类的实例称为 WPF 内容对象。
使用 C++/CLI 实现 Win32 应用程序。 如果您要从现有的非托管 C++ 应用程序开始操作,您通常可以通过将项目设置更改为包含 /clr 编译器标志来使该非托管应用程序可以调用托管代码(支持 /clr 编译所必需内容的完整范围没有在本主题中描述)。
将线程模型设置为单线程单元 (STA)。 WPF 使用此线程模型。
在您的窗口过程中处理 WM_CREATE 通知。
在处理程序(或处理程序调用的函数)中,执行以下操作:
创建一个新 HwndSource 对象,并使用父窗口 HWND 作为其 parent 参数。
创建 WPF 内容类的一个实例。
将对 WPF 内容对象的引用分配给 HwndSource 对象的 RootVisual 属性。
HwndSource 对象的 Handle 属性包含窗口句柄 (HWND)。 若要获得可在您的应用程序的非托管部分中使用的 HWND,请将 Handle.ToPointer() 强制转换为 HWND。
实现包含静态字段(该字段保留对您的 WPF 内容对象的引用)的托管类。 此类允许您从您的 Win32 代码获得对 WPF 内容对象的引用,但是更为重要的是它可以防止无意中对您的 HwndSource 进行垃圾回收。
通过将一个处理程序附加到一个或多个 WPF 内容对象事件,从 WPF 内容对象接收通知。
通过使用您在静态字段中存储的引用来设置属性、调用方法等,与 WPF 内容对象进行通信。
注意 |
---|
如果您单独生成一个程序集并引用它,则可以在 XAML 中,使用 WPF 内容类的默认分部类为步骤 1 实现该类的部分或全部定义。虽然您通常在将 XAML 编译为程序集时包括 Application 对象,但是您在互操作中并没有停止使用该 Application,您使用的是一个或多个由该应用程序引用的 XAML 文件的根类,并引用了其分部类。此过程的其余部分实际上类似于以上列出的过程。 主题演练:在 Win32 中承载 WPF 内容中的代码演示了每个步骤。 |
在 WPF 中承载 Microsoft Win32 窗口
在其他 WPF 内容中承载 Win32 窗口的关键是使用 HwndHost 类。 此类在可添加到 WPF 元素树的 WPF 元素中包装该窗口。 HwndHost 还支持 APIs,它允许您为被承载的窗口执行处理消息之类的任务。 基本过程是:
为 WPF 应用程序创建一个元素树(可以通过代码或标记创建)。 在可将 HwndHost 实现添加为子元素的元素树中找出一个合适且允许的位置。 在这些步骤的剩余步骤中,此元素称为保留元素。
从 HwndHost 派生以创建包含 Win32 内容的对象。
在该承载类中,重写 HwndHost 方法 BuildWindowCore。 返回所承载的窗口的 HWND。 您可能希望将实际控件包装为所返回窗口的子窗口;包装宿主窗口中的控件为 WPF 内容接收控件的通知提供了一种简单的方法。 此技术有助于更正一些有关在承载的控件边界处进行消息处理的 Win32 问题。
重写 HwndHost 的 DestroyWindowCore 方法和 WndProc 方法。 此处的目的是处理清理和移除对承载的内容的引用(特别是如果您创建了对非托管对象的引用)。
在代码隐藏文件中,创建承载类的控件的实例,然后使其成为保留元素的子元素。 通常,您将使用事件处理程序,例如 Loaded,或使用分部类构造函数。 但是您也可以通过运行时行为添加互操作内容。
处理选定的窗口消息,例如控件通知。 有两种处理方法。 这两种方法提供对消息流的相同访问,因此您的选择很大程度上依赖于您的编程习惯。
让承载 WPF 元素通过处理 MessageHook 事件来处理消息。 对于发送到承载的窗口的主窗口过程的每个消息,都会引发该事件。
您不能使用 WndProc 来处理位于进程外的窗口中的消息。
通过使用平台调用来调用非托管 SendMessage 函数来与承载的窗口进行通信。
按照以下这些步骤创建处理鼠标输入的应用程序。 您可以通过实现 IKeyboardInputSink 接口来为承载的窗口添加 Tab 键支持。
主题演练:在 WPF 中承载 Win32 控件中的代码演示了每个步骤。
WPF 中的 HWND
可以将 HwndHost 想像为一个特殊控件。 (技术上讲,HwndHost 是 FrameworkElement 派生的类,不是 Control 派生的类,但可以将其视为以互操作为用途的控件。)HwndHost 对所承载内容的基础 Win32 性质进行抽象,以使 WPF 的余下部分将所承载内容视为另一个类似于控件的对象,而此类对象应呈现并处理输入。 HwndHost 的行为通常类似于任何其他 WPF FrameworkElement,但受基础 HWND 支持程度的限制,在输出(绘图和图形)和输入(鼠标和键盘)方面有一些重大的差异。
输出行为的显著差异
FrameworkElement 是 HwndHost 基类,它具有很多能表示对 UI 的更改的属性。 这些属性包括 FrameworkElement.FlowDirection 之类的属性,该属性能更改父元素内的元素布局。 但是,大部分此类属性不能映射到可能存在的 Win32 等效项,即使这些等效项可能存在也是如此。 这些属性及其意义太多会使得呈现技术过于具体,以致于无法产生实际的映射。 因此,在 HwndHost 上设置属性(例如 FlowDirection)没有效果。
Transform 无法旋转、缩放、扭曲或影响 HwndHost。
HwndHost 不支持 Opacity 属性(Alpha 混合)。 如果 HwndHost 中的内容执行 System.Drawing 操作(这些操作包含 Alpha 信息),它自身不会产生冲突,但是 HwndHost 作为一个整体仅支持 Opacity = 1.0 (100%)。
HwndHost 将显示在同一顶级窗口中的其他 WPF 元素的上边。 但是,ToolTip 或 ContextMenu 生成的菜单是单独的顶级窗口,因此将正确处理 HwndHost。
HwndHost 与其父 UIElement 的剪辑区域不相关。 如果您尝试将 HwndHost 类放入滚动区域或 Canvas,则可能造成问题。
输入行为的显著差异
通常,输入设备的范围限定在 HwndHost 承载的 Win32 区域内,输入事件直接进入 Win32。
当鼠标悬停在 HwndHost 上时,您的应用程序不会收到 WPF 鼠标事件,WPF 属性 IsMouseOver 的值将为 false。
当 HwndHost 拥有键盘焦点时,您的应用程序不会收到 WPF 键盘事件,WPF 属性 IsKeyboardFocusWithin 的值将为 false。
当焦点位于 HwndHost 内,并更改为 HwndHost 中的其他控件时,您的应用程序不会收到 WPF 事件 GotFocus 或 LostFocus。
相关触笔属性和事件是相似的,当触笔悬浮于 HwndHost 上时不会报告信息。
Tab 键、助记键和快捷键
IKeyboardInputSink 和 IKeyboardInputSite 接口使您可以为混合的 WPF 和 Win32 应用程序创建无缝键盘体验:
在 Win32 和 WPF 组件之间进行 Tab 键切换
当焦点位于 Win32 组件中,以及位于 WPF 组件中时,助记键和快捷键都会起作用。
HwndHost 和 HwndSource 类都提供 IKeyboardInputSink 的实现,但是它们可能不会处理更高级情况下所需的所有输入消息。 重写适当的方法以获取所需的键盘行为。
接口仅提供对 WPF 和 Win32 区域之间的过渡上发生的事件的支持。 在 Win32 区域内,Tab 键行为完全由 Tab 键的 Win32 实现的逻辑(如果有)来控制。