Delen via


Win32-inhoud hosten in WPF

Voorwaarden

Zie WPF en Win32 Interoperation.

Een overzicht van Win32 Inside Windows Presentation Framework (HwndHost)

Als u Win32-inhoud in WPF-toepassingen opnieuw wilt gebruiken, gebruikt u HwndHost. Dit is een besturingselement waarmee HWND's eruitzien als WPF-inhoud. Net als HwndSourceis HwndHost eenvoudig te gebruiken: afgeleid van HwndHost en BuildWindowCore en DestroyWindowCore methoden implementeren, vervolgens uw HwndHost afgeleide klasse instantiëren en in uw WPF-toepassing plaatsen.

Als uw Win32-logica al als besturingselement is verpakt, is uw BuildWindowCore implementatie weinig meer dan een aanroep van CreateWindow. Als u bijvoorbeeld een Win32 LISTBOX-besturingselement wilt maken 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
}

Maar stel dat de Win32-code niet helemaal zo zelfstandig is? Zo ja, dan kunt u een Win32-dialoogvenster maken en de inhoud ervan insluiten in een grotere WPF-toepassing. In het voorbeeld ziet u dit in Visual Studio en C++, hoewel het ook mogelijk is om dit in een andere taal of op de opdrachtregel te doen.

Begin met een eenvoudig dialoogvenster, dat is gecompileerd in een C++ DLL-project.

Introduceer vervolgens het dialoogvenster in de grotere WPF-toepassing:

  • Compileer de DLL als 'managed' (/clr)

  • Het dialoogvenster omzetten in een besturingselement

  • De afgeleide klasse van HwndHost definiëren met BuildWindowCore en DestroyWindowCore methoden

  • Methode TranslateAccelerator overschrijven voor de afhandeling van dialoogvenstertoetsen

  • Overschrijf de TabInto-methode voor ondersteuning van tabelnavigatie.

  • Overschrijf methode OnMnemonic ter ondersteuning van mnemonieken

  • Instantieer de HwndHost subklasse en plaats deze onder het juiste WPF-element

Het dialoogvenster omzetten in een besturingselement

U kunt een dialoogvenster omzetten naar een kind-HWND met behulp van de stijlen WS_CHILD en DS_CONTROL. Ga naar het resourcebestand (.rc) waar het dialoogvenster is gedefinieerd en zoek het begin van de definitie van het dialoogvenster:

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

Wijzig de tweede regel in:

STYLE DS_SETFONT | WS_CHILD | WS_BORDER | DS_CONTROL

Deze actie verpakt deze niet volledig in een zelfstandig besturingselement; U moet nog steeds IsDialogMessage() aanroepen, zodat Win32 bepaalde berichten kan verwerken, maar de wijziging van het besturingselement biedt een eenvoudige manier om deze besturingselementen in een andere HWND te plaatsen.

Subklasse HwndHost

Importeer de volgende naamruimten:

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;

Maak vervolgens een afgeleide klasse van HwndHost en overschrijf de methoden BuildWindowCore en 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
        }

Hier gebruikt u de CreateDialog om het dialoogvenster te maken dat echt een besturingselement is. Omdat dit een van de eerste methoden is die in het DLL-bestand worden aangeroepen, moet u ook een standaard-Win32-initialisatie uitvoeren door een functie aan te roepen die u later definieert, genaamd 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);

De Methode TranslateAccelerator overschrijven om dialoogtoetsen te verwerken

Als u dit voorbeeld nu zou uitvoeren, zou u een dialoogvenster krijgen dat wordt weergegeven, maar de volledige toetsenbordverwerking die een dialoogvenster functioneel maakt, zou worden genegeerd. U moet nu de TranslateAccelerator-implementatie overschrijven (die afkomstig is van IKeyboardInputSink, een interface die HwndHost implementeert). Deze methode wordt aangeroepen wanneer de toepassing WM_KEYDOWN en WM_SYSKEYDOWN ontvangt.

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

Dit is veel code in één stuk, dus het kan wat gedetailleerdere uitleg gebruiken. Eerst de code met behulp van C++ en C++ macro's; U moet er rekening mee houden dat er al een macro met de naam TranslateAcceleratoris, die is gedefinieerd in winuser.h:

