Udostępnij za pośrednictwem


Hosting zawartości Win32 w WPF

Wymagania wstępne

Zobacz Współdziałanie WPF i Win32.

Przewodnik po systemie Win32 inside Windows Presentation Framework (HwndHost)

Aby ponownie użyć zawartości Win32 w aplikacjach WPF, użyj HwndHostmetody , która jest kontrolką, która sprawia, że HWNDs wyglądają jak zawartość WPF. Podobnie jak HwndSourcew przypadku metody , HwndHost można łatwo użyć: pochodzą z HwndHost metody i BuildWindowCore i DestroyWindowCore są tworzone, a następnie tworzy wystąpienie HwndHost klasy pochodnej i umieszczaj ją w aplikacji WPF.

Jeśli logika Win32 jest już spakowana jako kontrolka, BuildWindowCore implementacja jest nieco większa niż wywołanie metody CreateWindow. Aby na przykład utworzyć kontrolkę Win32 LISTBOX w języku C++:

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
}

Załóżmy jednak, że kod Win32 nie jest tak samodzielny? Jeśli tak, możesz utworzyć okno dialogowe Win32 i osadzić jego zawartość w większej aplikacji WPF. W przykładzie pokazano to w programach Visual Studio i C++, chociaż można to zrobić również w innym języku lub w wierszu polecenia.

Zacznij od prostego okna dialogowego, które jest kompilowane w projekcie dll języka C++.

Następnie wprowadź okno dialogowe w większej aplikacji WPF:

  • Kompilowanie biblioteki DLL jako zarządzanej (/clr)

  • Przekształcanie okna dialogowego w kontrolkę

  • Definiowanie klasy pochodnej klasy HwndHost z metodami BuildWindowCore i DestroyWindowCore

  • Zastąpij TranslateAccelerator metodę do obsługi kluczy okien dialogowych

  • Zastępowanie TabInto metody do obsługi tabulacji

  • Zastępowanie OnMnemonic metody do obsługi mnemonics

  • Utwórz wystąpienie podklasy HwndHost i umieść ją pod odpowiednim elementem WPF

Przekształcanie okna dialogowego w kontrolkę

Okno dialogowe można przekształcić w podrzędny HWND przy użyciu stylów WS_CHILD i DS_CONTROL. Przejdź do pliku zasobu (rc), w którym zdefiniowano okno dialogowe, i znajdź początek definicji okna dialogowego:

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

Zmień drugi wiersz na:

STYLE DS_SETFONT | WS_CHILD | WS_BORDER | DS_CONTROL

Ta akcja nie w pełni pakuje jej do samodzielnej kontrolki; Nadal trzeba wywołać metodę IsDialogMessage() , aby system Win32 mógł przetwarzać pewne komunikaty, ale zmiana kontrolki zapewnia prosty sposób umieszczania tych kontrolek wewnątrz innego HWND.

Podklasa HwndHost

Zaimportuj następujące przestrzenie nazw:

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;

Następnie utwórz klasę pochodną i HwndHost zastąp BuildWindowCore metody i :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
        }

W tym miejscu użyjesz polecenia CreateDialog , aby utworzyć okno dialogowe, które jest naprawdę kontrolką. Ponieważ jest to jedna z pierwszych metod wywoływanych wewnątrz biblioteki DLL, należy również wykonać niektóre standardowe inicjowanie Win32, wywołując funkcję, którą zdefiniujesz później, o nazwie 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);

Zastąp metodę TranslateAccelerator do obsługi kluczy dialogowych

Jeśli teraz uruchomisz ten przykład, zostanie wyświetlona kontrolka okna dialogowego, ale zignoruje wszystkie przetwarzanie klawiatury, które sprawia, że okno dialogowe jest funkcjonalne okno dialogowe. Teraz należy zastąpić implementację TranslateAccelerator (która pochodzi z IKeyboardInputSinkinterfejsu HwndHost implementujący). Ta metoda jest wywoływana, gdy aplikacja odbiera WM_KEYDOWN i 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
        }

Jest to dużo kodu w jednym kawałku, więc może użyć bardziej szczegółowych wyjaśnień. Najpierw kod korzystający z makr C++ i C++; Należy pamiętać, że istnieje już makro o nazwie TranslateAccelerator, które jest zdefiniowane w pliku winuser.h:

