从 C# 使用低级 Windows API 挂钩以阻止不必要的按键






4.89/5 (80投票s)
2006年6月17日
12分钟阅读

630733

19971
婴儿和其他动物最喜欢做的事情莫过于乱敲键盘,从而产生各种不可预测的结果。本应用程序演示了如何在它们造成任何损害之前捕获按键。
引言
猫和婴儿有很多共同点。它们都喜欢吃室内植物,并且对关着的门有着同样的憎恨。它们还喜欢使用键盘,结果就是您正在发送给老板的重要电子邮件会在句子中间被发送出去,您的 Excel 账户会充斥着四行乱码,而您未能注意到 Windows 资源管理器已打开,结果导致几个文件被移入回收站。
解决方案是创建一个应用程序,一旦键盘受到威胁就可以切换到该应用程序,并确保所有键盘活动都是无害的。本文将介绍如何使用低级 Windows API 挂钩在 C# 应用程序中禁用键盘。
背景
有许多关于 Windows 挂钩的文章和代码示例,其中一些列在本文章的结尾。当孩子在身边时禁用键盘的需求肯定很普遍——这里有人用 C++ 编写了几乎完全相同的东西[^]!然而,当我为创建我的应用程序寻找源代码时,我发现 .NET 示例非常少,而且没有一个是自包含的 C# 类。
.NET 框架通过 KeyPress
、KeyUp
和 KeyDown
为大多数普通用途提供了对键盘事件的托管访问。不幸的是,这些事件不能用于阻止 Windows 处理诸如 Alt+Tab 或 Windows 开始键之类的组合键,这些组合键允许用户导航到其他应用程序。特别是方便放置的 Windows 键对我儿子来说是无法抗拒的!
解决方案是在操作系统级别而不是通过框架来捕获键盘事件。为此,应用程序需要使用 Windows API 函数将自身添加到侦听操作系统键盘消息的应用程序的“挂钩链”中。当它收到此类消息时,应用程序可以选择性地传递消息(如正常情况一样),或者抑制它,使其无法进一步传递给任何应用程序——包括 Windows。本文将对此进行解释。
请注意,此代码仅适用于基于 NT 的 Windows 版本(NT、2000 和 XP),并且无法使用此方法禁用 Ctrl+Alt+Delete(有关如何执行此操作的建议,请参见 此 MSDN 杂志问答[^])。
使用代码
为方便使用,我将三个独立的 zip 文件附加到本文中。其中一个仅包含 KeyboardHook
类,这是本文的重点。其他是名为“Baby Keyboard Bash”的应用程序的完整项目,该应用程序响应按键显示按键名称或彩色形状。为方便使用,我包含了两个项目,一个用于 Microsoft Visual C# 2005 Express Edition,另一个用于 Visual Studio 2003。
实例化类
键盘挂钩由 keyboard.cs 中的 KeyboardHook
类设置和处理。此类实现了 IDisposable
,因此最简单的实例化方法是在应用程序的 Main()
方法中使用 using
关键字来包含 Application.Run()
调用。这将确保在应用程序启动时设置挂钩,更重要的是,在应用程序关闭时禁用挂钩。
该类会引发一个事件来警告应用程序已按下某个键,因此应用程序的主窗体能够访问在 Main()
方法中创建的 KeyboardHook
实例非常重要;最简单的解决方案是将此实例存储在公共成员变量中。在 Visual Studio 2003 项目中,这通常会放在 Form1
类(或应用程序的主类名)中,而在 Visual Studio 2005 中,则放在 Program.cs 的 Program
类中。
KeyboardHook
有三个构造函数,用于启用或禁用特定设置
KeyboardHook()
:捕获所有按键,不将任何内容传递给 Windows 或其他应用程序。KeyboardHook(string param)
:将字符串转换为Parameters
枚举中的一个值,然后调用以下构造函数KeyboardHook(KeyboardHook.Parameters enum)
:根据从Parameters
枚举中选择的值,可以启用以下设置Parameters.AllowAltTab
:允许用户使用 Alt+Tab 切换到其他应用程序。Parameters.AllowWindowsKey
:允许用户使用 Ctrl+Esc 或 Windows 键访问任务栏和开始菜单。Parameters.AllowAltTabAndWindows
:启用 Alt+Tab、Ctrl+Esc 和 Windows 键。Parameters.PassAllKeysToNextApp
:如果参数为 true,则所有按键都将传递给任何其他侦听应用程序,包括 Windows。
启用 Alt+Tab 和/或 Windows 键允许实际使用计算机的人切换到另一个应用程序并使用鼠标与之交互,同时键盘挂钩继续捕获按键。PassAllKeysToNextApp
设置有效地禁用了按键捕获;该类仍然会设置一个低级键盘挂钩并引发其 KeyIntercepted
事件,但它也会将键盘事件传递给其他侦听应用程序。
因此,实例化类以捕获所有按键的最简单方法是
public static KeyboardHook kh;
[STAThread]
static void Main()
{
//Other code
using (kh = new KeyboardHook())
{
Application.Run(new Form1());
}
处理 KeyIntercepted 事件
当按下某个键时,KeyboardHook
类会引发一个 KeyIntercepted
事件,其中包含一些 KeyboardHookEventArgs
。这需要由 KeyboardHookEventHandler
类型的函数来处理,可以这样设置
kh.KeyIntercepted += new KeyboardHook.KeyboardHookEventHandler(kh_KeyIntercepted);
KeyboardHookEventArgs
返回有关按下的键的以下信息
KeyName
:按键的名称,通过将捕获的按键代码转换为System.Windows.Forms.Keys
来获取。KeyCode
:键盘挂钩返回的原始按键代码PassThrough
:此KeyboardHook
实例是否配置为允许此按键传递给其他应用程序。如果希望允许用户通过 Alt+Tab 或 Ctrl+Esc/Windows 键切换到其他应用程序,则检查此项很有用。
然后可以使用具有适当签名的函数来执行按键所要求的任何任务。以下是附带的示例应用程序中的一个示例
void kh_KeyIntercepted(KeyboardHookEventArgs e)
{
//Check if this key event is being passed to
//other applications and disable TopMost in
//case they need to come to the front
if (e.PassThrough)
{
this.TopMost = false;
}
ds.Draw(e.KeyName);
}
本文的其余部分将解释低级键盘挂钩如何在 KeyboardHook
中实现。
实现低级 Windows API 键盘挂钩
Windows API 在 user32.dll 中包含三个对本目的有用的方法
SetWindowsHookEx
,用于设置键盘挂钩UnhookWindowsHookEx
,用于移除键盘挂钩CallNextHookEx
,用于将按键信息传递给下一个侦听键盘事件的应用程序
创建一个可以劫持键盘的应用程序的关键在于实现前两个方法,而忽略第三个。结果是,任何按下的键都不会超出应用程序。
为了实现这一点,第一步是包含 System.Runtime.InteropServices
命名空间并导入 API 方法,首先是 SetWindowsHookEx
using System.Runtime.InteropServices
...
//Inside class:
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook,
LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
导入 UnhookWindowsHookEx
和 CallNextHookEx
的代码列在本文章稍后关于这些方法的章节中。
下一步是调用 SetWindowsHookEx
来设置挂钩,传递以下四个参数
- idHook:此数字确定要设置的挂钩类型。例如,
SetWindowsHookEx
也可用于挂钩鼠标事件;MSDN 上有完整的列表[^]。在此情况下,我们只关心数字 13,即键盘挂钩的 id。为了使代码更易读,已将其分配给常量WH_KEYBOARD_LL
。 - lpfn:指向将处理键盘事件的函数的长指针。在 C# 中,“指针”是通过传递委托类型实例来实现的,该实例引用适当的方法。这是每次使用挂钩时都会调用的方法。
这里需要注意的一个重要点是,委托实例需要存储在类的成员变量中。这是为了防止它在第一个方法调用结束后立即被垃圾回收。 - hMod:设置挂钩的应用程序的实例句柄。我发现的大多数示例都只是将其设置为
IntPtr.Zero
,理由是应用程序不太可能只有一个实例。但是,此代码使用 kernel32.dll 中的GetModuleHandle
来标识确切实例,以使类更具灵活性。 - dwThreadId:当前线程的 ID。将其设置为 0 使挂钩成为全局挂钩,这是低级键盘挂钩的适当设置。
SetWindowsHookEx
返回一个挂钩 ID,该 ID 将用于在应用程序关闭时取消挂钩,因此需要将其存储在成员变量中以备将来使用。KeyboardHook
类中的相关代码如下所示
private HookHandlerDelegate proc;
private IntPtr hookID = IntPtr.Zero;
private const int WH_KEYBOARD_LL = 13;
public KeyboardHook()
{
proc = new HookHandlerDelegate(HookCallback);
using (Process curProcess = Process.GetCurrentProcess())
using (ProcessModule curModule = curProcess.MainModule)
{
hookID = SetWindowsHookEx(WH_KEYBOARD_LL, proc,
GetModuleHandle(curModule.ModuleName), 0);
}
}
处理键盘事件
如上所述,SetWindowsHookEx
需要一个指向用于处理键盘事件的回调函数的指针。它期望一个具有以下签名的函数
LRESULT CALLBACK LowLevelKeyboardProc
( int nCode,
WPARAM wParam,
LPARAM lParam
);
用于设置“函数指针”的 C# 方法是使用委托,因此为 SetWindowsHookEx
提供所需内容的第一步是声明一个具有正确签名的委托
private delegate IntPtr HookHandlerDelegate(
int nCode, IntPtr wParam, ref KBDLLHOOKSTRUCT lParam);
然后编写一个具有相同签名的回调方法;此方法将包含实际处理键盘事件的所有代码。对于 KeyboardHook
,它会检查按键是否应传递给其他应用程序,然后引发 KeyIntercepted
事件。下面是一个简化的版本,不包含按键处理代码
private const int WM_KEYUP = 0x0101;
private const int WM_SYSKEYUP = 0x0105;
private IntPtr HookCallback(int nCode, IntPtr wParam, ref KBDLLHOOKSTRUCT lParam)
{
//Filter wParam for KeyUp events only - otherwise this code
//will execute twice for each keystroke (ie: on KeyDown and KeyUp)
//WM_SYSKEYUP is necessary to trap Alt-key combinations
if (nCode >= 0)
{
if (wParam == (IntPtr)WM_KEYUP || wParam == (IntPtr)WM_SYSKEYUP)
{
//Raise the event
OnKeyIntercepted(new KeyboardHookEventArgs(lParam.vkCode, AllowKey));
}
//Return a dummy value to trap the keystroke
return (System.IntPtr)1;
}
//The event wasn't handled, pass it to next application
return CallNextHookEx(hookID, nCode, wParam, ref lParam);
}
然后,将 HookCallback
的引用分配给 HookHandlerDelegate
的实例,并在调用 SetWindowsHookEx
时传递,如上一节所示。
每当发生键盘事件时,以下参数将传递给 HookCallBack
- nCode:根据 MSDN 文档[^],如果此值为负数,则回调函数应返回
CallNextHookEx
的结果。正常的键盘事件将返回 0 或更大的nCode
。
- wParam:此值指示发生了哪种类型的事件:按键抬起或按下,以及按下的键是否为系统键(左或右 Alt 键)。
- lParam:一个结构,用于存储有关按键的精确信息,例如按下的键的代码。在
KeyboardHook
中声明的结构如下
private struct KBDLLHOOKSTRUCT
{
public int vkCode;
int scanCode;
public int flags;
int time;
int dwExtraInfo;
}
KeyboardHook
中的回调方法只使用这两个公共参数。vkCoke
返回虚拟键码,可以将其转换为 System.Windows.Forms.Keys
来获取按键的名称,而 flags
指示这是扩展键(例如 Windows 开始键)还是同时按下了 Alt 键。HookCallback
方法的完整代码说明了在每种情况下要检查哪些 flags
值。
如果不需要 flags
和 KBDLLHOOKSTRUCT
其他组件提供的信息,则回调方法和委托的签名可以更改为如下
private delegate IntPtr HookHandlerDelegate(
int nCode, IntPtr wParam, IntPtr lParam);
在这种情况下,lParam
将仅返回 vkCode
。
将按键传递给下一个应用程序
一个行为良好的键盘挂钩回调方法应以调用 CallNextHookEx
函数并返回其结果结束。这可确保其他应用程序有机会处理它们接收的按键。
但是,KeyboardHook
类的关键功能是阻止按键传播到任何进一步的应用程序。因此,每当它处理按键时,HookCallback
会返回一个占位符值
return (System.IntPtr)1;
另一方面,如果它未处理事件,或者 KeyboardHook
的重载构造函数中传递的参数允许某些按键组合通过,它会调用 CallNextHookEx
。
通过导入 user32.dll 中的函数来启用 CallNextHookEx
,如下面的代码所示
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,
IntPtr wParam, ref KeyInfoStruct lParam);
然后,在 HookCallback
方法中的这一行调用导入的函数,这可确保通过挂钩接收到的所有参数都传递给下一个应用程序
CallNextHookEx(hookID, nCode, wParam, ref lParam);
如前所述,如果 lParam
中的标志不相关,则导入的 CallNextHookEx
的签名可以更改为将 lParam
定义为 System.IntPtr
。
移除挂钩
处理挂钩的最后一步是在 KeyboardHook
类实例被销毁时将其移除,方法是使用从 user32.dll 导入的 UnhookWindowsHookEx
函数,如下所示。
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
由于 KeyboardHook
实现 IDisposable
,这可以在 Dispose
方法中完成。
public void Dispose()
{
UnhookWindowsHookEx(hookID);
}
hookID
是在构造函数中调用 SetWindowsHookEx
返回的 id。这会将应用程序从挂钩链中移除。
来源
以下是一些关于 C# 和通用 Windows 挂钩的优秀资源
- 在 Stephen Toub 的 MSDN 博客[^] 中可以找到一个不错的 C# 控制台应用程序示例
- 有关通用 Windows API 挂钩的信息,请参阅 MSDN 关于 SetWindowsHookEx 的文章[^] 及其相关链接
- Code Project 上的一篇文章[^],介绍了一个用于 C# 中全局挂钩的类,演示了如何设置其他可用挂钩,包括鼠标活动。
似乎任何语言的文章都会吸引至少一个关于“如何在(其他语言)中做同样的事情?”的回应,所以这里有一些我找到的示例
- VB6 中的键盘挂钩示例[^]
- VB.NET 中相同内容的示例和文章[^]
- 以及 C++ 中的另一个示例[^],它是一个与本文目标完全相同的项目的组成部分!
与 Windows API 挂钩或 C# 无关,但我想推荐 Mike Ellison 出色的 Word 2003 模板,如果您计划为 Code Project 撰写文章的话!
历史
- 1.4 (2007年3月23日) - 修复了一个导致程序退出时 Alt 键看似卡住的 bug
- Changed
if (nCode >= 0 && (wParam == (IntPtr)WM_KEYUP || wParam == (IntPtr)WM_SYSKEYUP))
to
if (nCode >= 0)
{
if (wParam == (IntPtr)WM_KEYUP || wParam == (IntPtr)WM_SYSKEYUP)
非常感谢“Scottie Numbnuts”提供的解决方案。
- Changed
- 1.3 (2007年2月13日) - 改进了修饰键支持
- 添加了 CheckModifiers() 方法
- 删除了 LoWord/HiWord 方法,因为它们不是必需的
- 实现了 Barry Dorman 的建议,通过与 0x8000 进行 AND 运算来获取 GetKeyState 的结果
- 1.2 (2006年7月10日) - 添加了对修饰键的支持
- 将 HookCallback 中的过滤器从 WM_KEYDOWN 更改为 WM_KEYUP
- 从 user32.dll 导入 GetKeyState
- 根据微软的指南,将本机 DLL 导入移动到一个单独的内部类,因为这是一个好主意
- 1.1 (2006年6月18日) - 修改了构造函数中的 proc 赋值,以使类与 2003 版本向后兼容。
- 1.0 (2006年6月17日) - 在 Code Project 上发布的第一个版本。