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

2023-10-22 11:02:06 浏览数 (3)

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

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

子类化

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

SetWindowLong 的 API 如下:

1 2 3 4 5

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

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

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

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

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

1 2 3 4 5 6 7 8

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) { // 在这里处理消息。 }

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

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 以调用此前原来的那个消息处理函数。

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

1 2

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

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

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 的方式来添加钩子,具体你可以阅读我的另一篇博客来了解如何实现:

  • .NET/C# 使用 SetWindowsHookEx 监听鼠标或键盘消息以及此方法的坑 - walterlv

参考资料

  • Using Window Procedures - Win32 apps - Microsoft Docs

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

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

1 人点赞