#define TranslateAccelerator  TranslateAcceleratorW

Dlatego należy zdefiniować metodę TranslateAccelerator , a nie metodę TranslateAcceleratorW .

Podobnie istnieje zarówno niezarządzana grupa msg winuser.h, jak i zarządzana Microsoft::Win32::MSG struktura. Można uściślać między nimi przy użyciu operatora C++ :: .

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

Obie grupy msg mają te same dane, ale czasami łatwiej jest pracować z definicją niezarządzaną, więc w tym przykładzie można zdefiniować oczywistą procedurę konwersji:

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

Wróć do TranslateAccelerator. Podstawową zasadą jest wywołanie funkcji IsDialogMessage Win32, aby wykonać jak najwięcej pracy, ale IsDialogMessage nie ma dostępu do czegokolwiek spoza okna dialogowego. W przypadku karty użytkownika okna dialogowego, gdy tabulatory są uruchamiane obok ostatniej kontrolki w naszym oknie dialogowym, musisz ustawić fokus na część WPF przez wywołanie metody IKeyboardInputSite::OnNoMoreStops.

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

Na koniec wywołaj metodę IsDialogMessage. Ale jedną z obowiązków TranslateAccelerator metody jest mówi WPF, czy obsłużyłeś naciśnięcie klawiszy, czy nie. Jeśli go nie obsłużysz, zdarzenie wejściowe może tunelować i bąbelkować przez pozostałą część aplikacji. Tutaj uwidocznisz dziwactwa obsługi messange klawiatury i charakter architektury wejściowej w Win32. Niestety, nie zwraca w żaden sposób, IsDialogMessage czy obsługuje określone naciśnięcie klawisza. Co gorsza, wywoła DispatchMessage() naciśnięć klawiszy, których nie powinien obsługiwać! Dlatego trzeba będzie odwrócić inżyniera IsDialogMessagei wywołać go tylko dla kluczy, które wiesz, że będzie obsługiwać:

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

Zastąpij metodę TabInto do obsługi tabbingu

Teraz, po zaimplementowaniu TranslateAccelerator, użytkownik może obejść się wewnątrz okna dialogowego i na karcie z niej do większej aplikacji WPF. Użytkownik nie może jednak wrócić do okna dialogowego. Aby rozwiązać ten problem, należy zastąpić TabIntopolecenie :

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

Parametr TraversalRequest informuje, czy akcja karty jest kartą, czy tabulatorem.

Zastąpij metodę OnMnemonic do obsługi Mnemonics

Obsługa klawiatury jest prawie kompletna, ale brakuje jednej rzeczy — mnemonics nie działają. Jeśli użytkownik naciska klawisze alt-F, fokus nie przechodzi do pola edycji "Imię:". Dlatego zastąpisz metodę 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
};

Dlaczego nie dzwonić IsDialogMessage tutaj? Masz ten sam problem co wcześniej — musisz mieć możliwość informowania kodu WPF o tym, czy kod obsłużył naciśnięcie klawiszy, czy nie, i IsDialogMessage nie może tego zrobić. Istnieje również drugi problem, ponieważ IsDialogMessage odmawia przetwarzania mnemonic, jeśli skoncentrowany HWND nie znajduje się wewnątrz okna dialogowego.

Utworzenie wystąpienia klasy pochodnej HwndHost

Na koniec, teraz, gdy jest dostępna obsługa wszystkich klawiszy i kart, możesz umieścić plik HwndHost w większej aplikacji WPF. Jeśli główna aplikacja jest napisana w języku XAML, najprostszym sposobem, aby umieścić ją w odpowiednim miejscu, jest pozostawienie pustego Border elementu, w którym chcesz umieścić HwndHostelement . W tym miejscu utworzysz Border nazwę 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>

Następnie wystarczy znaleźć dobre miejsce w sekwencji kodu, aby utworzyć wystąpienie HwndHost obiektu i połączyć go z elementem Border. W tym przykładzie umieścisz go wewnątrz konstruktora dla klasy pochodnej Window :

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

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

Co daje:

Screenshot of the WPF app running.

Zobacz też