在 WPF 應用程式或 Windows Forms 應用程式中攔截視窗程序(WndProc)並不難,可是如果要在 WPF 控制項中攔截 Windows Form 的視窗程序,就得動點手腳了。這篇筆記整理幾種攔截視窗訊息的方法和範例。
透過 HwndSource 攔截視窗程序
要在 WPF 應用程式中攔截視窗程序,基本上有兩個步驟:
如此一來,每當 hwndSource 物件所關聯的視窗有任何動靜,作業系統就會發送視窗訊息到我們的 WndProc 函式了。
剛才的範例並沒有提供 GetHwndSource() 的實作,是因為取得視窗所關聯的 HwndSource 物件的方法不只一個,各種方法的適用場合也不大一樣。接著就來看幾個取得 HwndSource 物件的範例。
在 WPF 應用程式中取得 HwndSource
適用場合:當你想要在 WPF 應用程式中攔截父視窗的 WndProc,都可以使用此方法。
最後一行的 HwndSource.FromHwnd() 可以從視窗 handle 取得該視窗所關聯的 HwndSource 物件。
取得 HwndSource 物件之後,就可以攔截視窗程序了。這個部分的寫法前面已經看過,就不再重複。
此外,如果是在 WPF 視窗裡面攔截自己的視窗程序,還有另一個方法,也是使用 WindowInteropHelper 類別來取得視窗 handle。這個部分可參考 Level Up 的文章<WPF程式接收視窗訊息>。
Windowns Forms + WPF 控制項
場景:在 Windows Forms 應用程式中,要在某個 Form 上面嵌入你的 WPF 控制項,而且需要在這個 WPF 控制項中攔截父視窗(Form)的視窗程序。
碰到這種情況,可以先用 PresentationSource 的 FromVisual() 方法來取得 HwndSource 物件:
上例會取得 textBox1 控制項所屬的父層視覺元件的 HwndSource 物件。
此方法用在純 WPF 應用程式,其效果與前面提過的 Window.GetWindow(textBox1) 雷同。但如果用在 Windows Forms 內嵌 WPF 控制項的場合就不一樣了,因為 PresentationSource 的 FromVisual() 方法只會取到 WPF 控制項的父層 UI 元素的 HwndSource 物件,而無法取得外層那個 WinForm 的 HwndSource。
舉例來說,當你在 WinForm 上面嵌入 WPF 控制項時,通常得先在 Form 上面放一個 ElementHost,然後將你的 WPF 使用者控制項放在這個 ElementHost 裡面。例如:
這裡用巢狀縮排的方式來表示控制項的父子階層關係。當你在 myWpfUserControl 類別中使用 PresentationSource.FromVisual(textBox1) 時,傳回的會是 elementHost1 所關聯的那個 HwndSource 物件,而不是最外層的 form1。也就是說,這種寫法並沒有辦法讓你在 WPF 控制項中取得外層的 Form 所關聯的 HwndSource 物件。
那麼,我們可以用先前範例中的 HwndSource.FromHwnd() 來取得 form1 所關聯的 HwndSource 物件嗎?反正 Form 有 Handle 屬性可取得視窗 handle,只要將這個 handle 丟給 HwndSource.FromHwnd() 方法,不就解決了嗎?
可惜,這樣還是行不通。就算我們的 WPF 控制項能夠得到外層(hosted)WinForm 的視窗 handle,HwndSource.FromHwnd() 也無濟於事--它只會傳回 null。因為....
只有 WPF 視窗才會有關聯一個 HwndSource 物件;Windows Forms 的 Form 物件不會有 HwndSource 物件。
這也是我在撰寫 WPF 控制項的時候碰到的麻煩--控制項在 WPF 視窗上運作一切正常,可是一旦將它用在 Windows Forms 專案中,就會出現一堆問題(因為透過此方法取得的父視窗 handle 永遠是 null)。
NativeWindow 來幫忙
現在我們知道,在 Windows Forms 應用程式中,把 Form 的 Handle 餵給 HwndSource.FromHwnd() 是沒用的。
還好,只要能取得視窗 handle,便可以利用 Windows Forms 提供的 NativeWindow 類別來攔截視窗程序。
我寫了一個簡單的工具類別來將指定的 Form 物件的視窗程序銜接到另一個物件的 WndProc 方法,以便我在任何 WPF 類別裡面都可以攔截特定 Form 的視窗程序。姑且將它命名為 WndProcBridge,程式碼如下:
WndProcBridge 類別的建構子需要傳入兩個參數:作為父視窗的 Form 物件,以及一個實作了 IWndProcHandler 介面的物件。也就是說,任何類別只要實作了 IWndProcHandler,就可以「監聽」指定 Form 物件的視窗程序。該介面很單純,只有一個方法:
OK,萬事俱備。以後要在 WPF 控制項中攔截 Windows Form 的視窗程序,便可以這樣寫:
其中 HookWindowProc 函式是利用 ElementHost 提供的 FindForm() 方法來取得 WPF 控制項所屬的父層 Form 物件,然後透過 WndProcBridge 將 Form 的視窗程序銜接到此控制項自己的 WndProc() 方法。
小結
最後簡單整理一下,攔截視窗程序的幾種狀況和解法:
透過 HwndSource 攔截視窗程序
要在 WPF 應用程式中攔截視窗程序,基本上有兩個步驟:
- 取得視窗所關聯的 HwndSource 物件。
- 呼叫剛才取得的 HwndSource 物件的 AddHook() 方法來攔截視窗程序。像這樣:
HWndSource hwndSource = GetHwndSource(); // 稍後會說明如何取得 HwndSource hwndSource.AddHook(WndProc); .... private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { handled = false; switch (msg) { case WM_NCLBUTTONDOWN: // Do something. break; } return IntPtr.Zero; }
如此一來,每當 hwndSource 物件所關聯的視窗有任何動靜,作業系統就會發送視窗訊息到我們的 WndProc 函式了。
剛才的範例並沒有提供 GetHwndSource() 的實作,是因為取得視窗所關聯的 HwndSource 物件的方法不只一個,各種方法的適用場合也不大一樣。接著就來看幾個取得 HwndSource 物件的範例。
在 WPF 應用程式中取得 HwndSource
var parentWindow = Window.GetWindow(textBox1) // 取得 texBox1 的父視窗 var wih = new WindowInteropHelper(parentWindow); HwndSource hwndSource = HwndSource.FromHwnd(wih.Handle);
適用場合:當你想要在 WPF 應用程式中攔截父視窗的 WndProc,都可以使用此方法。
最後一行的 HwndSource.FromHwnd() 可以從視窗 handle 取得該視窗所關聯的 HwndSource 物件。
取得 HwndSource 物件之後,就可以攔截視窗程序了。這個部分的寫法前面已經看過,就不再重複。
Windowns Forms + WPF 控制項
場景:在 Windows Forms 應用程式中,要在某個 Form 上面嵌入你的 WPF 控制項,而且需要在這個 WPF 控制項中攔截父視窗(Form)的視窗程序。
HwndSource hwndSource = PresentationSource.FromVisual(textBox1) as HwndSource;
上例會取得 textBox1 控制項所屬的父層視覺元件的 HwndSource 物件。
此方法用在純 WPF 應用程式,其效果與前面提過的 Window.GetWindow(textBox1) 雷同。但如果用在 Windows Forms 內嵌 WPF 控制項的場合就不一樣了,因為 PresentationSource 的 FromVisual() 方法只會取到 WPF 控制項的父層 UI 元素的 HwndSource 物件,而無法取得外層那個 WinForm 的 HwndSource。
舉例來說,當你在 WinForm 上面嵌入 WPF 控制項時,通常得先在 Form 上面放一個 ElementHost,然後將你的 WPF 使用者控制項放在這個 ElementHost 裡面。例如:
form1: System.Windows.Forms.Form elementHost1: System.Windows.Forms.Integration myWpfUserControl: inherited from System.Windows.Controls.UserControl textBox1: System.Windows.Controls.TextBox textBox2: System.Windows.Controls.TextBox
這裡用巢狀縮排的方式來表示控制項的父子階層關係。當你在 myWpfUserControl 類別中使用 PresentationSource.FromVisual(textBox1) 時,傳回的會是 elementHost1 所關聯的那個 HwndSource 物件,而不是最外層的 form1。也就是說,這種寫法並沒有辦法讓你在 WPF 控制項中取得外層的 Form 所關聯的 HwndSource 物件。
那麼,我們可以用先前範例中的 HwndSource.FromHwnd() 來取得 form1 所關聯的 HwndSource 物件嗎?反正 Form 有 Handle 屬性可取得視窗 handle,只要將這個 handle 丟給 HwndSource.FromHwnd() 方法,不就解決了嗎?
可惜,這樣還是行不通。就算我們的 WPF 控制項能夠得到外層(hosted)WinForm 的視窗 handle,HwndSource.FromHwnd() 也無濟於事--它只會傳回 null。因為....
只有 WPF 視窗才會有關聯一個 HwndSource 物件;Windows Forms 的 Form 物件不會有 HwndSource 物件。
這也是我在撰寫 WPF 控制項的時候碰到的麻煩--控制項在 WPF 視窗上運作一切正常,可是一旦將它用在 Windows Forms 專案中,就會出現一堆問題(因為透過此方法取得的父視窗 handle 永遠是 null)。
NativeWindow 來幫忙
現在我們知道,在 Windows Forms 應用程式中,把 Form 的 Handle 餵給 HwndSource.FromHwnd() 是沒用的。
還好,只要能取得視窗 handle,便可以利用 Windows Forms 提供的 NativeWindow 類別來攔截視窗程序。
我寫了一個簡單的工具類別來將指定的 Form 物件的視窗程序銜接到另一個物件的 WndProc 方法,以便我在任何 WPF 類別裡面都可以攔截特定 Form 的視窗程序。姑且將它命名為 WndProcBridge,程式碼如下:
public class WndProcBridge : System.Windows.Forms.NativeWindow { private System.Windows.Forms.Form _parent; private IWndProcHandler _wndProcHandler; public WndProcBridge(System.Windows.Forms.Form parent, IWndProcHandler wndProcHandler) { AssignHandle(parent.Handle); // Intercept parent form's WndProc to my own WndProc method parent.HandleDestroyed += parent_HandleDestroyed; _parent = parent; _wndProcHandler = wndProcHandler; } private void parent_HandleDestroyed(object sender, EventArgs e) { ReleaseHandle(); } [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, protected override void WndProc(ref System.Windows.Forms.Message m) { bool handled = false; IntPtr result = _wndProcHandler.WndProc(m.HWnd, m.Msg, m.WParam, m.LParam, ref handled); base.WndProc(ref m); } public static void Link(System.Windows.Forms.Form parent, IWndProcHandler wndProcHandler) { new WndProcBridge(parent, wndProcHandler); } }
WndProcBridge 類別的建構子需要傳入兩個參數:作為父視窗的 Form 物件,以及一個實作了 IWndProcHandler 介面的物件。也就是說,任何類別只要實作了 IWndProcHandler,就可以「監聽」指定 Form 物件的視窗程序。該介面很單純,只有一個方法:
public interface IWndProcHandler { IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled); }
OK,萬事俱備。以後要在 WPF 控制項中攔截 Windows Form 的視窗程序,便可以這樣寫:
public class MyWpfControl : IWndProcHandler { private void HookWindowProc() { HwndSource wpfHandle = PresentationSource.FromVisual(this) as HwndSource; ElementHost wpfHost = System.Windows.Forms.Control.FromChildHandle(wpfHandle.Handle) as ElementHost; System.Windows.Forms.Form aForm = wpfHost.FindForm(); WndProcBridge.Link(aForm, this); } public IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { handled = false; switch (msg) { case WM_WINDOWPOSCHANGED: case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: case WM_NCLBUTTONDOWN: case WM_NCRBUTTONDOWN: // Do something break; } return IntPtr.Zero; } }
其中 HookWindowProc 函式是利用 ElementHost 提供的 FindForm() 方法來取得 WPF 控制項所屬的父層 Form 物件,然後透過 WndProcBridge 將 Form 的視窗程序銜接到此控制項自己的 WndProc() 方法。
小結
最後簡單整理一下,攔截視窗程序的幾種狀況和解法:
- 純 WPF 程式:可使用 Windows.GetWindow() + WindowInteropHelper + HwndSource.FromHwnd(),或者用 PresentationSource.FromVisual() 來取得 HwndSource 物件,然後呼叫 HwndSource 的 AddHook() 來攔截視窗程序。
- 純 Windows Forms:可在你的 Form 類別中直接改寫(override)WndProc 虛擬方法。
- Windows Form + WPF 控制項:使用 PresentationSource.FromVisual() 和 ElementHost.FindForm(),然後搭配 NativeWindow 類別來攔截 Form 的視窗程序。
沒有留言: