在 WPF 中托管 Win32 内容

先决条件

请参阅 WPF 和 Win32 互操作

Windows 演示文稿框架中的 Win32 演练 (HwndHost)

若要重用 WPF 应用程序中的 Win32 内容,请使用 HwndHost,该控件使 HWND 看起来像 WPF 内容。 同样 HwndSourceHwndHost 使用起来非常简单:派生自 HwndHost 和实现 BuildWindowCoreDestroyWindowCore 方法,然后实例化 HwndHost 派生类并将其放置在 WPF 应用程序中。

如果 Win32 逻辑已打包为控件,那么 BuildWindowCore 实现几乎只是对 CreateWindow 的调用。 例如,若要在 C++中创建 Win32 LISTBOX 控件:

virtual HandleRef BuildWindowCore(HandleRef hwndParent) override {
    HWND handle = CreateWindowEx(0, L"LISTBOX",
    L"this is a Win32 listbox",
    WS_CHILD | WS_VISIBLE | LBS_NOTIFY
    | WS_VSCROLL | WS_BORDER,
    0, 0, // x, y
    30, 70, // height, width
    (HWND) hwndParent.Handle.ToPointer(), // parent hwnd
    0, // hmenu
    0, // hinstance
    0); // lparam

    return HandleRef(this, IntPtr(handle));
}

virtual void DestroyWindowCore(HandleRef hwnd) override {
    // HwndHost will dispose the hwnd for us
}

假设 Win32 代码并非如此自成一体? 如果是这样,可以创建 Win32 对话框并将其内容嵌入更大的 WPF 应用程序中。 该示例在 Visual Studio 和 C++中演示了这一点,不过也可以以其他语言或命令行执行此作。

从简单对话框开始,该对话框编译为C++ DLL 项目。

