Share via


教學課程:建立裝載 Win32 內容的 WPF 應用程式

更新:2007 年 11 月

必要條件

請參閱 WPF 和 Win32 互通性概觀

Windows Presentation Framework 內的 Win32 逐步解說 (HwndHost)

若要重複使用 WPF 應用程式內的 Win32 內容,請使用 HwndHost;這個控制項可以讓 HWND 看起來就像 WPF 內容一樣。如同 HwndSourceHwndHost 的使用方式非常直接:從 HwndHost 衍生並實作 BuildWindowCore 和 DestroyWindowCore 方法,然後執行個體化 HwndHost 衍生類別 (Derived Class),並將它放在 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 程式碼不全然是獨立的 (Self-Contained) 程式碼呢?如果是這樣,您可以建立 Win32 對話方塊,並將它的內容嵌入較大的 WPF 應用程式中。此範例顯示 Microsoft Visual Studio 和 C++ 中的這項作業,不過您也可以使用不同的語言或在命令列中執行此程序。

從簡單的對話方塊開始,將它編譯成 C++DLL 專案。

接著,將對話方塊引入較大的 WPF 應用程式。

  • 將 DLL 編譯成 (/clr)

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

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

  • 覆寫 TranslateAccelerator 方法以處理對話方塊按鍵

  • 覆寫 TabInto 方法以支援定位

  • 覆寫 OnMnemonic 方法以支援助憶鍵 (Mnemonic)

  • 執行個體化 HwndHost 子類別 (Subclass),並將它放在正確的 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 的衍生類別,並覆寫 BuildWindowCore 和 DestroyWindowCore 方法:

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 內率先呼叫的其中一個方法,所以您也應該呼叫稍後所要定義的函式 (名稱為 InitializeGlobals()),以執行某些標準的 Win32 初始設定:

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++ 巨集的程式碼;請注意,winuser.h 中已定義一個名為 TranslateAccelerator 的巨集:

#define TranslateAccelerator  TranslateAcceleratorW

因此,請務必定義 TranslateAccelerator 方法,而非 TranslateAcceleratorW 方法。

同樣地,此程式碼也同時有 Unmanaged winuser.h MSG 和 Managed Microsoft::Win32::MSG 結構 (Struct)。您可以使用 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 無法存取對話方塊以外的任何項目。當使用者使用定位鍵瀏覽對話方塊時,若定位處超出對話方塊中的最後一個控制項,您必須呼叫 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 方法以支援定位

現在您已實作 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 參數會告訴您定位動作是 TAB 還是 SHIFT+TAB。

覆寫 OnMnemonic 方法以支援助憶鍵

鍵盤處理已接近完成的階段,但是還有一項工作尚未完成:助憶鍵沒有作用。如果使用者按下 ALT-F,焦點不會跳到 "First name:" 編輯方塊。因此,請覆寫 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 的位置,保留空白的 Border 項目。在這裡您會建立一個名為 insertHwndHostHere 的 Border

<Window x:Class="WPFApplication1.Window1"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://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 應用程式螢幕擷取畫面

請參閱

概念

WPF 和 Win32 互通性概觀