2020-12-29
关键字:.NET framework、.NET CORE、.NET、WPF、windows forms、SetWindowsHookEx、钩子函数
1、如何捕获键鼠事件?
在windows桌面编程中,要想捕获应用内的键鼠事件还是非常简单的。直接在XAML上对应window或控件的对应事件上注册回调就可以了。
但全局键鼠事件就没这么容易了。
全局键鼠事件需要用到“钩子函数”--向系统注册一个自己的钩子函数以“钩取”来自底层的键鼠事件。
这个关键的向系统注册钩子函数的API原型如下:
HHOOK SetWindowsHookExA( int idHook, HOOKPROC lpfn, HINSTANCE hmod, DWORD dwThreadId );
这是一段C++代码。我们来解读一下这个函数。
早期的Windows软件开发用的是C++,即使现在微软主推 .NET 也依然有很多系统级功能和接口是用非C#语言实现的,正如这个注册钩子函数的系统接口。
不过我们大可不必担心C#和C++两门语言之间的兼容性问题,微软早就搞定一切了。只需要我们老老实实引入相应的DLL和声明函数原型就可以直接调用了。
另外,SetWindowsHookEx 函数似乎是有两个变种:SetWindowsHookExA 与 SetWindowsHookExW 。这三者之间是完全通用的,一般直接写 SetWindowsHookEx 即可。
回到这个接口的原型解读。它的返回值我们理解成是一个指针变量就可以了,当我们成功向系统注册钩子函数时会返回一个地址用于标识我们的钩子。这个返回值最好好好保存,因为在注销钩子函数时需要用到。如果实在是因为“不小心”弄丢了返回值,也不要紧。系统会在你退出应用时注销你的钩子函数的。
接下来看看它的四个参数。
参数1:idHook。表示我们需要钩取哪种类型的事件。数值13表示全局键盘事件,数值14表示全局鼠标事件,其它事件值不在本文讨论范围内,有需要的同学请自行查阅官方文档。
参数2:lpfn。在C#中就是一个委托类型值,填入要注册的钩子函数名。具体的委托类型会在后面说明。
参数3:hmod。无须过多理会,表示持有钩子函数的进程号,填0再强转为IntPtr即可。
参数4:dwThreadId。无须过多理会,直接填0即可。
这个接口更详细的解释还得查阅微软官方文档,相关链接如下:
https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowshookexa
上述参数2的钩子函数类型在C#中的委托原型如下所示:
public delegate int HookProc(int code, IntPtr wParam, IntPtr lParam);
只要是使用 SetWindowsHookEx 注册的钩子函数都可以使用这种形式。
钩子函数的原理大致是当键鼠设备产生了一个事件后首先上报到驱动,再上报到系统层,系统层会检测是否有应用注册了相应钩子函数,如果有,则将事件逐个回调给应用,待应用处理完后再根据其返回值来决定事件的后续处理方式。因此,千万不要在钩子函数内做耗时操作,否则系统会因为事件传递过程被阻塞而出问题的,听说严重的情况下系统会主动注销你的钩子。
2、捕获全局键盘事件
全局键盘事件的 idHook 值是13,其实还有另一个值2也表示键盘事件。但数值2的会多出一些限制,导致部分情况下的键盘事件接收不到,因为我们都是直接使用数值13的。
接下来要重点讨论的就是键盘事件的钩子函数的定义了。
键盘事件钩子函数的详细说明可以查阅下方链接:
https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms644985(v=vs.85)
这里作个简要的中文解释。其原型如下所示:
LRESULT CALLBACK LowLevelKeyboardProc( _In_ int nCode, _In_ WPARAM wParam, _In_ LPARAM lParam );
这同样是个C++函数,不过同样不要紧。
首先它的返回值是一个整型数。返回数值0表示允许该事件继续传播,返回大于0的数表示此事件到此为止,其它尚未接收到该事件的钩子或系统函数将不再能接收到了。同时,微软还重点指出,如果回调函数中的参数 nCode 的值小于0,则必须将这一事件交由 CallNextHookEx 函数去处理,并返回这一函数的返回值。
其次是它的参数。
参数1:nCode。事件状态码,当值为0时处理按键事件,小于0时最好将事件交由 CallNextHookEx 函数处理。
参数2:wParam。按键事件码,有四个可能值:1、普通键按下:0x100;2、普通键抬起:0x101;3、系统键按下:0x104;4、系统键抬起:0x105。在本文中我们只需关心前两个事件。
参数3:lParam。事件详细信息结构体的地址,下面展开聊聊。
上述参数3 lParam 所指向的结构体原型如下:
typedef struct tagKBDLLHOOKSTRUCT { DWORD vkCode; DWORD scanCode; DWORD flags; DWORD time; ULONG_PTR dwExtraInfo; } KBDLLHOOKSTRUCT, *LPKBDLLHOOKSTRUCT, *PKBDLLHOOKSTRUCT;
vkCode 表示按键码,即被按下按键的键码。其值有效范围为 1 ~ 254。具体的键值对应关系参见: https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
scanCode 是扫描码,本文不关心这一数值。
flags 用于记载一些额外信息,本文同样不关心这一数值。
time 则是事件的发生时间,单位为毫秒,表示的是系统启动以来的相对时间值。
dwExtraInfo 是额外信息,无须关心。
3、捕获全局鼠标事件
全局鼠标事件与全局键盘事件几无差别。这里主要聊聊钩子函数的委托类型。它的原型定义如下:
LRESULT CALLBACK LowLevelMouseProc( _In_ int nCode, _In_ WPARAM wParam, _In_ LPARAM lParam );
函数返回值与参数nCode与上一节键盘钩子函数一样。
参数 wParam 表示鼠标事件类型。几个主要的数值如下表所示:
鼠标移动 | 0x200 |
鼠标左键按下 | 0x201 |
鼠标左键抬起 | 0x202 |
鼠标右键按下 | 0x204 |
鼠标右键抬起 | 0x205 |
鼠标滚轮滚动 | 0x20a |
鼠标侧键按下 | 0x20b |
鼠标侧健抬起 | 0x20c |
鼠标水平滚轮滚动 | 0x20e |
参数 lParam 同样是事件详细信息结构体的地址,该结构体的原型如下所示:
typedef struct tagMSLLHOOKSTRUCT { POINT pt; DWORD mouseData; DWORD flags; DWORD time; ULONG_PTR dwExtraInfo; } MSLLHOOKSTRUCT, *LPMSLLHOOKSTRUCT, *PMSLLHOOKSTRUCT;
成员 pt 表示事件的坐标值结构体,其原型如下所示:
typedef struct tagPOINT { LONG x; LONG y; } POINT, *PPOINT;
需要注意的是,虽然它被声明为 'LONG' 类型,但它实际上只有4个字节长度。另外,如果你对结构体的了解足够深刻,一定能理解在实际开发中直接用一个 long 型来替代 POINT 类型是完全可行的,只需要知道Windows桌面编程使用的是小端序就可以了,当然,如果你理解不了这句话,那老老实实再创建一个POINT结构体来套进去就是了。
成员mouseData 不太需要关注。当事件是滚轮滚动时,它的高16位记录的是滚动方向及距离。正值表示远离用户的滚动,负值表示靠近用户的滚动,其数值恒定为120,可以理解为表示一格滚动。
成员 flags 不需要理会。
成员 time 表示事件发生时间,单位为毫秒,自系统启动以来的相对时间值。
成员 dwExtraInfo 不需要理会。
4、实现
本小节我们直接贴上一个示例代码,用于捕获Windows系统的全局键鼠事件。
我们的需求是实现一个应用,其中有两个按钮,一个用于注册钩子事件,另一个用于注销钩子事件,使用的框架是 .NET core 3.1,软件界面如下图所示:
程序运行并注册钩子函数后操作鼠标时的打印信息如下:
操作键盘后的打印信息如下:
具体的源码如下所示:
using System; using System.Runtime.InteropServices; using System.Windows; namespace KMHook { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } internal struct Keyboard_LL_Hook_Data { public UInt32 vkCode; public UInt32 scanCode; public UInt32 flags; public UInt32 time; public IntPtr extraInfo; } internal struct Mouse_LL_Hook_Data { internal long yx; internal readonly int mouseData; internal readonly uint flags; internal readonly uint time; internal readonly IntPtr dwExtraInfo; } private static IntPtr pKeyboardHook = IntPtr.Zero; private static IntPtr pMouseHook = IntPtr.Zero; //钩子委托声明 public delegate int HookProc(int code, IntPtr wParam, IntPtr lParam); private static HookProc keyboardHookProc; private static HookProc mouseHookProc; //安装钩子 [DllImport("user32.dll")] public static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr pInstance, int threadID); //卸载钩子 [DllImport("user32.dll", CallingConvention = CallingConvention.StdCall)] public static extern bool UnhookWindowsHookEx(IntPtr pHookHandle); [DllImport("user32.dll")] public static extern int CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); //parameter 'hhk' is ignored. private static int keyboardHookCallback(int code, IntPtr wParam, IntPtr lParam) { if (code < 0) { return CallNextHookEx(IntPtr.Zero, code, wParam, lParam); } Keyboard_LL_Hook_Data khd = (Keyboard_LL_Hook_Data)Marshal.PtrToStructure(lParam, typeof(Keyboard_LL_Hook_Data)); System.Diagnostics.Debug.WriteLine($"key event:{wParam}, key code:{khd.vkCode}, event time:{khd.time}"); return 0; } private static int mouseHookCallback(int code, IntPtr wParam, IntPtr lParam) { if (code < 0) { return CallNextHookEx(IntPtr.Zero, code, wParam, lParam); } Mouse_LL_Hook_Data mhd = (Mouse_LL_Hook_Data)Marshal.PtrToStructure(lParam, typeof(Mouse_LL_Hook_Data)); System.Diagnostics.Debug.WriteLine($"mouse event:{wParam}, ({mhd.yx & 0xffffffff},{mhd.yx >> 32})"); return 0; } internal static bool InsertHook() { bool iRet; iRet = InsertKeyboardHook(); if (!iRet) { return false; } iRet = InsertMouseHook(); if (!iRet) { removeKeyboardHook(); return false; } return true; } //安装钩子方法 private static bool InsertKeyboardHook() { if (pKeyboardHook == IntPtr.Zero)//不存在钩子时 { //创建钩子 keyboardHookProc = keyboardHookCallback; pKeyboardHook = SetWindowsHookEx(13, //13表示全局键盘事件。 keyboardHookProc, (IntPtr)0, 0); if (pKeyboardHook == IntPtr.Zero)//如果安装钩子失败 { removeKeyboardHook(); return false; } } return true; } private static bool InsertMouseHook() { if (pMouseHook == IntPtr.Zero) { mouseHookProc = mouseHookCallback; pMouseHook = SetWindowsHookEx(14, //14表示全局鼠标事件 mouseHookProc, (IntPtr)0, 0); if (pMouseHook == IntPtr.Zero) { removeMouseHook(); return false; } } return true; } internal static bool RemoveHook() { bool iRet; iRet = removeKeyboardHook(); if (iRet) { iRet = removeMouseHook(); } return iRet; } private static bool removeKeyboardHook() { if (pKeyboardHook != IntPtr.Zero) { if (UnhookWindowsHookEx(pKeyboardHook)) { pKeyboardHook = IntPtr.Zero; } else { return false; } } return true; } private static bool removeMouseHook() { if (pMouseHook != IntPtr.Zero) { if (UnhookWindowsHookEx(pMouseHook)) { pMouseHook = IntPtr.Zero; } else { return false; } } return true; } private void Button_Install_Click(object sender, RoutedEventArgs e) { InsertHook(); } private void Button_Remove_Click(object sender, RoutedEventArgs e) { RemoveHook(); } } }