在现代软件开发中,经常需要将不同的技术栈整合在一起,以利用各自的优势。例如,Win32应用程序因其性能和广泛的API支持而广泛使用,而WPF(Windows Presentation Foundation)则因其强大的用户界面和丰富的控件集而受到青睐。然而,将WPF控件嵌入Win32应用程序时,由于这两种技术的差别,可能会出现各种问题。其中一个常见问题就是键盘输入的处理。本文将介绍如何在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
消息。
尽管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
接口,HwndSource
和HwndHost
都实现了这个接口。这个接口为管理制表符、快捷键和助记符的组件提供了键盘输入池,跨越互操作边界和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
并且wParam
是VK_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演示应用程序,可以尝试。