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。 但是,通过使用 Microsoft Visual C++ 编译器的 /clr 选项,可创建托管与非托管混合的程序,在此程序中可无缝混合托管和非托管 API 调用。

存在一个项目级问题,即,无法将 Extensible Application Markup Language (XAML) 文件编译到 C++ 项目中。 可通过一些项目分离技术对此进行弥补。

  • 创建一个包含所有 XAML 页面的 C# DLL 作为编译程序集,然后使 C++ 可执行文件包含此 DLL 作为引用。

  • 为 WPF 内容创建一个 C# 可执行文件,然后使其引用包含 Win32 内容的 C++ DLL。

  • 运行时使用 Load 加载任何 XAML,而不是编译 XAML。

  • 切勿使用 XAML,并在代码中编写所有 WPF,从 Application 构建元素树。

请使用最适合你的方法。

注意

如果之前未使用过 C++/CLI,你可能会在互操作代码示例中看到诸如 gcnewnullptr 之类的一些“新”关键字。 这些关键字取代旧版双下划线语法 (__gc),为 C++ 中的托管代码提供了更加自然的语法。 若要详细了解 C++/CLI 托管功能,请参阅适用于运行时平台的组件扩展

WPF 如何使用 Hwnd

若要充分利用 WPF“HWND 互操作”,需要了解 WPF 如何使用 HWND。 对于任何 HWND,无法将 WPF 呈现与 DirectX 呈现或 GDI / GDI+ 呈现混合。 这具有许多影响。 首先,若要混合这些绘制模型,必须创建互操作解决方案,并对选择使用的每个绘制模型使用互操作的指定段。 此外,绘制行为会为互操作解决方案可实现的操作创建一个“空域”限制。 有关“空域”概念的详细信息,请参见技术区概述主题。

屏幕上的所有 WPF 元素最终受 HWND 支持。 创建 WPF Window 时,WPF 会创建一个顶级 HWND,并使用 HwndSourceWindow 及其 WPF 内容放入 HWND 内。 应用程序中的其余 WPF 内容共享单个 HWND。 菜单、组合框下拉列表和其他弹出窗口例外。 这些元素会创建自己的顶级窗口,因此 WPF 菜单可能超出所在窗口 HWND 的边缘。 使用 HwndHost 将 HWND 放入 WPF 内时,WPF 会告知 Win32 如何相对于 WPF Window HWND 放置新的子 HWND。

与之相关的 HWND 概念是每个 HWND 内及其之间的透明度。 技术区概述主题中对此也有相关介绍。

在 Microsoft Win32 窗口中承载 WPF 内容

在 Win32 窗口中托管 WPF 的关键在于 HwndSource 类。 此类在 Win32 窗口中包装 WPF 内容,以便将 WPF 内容并入 UI 作为子窗口。 以下方法在单个应用程序中合并 Win32 和 WPF。

  1. 将 WPF 内容(内容根元素)作为托管类实现。 通常,该类继承自可以包含多个子元素和/或用作根元素的类之一,例如 DockPanelPage。 在后续步骤中,此类称为 WPF 内容类,此类的实例称为 WPF 内容对象。

  2. 使用 C++/CLI 实现 Windows 应用程序。 若要从现有的非托管 C++ 应用程序开始,通常可以更改项目设置将 /clr 编译器标记包括在内,以此允许应用程序调用托管代码(本主题未介绍支持 /clr 编译可能需要的内容的完整范围)。

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

  4. 处理窗口过程中的 WM_CREATE 通知。

  5. 在处理程序(或处理程序调用的函数)中,执行以下操作:

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

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

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

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

  6. 实现一个托管类,该类包含一个用于保存对 WPF 内容对象的引用的静态字段。 通过此类可从 Win32 代码获取对 WPF 内容对象的引用,更重要的是,该类可防止意外对 HwndSource 进行垃圾回收。

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

  8. 通过使用存储在静态字段中的引用来设置属性、调用方法等,与 WPF 内容对象进行通信。

注意

如果生成一个单独的程序集然后对其进行引用,对于步骤 1,可使用内容类的默认分部类在 XAML 中完成部分或全部 WPF 内容类定义。 虽然在将 XAML 编译到程序集的过程中,通常包含 Application 对象,但最后不会在互操作过程中使用此 Application,而是仅使用由应用程序引用的 XAML 文件的一个或多个根类并引用其分部类。 该过程的其余部分基本与上述相似。

演练:在 Win32 中承载 WPF 内容主题中对这些每个步骤通过代码进行了说明。

在 WPF 中承载 Microsoft Win32 窗口

