先決條件
請參閱 WPF 和 Win32 交互操作。
Windows 簡報架構(HwndHost)內的 Win32 逐步解說
若要重複使用 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 編譯為 Managed (
/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 方法。
同樣地,還有 Unmanaged winuser.h MSG 和 managed Microsoft::Win32::MSG 結構。 您可以使用 C++ :: 運算符來釐清這兩者。
virtual bool TranslateAccelerator(System::Windows::Interop::MSG% msg,
ModifierKeys modifiers) override
{
::MSG m = ConvertMessage(msg);
}
這兩個 MSG 都有相同的資料,但有時候使用 Unmanaged 定義會比較容易,因此在此範例中,您可以定義明顯的轉換例程:
::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 無法存取對話方塊以外的任何東西。 在對話框周圍的使用者索引標籤中,當 Tabbing 超過對話中的最後一個控制項時,您必須呼叫 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 參數會告訴您索引標籤動作是索引標籤或移位索引標籤。
覆寫 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 撰寫的,將它放在正確位置的最簡單方式是留出空白 Border元素用來放置 HwndHost。 在這裡,您會建立名為 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 應用程式的螢幕截圖。