#define TranslateAccelerator  TranslateAcceleratorW

Zorg er dus voor dat u een TranslateAccelerator methode definieert en niet een TranslateAcceleratorW methode.

Op dezelfde manier zijn er zowel de onbeheerde winuser.h MSG als de beheerde Microsoft::Win32::MSG struct. U kunt onderscheid maken tussen de twee met behulp van de operator C++ ::.

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

Beide MSG's hebben dezelfde gegevens, maar soms is het eenvoudiger om te werken met de niet-beheerde definitie, dus in dit voorbeeld kunt u de voor de hand liggende conversieroutine definiëren:

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

Terug naar TranslateAccelerator. Het basisprincipe is het aanroepen van de Win32-functie IsDialogMessage zoveel mogelijk werk te doen, maar IsDialogMessage heeft geen toegang tot iets buiten het dialoogvenster. Als gebruikerstabblad rondom het dialoogvenster, moet u de focus naar het WPF-gedeelte instellen door IKeyboardInputSite::OnNoMoreStopsaan te roepen wanneer tabbladen voorbij het laatste besturingselement in ons dialoogvenster worden uitgevoerd.

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

Bel ten slotte IsDialogMessage. Maar een van de verantwoordelijkheden van een TranslateAccelerator-methode is om WPF te vertellen of u de toetsaanslag heeft verwerkt of niet. Als u deze niet hebt verwerkt, kan de invoergebeurtenis door de rest van de toepassing tunnelen en bubbelen. Hier maakt u een vreemde manier van toetsenbord messange-verwerking en de aard van de invoerarchitectuur in Win32 beschikbaar. Helaas retourneert IsDialogMessage op geen enkele manier of het een bepaalde toetsaanslag afhandelt. Nog erger is dat het bij toetsaanslagen die het niet zou moeten verwerken DispatchMessage() aanroept! U moet dus IsDialogMessagereverse-engineeren en het alleen aanroepen voor de sleutels waarvan u weet dat ze verwerkt kunnen worden.

// 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-methode overschrijven ter ondersteuning van tabbing

Nu u TranslateAcceleratorhebt geïmplementeerd, kan een gebruiker in het dialoogvenster navigeren en er een tab van maken in de grotere WPF-toepassing. Een gebruiker kan echter niet teruggaan naar het dialoogvenster. Om dit op te lossen, overschrijft u 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;
    }

De parameter TraversalRequest geeft aan of de tabactie een tab, of een shift-tab is.

OnMnemonic-methode overschrijven ter ondersteuning van Mnemonics

Toetsenbordverwerking is bijna voltooid, maar er ontbreekt één ding: nemonics werken niet. Als een gebruiker op Alt-F drukt, gaat de focus niet naar het invoervak Voornaam:. U overschrijft dus de methode 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
};

Waarom bel IsDialogMessage hier niet? U hebt hetzelfde probleem als voorheen. U moet WPF-code kunnen informeren of uw code de toetsaanslag heeft verwerkt of niet, en IsDialogMessage dat niet kunnen doen. Er is ook een tweede probleem, omdat IsDialogMessage weigert de mnemoniek te verwerken als de gefocuste HWND zich niet in het dialoogvenster bevindt.

Instantieer de HwndHost-afgeleide klasse

Nu eindelijk alle ondersteuning voor toetsen en tabbladen is afgerond, kunt u uw HwndHost in de grotere WPF-toepassing integreren. Als de hoofdtoepassing is geschreven in XAML, is de eenvoudigste manier om deze op de juiste plaats te plaatsen, door een leeg Border element achter te laten waar u de HwndHostwilt plaatsen. Hier maakt u een Border met de naam 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>

Dan hoeft u alleen maar een goede plaats in de codereeks te vinden om de HwndHost te instantiëren en deze te verbinden met de Border. In dit voorbeeld plaatst u deze in de constructor voor de Window afgeleide klasse:

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

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

Dit geeft u het volgende:

Schermopname van de WPF-app die wordt uitgevoerd.

Zie ook