五. 处理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)]
private static extern IntPtr SetWindowsHookEx(int idHook,
LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
导入UnhookWindowsHookEx和CallNextHookEx的代码请见后面的讨论。
下一步是调用SetWindowsHookEx来建立钩子,这时需要传递下列四个参数:
idHook:
这个数字决定了要建立的钩子的类型。例如,SetWindowsHookEx可以被用于钩住鼠标事件(当然还有其它事件)。在本文情况下,我们仅对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_KEYDOWN = 0x0100;
private const int WM_SYSKEYDOWN = 0x0104;
private IntPtr HookCallback(int nCode, IntPtr wParam, ref KBDLLHOOKSTRUCT lParam)
{
//仅为KeyDown事件过滤wParam,否则该代码将再次执行-对于每一次击键(也就是,相应于KeyDown和KeyUp)
//WM_SYSKEYDOWN是捕获Alt相关组合键所必需的
if (nCode >= 0 && (wParam == (IntPtr)WM_KEYDOWN || wParam == (IntPtr)WM_SYSKEYDOWN))
{
//激发事件
OnKeyIntercepted(new KeyboardHookEventArgs(lParam.vkCode, AllowKey));
//返回一个"哑"值以捕获击键
return (System.IntPtr)1;
}
//事件没有被处理,把它传递给下一个应用程序
return CallNextHookEx(hookID, nCode, wParam, ref lParam);
}
接下来,一个到HookCallback的参考被指派给HookHandlerDelegate的一个实例并且被传递到SetWindowsHookEx的调用,正如前一节所展示的。
无论何时一个键盘事件发生,下列参数将被传递给HookCallBack:
nCode:
根据MSDN文档,回调函数应该返回CallNextHookEx的结果,如果这个值小于零的话。正常的键盘事件将返回一个大于或等于零的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 Start键)或是否同时按下了Alt键。有关于Hook回调方法的完整代码展示在每一种情况下要检查哪些flags值。
如果flags提供的信息和KBDLLHOOKSTRUCT的其它组成元素不需要,那么这个回调方法和代码的签名可以按如下进行修改:
private delegate IntPtr HookHandlerDelegate(
int nCode, IntPtr wParam, IntPtr lParam);
在这种情况中,lParam将仅返回vkCode。
八. 把击键传递到下一个应用程序
一个良好的键盘钩子回调方法应该以调用CallNextHookEx函数并且返回它的结果结束。这可以确保其它应用程序能够有机会处理针对于它们的击键。
然而,KeyboardHook类的主要功能在于,阻止击键被传播到任何其它更多的应用程序。因此它无论在何时处理一次击键,HookCallback都将返回一个哑值:
return (System.IntPtr)1;
另一方面,它确实调用CallNextHookEx-如果它不处理该事件,或如果重载的构造器中的使用KeyboardHook传递的参数允许某些组合键通过。
CallNextHookEx被启用-通过从user32.dll导入该函数,如下列代码所示:
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,
IntPtr wParam, ref KeyInfoStruct lParam);
然后,被导入的方法被HookCallMethod所调用,这可以确保所有的通过钩子接收到的参数被继续传递到下一个应用程序中:
CallNextHookEx(hookID, nCode, wParam, ref lParam);
如前面所提及,如果在lParam中的flags是不相关的,那么可以修改导入的CallNextHookEx的签名以把lParam定义为System.IntPtr。
九. 移去钩子
处理钩子的最后一步是使用从user32.dll中导入的UnhookWindowsHookEx函数移去它(当破坏KeyboardHook类的实例时),如下所示:
[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()