在 WPF 控制項中攔截 WinForm 的視窗程序

在 WPF 應用程式或 Windows Forms 應用程式中攔截視窗程序(WndProc)並不難,可是如果要在 WPF 控制項中攔截 Windows Form 的視窗程序,就得動點手腳了。這篇筆記整理幾種攔截視窗訊息的方法和範例。

透過 HwndSource 攔截視窗程序

要在 WPF 應用程式中攔截視窗程序,基本上有兩個步驟:
  1. 取得視窗所關聯的 HwndSource  物件。
  2. 呼叫剛才取得的 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 物件之後,就可以攔截視窗程序了。這個部分的寫法前面已經看過,就不再重複。

此外,如果是在 WPF 視窗裡面攔截自己的視窗程序,還有另一個方法,也是使用 WindowInteropHelper 類別來取得視窗 handle。這個部分可參考 Level Up 的文章<WPF程式接收視窗訊息>。

Windowns Forms + WPF 控制項

場景:在 Windows Forms 應用程式中,要在某個 Form 上面嵌入你的 WPF 控制項,而且需要在這個 WPF 控制項中攔截父視窗(Form)的視窗程序。

碰到這種情況,可以先用 PresentationSource 的 FromVisual() 方法來取得 HwndSource 物件:

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 的視窗程序。 

沒有留言:

技術提供:Blogger.
回頂端⬆️