Compartilhar via


Tutorial: Criar um aplicativo WPF hospedando o conteúdo do Win32

Pré-requisitos

Consulte Visão geral sobre interoperabilidade entre WPF e Win32.

Um guia passo a passo de Win32 dentro do Windows Presentation Framework (HwndHost)

Para reutilizar conteúdo Win32 em aplicativos WPF use HwndHost, que é um controle que faz com que HWNDs pareçam conteúdo WPF. Como HwndSource, HwndHost é simples de usar: derivar de HwndHost e implementar BuildWindowCore e DestroyWindowCore métodos, em seguida, instanciar seu HwndHost classe derivada e colocá-lo dentro de seu WPF aplicativo.

Se sua lógica Win32 já está empacotada como um controle, então sua implementação de BuildWindowCore é pouco mais que uma chamada a CreateWindow. Por exemplo, para criar um controle ListBox Win32 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 e supondo que o código Win32 não é tão autocontido? 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 Microsoft Visual Studio e em C++, embora também seja possível fazer isso em uma linguagem diferente ou na linha de comando.

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

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

  • Compile a DLL como gerenciada (/clr)

  • Transforme a caixa de diálogo em um controle

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

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

  • Sobrescreva o método TabInto para dar suporte ao uso da tecla TAB

  • Sobrescreva o método OnMnemonic para dar suporte a mnemônicos

  • Crie uma instância da subclasse de HwndHost e coloque-a sob o elemento WPF certo

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 recursos (.rc) onde 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

Esta ação não vai empacotá-lo totalmente em um controle autocontido; você ainda precisará chamar IsDialogMessage() para que o Win32 possa processar determinadas mensagens, mas a alteração do controle fornece uma maneira simples de colocar estes controles dentro de outro HWND.

Subclasse HwndHost

Importe os seguintes espaços de nomes:

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 um classe derivada de HwndHost e sobrescreva os métodos BuildWindowCore 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 uma inicialização padrão Win32 chamando uma função que você irá definir posteriormente, 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);

Se você executar este exemplo agora, você obterá um controle de diálogo que é exibido, mas ele iria ignorar todo o processamento de teclado que torna uma caixa de diálogo uma caixa de diálogo funcional. Agora você deve sobrescrever a implementação de TranslateAccelerator (que vem da IKeyboardInputSink, uma interface que o HwndHost implementa). Este 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
        }

Isso é muito de código de uma vez, então poderiamos usar algumas explicações mais detalhadas. Primeiro, no código que usa C++ e macros C++; você precisa estar ciente de que já existe uma macro denominada TranslateAccelerator, que é definida em winuser.h:

#define TranslateAccelerator  TranslateAcceleratorW

Portanto, certifique-se de definir um método TranslateAccelerator e não um método TranslateAcceleratorW.

Da mesma forma, há tanto a MSG não gerenciada de winuser.h quanto a estrutura Microsoft::Win32::MSG gerenciada. Você pode resolver essa ambiguidade entre as duas usando o operador :: de C++.

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

As duas MSGs têm os mesmos dados, mas às vezes é mais fácil trabalhar com a definição não gerenciada, então neste exemplo você pode 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;
}

De volta ao TranslateAccelerator. O princípio básico é chamar o Win32 função IsDialogMessage Para fazer tanto trabalho quanto possível, mas IsDialogMessage não tem acesso a qualquer coisa fora a caixa de diálogo. sistema autônomo uma guia do usuário ao redor da caixa de diálogo, quando efetuando a tabulação é executado após o último controle na nossa caixa de diálogo, você precisa conjunto o foco para o WPF parte 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 do método TranslateAccelerator é informar o WPF se você tratou os pressionamentos de teclas ou não. Se você não tratá-los, o evento de entrada pode ser tunelado ou propagado como bolha pelo resto do aplicativo. Assim você vai expor um capricho do tratamento de mensagens de teclado e da natureza da arquitetura de entrada no Win32. Infelizmente, IsDialogMessage não retorna de nenhuma forma se lida ou não com uma determinada tecla. Ainda pior, ele chamará DispatchMessage() para pressionamentos de teclas que ele não deveria tratar! Portanto, você terá que fazer engenharia reversa do IsDialogMessage, e apenas chamá-lo para as teclas que você sabe que vai tratar:

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

Sobrescrever o método TabInto para dar suporte ao uso da tecla TAB

Agora que você implementou TranslateAccelerator, um usuário pode usar TAB dentro da caixa de diálogo e sair dela para o aplicativo WPF maior. Mas um usuário não pode voltar para a caixa de diálogo usando TAB. Para resolver isso, você sobrescrever 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 de "tab" é uma tecla TAB ou SHIFT TAB.

Sobrescreva o método OnMnemonic para dar suporte a mnemônicos

O tratamento do teclado está quase concluído, mas há uma coisa faltando – mnemônicos não funcionam. Se um usuário pressiona alt-F, foco doe não vai para a “ nome: ” caixa de edição. Portanto, você sobrescreve 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 que antes -- precisa poder informar ao código WPF se o seu código tratou o pressionamento de teclas ou não, e IsDialogMessage não pode fazer isso. Há também um segundo problema, pois IsDialogMessage se recusa a processar o mnemônico se o HWND com o foco não estiver dentro da caixa de diálogo.

Crie uma instância da classe derivada de HwndHost

Finalmente, agora que todo o suporte a teclas e TAB está pronto, você pode colocar seu HwndHost no aplicativo WPF maior. Se o aplicativo principal é escrito em XAML, a maneira mais fácil de colocá-lo no local correto é deixar um elemento Border vazio onde você deseja colocar o HwndHost. Aqui você cria uma Border chamada insertHwndHostHere:

<Window x:Class="WPFApplication1.Window1"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://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>

Em seguida, tudo o que falta é localizar um bom lugar na sequência de código para criar uma instância do HwndHost e conectá-lo à Border. Nesse exemplo, você irá colocá-lo dentro do construtor para a classe derivada de Window:

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

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

Que lhe dá:

Captura de tela de aplicativo HTML

Consulte também

Conceitos

Visão geral sobre interoperabilidade entre WPF e Win32