通过子类化窗口(SubClass)来为现有的某个窗口添加新的窗口处理程序(或者叫钩子,Hook)

创建窗口的时候,可以传一个消息处理函数。然而如果窗口不是自己创建的,还能增加消息处理函数吗?答案是可以的,除了 SetWindowsHookEx 来添加钩子之外,更推荐用子类化的方式来添加。

本文介绍如何通过子类化(SubClass)的方式来为窗口添加额外的消息处理函数。


子类化

子类化的本质是通过 SetWindowLong 传入 GWL_WNDPROC 参数。

SetWindowLong 的 API 如下:

LONG SetWindowLongA(
  HWND hWnd,
  int  nIndex,
  LONG dwNewLong
);

nIndex 指定为 GWL_WNDPROC,在此情况下,后面的 dwNewLong 就可以指定为一个函数指针,返回值就是原始的消息处理函数。

对于 .NET/C# 来说,我们需要拿到窗口句柄,拿到一个消息处理函数的指针。

窗口句柄在不同的 UI 框架拿的方法不同,WPF 是通过 HwndSource 或者 WindowInteropHelper 来拿。而将委托转换成函数指针则可通过 Marshal.GetFunctionPointerForDelegate 来转换。

你可别吐槽 WPF 另有它法来加消息处理函数啊!本文说的是 Win32,方法需要具有普适性。特别是那种你只能拿到一个窗口句柄,其他啥也不知道的窗口。

var hWnd = new WindowInteropHelper(this).EnsureHandle();
var wndProc = Marshal.GetFunctionPointerForDelegate<WndProc>(OnWndProc);
_originalWndProc = SetWindowLongPtr(hWnd, GWL_WNDPROC, wndProc);

IntPtr OnWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
    // 在这里处理消息。
}

将完整的代码贴下来,大约是这样:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        SourceInitialized += MainWindow_SourceInitialized;
    }

    private void MainWindow_SourceInitialized(object sender, EventArgs e)
    {
        var hWnd = new WindowInteropHelper(this).EnsureHandle();
        _wndProc = OnWndProc;
        var wndProc = Marshal.GetFunctionPointerForDelegate<WndProc>(_wndProc);
        _originalWndProc = SetWindowLongPtr(hWnd, GWL_WNDPROC, wndProc);
    }

    private WndProc _wndProc;
    private IntPtr _originalWndProc;

    private IntPtr OnWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
    {
        switch (msg)
        {
            case WM_NCHITTEST:
                return CallWindowProc(_originalWndProc, hWnd, msg, wParam, lParam);
            default:
                return CallWindowProc(_originalWndProc, hWnd, msg, wParam, lParam);
        }
    }
}

其中,我将委托存成了一个字段,这样可以避免 GC 回收掉这个委托对象造成崩溃。

在示例的消息处理函数中,我示例处理了一下 WM_NCHITTEST(虽然依然什么都没做)。最后,必须调用 CallWindowProc 以调用此前原来的那个消息处理函数。

最后,如果你又不希望处理这个消息了,那么使用以下方法注销掉这个委托:

// 嗯,没错,就是前面更换消息处理函数时返回的那个指针。
SetWindowLongPtr(hWnd, GWL_WNDPROC, _originalWndProc);

上面需要的所有的 P/Invoke 我都贴到了下面,需要的话放到你的代码当中。

private static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong)
{
    if (IntPtr.Size == 8)
    {
        return SetWindowLongPtr64(hWnd, nIndex, dwNewLong);
    }
    else
    {
        return new IntPtr(SetWindowLong32(hWnd, nIndex, dwNewLong.ToInt32()));
    }
}

[DllImport("user32.dll", EntryPoint = "SetWindowLong")]
private static extern int SetWindowLong32(IntPtr hWnd, int nIndex, int dwNewLong);

[DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
private static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

[DllImport("user32.dll")]
static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

private delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);

private const int GWL_WNDPROC = -4;
private const int WM_NCHITTEST = 0x0084;
private const int HTTRANSPARENT = -1;

其他方法

本文一开始说到了使用 SetWindowsHookEx 的方式来添加钩子,具体你可以阅读我的另一篇博客来了解如何实现:


参考资料

本文会经常更新,请阅读原文: https://blog.walterlv.com/post/hook-a-window-by-sub-classing-it.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

如果你想持续阅读我的最新博客,请点击 RSS 订阅,或者前往 CSDN 关注我的主页

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://blog.walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 ([email protected])