Hooks
★概述
勾子是系统消息处理机制中的一个地方,应用程序可以在该处安装子例程,用于监控系统的消息流,并在消息到达目的窗口过程之前处理特定类型的消息。
勾子往往会拖慢系统的速度,因为它们增加了系统对每个消息的处理量。你应该只在需要的时候才安装勾子,并且尽早地移除它。
★勾子链
系统支持许多不同类型的勾子;每种类型提供了对系统消息处理机制不同方面的访问。例如,应用程序可以使用WH_MOUSE勾子来监视鼠标消息流。
系统为每种类型的勾子维护了一个单独的勾子链。一个勾子链是一个指针列表,其中的指针指向特定的,应用程序定义的被称为勾子过程的回调函数。当与特定类型的勾子相关的消息发生时,系统把它顺序传给勾子链中指向的每个勾子过程。勾子过程可以执行的动作取决于勾子的类型。一些类型的勾子的勾子过程只能监视消息,其他的可以修改消息以停止它们在链中的传递,防止它们到达下一个勾子过程或目标窗口。
★勾子过程
要利用特定类型的勾子,开发者必须提供勾子过程,并用SetWindowsHookEx函数把它装入该类型的勾子链。勾子过程有下列形式:
nCode指示勾子过程要执行的动作。其取值随勾子类型不同而不同;各种类型的勾子有它特有的code集。wParam和lParam取决于勾子编码,不过通常包含的是消息的信息。
SetWindowsHookEx总是把勾子过程安装到勾子链的开头。当特定类型勾子监视的事件发生时,系统调用该类型勾子链开头的勾子过程。链中每个勾子过程决定是否把事件往下一个过程传递。勾子过程通过CallNextHookEx函数来传递事件。
某些类型的勾子只能监视消息,系统把消息传给其中的每个勾子过程,而不管它们是否调用了CallNextHookEx。
全局勾子监视与调用线程同一桌面的所有线程。线程勾子只监视个别的线程。全局勾子过程会在任意应用程序的上下文中被调用,故该过程必须位于一个单独的DLL模块里。线程相关的勾子过程只在相关线程的上下文中调用。如果程序为它自己的一个线程安装了勾子过程,该过程可以作为程序代码的部分位于同一模块中,也可位于DLL中。如果应用程序为不同应用程序的线程安装勾子过程,过程必须位于DLL中。
要注意的是要尽量避免全局勾子,因为会伤害系统性能,并导致与其它实现同样类型全局勾子的应用程序冲突。
★勾子类型
·WH_CALLWNDPROC 和 WH_CALLWNDPROCRET 勾子
WH_CALLWNDPROC 和 WH_CALLWNDPROCRET 勾子使你可以监视发送到窗口过程的消息。系统在把消息传到窗口过程前调用WH_CALLWNDPROC勾子过程,在窗口过程处理完消息后调用WH_CALLWNDPROCRET勾子过程。
WH_CALLWNDPROCRET勾子传递一个CWPRETSTRUCT结构指针到勾子过程。该结构包含了窗口过程的返回值,以及和消息相关的参数。
·WH_CBT 勾子
系统在下列情况下调用WH_CBT勾子过程:
窗口激活、创建、销毁、最小化、最大化、移动、改变大小;完成系统命令之前;把鼠标或键盘事件从系统消息队列移除之前;设置输入焦点之前;在与系统消息队列同步之前。
勾子过程的返回值决定系统允许还是阻止这些操作。WH_CBT勾子的本意主要在于基于计算机的训练(Computer-based training -- CBT)的应用程序。
·WH_DEBUG 勾子
系统在调用任何其他类型的勾子过程前调用WH_DEBUG勾子过程。你可以使用这种勾子来决定是否允许系统调用其他类型的勾子过程。
·WH_FOREGROUNDIDLE 勾子
WH_FOREGROUNDIDLE勾子使你能在前端线程空闲时执行低优先级的任务。系统在应用程序的前台线程即将空闲时调用WH_FOREGROUNDIDLE勾子过程。
·WH_GETMESSAGE 勾子
WH_GETMESSAGE勾子使应用程序能够监视即将由GetMessage或PeekMessage函数返回的消息。你可以使用WH_GETMESSAGE勾子来监视鼠标和键盘输入,以及其它被投递到消息队列的消息。
·WH_JOURNALPLAYBACK 勾子
WH_JOURNALPLAYBACK勾子使应用程序可以把消息插入到系统消息队列中。你可以使用这种勾子来回放之前由WH_JOURNALRECORD勾子记录的一系列的鼠标和键盘事件。常规的鼠标和键盘输入在安装WH_JOURNALPLAYBACK勾子后被禁用。WH_JOURNALPLAYBACK勾子是一种全局勾子,它不能用作线程相关的勾子。
WH_JOURNALPLAYBACK勾子返回一个超时值。此值告诉系统在处理来自回放勾子的当前消息前要等待多少毫秒。这使它可以控制回放事件的速度。
·WH_JOURNALRECORD 勾子
WH_JOURNALRECORD勾子使你可以监视和记录输入事件。典型地,你使用这种勾子来记录一系列的鼠标和键盘事件,以便之后可以用WH_JOURNALPLAYBACK勾子来回放。WH_JOURNALRECORD勾子是一种全局勾子,它不能用作线程相关的勾子。
·WH_KEYBOARD 勾子
WH_KEYBOARD勾子使应用程序可以监视即将返回到GetMessage或PeekMessage函数的WM_KEYDOWN和WM_KEYUP消息。你可以使用WH_KEYBOARD勾子来监视消息队列中的键盘输入。
·WH_KEYBOARD_LL 勾子
WH_KEYBOARD_LL勾子使你可以监视即将投递到线程输入队列的键盘输入事件。
·WH_MOUSE 勾子
WH_MOUSE勾子使你可以监视即将由GetMessage或PeekMessage返回的鼠标消息。你可以使用WH_MOUSE勾子来监视投递到消息队列的鼠标输入。
·WH_MOUSE_LL 勾子
WH_MOUSE_LL勾子使你可以监视即将投递到线程输入队列的鼠标输入事件。
·WH_MSGFILTER 和 WH_SYSMSGFILTER 勾子
WH_MSGFILTER 和 WH_SYSMSGFILTER 勾子使你可以监视即将被菜单、滚动条、消息框,或对话框处理的消息,并可以检测当用户按下 ATL+TAB 或 ATL+ESC 组合键时,另外的窗口将被激活的事件。WH_MSGFILTER勾子只能监视传送给安装勾子过程的应用程序创建的菜单、滚动条、消息框,或对话框的消息。WH_SYSMSGFILTER勾子监视所有程序中的此类消息。
WH_MSGFILTER 和 WH_SYSMSGFILTER 勾子使你可以在模态消息循环中进行消息过滤。比如,应用程序经常在主消息循环中,在从队列取出消息和派发消息之间执行一些特定的处理。然而,在模态消息循环中,系统获取及派发消息,并未提供应用程序过滤消息的机会。如果程序安装了WH_MSGFILTER或WH_SYSMSGFILTER勾子过程,系统将在模态消息循环中调用它。
应用程序可以通过CallMsgFilter函数直接调用WH_MSGFILTER勾子。通过这个函数,应用程序可以在主消息循环和模态消息循环中使用相同的代码。要实现这点,把过滤操作封闭在一个WH_MSGFILTER勾子过程中,然后在GetMessage和DispatchMessage函数间调用CallMsgFilter。
CallMsgFilter的最后一个参数被简单地传入勾子过程;你可以传送任意值。通过定义诸如MSGF_MAINLOOP的常量,勾子过程可以借助它来检测调用的上下文。
·WH_SHELL 勾子
外壳应用程序可以使用WH_SHELL勾子来获得重要的通知。系统在外壳程序即将被激活,或者一个顶层窗口被创建或销毁时调用WH_SHELL勾子过程。
要注意的是自定义的外壳应用程序不会收到WH_SHELL消息。因此,把自己注册为默认外壳的应用程序在能够接收WH_SHELL消息之前,必须调用带SPI_SETMINIMIZEDMETRICS参数的SystemParametersInfo函数。
★安装和释放勾子过程
通过SetWindowsHookEx函数来安装勾子过程。可以指定勾子类型,是否全局勾子,以及勾子过程的入口点。
必须把全局勾子过程放在与安装勾子的应用程序相分离的DLL中。安装勾子的应用程序在安装前必须有该DLL模块的句柄。要获取DLL模块的句柄,调用LoadLibrary。在获得句柄后,调用GetProcAddress函数来获取勾子过程的指针。最后,使用SetWindowsHookEx来把勾子过程的地址放入适当的勾子链中。SetWindowsHookEx传递模块句柄,勾子过程的入口点指针,以及0作为线程标识符,这样即说明该勾子过程应该被关联到与调用线程同一桌面上的所有线程。代码如下所示:
你可以通过调用UnhookWindowsHookEx函数来释放一个线程相关的勾子过程,其中要指定要释放的勾子过程的句柄。
你可以用UnhookWindowsHookEx来释放全局勾子过程,不过这个函数不会释放包含勾子过程的DLL。这是因为全局勾子过程在桌面上所有程序的进程上下文中调用,导致所有那些进程隐式调用了LoadLibrary。由于不能为其他进程调用FreeLibrary,DLL就无法释放了。在所有显式链接到DLL的进程或者终结或者调用了FreeLibrary,且调用勾子过程的所有进程不再执行DLL里的代码时,系统最终释放该DLL。
安装全局勾子过程的另一种办法是在DLL中与勾子过程一起提供一个安装函数。通过这个方法,安装应用程序不需要DLL模块的句柄。通过链接到DLL,应用程序得以访问安装函数。安装函数在调用SetWindowsHookEx时可以提供DLL模块句柄和其他的细节。该DLL也可以包含释放全局勾子过程的函数,应用程序可以在终结时调用它。