Partilhar via


Hospedando conteúdo do Win32 no WPF

Pré-requisitos

Consulte Interoperação Win32 e WPF.

Um passo a passo do Win32 no Windows Presentation Framework (HwndHost)

Para reutilizar o conteúdo do Win32 dentro de aplicativos WPF, use HwndHost, que é um controle que faz com que os HWNDs se pareçam com o conteúdo do WPF. Como HwndSource, é simples de usar: derive de e implemente BuildWindowCore e DestroyWindowCore métodos, em seguida, HwndHost instancie sua HwndHost classe derivada HwndHost e coloque-a dentro de seu aplicativo WPF.

Se sua lógica Win32 já está empacotada como um controle, sua BuildWindowCore implementação é pouco mais do que uma chamada para CreateWindow. Por exemplo, para criar um controle Win32 LISTBOX em 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
}

Mas suponha que o código Win32 não é tão auto-contido? Nesse caso, você pode criar uma caixa de diálogo Win32 e incorporar seu conteúdo em um aplicativo WPF maior. O exemplo mostra isso no Visual Studio e C++, embora também seja possível fazer isso em uma linguagem diferente ou na linha de comando.

Comece com uma caixa de diálogo simples, que é compilada em um projeto DLL C++.

Em seguida, introduza a caixa de diálogo no aplicativo WPF maior:

  • Compilar a DLL como gerenciado (/clr)

  • Transforme a caixa de diálogo em um controle

  • Definir a classe derivada de HwndHost com BuildWindowCore e DestroyWindowCore métodos

  • Substitua o método TranslateAccelerator para manipular as teclas do diálogo

  • Substitua o método TabInto para dar suporte à tabulação

  • Substitua o método OnMnemonic para dar suporte aos mnemônicos

  • Instancie a subclasse e coloque-a HwndHost sob o elemento WPF correto

Transforme a Caixa de Diálogo em um Controle

Você pode transformar uma caixa de diálogo em um HWND filho usando os estilos WS_CHILD e DS_CONTROL. Vá para o arquivo de recurso (.rc) no qual a caixa de diálogo está definida e localize o início da definição da caixa de diálogo:

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

Altere a segunda linha para:

STYLE DS_SETFONT | WS_CHILD | WS_BORDER | DS_CONTROL

Essa ação não a empacota totalmente em um controle autônomo; você ainda precisa chamar IsDialogMessage() para que o Win32 possa processar determinadas mensagens, mas a alteração de controle fornece uma maneira direta de colocar esses controles dentro de outro HWND.

Subclasse HwndHost

Importe os seguintes namespaces:

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;

Em seguida, crie uma classe derivada de HwndHost e substitua os BuildWindowCore métodos 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
        }

Aqui você vai usar o CreateDialog para criar a caixa de diálogo que é, na realidade, um controle. Como este é um dos primeiros métodos chamados dentro da DLL, você também deve fazer alguma inicialização padrão do Win32 chamando uma função que você definirá mais tarde, chamada 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);

Substitua o método TranslateAccelerator para manipular as teclas do diálogo

Se executar este exemplo agora, você obterá um controle de caixa de diálogo que exibe, mas ele ignoraria todo o processamento do teclado que torna uma caixa de diálogo funcional. Agora você deve substituir a TranslateAccelerator implementação (que vem de IKeyboardInputSink, uma interface que HwndHost implementa). Esse método é chamado quando o aplicativo recebe 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
        }

É muito código de uma vez, então as explicações poderiam ser um pouco mais detalhadas. Primeiro, o código usando macros C++ e C++; Você precisa estar ciente de que já existe uma macro chamada TranslateAccelerator, que é definida em Winuser.h:

#define TranslateAccelerator  TranslateAcceleratorW

Certifique-se de definir um método TranslateAccelerator e não um método TranslateAcceleratorW.

Da mesma forma, há o winuser.h MSG não gerenciado e o struct Microsoft::Win32::MSG gerenciado. Você pode desambiguar entre os dois usando o operador C++ :: .

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

Os MSGs têm os mesmos dados, mas, às vezes, é mais fácil trabalhar com a definição não gerenciada, portanto, neste exemplo, você poderá definir a rotina de conversão óbvia:

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

Voltar para TranslateAccelerator. O princípio básico é chamar a função IsDialogMessage Win32 para fazer o máximo de trabalho possível, mas IsDialogMessage não tem acesso a nada fora da caixa de diálogo. Como uma guia de usuário ao redor da caixa de diálogo, quando a tabulação é executada após o último controle em nossa caixa de diálogo, você precisa definir o foco para a parte WPF chamando 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);
}

Finalmente, chame IsDialogMessage. Mas uma das responsabilidades de um TranslateAccelerator método é dizer ao WPF se você manipulou o pressionamento de tecla ou não. Se você não lidou com isso, o evento de entrada poderá criar um túnel e uma bolha pelo resto do aplicativo. Aqui, você vai expor uma peculiaridade do manuseio de messange do teclado e a natureza da arquitetura de entrada no Win32. Infelizmente, IsDialogMessage não é retornado de nenhuma forma independentemente de lidar ou não com um determinado pressionamento de tecla. Ainda pior, ele chamará DispatchMessage() nos pressionamentos de teclas que ele não deveria manipular! Assim você terá que fazer engenharia reversa de IsDialogMessage e apenas chamá-lo para as teclas que você sabe que ele manipulará:

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

Substituir o método TabInto para dar suporte à tabulação

Agora que você implementou TranslateAcceleratoro , um usuário pode acessar a guia dentro da caixa de diálogo e sair dela para o aplicativo WPF maior. Mas um usuário não pode tabular novamente na caixa de diálogo. Para resolver isso, você deve substituir 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;
    }

O parâmetro TraversalRequest informa se a ação da guia é uma guia ou guia de deslocamento.

Substitua o método OnMnemonic para dar suporte aos mnemônicos

A manipulação de teclado está quase concluída, mas há uma coisa faltando: os mnemônicos não funcionam. Se um usuário pressionar alt-F, o foco não pulará para a caixa de edição “Nome:”. Portanto, você substitui o 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 que não chamar IsDialogMessage aqui? Você tem o mesmo problema de antes - você precisa ser capaz de informar ao código WPF se seu código manipulou o pressionamento de tecla ou não, e IsDialogMessage não pode fazer isso. Há também um segundo problema, pois IsDialogMessage se recusará a processar o mnemônico se o HWND com o foco não estiver dentro da caixa de diálogo.

Instanciar a classe derivada de HwndHost

Finalmente, agora que todo o suporte a chaves e guias está em vigor, você pode colocá-lo HwndHost no aplicativo WPF maior. Se o aplicativo principal for escrito em XAML, a maneira mais fácil de colocá-lo no lugar certo é deixar um elemento vazio Border onde você deseja colocar o HwndHost. Aqui você cria um Border nome 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>

Então tudo o que resta é encontrar um bom lugar na sequência de código para instanciar o HwndHostBordere conectá-lo ao . Neste exemplo, você o colocará dentro do construtor para a Window classe derivada:

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

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

O que fornece:

Screenshot of the WPF app running.

Confira também