다음을 통해 공유


WPF에서 Win32 콘텐츠 호스팅

필수 구성 요소

WPF 및 Win32 상호 운용성을 참조하세요.

Windows Presentation Framework 내 Win32 연습(HwndHost)

WPF 애플리케이션 내에서 Win32 콘텐츠를 다시 사용하려면 HWND를 WPF 콘텐츠처럼 보이게 하는 컨트롤인 HwndHost를 사용합니다. HwndSource와 마찬가지로 HwndHost는 사용하기 간단합니다. HwndHost에서 파생하고 BuildWindowCoreDestroyWindowCore 메서드를 구현한 다음 HwndHost 파생 클래스를 인스턴스화하여 WPF 애플리케이션 내에 배치합니다.

Win32 논리가 이미 컨트롤로 패키징된 경우 BuildWindowCore 구현은 CreateWindow에 대한 호출과 마찬가지입니다. 예를 들어 C++에서 Win32 LISTBOX 컨트롤을 만들려면 다음을 수행합니다.

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
}

하지만 Win32 코드가 자체 포함이 아니라고 가정하면 어떻게 될까요? 이 경우 Win32 대화 상자를 만들고 콘텐츠를 더 큰 WPF 애플리케이션에 포함할 수 있습니다. 샘플은 이를 Visual Studio와 C++로 보여 주지만 다른 언어나 명령줄에서도 이를 수행할 수 있습니다.

C++ DLL 프로젝트로 컴파일되는 간단한 대화 상자로 시작합니다.

다음으로 더 큰 WPF 애플리케이션에 대화 상자를 도입합니다.

  • DLL을 관리형으로 컴파일(/clr)

  • 대화 상자를 컨트롤로 전환

  • BuildWindowCoreDestroyWindowCore 메서드를 사용하여 HwndHost 파생 클래스 정의

  • 대화 상자 키를 처리하도록 TranslateAccelerator 메서드 재정의

  • 탭 이동을 지원하도록 TabInto 메서드 재정의

  • 니모닉을 지원하도록 OnMnemonic 메서드 재정의

  • HwndHost 서브클래스를 인스턴스화하여 오른쪽 WPF 요소 아래 배치

대화 상자를 컨트롤로 전환

WS_CHILD 및 DS_CONTROL 스타일을 사용하여 대화 상자를 자식 HWND로 전환할 수 있습니다. 대화 상자가 정의된 리소스 파일(.rc)로 이동하여 대화 상자 정의가 시작되는 곳을 찾습니다.

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

두 번째 줄을 다음으로 변경합니다.

STYLE DS_SETFONT | WS_CHILD | WS_BORDER | DS_CONTROL

이 작업은 대화 상자를 자체 포함 컨트롤로 완전히 패키징하지 않습니다. Win32가 특정 메시지를 처리할 수 있도록 여전히 IsDialogMessage()를 호출해야 하지만 컨트롤 변경은 해당 컨트롤을 다른 HWND 내에 배치하는 간단한 방법을 제공합니다.

서브클래스 HwndHost

다음 네임스페이스를 가져옵니다.

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;

그런 다음 HwndHost 파생 클래스를 만들고 BuildWindowCoreDestroyWindowCore 메서드를 재정의합니다.

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
        }

여기서는 CreateDialog를 사용하여 실제로 컨트롤인 대화 상자를 만듭니다. 이것이 DLL 내에서 호출되는 첫 번째 메서드 중 하나이므로 나중에 정의할 InitializeGlobals()라는 함수를 호출하여 표준 Win32 초기화도 수행해야 합니다.

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

대화 상자 키를 처리하도록 TranslateAccelerator 메서드 재정의

이 샘플을 지금 실행하면 표시되는 대화 상자 컨트롤을 얻게 되지만 대화 상자를 작동하는 대화 상자로 만드는 모든 키보드 처리가 무시됩니다. 이제 TranslateAccelerator 구현(HwndHost가 구현하는 인스턴스인 IKeyboardInputSink에서 비롯됨)을 재정의해야 합니다. 이 메서드는 애플리케이션이 WM_KEYDOWN 및 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
        }

많은 코드가 하나로 모여 있으므로 더 자세히 설명할 수 있습니다. 먼저 C++ 및 C++ 매크로를 사용하는 코드입니다. winuser.h에 정의된 TranslateAccelerator라는 매크로가 이미 있음을 알아야 합니다.