在其他 WPF 内容中托管 Win32 窗口的关键在于 HwndHost 类。 该类在可添加到 WPF 元素树的 WPF 元素中包装窗口。 HwndHost 也支持 API,使你可执行诸如处理托管窗口的消息之类的任务。 基本过程:

  1. 为 WPF 应用程序创建一个元素树(可通过代码或标记)。 在元素树中找到一个合适的许可点,在元素树中可将 HwndHost 实现添加为子元素。 剩余步骤中,此元素称为保留元素。

  2. HwndHost 派生,创建一个包含 Win32 内容的对象。

  3. 在此主机类中,替代 HwndHost 方法 BuildWindowCore。 返回承载窗口的 HWND。 可能需要将实际控件包装为返回窗口的子窗口;在托管窗口中包装控件为 WPF 内容从控件接收通知提供了一种简单的方式。 此方法有助于更正一些有关托管控件边界处消息处理的 Win32 问题。

  4. 替代 HwndHost 方法 DestroyWindowCoreWndProc。 这样做的目的是处理清除和删除对承载内容的引用,尤其是在已创建对非托管对象的引用的情况下。

  5. 在代码隐藏文件中,创建控件承载类的一个实例,并使其成为保留元素的子元素。 通常会使用事件处理程序(如 Loaded)或使用分部类构造函数。 但也可通过运行时行为添加互操作内容。

  6. 处理选择的窗口消息,例如控件通知。 方法有两种。 两种方法提供对消息流的相同访问权限,因此你的选择很大程度上取决于编程简便性。

    • HwndHost 方法 WndProc 的替代中,实现所有消息(不仅仅是关闭消息)的消息处理。

    • 通过处理 MessageHook 事件,使托管 WPF 元素处理消息。 对发送到承载窗口的主窗口过程的每个消息都会引发该事件。

    • 无法使用 WndProc 处理来自进程外窗口的消息。

  7. 通过使用平台调用来调用非托管 SendMessage 函数,与承载窗口通信。

按照这些步骤,创建一个处理鼠标输入的应用程序。 通过实现 IKeyboardInputSink 接口,可为托管窗口添加 Tab 键支持。

演练:在 WPF 中承载 Win32 控件主题中对这些每个步骤通过代码进行了说明。

WPF 内部的 Hwnd

可将 HwndHost 视为一个特殊控件。 (虽然从技术层面讲,HwndHostFrameworkElement 派生类,而不是 Control 派生类,但出于互操作目的可将其视为一个控件。)HwndHost 抽象化托管内容的基础 Win32 性质,以便剩余 WPF 将托管内容视为另一个应呈现和处理输入的类似控件的对象。 虽然基于基础 HWND 可支持项的限制,在输出(绘图和图形)和输入(鼠标和键盘)方面存在重大差异,但 HwndHost 的行为方式通常与其他所有 WPF FrameworkElement 类似。

输出行为的显著差异

  • FrameworkElement(即 HwndHost 基类)具有众多表示 UI 更改的属性。 例如属性 FrameworkElement.FlowDirection,该属性会更改该元素(作为父级)内元素的布局。 但是,这些属性大多数未映射到可能的 Win32 等效项(即使这类等效项可能存在)。 过多这些属性及其含义具有过高的绘制技术针对性,这使得映射并不可行。 因此,在 HwndHost 上设置 FlowDirection 等属性毫无作用。

  • HwndHost 无法旋转、缩放、倾斜或受 Transform 影响。

  • HwndHost 不支持 Opacity 属性(alpha 值混合处理)。 如果 HwndHost 内的内容执行包含 alpha 信息的 System.Drawing 操作,虽然这本身不是冲突,但 HwndHost 作为整体仅支持不透明度 = 1.0 (100%)。

  • HwndHost 出现在同一顶级窗口中的其他 WPF 元素之上。 但是,ToolTipContextMenu 生成的菜单是一个单独的顶级窗口,因此将对 HwndHost 采取正确的行为。

  • HwndHost 不遵从其父级 UIElement 的剪切区域。 如果试图将 HwndHost 类放入滚动区域或 Canvas,这可能是个问题。

输入行为的显著差异

  • 通常而言,虽然输入设备作用域在 HwndHost 托管的 Win32 区域内,但是输入事件会直接转到 Win32。

  • 尽管鼠标位于 HwndHost 上方,但是应用程序不会接收 WPF 鼠标事件,且 WPF 属性 IsMouseOver 的值为 false

  • 尽管键盘焦点位于 HwndHost,但是应用程序不会接收 WPF 键盘事件,且 WPF 属性 IsKeyboardFocusWithin 的值为 false

  • 焦点位于 HwndHost 内并转至 HwndHost 内的另一控件时,应用程序不会接收 WPF 事件 GotFocusLostFocus

  • 相关触笔属性和事件相似,且触笔位于 HwndHost 上方时不会报告信息。

Tab 键、助记键和加速键

通过 IKeyboardInputSinkIKeyboardInputSite 接口,可为 WPF 和 Win32 混合应用程序创建无缝键盘体验:

  • Win32 和 WPF 组件之间的 Tab 键

  • 焦点位于 Win32 组件内和 WPF 组件内时皆起作用的助记键和加速键。

虽然 HwndHostHwndSource 类都提供 IKeyboardInputSink 的实现,但在更高级的方案中,它们可能无法处理你需要的所有输入消息。 替换为适当方法,获取所需的键盘行为。

接口仅对 WPF 和 Win32 区域间的转换过程中发生的事件提供支持。 在 Win32 区域内,Tab 键行为完全受 Tab 键的 Win32 实现逻辑(若有)控制。

另请参阅