一. 简介 猫和婴儿有很多共同之处。他们都喜欢吃家中养植的植物,都非常讨厌关门。他们也都爱玩弄你的键盘,结果是,你正发送给你的老板的电子邮件可能是以半截句子发送出去的,你的Excel帐户也被加入了一些乱七八糟的内容,并且你还没有注意到,当打开Windows资源管理器时,若干文件已经被移到了回收站!
其解决方案是,开发一个应用程序实现如下功能:只要键盘处于"威胁状态"你就可以进行切换,并确保任何键盘输入活动都不会造成危害。本文想展示如何使用一种低级Windows API钩子在一个C#应用程序中实现键盘"控制"。下图是本文示例程序的一个运行快照。
二. 背景 其实,已经存在许多有关于Windows钩子的文章和示例代码,并且已经有人编写过与本文几乎一样的C++示例程序。然而,当我搜索相应的C#应用程序的源码时,却找到极少的.NET示例,而且没有一个程序能够提供一个方便的自包含的C#类。
.NET框架能够使你以托管方式来存取你最常使用的键盘事件(通过KeyPress,KeyUp和KeyDown)。遗憾的是,这些事件都不能被用来停止Windows组合键(如Alt+Tab或Windows"开始"键),从而允许用户"远离"某一个应用程序。
本文的想法在操作系统级上捕获键盘事件而不是通过框架级来实现。为此,应用程序需要使用Windows API函数来把它自身添加到应用程序"钩子链"中以监听来自操作系统的键盘消息。当它收到这种类型的消息时,该应用程序能够选择性地传递消息,或者进行正常处理,或者"镇压"它以便不再有其它应用程序(包括Windows)来影响它。本文正是想解释其实现机理。
然而,请注意,本文中的代码仅适用于基于NT版本的Windows(NT,2000和XP),并且无法使用这个方法来停用Ctrl+Alt+Delete。有关于如何实现这一点,你可以参考MSDN有关资料。
三. 使用代码 为了易于使用,我在本文中提供了两个独立的zip文件。一个仅包含KeyboardHook类,这是本文介绍的重点。另一个是一个完整的微软Visual C# 2005 Express Edition应用程序工程,名叫"Baby Keyboard Bash",它实现显示击键的名字或彩色的形状以响应于击键。
四. 实例化类 键盘钩子是通过keyboard.cs中的KeyboardHook类来建立和管理的。这个类实现了IDisposable接口,因此,实例化它的最简单的方法是在应用程序的Main()方法中使用using关键字来封装Application.Run()调用。这将确保只要该应用程序开始即建立钩子并且,更重要的是,当该应用程序结束时立即使这个钩子失效。
这个类引发一个事件来警告应用程序已经有键被按下,因此主表单能够存取在Main()方法中创建的KeyboardHook实例就显得非常重要;最简单的方法是把这个实例存储在一个公共成员变量中。
KeyboardHook提供了三种构造器来启用或禁用某些设置:
€€ KeyboardHook():捕获所有击键,没有任何内容传递到Windows或另外的应用程序。
€€ KeyboardHook(string param):把参数串转换为Parameters枚举中的值之一,然后调用下面的构造器:
€€ KeyboardHook(KeyboardHook.Parameters enum):根据从Parameters枚举中选择的值的不同,分别启动下列设置:
o Parameters.AllowAltTab:允许用户使用Alt+Tab切换到另外的应用程序。
o Parameters.AllowWindowsKey:允许用户使用Ctrl+Esc或一种Windows键存取任务栏和开始菜单。
o Parameters.AllowAltTabAndWindows:启用Alt+Tab,Ctrl+Esc和Windows键。
o Parameters.PassAllKeysToNextApp:如果该参数为true,那么所有的击键将被传递给任何其它监听应用程序(包括Windows)。
当击键继续被键盘钩子捕获时,启用Alt+Tab和/或Windows键允许实际使用该计算机者切换到另一个应用程序并且使用鼠标与之交互。PassAllKeysToNextApp设置有效地禁用了击键捕获;这个类也是建立一个低级键盘钩子并且引发它的KeyIntercepted事件,但是它还负责把键盘事件传递到另一个监听程序。
因此,实例化该类以捕获所有击键的方法如下:
public static KeyboardHook kh;
[STAThread]
static void Main()
{
//其它代码
using (kh = new KeyboardHook())
{
Application.Run(new Form1());
}
五. 处理KeyIntercepted事件
当一外键被按下时,这个KeyboardHook类激活一个包含一些KeyboardHookEventArgs的KeyIntercepted事件。这是通过一个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)
{
//检查是否这个键击事件被传递到其它应用程序并且停用TopMost,以防他们需要调到前端
if (e.PassThrough)
{
this.TopMost = false;
}
ds.Draw(e.KeyName);
}
本文的剩下部分将解释低级键盘钩子是如何在KeyboardHook中实现的。
六. 实现一个低级Windows API键盘钩子
在user32.dll中,Windows API包含三个方法来实现此目的:
€€ SetWindowsHookEx,它负责建立键盘钩子
€€ UnhookWindowsHookEx,它负责移去键盘钩子
€€ CallNextHookEx,它负责把击键信息传递到下一个监听键盘事件的应用程序
创建一个能够拦截键盘的应用程序的关键是,实现前面两个方法,而"放弃"第三个。结果是,任何击键都只能传递到这个应用程序中。
为了实现这一目标,第一步是包括System.Runtime.InteropServices命名空间并且导入API方法,首先是SetWindowsHookEx:
using System.Runtime.InteropServices
...
//在类内部:
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
privat