接下来,将对话框引入到较大的 WPF 应用程序中:

  • 将 DLL 编译为托管 DLL (/clr

  • 将对话框转换为控件

  • 定义带有BuildWindowCoreDestroyWindowCore 方法的HwndHost派生类

  • 重写 TranslateAccelerator 方法以处理对话框键

  • 重写 TabInto 方法以支持制表符的功能

  • 重写 OnMnemonic 方法以支持快捷键

  • 实例化 HwndHost 子类并将其放在正确的 WPF 元素下

将对话框转换为控件

可以使用WS_CHILD和DS_CONTROL样式将对话框转换为子 HWND。 转到定义对话框的资源文件(.rc),找到对话框定义的开头:

IDD_DIALOG1 DIALOGEX 0, 0, 303, 121
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU

将第二行更改为:

STYLE DS_SETFONT | WS_CHILD | WS_BORDER | DS_CONTROL

此动作不会将其完全打包成一个自包含控件;你仍然需要调用 IsDialogMessage(),使 Win32 处理某些消息,但控件更改确实提供了一种简单的方法,可以将这些控件置于另一个 HWND 中。

子类 HwndHost

导入下列命名空间:

namespace ManagedCpp
{
    using namespace System;
    using namespace System::Windows;
    using namespace System::Windows::Interop;
    using namespace System::Windows::Input;
    using namespace System::Windows::Media;
    using namespace System::Runtime::InteropServices;

然后为 HwndHost 创建一个派生类,并重写 BuildWindowCoreDestroyWindowCore 方法:

public ref class MyHwndHost : public HwndHost, IKeyboardInputSink {
    private:
        HWND dialog;

    protected:
        virtual HandleRef BuildWindowCore(HandleRef hwndParent) override {
            InitializeGlobals();
            dialog = CreateDialog(hInstance,
                MAKEINTRESOURCE(IDD_DIALOG1),
                (HWND) hwndParent.Handle.ToPointer(),
                (DLGPROC) About);
            return HandleRef(this, IntPtr(dialog));
        }

        virtual void DestroyWindowCore(HandleRef hwnd) override {
            // hwnd will be disposed for us
        }

在这里,你使用 CreateDialog 创建一个实际上是控件的对话框。 由于这是 DLL 中调用的第一个方法之一,因此还应通过调用稍后定义的函数执行一些标准 Win32 初始化,调用 InitializeGlobals()

bool initialized = false;
    void InitializeGlobals() {
        if (initialized) return;
        initialized = true;

        // TODO: Place code here.
        MSG msg;
        HACCEL hAccelTable;

        // Initialize global strings
        LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
        LoadString(hInstance, IDC_TYPICALWIN32DIALOG, szWindowClass, MAX_LOADSTRING);
        MyRegisterClass(hInstance);

重写 TranslateAccelerator 方法以处理对话键

如果现在运行此示例,你将获得一个显示的对话控件,但它将忽略所有使对话框成为功能对话框的键盘处理。 现在应重写 TranslateAccelerator 的实现(它来自 IKeyboardInputSink 这个接口,而 HwndHost 实现了该接口)。 当应用程序收到WM_KEYDOWN和WM_SYSKEYDOWN时,将调用此方法。

#undef TranslateAccelerator
        virtual bool TranslateAccelerator(System::Windows::Interop::MSG% msg,
            ModifierKeys modifiers) override
        {
            ::MSG m = ConvertMessage(msg);

            // Win32's IsDialogMessage() will handle most of our tabbing, but doesn't know
            // what to do when it reaches the last tab stop
            if (m.message == WM_KEYDOWN && m.wParam == VK_TAB) {
                HWND firstTabStop = GetDlgItem(dialog, IDC_EDIT1);
                HWND lastTabStop = GetDlgItem(dialog, IDCANCEL);
                TraversalRequest^ request = nullptr;

                if (GetKeyState(VK_SHIFT) && GetFocus() == firstTabStop) {
                    // this code should work, but there’s a bug with interop shift-tab in current builds
                    request = gcnew TraversalRequest(FocusNavigationDirection::Last);
                }
                else if (!GetKeyState(VK_SHIFT) && GetFocus() == lastTabStop) {
                    request = gcnew TraversalRequest(FocusNavigationDirection::Next);
                }

                if (request != nullptr)
                    return ((IKeyboardInputSink^) this)->KeyboardInputSite->OnNoMoreTabStops(request);

            }

            // Only call IsDialogMessage for keys it will do something with.
            if (msg.message == WM_SYSKEYDOWN || msg.message == WM_KEYDOWN) {
                switch (m.wParam) {
                    case VK_TAB:
                    case VK_LEFT:
                    case VK_UP:
                    case VK_RIGHT:
                    case VK_DOWN:
                    case VK_EXECUTE:
                    case VK_RETURN:
                    case VK_ESCAPE:
                    case VK_CANCEL:
                        IsDialogMessage(dialog, &m);
                        // IsDialogMessage should be called ProcessDialogMessage --
                        // it processes messages without ever really telling you
                        // if it handled a specific message or not
                        return true;
                }
            }

            return false; // not a key we handled
        }

这段代码有很多内容,因此需要更详细的解释。 首先,使用C++和C++宏的代码。需要注意的是,已经存在一个名为TranslateAccelerator的宏,它是在 winuser.h 中定义的。

#define TranslateAccelerator  TranslateAcceleratorW

因此,请确保定义方法 TranslateAccelerator 而不是 TranslateAcceleratorW 方法。

同样,还有非托管的 winuser.h MSG 和托管的 Microsoft::Win32::MSG 结构体。 可以使用 C++ :: 运算符消除两者之间的歧义。

virtual bool TranslateAccelerator(System::Windows::Interop::MSG% msg,
    ModifierKeys modifiers) override
{
    ::MSG m = ConvertMessage(msg);
}

这两个 MSG 具有相同的数据,但有时使用非托管定义会更容易,因此在此示例中可以定义一个显而易见的转换程序:

::MSG ConvertMessage(System::Windows::Interop::MSG% msg) {
    ::MSG m;
    m.hwnd = (HWND) msg.hwnd.ToPointer();
    m.lParam = (LPARAM) msg.lParam.ToPointer();
    m.message = msg.message;
    m.wParam = (WPARAM) msg.wParam.ToPointer();

    m.time = msg.time;

    POINT pt;
    pt.x = msg.pt_x;
    pt.y = msg.pt_y;
    m.pt = pt;

    return m;
}

返回TranslateAccelerator。 基本原则是调用 Win32 函数 IsDialogMessage 以尽可能多地执行工作,但 IsDialogMessage 无权访问对话之外的任何内容。 当用户使用 Tab 键在对话框中导航时,如果越过了最后一个控件,就需要通过调用 IKeyboardInputSite::OnNoMoreStops 将焦点设置到 WPF 部分。

// Win32's IsDialogMessage() will handle most of the tabbing, but doesn't know
// what to do when it reaches the last tab stop
if (m.message == WM_KEYDOWN && m.wParam == VK_TAB) {
    HWND firstTabStop = GetDlgItem(dialog, IDC_EDIT1);
    HWND lastTabStop = GetDlgItem(dialog, IDCANCEL);
    TraversalRequest^ request = nullptr;

    if (GetKeyState(VK_SHIFT) && GetFocus() == firstTabStop) {
        request = gcnew TraversalRequest(FocusNavigationDirection::Last);
    }
    else if (!GetKeyState(VK_SHIFT) && GetFocus() ==  lastTabStop) { {
        request = gcnew TraversalRequest(FocusNavigationDirection::Next);
    }

    if (request != nullptr)
        return ((IKeyboardInputSink^) this)->KeyboardInputSite->OnNoMoreTabStops(request);
}

最后,调用 IsDialogMessage。 但 TranslateAccelerator 方法的职责之一是告诉 WPF 你是否处理了该按键。 如果你没有处理,它输入事件可能会在应用程序的其他部分之间传播和冒泡。 在这里,您将揭示键盘消息处理的怪异之处以及 Win32 中输入体系结构的特点。 遗憾的是,无论它如何处理特定的击键, IsDialogMessage 都不会以任何方式返回。 更糟的是,它会在不该处理的击键时调用 DispatchMessage()! 因此,你必须对 IsDialogMessage 进行逆向工程,并且只对确定它能处理的密钥进行调用。

// Only call IsDialogMessage for keys it will do something with.
if (msg.message == WM_SYSKEYDOWN || msg.message == WM_KEYDOWN) {
    switch (m.wParam) {
        case VK_TAB:
        case VK_LEFT:
        case VK_UP:
        case VK_RIGHT:
        case VK_DOWN:
        case VK_EXECUTE:
        case VK_RETURN:
        case VK_ESCAPE:
        case VK_CANCEL:
            IsDialogMessage(dialog, &m);
            // IsDialogMessage should be called ProcessDialogMessage --
            // it processes messages without ever really telling you
            // if it handled a specific message or not
            return true;
    }

重写 TabInto 方法以支持 Tabbing

现在您已经实现了 TranslateAccelerator,用户可以在对话框内部跳转,并且跳出对话框进入更大的 WPF 应用程序。 但用户无法重新进入对话框。 若要解决此问题,请覆盖 TabInto

public:
    virtual bool TabInto(TraversalRequest^ request) override {
        if (request->FocusNavigationDirection == FocusNavigationDirection::Last) {
            HWND lastTabStop = GetDlgItem(dialog, IDCANCEL);
            SetFocus(lastTabStop);
        }
        else {
            HWND firstTabStop = GetDlgItem(dialog, IDC_EDIT1);
            SetFocus(firstTabStop);
        }
        return true;
    }

TraversalRequest 参数指示您是执行选项卡操作还是 Shift 选项卡操作。

重写 OnMnemonic 方法以支持助记

键盘处理几乎已完成,但缺少一件事 - 助记不起作用。 如果用户按 alt-F,焦点不会跳转到“名字:”编辑框。 因此,重写 OnMnemonic 方法:

virtual bool OnMnemonic(System::Windows::Interop::MSG% msg, ModifierKeys modifiers) override {
    ::MSG m = ConvertMessage(msg);

    // If it's one of our mnemonics, set focus to the appropriate hwnd
    if (msg.message == WM_SYSCHAR && GetKeyState(VK_MENU /*alt*/)) {
        int dialogitem = 9999;
        switch (m.wParam) {
            case 's': dialogitem = IDOK; break;
            case 'c': dialogitem = IDCANCEL; break;
            case 'f': dialogitem = IDC_EDIT1; break;
            case 'l': dialogitem = IDC_EDIT2; break;
            case 'p': dialogitem = IDC_EDIT3; break;
            case 'a': dialogitem = IDC_EDIT4; break;
            case 'i': dialogitem = IDC_EDIT5; break;
            case 't': dialogitem = IDC_EDIT6; break;
            case 'z': dialogitem = IDC_EDIT7; break;
        }
        if (dialogitem != 9999) {
            HWND hwnd = GetDlgItem(dialog, dialogitem);
            SetFocus(hwnd);
            return true;
        }
    }
    return false; // key unhandled
};

为什么不在这里打电话 IsDialogMessage ? 你有与之前相同的问题—你需要能够通知 WPF 代码是否已处理该按键,并且 IsDialogMessage 无法做到这一点。 还有第二个问题,因为如果焦点 HWND 不在对话框中,IsDialogMessage 会拒绝处理助记符。

实例化 HwndHost 派生类

最后,现在所有键和选项卡的支持都已就绪,你可以将HwndHost 放入更大的 WPF 应用程序中。 如果主应用程序是用 XAML 编写的,将 HwndHost 放置在正确位置的最简单方法是在你想放置 HwndHost 的地方留一个空的 Border 元素。 在这里,您创建一个名为BorderinsertHwndHostHere

<Window x:Class="WPFApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Windows Presentation Framework Application"
    Loaded="Window1_Loaded"
    >
    <StackPanel>
        <Button Content="WPF button"/>
        <Border Name="insertHwndHostHere" Height="200" Width="500"/>
        <Button Content="WPF button"/>
    </StackPanel>
</Window>

然后,剩下的就是在代码序列中找到一个很好的位置来实例化 HwndHost 并将其连接到代码序列 Border。 在此示例中,将它放入派生类的 Window 构造函数中:

public partial class Window1 : Window {
    public Window1() {
    }

    void Window1_Loaded(object sender, RoutedEventArgs e) {
        HwndHost host = new ManagedCpp.MyHwndHost();
        insertHwndHostHere.Child = host;
    }
}

这将为你带来:

正在运行的 WPF 应用的屏幕截图。

另请参阅