Share via


在 WPF 中裝載 Win32 內容

必要條件

請參閱 WPF 和 Win32 交互操作

Windows 簡報架構內的 Win32 逐步解說 (HwndHost)

若要重複使用 WPF 應用程式內的 Win32 內容,請使用 HwndHost ,這是讓 HWND 看起來像 WPF 內容的控制項。 和 一樣 HwndSourceHwndHost 直接使用:衍生自 HwndHost 並實 BuildWindowCore 作 和 DestroyWindowCore 方法,然後具現化衍生 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 編譯為 Managed ( /clr

  • 將對話方塊轉換成控制項

  • 使用 和 DestroyWindowCore 方法定義 的 HwndHostBuildWindowCore 衍生類別

  • 覆寫 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 方法。

同樣地,還有 Unmanaged winuser.h MSG 和 Managed Microsoft::Win32::MSG 結構。 您可以使用 C++ :: 運算子來厘清這兩者。

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

這兩個 MSG 都有相同的資料,但有時候使用 Unmanaged 定義會比較容易,因此在此範例中,您可以定義明顯的轉換常式:

::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 無法存取對話方塊以外的任何專案。 身為對話周圍的使用者索引標籤,當 Tabbing 在對話中超過最後一個控制項時,您必須呼叫 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 標籤動作是索引標籤或移位索引標籤。

覆寫 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 無法執行此動作。 另外還有第二個問題,因為 IsDialogMessage 如果焦點 HWND 不在對話方塊中,拒絕處理助憶鍵。

具現化 HwndHost 衍生類別

最後,現在所有索引鍵和索引標籤支援都已就緒,您可以將 您 HwndHost 放入較大的 WPF 應用程式。 如果主要應用程式是以 XAML 撰寫,將它放在正確位置的最簡單方式是讓您想要放置 HwndHost 的空白 Border 元素。 在這裡,您會建立 Border 名為 insertHwndHostHere 的 :

<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;
    }
}

這會為您提供:

Screenshot of the WPF app running.

另請參閱