Hospedar contenido de Win32 en WPF

Requisitos previos

Consulte Interoperabilidad de WPF y Win32.

Tutorial de Win32 dentro de Windows Presentation Framework (HwndHost)

Para reutilizar el contenido de Win32 dentro de las aplicaciones WPF, utilice HwndHost, que es un control que hace que los HWND parezcan contenido WPF. Al igual que HwndSource, HwndHost es fácil de usar: derive de HwndHost e implemente los métodos BuildWindowCore y DestroyWindowCore, luego cree una instancia de su clase derivada HwndHost y colóquela dentro de su aplicación WPF.

Si su lógica Win32 ya está empaquetada como un control, entonces su implementación BuildWindowCore es poco más que una llamada a CreateWindow. Por ejemplo, para crear un control LISTBOX de Win32 en 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
}

Pero supongamos que el código Win32 no es tan independiente Si es así, puede crear un cuadro de diálogo Win32 e insertar su contenido en una aplicación WPF más grande. El ejemplo muestra esto en Visual Studio y C++, aunque también es posible hacerlo en un lenguaje diferente o en la línea de comandos.

Comience con un cuadro de diálogo simple, que se compila en un proyecto DLL de C++.

A continuación, introduzca el cuadro de diálogo en la aplicación WPF más grande:

  • Compile el archivo DLL como administrado (/clr)

  • Convierta el cuadro de diálogo en un control

  • Defina la clase derivada de HwndHost con los métodos BuildWindowCore y DestroyWindowCore

  • Reemplace el método TranslateAccelerator para controlar las teclas de diálogo

  • Reemplace el método TabInto para admitir la tabulación

  • Reemplace el método OnMnemonic para admitir las teclas de acceso

  • Cree una instancia de la subclase HwndHost y colóquela bajo el elemento WPF adecuado

Convierta el cuadro de diálogo en un control

Puede convertir un cuadro de diálogo en un HWND secundario mediante los estilos WS_CHILD y DS_CONTROL. Vaya al archivo de recursos (.rc) donde se define el cuadro de diálogo y busque el principio de la definición del diálogo:

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

Cambie la segunda línea a:

STYLE DS_SETFONT | WS_CHILD | WS_BORDER | DS_CONTROL

Esta acción no la empaqueta completamente en un control independiente; todavía necesita llamar a IsDialogMessage() para que Win32 pueda procesar ciertos mensajes, pero el cambio de control proporciona una forma directa de poner esos controles dentro de otro HWND.

Subclase HwndHost

Importe los siguientes espacios de nombres:

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;

A continuación, cree una clase derivada de HwndHost y anule los métodos BuildWindowCore y 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
        }

Aquí se usa el CreateDialog para crear el cuadro de diálogo que es realmente un control. Dado que se trata de uno de los primeros métodos llamados dentro del archivo DLL, también debe realizar una inicialización estándar de Win32 mediante una llamada a una función que definirá más adelante, denominada 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);

Reemplace el método TranslateAccelerator para controlar las claves de cuadro de diálogo

Si ejecutó este ejemplo ahora, obtendría un control de diálogo que se muestra, pero omitiría todo el procesamiento del teclado que convierte un cuadro de diálogo en un cuadro de diálogo funcional. Ahora debería reemplazar la implementación de TranslateAccelerator (que proviene de IKeyboardInputSink, una interfaz que implementa HwndHost). Se llama a este método cuando la aplicación recibe WM_KEYDOWN y 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
        }

Este es un montón de código en una pieza, por lo que podría usar algunas explicaciones más detalladas. En primer lugar, el código con macros de C++ y C++; Debe tener en cuenta que ya hay una macro denominada TranslateAccelerator, que se define en winuser.h:

#define TranslateAccelerator  TranslateAcceleratorW

Por lo tanto, asegúrese de definir un método TranslateAccelerator y no un método TranslateAcceleratorW.

Del mismo modo, existe el MSG no gestionado de winuser.h y la estructura gestionada Microsoft::Win32::MSG. Puede eliminar la ambigüedad entre los dos mediante el operador :: de C++.

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

Ambos MSG tienen los mismos datos, pero a veces es más fácil trabajar con la definición no administrada, por lo que en este ejemplo puede definir la rutina de conversión obvia:

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

Volver a TranslateAccelerator. El principio básico es llamar a la función IsDialogMessage de Win32 para que haga todo el trabajo posible, pero IsDialogMessage no tiene acceso a nada fuera del diálogo. Cuando el usuario se desplaza por el cuadro de diálogo, al pasar por el último control de nuestro cuadro de diálogo, debe establecer el foco en la parte de WPF llamando a 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);
}

Por último, llame a IsDialogMessage. Pero una de las responsabilidades de un método TranslateAccelerator es indicarle a WPF si controló la pulsación de tecla o no. Si no lo controló, el evento de entrada puede tunelizar y propagar el resto de la aplicación. Aquí, se expondrá una peculiaridad del control de los mensajes del teclado y la naturaleza de la arquitectura de entrada en Win32. Desafortunadamente, IsDialogMessage no devuelve de ninguna manera si controla una pulsación de tecla en particular. Peor aún, ¡llamará a DispatchMessage() en las pulsaciones de teclas que no debería controlar! Por lo tanto, tendrá que realizar ingeniería inversa a IsDialogMessage, y solo llamarlo para las claves que sabe que controlará:

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

Reemplace el método TabInto para admitir la tabulación

Ahora que ha implementado TranslateAccelerator, un usuario puede desplazarse por el cuadro de diálogo y salir de él a la aplicación WPF mayor. Pero un usuario no puede volver al cuadro de diálogo. Para resolverlo, reemplace 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;
    }

El parámetro TraversalRequest le indica si la acción de tabulación es un tabulador o un tabulador de desplazamiento.

Reemplace el método OnMnemonic para admitir las teclas de acceso

El control de teclado está casi completo, pero falta una cosa: las treclas de acceso no funcionan. Si un usuario pulsa alt-F, el foco no salta al cuadro de edición "Nombre:". Por lo tanto, reemplace el método 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
};

¿Por qué no llamar a IsDialogMessage aquí? Tiene el mismo problema que antes: necesita poder informar al código de WPF si su código controló la pulsación de la tecla o no, y IsDialogMessage no puede hacerlo. También hay un segundo problema, porque IsDialogMessage se niega a procesar la tecla de acceso si el HWND enfocado no está dentro del cuadro de diálogo.

Cree una instancia de la clase derivada HwndHost

Por último, ahora que se ha implementado toda la compatibilidad con teclas y pestañas, puede colocar HwndHost en la aplicación WPF más grande. Si la aplicación principal está escrita en XAML, la forma más fácil de colocarla en el lugar correcto es dejar un elemento Border vacío donde se quiere poner HwndHost. Aquí se crea una Border llamada 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>

Entonces todo lo que queda es encontrar un buen lugar en la secuencia de código para crear una instancia de HwndHost y conectarla a Border. En este ejemplo, la colocará dentro del constructor de la clase derivada Window:

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

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

Lo que le da:

Captura de pantalla de la aplicación WPF en ejecución.

Vea también