Hosting di contenuto Win32 in WPF

Prerequisiti

Vedere Interoperatività di WPF e Win32.

Procedura dettagliata di Win32 inside Windows Presentation Framework (HwndHost)

Per riutilizzare il contenuto Win32 all'interno di applicazioni WPF, usare HwndHost, ovvero un controllo che rende i file HWND simili al contenuto WPF. Come HwndSource, HwndHost è semplice da usare: derivare da HwndHost e implementare BuildWindowCore e DestroyWindowCore metodi, quindi creare un'istanza della HwndHost classe derivata e inserirla all'interno dell'applicazione WPF.

Se la logica Win32 è già inclusa nel pacchetto come controllo, l'implementazione BuildWindowCore è poco più di una chiamata a CreateWindow. Ad esempio, per creare un controllo LISTBOX Win32 in 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
}

Ma si supponga che il codice Win32 non sia abbastanza autonomo? In tal caso, è possibile creare una finestra di dialogo Win32 e incorporarne il contenuto in un'applicazione WPF più grande. L'esempio mostra questo aspetto in Visual Studio e C++, anche se è anche possibile eseguire questa operazione in un linguaggio diverso o nella riga di comando.

Iniziare con una finestra di dialogo semplice, compilata in un progetto DLL C++.

Introdurre quindi la finestra di dialogo nell'applicazione WPF più grande:

  • Compilare la DLL come gestita (/clr)

  • Trasformare il dialogo in un controllo

  • Definire la classe derivata di HwndHost con BuildWindowCore i metodi e DestroyWindowCore

  • Eseguire l'override TranslateAccelerator del metodo per gestire le chiavi di dialogo

  • Eseguire l'override TabInto del metodo per supportare la tabulazione

  • Eseguire l'override OnMnemonic del metodo per supportare i mnemonici

  • Creare un'istanza della HwndHost sottoclasse e inserirla sotto l'elemento WPF corretto

Trasformare il dialogo in un controllo

È possibile trasformare una finestra di dialogo in un HWND figlio usando gli stili WS_CHILD e DS_CONTROL. Passare al file di risorse (rc) in cui è definito il dialogo e trovare l'inizio della definizione della finestra di dialogo:

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

Modificare la seconda riga in:

STYLE DS_SETFONT | WS_CHILD | WS_BORDER | DS_CONTROL

Questa azione non lo inserisce completamente in un controllo autonomo; è comunque necessario chiamare IsDialogMessage() in modo che Win32 possa elaborare determinati messaggi, ma la modifica del controllo fornisce un modo semplice per inserire tali controlli all'interno di un altro HWND.

Sottoclasse HwndHost

Importare gli spazi dei nomi seguenti:

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;

Creare quindi una classe derivata di HwndHost ed eseguire l'override dei BuildWindowCore metodi e 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
        }

Qui si usa per CreateDialog creare la finestra di dialogo che è davvero un controllo. Poiché si tratta di uno dei primi metodi chiamati all'interno della DLL, è consigliabile eseguire anche alcune inizializzazione Win32 standard chiamando una funzione che verrà definita in un secondo momento, denominata 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);

Eseguire l'override del metodo TranslateAccelerator per gestire le chiavi della finestra di dialogo

Se è stato eseguito questo esempio, si otterrebbe un controllo finestra di dialogo che viene visualizzato, ma ignora tutte le elaborazioni della tastiera che rendono una finestra di dialogo funzionale. È ora necessario eseguire l'override dell'implementazione TranslateAccelerator ,che deriva da IKeyboardInputSink, un'interfaccia che HwndHost implementa. Questo metodo viene chiamato quando l'applicazione riceve WM_KEYDOWN e 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
        }

Si tratta di un sacco di codice in un unico pezzo, quindi potrebbe usare alcune spiegazioni più dettagliate. Prima di tutto, il codice che usa macro C++ e C++; è necessario tenere presente che è già presente una macro denominata TranslateAccelerator, definita in winuser.h:

#define TranslateAccelerator  TranslateAcceleratorW

Assicurarsi quindi di definire un TranslateAccelerator metodo e non un TranslateAcceleratorW metodo.

Analogamente, è presente sia il msg winuser.h non gestito che lo struct gestito Microsoft::Win32::MSG . È possibile disambiguare tra i due usando l'operatore C++ :: .

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

Entrambi i gruppi di sicurezza hanno gli stessi dati, ma a volte è più facile usare la definizione non gestita, quindi in questo esempio è possibile definire la routine di conversione ovvia:

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

Tornare a TranslateAccelerator. Il principio di base è chiamare la funzione IsDialogMessage Win32 per eseguire il maggior lavoro possibile, ma IsDialogMessage non ha accesso a nulla al di fuori del dialogo. Come scheda utente intorno alla finestra di dialogo, quando la tabulazione viene eseguita oltre l'ultimo controllo nella finestra di dialogo, è necessario impostare lo stato attivo sulla parte WPF chiamando 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);
}

Infine, viene chiamato IsDialogMessage. Tuttavia, una delle responsabilità di un TranslateAccelerator metodo è indicare a WPF se la sequenza di tasti è stata gestita o meno. Se non è stato gestito, l'evento di input può eseguire il tunneling e la bolla nel resto dell'applicazione. In questo caso, esporrai un'stranizza della gestione della messange della tastiera e la natura dell'architettura di input in Win32. Sfortunatamente, IsDialogMessage non restituisce in alcun modo se gestisce una sequenza di tasti specifica. Peggio ancora, chiamerà DispatchMessage() le sequenze di tasti che non deve gestire! Sarà quindi necessario eseguire il reverse engineer IsDialogMessagee chiamarlo solo per le chiavi che si sa che gestirà:

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

Eseguire l'override del metodo TabInto per supportare la tabulazione

Ora che è stato implementato TranslateAccelerator, un utente può spostarsi all'interno della finestra di dialogo e tabularlo nell'applicazione WPF più grande. Tuttavia, un utente non può tornare alla finestra di dialogo. Per risolvere questo problema, eseguire l'override TabIntodi :

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

Il TraversalRequest parametro indica se l'azione di tabulazioni è una scheda o una tabulazioni di spostamento.

Eseguire l'override del metodo OnMnemonic per supportare Mnemonics

La gestione della tastiera è quasi completa, ma c'è una cosa mancante: i mnemonici non funzionano. Se un utente preme alt-F, lo stato attivo non passa alla casella di modifica "Nome:". Quindi, si esegue l'override del OnMnemonic metodo :

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

Perché non chiamare IsDialogMessage qui? Si è verificato lo stesso problema di prima, è necessario essere in grado di informare il codice WPF se il codice ha gestito o meno la sequenza di tasti e IsDialogMessage non può farlo. C'è anche un secondo problema, perché IsDialogMessage rifiuta di elaborare il mnemonico se l'HWND con stato attivo non si trova all'interno della finestra di dialogo.

Creare un'istanza della classe derivata HwndHost

Infine, ora che è disponibile tutto il supporto di tasti e schede, è possibile inserire HwndHost l'elemento nell'applicazione WPF più grande. Se l'applicazione principale è scritta in XAML, il modo più semplice per inserirlo nel posto giusto consiste nell'lasciare un elemento vuoto Border in cui si vuole inserire .HwndHost Qui si crea un Border oggetto denominato 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>

Quindi tutto ciò che rimane consiste nel trovare una buona posizione nella sequenza di codice per creare un'istanza di HwndHost e connetterla all'oggetto Border. In questo esempio verrà inserito all'interno del costruttore per la Window classe derivata:

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

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

In questo modo è possibile:

Screenshot of the WPF app running.

Vedi anche