Dela via


Använda Win32-innehåll i WPF

Förutsättningar

Se WPF och Win32 Interoperation.

En genomgång av Win32 inom Windows Presentation Foundation (HwndHost)

Om du vill återanvända Win32-innehåll i WPF-program använder du HwndHost, vilket är en kontroll som gör att HWND:er ser ut som WPF-innehåll. Precis som HwndSource, HwndHost är det enkelt att använda: härled från HwndHost och implementera BuildWindowCore och DestroyWindowCore metoder, sedan instansiera din HwndHost härledda klass och placera den i ditt WPF-program.

Om Win32-logiken redan är paketerad som en kontroll BuildWindowCore är implementeringen inte mycket mer än ett anrop till CreateWindow. Om du till exempel vill skapa en Win32 LISTBOX-kontroll i 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
}

Men anta att Win32-koden inte är så självständig? I så fall kan du skapa en Win32-dialogruta och bädda in innehållet i ett större WPF-program. Exemplet visar detta i Visual Studio och C++, även om det också är möjligt att göra detta på ett annat språk eller på kommandoraden.

Börja med en enkel dialogruta som kompileras till ett C++ DLL-projekt.

Introducera sedan dialogrutan i det större WPF-programmet:

  • Kompilera DLL som hanterad (/clr)

  • Omvandla dialogrutan till en kontroll

  • Definiera den härledda klassen HwndHost med metoderna BuildWindowCore och DestroyWindowCore

  • Åsidosätt TranslateAccelerator metod för att hantera dialognycklar

  • Åsidosätt TabInto metod för att stödja tabbning

  • Åsidosätt OnMnemonic metod för att stödja mnemonics

  • Instansiera underklassen HwndHost och placera den under rätt WPF-element

Omvandla dialogrutan till en kontroll

Du kan omvandla en dialogruta till en underordnad HWND genom att använda stilarna WS_CHILD och DS_CONTROL. Gå till resursfilen (.rc) där dialogrutan har definierats och leta reda på början av definitionen av dialogrutan:

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

Ändra den andra raden till:

STYLE DS_SETFONT | WS_CHILD | WS_BORDER | DS_CONTROL

Den här åtgärden paketerar den inte helt i en självständig kontroll; du måste fortfarande anropa IsDialogMessage() så att Win32 kan bearbeta vissa meddelanden, men kontrolländringen ger ett enkelt och direkt sätt att placera kontrollerna i en annan HWND.

Subklass HwndHost

Importera följande namnområden:

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;

Skapa sedan en härledd klass av HwndHost och åsidosätt BuildWindowCore metoderna och 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
        }

Här använder CreateDialog du för att skapa dialogrutan som verkligen är en kontroll. Eftersom det här är en av de första metoderna som anropas i DLL:en bör du också göra någon standardinitiering av Win32 genom att anropa en funktion som du definierar senare, med namnet 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);

Åsidosätt TranslateAccelerator-metod för att hantera dialognycklar

Om du körde det här exemplet nu skulle du få en dialogkontroll som visas, men den skulle ignorera all tangentbordsbearbetning som gör en dialogruta till en funktionell dialogruta. Nu bör du åsidosätta implementeringen TranslateAccelerator (som kommer från IKeyboardInputSink, ett gränssnitt som HwndHost implementerar). Den här metoden anropas när programmet tar emot WM_KEYDOWN och 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
        }

Detta är mycket kod i ett stycke, så det kan använda några mer detaljerade förklaringar. Först koden med C++ och C++-makron. du måste vara medveten om att det redan finns ett makro med namnet TranslateAccelerator, som definieras i winuser.h:

#define TranslateAccelerator  TranslateAcceleratorW

Så se till att definiera en TranslateAccelerator metod och inte en TranslateAcceleratorW metod.

På samma sätt finns det både den ohanterade winuser.h MSG och den hanterade Microsoft::Win32::MSG structen. Du kan skilja mellan de två med hjälp av C++ :: -operatorn.

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

Båda MSG:erna har samma data, men ibland är det lättare att arbeta med den ohanterade definitionen, så i det här exemplet kan du definiera den uppenbara konverteringsrutinen:

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

Tillbaka till TranslateAccelerator. Den grundläggande principen är att anropa win32-funktionen IsDialogMessage för att utföra så mycket arbete som möjligt, men IsDialogMessage inte har åtkomst till något utanför dialogrutan. När du tabbar förbi den sista kontrollen i vår dialogruta behöver du sätta fokus på WPF-delen genom att anropa 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);
}

Anropa slutligen IsDialogMessage. Men ett av ansvarsområdena för en TranslateAccelerator metod är att berätta för WPF om du hanterade tangenttryckningen eller inte. Om du inte hanterade den kan indatahändelsen tunnla och bubbla genom resten av programmet. Här kommer du att exponera en egenhet av tangentbordshantering av meddelanden och strukturen av indataarkitekturen i Win32. Tyvärr anger IsDialogMessage inte på något sätt om det hanterar en viss tangenttryckning. Ännu värre är att det kommer att kalla DispatchMessage() på tangenttryckningar som det inte ska hantera! Så du måste bakåtkompilera IsDialogMessage och bara anropa IsDialogMessage för de nycklar som du vet att det kommer att hantera.

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

Åsidosätt TabInto-metoden för att stödja tabbning

Nu när du har implementerat TranslateAccelerator kan en användare navigera med tabbtangenten inom dialogrutan och sedan gå vidare till det större WPF-programmet. Men en användare kan inte tabba tillbaka till dialogrutan. För att lösa det, du åsidosätter 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;
    }

Parametern TraversalRequest anger om tabbåtgärden är en flik eller skiftflik.

Åsidosätt OnMnemonic-metod för att stödja Mnemonics

Tangentbordshantering är nästan klar, men det finns en sak saknas - mnemonics fungerar inte. Om en användare trycker på alt-F hoppar fokus inte till redigeringsrutan "Förnamn:". Därför åsidosätter du metoden 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
};

Varför inte ringa IsDialogMessage hit? Du har samma problem som tidigare – du måste kunna informera WPF-koden om koden hanterade tangenttryckningen eller inte och IsDialogMessage inte kan göra det. Det finns ett annat problem, eftersom IsDialogMessage vägrar att bearbeta minneskoden om det fokuserade HWND inte är inuti dialogrutan.

Instansiera HwndHost-härledd klass

Slutligen, nu när allt stöd för nyckel och flik är på plats, kan du lägga till ditt HwndHost i det större WPF-programmet. Om huvudprogrammet är skrivet i XAML är det enklaste sättet att placera det på rätt plats att lämna ett tomt Border element där du vill placera HwndHost. Här skapar du ett Border med namnet 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>

Sedan är allt som återstår att hitta en bra plats i kodsekvensen för att instansiera HwndHost och ansluta den till Border. I det här exemplet placerar du den i konstruktorn för den Window härledda klassen:

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

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

Vilket ger dig:

Skärmbild av WPF-appen som körs.

Se även