برنامج تعليمي إنشاء تطبيق WPF يستضيف محتوى Win32

المتطلبات الأساسية

انظر نظرة عامة حول التشغيل التفاعلي ل Win32 و WPF

شرح تفصيلى لـ Win32 Inside Windows Presentation Framework (HwndHost)

لإعادة استخدام محتوي Win32 داخل تطبيقات WPF، استخدم HwndHost ، وهو عنصر تحكم الذي يجعل HWNDs يبدو مثل محتوى WPF. مثل HwndSource، HwndHost هو مباشر للاستخدام: اشتق من HwndHost ثم قم بتطبيق أساليب BuildWindowCore و DestroyWindowCore، ثم قم بإنشاء فئة المشتقة HwndHost الخاصة بك و ضعه داخل التطبيق WPF الخاص بك.

إذا كان منطق Win32 الخاص بك تم بالفعل حزمه كعنصر تحكم إذاً تطبيق BuildWindowCore الخاص بك أكثر قليلاً من استدعاء ال CreateWindow. على سبيل المثال، لإنشاء عنصر تحكم Win32LISTBOX في ++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
}

ولكن افترض أن التعليمات البرمجية Win32 غير مُضمن تماماً ؟ و لذلك يمكنك إنشاء مربع حوار Win32 و تضمين محتوياته إلى تطبيق WPF أكبر. يبين نموذج هذا في Microsoft Visual Studio و ++C ، على الرغم من أنه من الممكن أيضاً القيام بذلك في لغة مختلفة أو في سطر الأوامر.

أبدأ بحوار بسيط مترجم إلى مشروع ++C DLL.

بعد ذلك قم تقديم مربع حوار إلى تطبيق WPF أكبر:

  • ترجم DLL كما تتم إدارته (/clr)

  • حول الحوار إلى عنصر تحكم

  • قم بتعريف فئة مشتقة من HwndHost مع أساليب BuildWindowCore و DestroyWindowCore

  • تجاوز أسلوب TranslateAccelerator لمعالجة مفاتيح الحوار

  • تجاوز أسلوب TabInto لدعم التبويب

  • تجاوز أسلوب OnMnemonic لدعم التعليمات الرمزية القصيرة

  • أنشأ الفئة الفرعية HwndHost و ضعها ضمن عنصر WPF الصحيح

حول الحوار إلى عنصر تحكم

يمكنك تحويل مربع الحوار إلى تابع HWND باستخدام أنماط WS_CHILD DS_CONTROL. انتقل إلى ملف المورد (.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

هذا الإجراء لا يحزمه بشكل كامل إلى عنصر تحكم مُضمن; لا تزال تحتاج لاستدعاء IsDialogMessage() حتى Win32 يمكنه معالجة رسائل معينة ولكن تغيير عنصر تحكم يوفر طريقة مباشرة لوضع عناصر التحكم هذه داخل 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 و تجاوز أساليب BuildWindowCore و 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
        }

هنا يمكنك استخدام CreateDialog لإنشاء مربع الحوار الذي هو فعلاً عنصر التحكم. بما أن هذه إحدى الطرق الأولية المستدعاة داخل DLL، يجب أن تقوم ببعض التهيئة Win32 القياسية بواسطة استدعاء دالة سيتم تعريفها فيما بعد، تسمى 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);

تجاوز أسلوب TranslateAccelerator لمعالجة مفاتيح الحوار

إذا قمت بتشغيل هذه العينة الآن، ستحصل على عنصر تحكم حوار الذي يعرض ولكن يتجاهل كافة معالجة المفاتيح التي تجعل مربع حوار مربع حوار وظيفي. يجب الآن تجاوز تطبيق TranslateAccelerator (الذي يأتي من IKeyboardInputSink ، الواجهة التي ينفذها HwndHost). هذا الأسلوب يتم استدعائه عندما يتلقى فيها التطبيق 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; ستحتاج أن تدرك أنه يوجد ماكرو باسم TranslateAccelerator ، الذي يتم تعريفه في winuser.h:

#define TranslateAccelerator  TranslateAcceleratorW

لذا تأكد من تحديد أسلوب TranslateAccelerator و ليس أسلوب TranslateAcceleratorW.

وبشكل مماثل، هناك winuser.h غير المدار MSG و بنية Microsoft::Win32::MSG المدار. يمكن disambiguate بينهما باستخدام عامل التشغيل ++C ::.

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

كلاً من MSGs يحتوا على نفس البيانات و لكن في بعض الأحيان يكون من السهل العمل مع التعريف غير مدار لذلك يمكنك تعريف إجراء التحويل الواضح في هذا النموذج:

::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 ليس لديه حق الوصول إلى أي شيء خارج مربع الحوار. أثناء تشغيل تبويب المستخدم حول مربع الحوار عندما يمر التبويب بعد آخر عنصر تحكم في مربع الحوار الخاص بنا, ستحتاج إلى تعيين تركيز إلى جزء WPF عن طريق استدعاء 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);
}

أخيراً، قم باستدعاء IsDialogMessage. و لكن إحدى مسؤوليات أسلوب TranslateAccelerator تسرد WPF ما إذا قمت بمعالجة ضغط المفاتيح أو لا. إذا كنت لم تعالج ذلك, يمكن لحدث الإدخال النفق و الفقاعية خلال باقي التطبيق. هنا، سوف يعرض quirk من معالجة messange لوحة المفاتيح و طبيعة بنية الإدخال في 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 لدعم Tabbing

الآن و قد طبقت 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 تخبرك ما إذا كان أداء التبويب هو تبويب أو تبويب مُحرَّك.

تجاوز أسلوب 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 لا يمكنه القيام بذلك. هناك أيضاً مشكلة ثانية لأن IsDialogMessage يرفض معالجة التعليمات الرمزية القصيرة إذا لم يكن HWND المركز داخل مربع الحوار.

إنشاء فئة HwndHost المشتقة

وأخيراً، الآن حيث دعم المفتاح و التبويب في مكانه، يمكنك وضع HwndHost الخاص بك إلى WPF أكبر. إذا تمت كتابة التطبيق الرئيسي في XAML ، أسهل طريقة لوضعه في المكان الصحيح هو بترك عنصر Border فارغ حيث تريد وضع HwndHost. هنا يمكنك إنشاء Border باسم 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>

يكون كل ما تبقى هو العثور على مكان جيد في تسلسل التعليمات البرمجية لإنشاء 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

راجع أيضًا:

المبادئ

نظرة عامة حول التشغيل التفاعلي ل Win32 و WPF