先决条件
请参阅 WPF 和 Win32 互操作。
Windows 演示文稿框架中的 Win32 演练 (HwndHost)
若要重用 WPF 应用程序中的 Win32 内容,请使用 HwndHost,该控件使 HWND 看起来像 WPF 内容。 同样 HwndSource, HwndHost 使用起来非常简单:派生自 HwndHost 和实现 BuildWindowCore
和 DestroyWindowCore
方法,然后实例化 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 编译为托管 DLL (
/clr
)将对话框转换为控件
定义带有
BuildWindowCore
和DestroyWindowCore
方法的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
此动作不会将其完全打包成一个自包含控件;你仍然需要调用 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
结构体。 可以使用 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
无权访问对话之外的任何内容。 当用户使用 Tab 键在对话框中导航时,如果越过了最后一个控件,就需要通过调用 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 方法以支持 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
参数指示您是执行选项卡操作还是 Shift 选项卡操作。
重写 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 放置在正确位置的最简单方法是在你想放置 HwndHost 的地方留一个空的 Border 元素。 在这里,您创建一个名为Border的insertHwndHostHere
。
<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;
}
}
这将为你带来: