在Win32应用程序中嵌入WPF控件的键盘输入处理

在现代软件开发中,经常需要将不同的技术栈整合在一起,以利用各自的优势。例如,Win32应用程序因其性能和广泛的API支持而广泛使用,而WPF(Windows Presentation Foundation)则因其强大的用户界面和丰富的控件集而受到青睐。然而,将WPF控件嵌入Win32应用程序时,由于这两种技术的差别,可能会出现各种问题。其中一个常见问题就是键盘输入的处理。本文将介绍如何在Win32应用程序中嵌入WPF控件,并处理键盘输入。

Win32中嵌入WPF控件

要在Win32应用程序中嵌入WPF控件,需要遵循以下步骤:

1. 创建一个新的HwndSource对象,将其父窗口设置为Win32窗口。这是关键对象,它使得WPF内容能够在Win32窗口中显示。

2. 实例化WPF控件或窗口。

3. 将WPF控件或窗口的实例赋值给HwndSource对象的RootVisual属性。

为了简化这个过程,创建了一个小助手类:

#pragma once #include class CWpfControlWrapper { HWND m_hwndWPF; gcroot m_source; gcroot m_control; public: CWpfControlWrapper(void) : m_hwndWPF(NULL) {} ~CWpfControlWrapper(void) {} template T^ Control() { System::Windows::Controls::Control^ obj = m_control; return dynamic_cast(obj); } BOOL CreateControl(System::Type^ type, HWND parent, DWORD style, int x, int y, int width, int height) { System::Windows::Interop::HwndSourceParameters^ sourceParams = gcnew System::Windows::Interop::HwndSourceParameters("WpfControlWrapper"); sourceParams->PositionX = x; sourceParams->PositionY = y; sourceParams->Height = height; sourceParams->Width = width; sourceParams->ParentWindow = System::IntPtr(parent); sourceParams->WindowStyle = style; m_source = gcnew System::Windows::Interop::HwndSource(*sourceParams); m_control = (System::Windows::Controls::Control^)System::Activator::CreateInstance(type); m_source->RootVisual = m_control; m_hwndWPF = (HWND)m_source->Handle.ToPointer(); return m_hwndWPF == NULL ? FALSE : TRUE; } };

使用这个类,可以这样创建WPF控件:

CWpfControlWrapper btn1; btn1.CreateControl(System::Windows::Controls::Button::typeid, m_hWnd, WS_CHILD|WS_VISIBLE|WS_TABSTOP, 10, 10, 210, 24); btn1.Control()->Content = "Button 1";

启用键盘输入

虽然可以通过鼠标使用这些嵌入的WPF控件,但键盘输入并未启用。为了为WPF控件提供键盘输入,需要钩住HwndSource,添加一个处理所有窗口消息的处理器。必须处理WM_GETDLGCODE消息,以让系统知道想要自行处理哪些消息(在WPF控件中)。

这是添加钩子的方式:

m_source->AddHook(gcnew System::Windows::Interop::HwndSourceHook(&CWpfControlWrapper::ChildHwndSourceHook));

这是钩子过程的样子(定义为CWpfControlWrapper的静态成员):

static System::IntPtr ChildHwndSourceHook(System::IntPtr hwnd, int msg, System::IntPtr wParam, System::IntPtr lParam, bool% handled) { if (msg == WM_GETDLGCODE) { handled = true; return System::IntPtr(DLGC_WANTCHARS | DLGC_WANTTAB | DLGC_WANTARROWS | DLGC_WANTALLKEYS); } return System::IntPtr::Zero; }

通过返回所有这些对话框代码,系统将知道窗口想要处理箭头键、制表符键、所有键并接收WM_CHAR消息。

启用TAB导航

尽管WPF控件现在有了键盘输入,但使用TAB键(向前)或TAB+SHIFT(向后)导航却不起作用。

这里有一个例子,有一个MFC应用程序,里面有四个WPF控件,两个按钮和两个文本框。一个按钮和一个文本框,以及“确定”和“取消”按钮都有制表符停靠。

CWpfControlWrapper btn1; btn1.CreateControl(System::Windows::Controls::Button::typeid, m_hWnd, WS_CHILD|WS_VISIBLE|WS_TABSTOP, 10, 10, 210, 24); btn1.Control()->Content = "Button 1 (tab stop)";

按下TAB键应该允许从按钮1导航到编辑1,然后是按钮“确定”,按钮“取消”,然后回到按钮1。按钮2和编辑2没有定义制表符停靠样式,因此不应该包含在导航中。

然而,正如已经提到的,这不起作用。在阅读了这个问题的解决方案后,看起来关键在于IKeyboardInputSink接口,HwndSourceHwndHost都实现了这个接口。这个接口为管理制表符、快捷键和助记符的组件提供了键盘输入池,跨越互操作边界和HWND之间。显然,解决方案是:

1. 派生HwndSource

2. 覆盖TabInto方法(实际上,由于这是一个密封的方法,需要为它定义一个新的覆盖)并在那里实现制表符逻辑

3. 使用这个派生的HwndSource在Win32窗口中呈现WPF内容

尽管尝试了几种方法,但没有成功。然而,由于已经为所有窗口消息添加了一个钩子,并明确要求接收WM_CHAR,因此可以使用这个来处理TAB和TAB+SHIFT。所以,这是对上面ChildHwndSourceHook的补充:

else if (msg == WM_CHAR) { if (wParam.ToInt32() == VK_TAB) { handled = true; HWND nextTabStop = FindNextTabStop((HWND)hwnd.ToPointer(), (GetKeyState(VK_SHIFT) & 0x8000) != 0x8000); if (nextTabStop) ::SetFocus(nextTabStop); } }

所以如果得到一个WM_CHAR并且wParamVK_TAB,那么查询父窗口的下一个制表符停靠(如果SHIFT没有被按下,就向前导航,或者如果SHIFT也被按下,就向后导航)。如果有这样一个制表符停靠,就将焦点设置在那个窗口上。

下面是FindNextTabStop方法(添加为CWpfControlWrapper类的成员):

static HWND FindNextTabStop(HWND wnd, bool forward) { HWND nextstop = NULL; HWND nextwnd = wnd; do { // get the next/previous window in the z-order nextwnd = ::GetWindow(nextwnd, forward ? GW_HWNDNEXT : GW_HWNDPREV); // if we are at the end of the z-order, start from the top/bottom if (nextwnd == NULL) nextwnd = ::GetWindow(wnd, forward ? GW_HWNDFIRST : GW_HWNDLAST); // if we returned to the same control then we iterated the entire z-order if (nextwnd == wnd) break; // get the window style and check the WS_TABSTOP style DWORD style = ::GetWindowLongPtr(nextwnd, GWL_STYLE); if ((style & WS_TABSTOP) == WS_TABSTOP) nextstop = nextwnd; } while (nextstop == NULL); return nextstop; }

它执行以下操作:

1. 获取z顺序中的下一个/上一个窗口(这定义了制表符停靠顺序)。

2. 当它到达z顺序的末尾/顶部时,它从头/底部开始,这使得循环遍历父窗口的子窗口成为可能。

3. 如果下一个子窗口在z顺序中是当前控件,那么它完成了遍历父窗口的子窗口,并且它停止。

4. 如果当前子窗口在z顺序中有WS_TABSTOP样式设置,那么这就是要找的窗口。

有了这个定义,就可以使用TAB键在Win32窗口上的WPF控件之间导航了。

这里是一个MFC演示应用程序,可以尝试。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485