#define TranslateAccelerator  TranslateAcceleratorW

따라서 TranslateAcceleratorW 메서드가 아니라 TranslateAccelerator 메서드를 정의해야 합니다.

마찬가지로 비관리 winuser.h MSG와 관리 Microsoft::Win32::MSG 구조체가 모두 있습니다. C++ :: 연산자를 사용하여 두 구조체를 명확하게 구분할 수 있습니다.

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

두 MSG 모두 동일한 데이터를 가지고 있지만 비관리 정의를 사용하는 것이 더 쉬운 경우도 있으므로 이 샘플에서는 명백한 변환 루틴을 정의할 수 있습니다.

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

TranslateAccelerator로 돌아갑니다. 기본 원칙은 Win32 함수 IsDialogMessage를 호출하여 가능한 많은 작업을 수행하는 것이지만 IsDialogMessage는 대화 상자 외부 항목에 액세스할 수 없습니다. 사용자가 대화 상자 탭을 이동함에 따라 탭 이동이 대화 상자의 마지막 컨트롤을 지날 때 IKeyboardInputSite::OnNoMoreStops를 호출하여 WPF 부분에 포커스를 설정해야 합니다.

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

마지막으로 IsDialogMessage를 호출합니다. 하지만 TranslateAccelerator 메서드의 책임 중 하나는 사용자가 키 입력을 처리했는지 여부를 WPF에 알리는 것입니다. 처리하지 않은 경우 입력 이벤트는 애플리케이션의 나머지 부분을 터널 및 버블할 수 있습니다. 여기서는 Win32에서 키보드 메시지 처리의 이상한 점과 입력 아키텍처의 특성을 노출합니다. 아쉽게도 IsDialogMessage는 특정 키 입력을 처리하는지 여부를 반환하지 않습니다. 더 나쁜 것은 처리해서는 안 되는 키 입력에서 DispatchMessage()를 호출한다는 것입니다. 따라서 IsDialogMessage를 리버스 엔지니어링하고, 처리할 것을 알고 있는 키에 대해서만 호출해야 합니다.

// 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 메서드 재정의

TranslateAccelerator를 구현했으므로 사용자는 대화 상자 안의 탭을 이동하고 대화 상자 밖의 탭을 이동하여 더 큰 WPF 애플리케이션으로 들어갈 수 있습니다. 그러나 사용자는 탭을 통해 대화 상자로 다시 들어갈 수 없습니다. 이를 해결하려면 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;
    }

TraversalRequest 매개 변수는 탭 동작이 Tab인지 Shift -Tab인지 알려 줍니다.

니모닉을 지원하도록 OnMnemonic 메서드 재정의

키보드 처리는 거의 완료되었지만 한 가지 누락된 것이 있습니다. 니모닉이 작동하지 않습니다. 사용자가 Alt-F를 눌러도 포커스가 "이름:" 편집 상자로 이동하지 않습니다. 따라서 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
};

여기서 IsDialogMessage를 호출하면 어떨까요? 이전과 동일한 문제가 있습니다. 코드가 키 입력을 처리했는지 여부를 WPF 코드에 알릴 수 있어야 하는데, IsDialogMessage는 이를 할 수 없습니다. 포커스가 있는 HWND가 대화 상자 안에 없으면 IsDialogMessage가 니모닉 처리를 거부하기 때문에 두 번째 문제도 있습니다.

HwndHost 파생 클래스 인스턴스화

마지막으로 이제 모든 키 및 탭 지원이 설정되었으므로 HwndHost를 더 큰 WPF 애플리케이션에 넣을 수 있습니다. 주 애플리케이션이 XAML로 작성된 경우 올바른 위치에 배치하는 가장 쉬운 방법은 HwndHost를 배치하려는 곳에 빈 Border 요소를 그대로 두는 것입니다. 여기서는 insertHwndHostHere라는 Border를 만듭니다.

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

그러면 남은 것은 코드 시퀀스에서 HwndHost를 인스턴스화하기에 적합한 위치를 찾아 Border에 연결하는 것뿐입니다. 이 예제에서는 Window 파생 클래스의 생성자 내에 배치합니다.

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

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

그러면 다음이 제공됩니다.

WPF 앱 실행 중 스크린샷.

참고 항목