WPF での Win32 コンテンツのホスト

必須コンポーネント

WPF と Win32 の相互運用性」を参照してください。

Windows Presentation Framework (HwndHost) 内の Win32 のチュートリアル

WPF アプリケーション内で Win32 コンテンツを再利用するには、HwndHost を使用します。HwndHost は、HWND を WPF コンテンツのように見せるコントロールです。 HwndSource と同様に、HwndHost は簡単に使用できます。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 アプリケーションに埋め込みます。 サンプルでは、これを Microsoft Visual Studio と C++ で示していますが、別の言語またはコマンド ラインで実行することも可能です。

まず、C++ DLL プロジェクトにコンパイルされる単純なダイアログから始めます。

次に、より大きい WPF アプリケーションにダイアログを導入します。

  • DLL をマネージとしてコンパイルします (/clr)。

  • ダイアログをコントロールに変換します。

  • HwndHost の派生クラスを BuildWindowCore メソッドと DestroyWindowCore メソッドで定義します。

  • TranslateAccelerator メソッドをオーバーライドして、ダイアログ キーを処理します。

  • TabInto メソッドをオーバーライドして、Tab キーによる移動をサポートします。

  • 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

2 行目を次のように変更します。

STYLE DS_SETFONT | WS_CHILD | WS_BORDER | DS_CONTROL

この操作では、ダイアログが自己完結的なコントロールに完全にパッケージ化されることはありません。Win32 が特定のメッセージを処理できるように、IsDialogMessage() を呼び出す必要があります。ただし、コントロールを変更すると、それらのコントロールを別の 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 内で最初に呼び出されるメソッドの 1 つであるため、後から定義する、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 Method のオーバーライドによるダイアログ キーの処理

このサンプルをこのまま実行すると、ダイアログ コントロールは表示されますが、ダイアログ ボックスが実際に使用できるようになるためのキーボード処理は、すべて無視されます。 ここでは、(HwndHost が実装するインターフェイスである IKeyboardInputSink から取得する) TranslateAccelerator 実装をオーバーライドする必要があります。 このメソッドは、アプリケーションが 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

このため、TranslateAcceleratorW メソッドではなく TranslateAccelerator メソッドを定義する必要があります。

同様に、アンマネージ winuser.h MSG とマネージ Microsoft::Win32::MSG struct の両方が存在します。 C++ :: 演算子を使用して、この 2 つの MSG のあいまいさを解消できます。

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

2 つの 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 キーで移動し、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 メソッドが行う処理の 1 つは、キーストロークを処理しているかどうかを WPF に伝えることです。 キーストロークを処理していない場合、入力イベントはアプリケーションの残りをトンネリングおよびバブリングできます。 ここで、キーボード メッセージ処理の特性と、Win32 の入力アーキテクチャの性質を公開します。 IsDialogMessage は、特定のキーストロークを処理しているかどうかの値を返しません。 さらに、処理する必要のないキーストロークで DispatchMessage() を呼び出します。 このため、IsDialogMessage をリバースエンジニアリングして、処理することがわかっているキーに対してのみ 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 メソッドのオーバーライドによる Tab キーによる移動のサポート

TranslateAccelerator の実装が完了したので、ユーザーはダイアログ ボックス内を Tab キーで移動し、ダイアログ ボックスからより大きい WPF アプリケーションへ移動することができます。 ただし、ユーザーはダイアログ ボックスに Tab キーで戻ることができません。 この問題を解決するためには、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 キーのアクションが Tab キーであるか Shift + Tab キーであるかを示します。

OnMnemonic メソッドのオーバーライドによるニーモニックのサポート

キーボード処理はほぼ完了ですが、1 つ残っている処理があります。それはニーモニックが機能しないという点です。 ユーザーが 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 派生クラスのインスタンス化

最後に、すべてのキーと Tab キーがサポートされるようになったので、HwndHost をより大きい WPF アプリケーションに入れることができます。 メインのアプリケーションが XAML で記述される場合、HwndHost を正しい場所に入れる最も簡単な方法は、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 の相互運用性