创建窗口的时候,可以传一个消息处理函数。然而如果窗口不是自己创建的,还能增加消息处理函数吗?答案是可以的,除了 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
的方式来添加钩子,具体你可以阅读我的另一篇博客来了解如何实现:
参考资料
本文会经常更新,请阅读原文: https://blog.walterlv.com/post/hook-a-window-by-sub-classing-it.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://blog.walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 ([email protected]) 。