Condividi tramite


Hosting di contenuto Win32 in WPF

Prerequisiti

Vedere WPF e l'interoperabilità Win32.

Guida a Win32 all'interno del 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 i metodi BuildWindowCore e DestroyWindowCore

  • Sovrascrivere il metodo TranslateAccelerator per gestire i tasti di dialogo

  • Sovrascrivere il metodo TabInto per supportare la navigazione tramite tabulazione

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

  • Creare un'istanza della sottoclasse HwndHost e collocarla nell'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

Importa i seguenti namespace:

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 utilizza CreateDialog per creare la finestra di dialogo che è realmente 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 eseguissi questo esempio ora, otterresti un controllo di dialogo che si visualizza, ma ignorerebbe tutta la gestione della tastiera che rende 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 l'elemento non gestito MSG di winuser.h 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 MSG hanno gli stessi dati, ma a volte è più facile usare la definizione non gestito, quindi in questo esempio è possibile definire la routine di conversione evidente.

::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. Quando un utente scorre tra i controlli della finestra di dialogo, una volta superato l'ultimo controllo, è 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, chiama IsDialogMessage. Tuttavia, una delle responsabilità di un TranslateAccelerator metodo è comunicare a WPF se la pressione dei tasti è stata gestita o meno. Se non viene gestito, l'evento di input può propagarsi in modalità di tunneling e bubbling attraverso il resto dell'applicazione. In questo caso, esporrete una stranezza della gestione dei messaggi della tastiera e la natura dell'architettura dell'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 l'ingegneria inversa di IsDialogMessage e 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;
    }

Sostituire il metodo TabInto per supportare la tabulazione

Ora che hai implementato TranslateAccelerator, un utente può spostarsi all'interno della finestra di dialogo e uscirne per passare all'applicazione WPF principale. Tuttavia, un utente non può tornare alla finestra di dialogo. Per risolvere il problema, eseguire l'override di 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;
    }

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

Sovrascrivere il metodo OnMnemonic per supportare i mnemonici

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, poiché IsDialogMessage rifiuta di elaborare il mnemonico se l'HWND attivo non si trova nella finestra di dialogo.

Creare un'istanza della classe derivata HwndHost

Infine, ora che tutto il supporto per tasti e schede è pronto, puoi inserire il tuo HwndHost 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 della classe derivata Window.

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 dell'app WPF in esecuzione.

Vedere anche