2024年3月21日发(作者:)

第4章钩子函数和窗口子类化

钩子是操作系统消息处理的一种机制。通过钩子,应用程序可以安装一个钩子回调过程让系统调用,从而监视系统

中的消息队列,在这些消息到达目标窗口之前对这些消息进行处理。

本章主要介绍钩子函数的基本概念以及几种常用钩子的应用举例。

4.1 钩子函数

早在Windows3.x的时候,就有了钩子函数,它经历了Windows9x/NT/2000/XP/2003各个操作系统,始终保持了最大

的兼容性。可以说大部分的钩子函数适用于现在所有的Win32操作系统,钩子函数在系统编程方面有着广泛的应用前景。

首先应该承认钩子会降低系统的性能,因为它增加系统处理每一个消息的开销,所以用户除非必要才安装钩子,而

且还要尽可能早地去除钩子。

操作系统支持多种类型的钩子,每种类型都提供了它特有的消息处理机制,比如应用程序使用WH_MOUSE钩子只能

监视鼠标的消息队列。对于每种类型的钩子,系统都维护一个各自独立的钩子链,钩子链是一个指向用户提供的回调函

数钩子过程的链表指针。当与特定类型的钩子相关的窗口消息发生时,系统会把消息依次传递给钩子链中的每一个回调

过程,传递的过程由用户定义的回调过程实现。一般情况下,用户提供的钩子回调过程必须调用钩子链中的下一个回调

过程。否则钩子处理可能会中断,出现不可预测的结果。钩子过程可以监视窗口消息,也可以修改甚至停止钩子消息的

继续传递,不让它到达钩子链中的下一个目标过程。

钩子过程需要用户调用SetWindowsHookEx函数进行安装。钩子过程一般遵循下面的调用规范。

LRESULT CALLBACK HookProc(intnCode,WPARAMwParam,LPARAMlParam);其中HookProc是应用程序提供的函数名。

nCode参数是一个钩子标识码,钩子过程会利用它决定下一步进行的操作。这个标识码的值与安装的钩子类型有关。每种

类型都有它的自身定义。后面两个参数的定义依赖于nCode参数,一般用于存放与窗口消息相关的内容。

SetWindowsHookEx函数会自动安装一个钩子过程,这个过程位于钩子链表的头部,最后安装的钩子函数总是最先得到响

应。前面的钩子处理过程可以决定是否调用钩子链中的下一个过程,这可以通过调用CallNextHookEx函数实现。

注意:某些钩子类型能够监视发生的窗口消息系统自动把消息依次传递给钩子链中的每一个钩子过程,而不管用户是否

调用CallNextHookEx函数。

全局钩子会监视同一桌面环境下所有的窗口消息,而线程钩子只能监视单个线程内发生的消息。由于全局钩子能够

在同一桌面的所有应用环境下调用,所有这个钩子过程必须在一个动态链接库中实现。

注意:全局钩子一般只用于调试目的,应尽可能地避免使用。全局钩子会显著地降低系统的性能,增加系统的开销,并

可能会与安装同一全局钩子的应用程序发生冲突。钩子函数的处理应该尽可能简单,并要快速退出。对于处理复杂的过

程,可以借助于发送异步处理窗口消息的方式实现。

操作系统提供了以下一些钩子,这些钩子允许用户监视系统消息处理的某一个方面。如表4-1所示:

表4-1 钩子类型描述

钩子类型 说明

WH_WNDPROC 安装一个钩子例程,监视传递给目标窗口例程之前的消息

WH_WNDPROCRET 安装一个钩子例程,监视已被目标窗口例程处理之后的消息

WH_CBT 安装一个钩子例程,这个钩子过程在窗口被激活、创建、破坏、最小化、最大化、移动或者窗

口大小发生变化时调用。这个调用发生在系统命令完成之前,在鼠标好键盘事件从系统消息队

列去除之前,在设置输入焦点或者在同步系统消息队列之前。钩子返回值与系统是否允许或者

阻值某种操作有关

WH_DEBUG 安装一个钩子例程,用于调试其他钩子例程

WH_FOREGROUNDIDLE

WH_GETMESSAGE

WH_JOURNALPLAYBACK

WH_JOURNALRECORD

WH_KEYBOARD

WH_KEYBOARD_LL

WH_MOUSE

WH_MOUSE_LL

WH_MSGFILTER

WH_SHELL

WH_SYSMSGFILTER

安装钩子函数要用到SetWindowsHookEx函数。对于全局钩子而言,钩子过程必须在一个动态链接库模块中实现,这

个过程必须作为动态链接库的输出函数,以便能够在安装钩子程序中通过调用LoadLibrary/GetProcAddress函数获得回调

过程的地址,然后把回调函数的地址传递给SetWindowsHookEx函数。

HOOK PROC hkprcSys Msg;

Static HINSTANCE hinstDLL;

Static HHOOK hhookSysMsg;

hinstDLL=LoadLibrary((LPCTSTR)"c:");

安装一个钩子例程,用于当应用程序前台线程线程空闲时被调用,这个钩子对于在系统空闲时

运行一些低优先权的任务特别有用

安装一个钩子例程,用于监视加入到消息队列中的消息,这些消息即将被GetMessage和

PeekMessage函数返回

安装一个钩子例程,用于投递先前被WH_JOURNALRECORD钩子记录的消息

安装一个钩子例程,用于记录投递系统消息队列的输入消息,这个钩子对于记录宏特别有用

安装一个钩子例程,用于监视键盘消息

Windows NT:安装一个钩子例程,用于监视低级别的键盘输入事件

安装一个钩子例程,用于监视鼠标消息

Windows NT:安装一个钩子例程,用于监视低级别的鼠标输入事件

安装一个钩子例程,用于监视由对话框消息框菜单或者滚动条输入事件产生的消息

安装一个钩子例程,用于接收对外壳应用程序有用的通知消息

安装一个钩子例程,用于监视由对话框消息框菜单或者滚动条输入事件产生的消息,这个钩子

将监视系统中所有应用程序中的所有消息

hkprcSysMsg=(HOOKPROC)GetProcAddress(hinstDLL,"SysMessageProc");

hhookSysMsg=SetWindowsHookEx(WH_SYSMSGFILTER,hkprcSysMsg,hinstDLL,0);

同样释放全局钩子函数要用到UnhookWindowsHookEx,这个钩子函数本身不会释放包含钩子过程的动态链接库。这

是因为全局钩子会被同一桌面的所有应用程序调用,系统会自动调用LoadLibrary函数,把实现钩子过程的动态链接库映

射到受它影响的当前进程的地址空间中去。同样,最后系统会自动在所有使用该动态链接库的应用程序不再使用这个钩

子时,调用FreeLibrary函数释放动态链接库。

编写全局钩子,最好不要使用MFC,也尽可能地不要使用C运行库函数,应该尽可能地使用API函数代替使用这些

函数,比如常用lstrcpy代理strcpy、_tcscpy等。这是因为全局钩子会被映射到受它影响的所有进程的地址空间,一个不

依赖这些运行库的运行的程序可能会产生更多的潜在冲突。比如一个采用VisualBasic或者Java编写的应用程序,它们不

会使用MFC类库。另外全局钩子中的全局变量,当映射到某个进程中时,它将变成属于该进程所有的私有变量,不能被

其他进程共享,因为它们映射的地址是不同的。为了实现变量共享,可以使用前面谈到的共享节的办法。

#pragma data_seg(".Share")

HANDLE hWnd=NULL;

#pragma dta_seg()

#pragma comment(linker,"/section:.Share,rws")

当然还可以借助于其他方式进行通信,比如共享内存等。如果用户采用窗口消息(SendMessge、PostMessage),一定

不要采用传递指针的方法,因为在另外一个进程中无法访问另外一个进程的地址空间。

4.2 键盘钩子的应用

键盘钩子有着广泛的用途,比如在WindowsNT环境下,用户可以直接调用GetLastInputInfo函数获得上一次输入的信

息,通过这个信息得到一个当时的时间戳,通过该时间决定在若干时间内没有用户输入(键盘和鼠标)触发某些任务。然而

Windows9x并没有提供这样的函数,为了得到上一次键盘和鼠标动作的时间,就必须实现一个全局键盘和鼠标,记录钩

子函数回调时发生的时间。另外键盘钩子禁止用户在多个进程间切换。Windows提供的多任务机制使用户可以自由地在

多个应用程序间自由切换,每一个应用程序作为一个进程都拥有独立的进程地址空间,各个程序之间互不影响。特别是

在WindowsNT环境下,一个应用程序的挂起,一般不会影响其他程序的运行,操作系统可以很轻松地把挂起的进程杀死,

从而使系统得到正常响应。然而这种机制同时也会助长用户同时运行多个程序,开多个窗口,从而使系统不堪重负,

反应迟缓。这对于普通的个人用机不会有什么影响,但对于工业实时控制计算机而言,情况就大不一样。由于WindowsNT

并不是一个实时的操作系统,一个程序的运行尽管不会直接影响其他程序,但是如果它对系统资源如CPU、内存占用过

多,就会直接影响其他程序的快速响应,比如完全格式化一个质量不好的软盘、浏览次品光盘、刻录光盘、复制大文件

等。因此对于运行重要程序的计算机而言,重要程序应该独占系统资源,禁止任务切换,以便提高系统的实时性和可靠

性,以防意外事件发生。另外,对于文献检索的公共机房,机房工作人员一般也不希望检索人员来回切换程序。

(1)在Windows9x环境下,应用程序可以通过不同的参数调用SystemParametersInfo函数,实现允许和禁止任务切换。

方法如下:

例4-1Windows9x环境下禁止任务切换。

UINT nPreviousState;

SystemParametersInfo(SPI_SETSCREENSAVERRUNNING,TRUE,&nPreviousState,0); //禁止任务切换

SystemParametersInfo(SPI_SETSCREENSAVERRUNNING,FALSE,&nPreviousState,0); //允许任务切换

只是应用程序退出前,必须恢复允许任务切换状态。

(2)对于WindowsNT4.0ServicePack3或更高版本,包括Windows2000和WindowsXP,应用程序可以通过安装低级键盘

钩子(WH_KEYBOARD_LL)实现禁止任务切换。

在Windows9x/Me环境下不起作用。

#define _WIN32_WINNT 0x0400

HHOOK hhkLowLevelKybd;

LRESULT CALLBACK LowLevelKeyboardProc(int nCode,WPARAM wParam,LPARAM lParam)

{ KBDLLHOOK STRUCT*pkbhs=(KBDLLHOOKSTRUCT*)lParam;

BOOL bControlKeyDown=0;

switch(nCode)

{ Case HC_ACTION:

bControlKeyDown=GetAsyncKeyState(VK_CONTROL)>>((sizeof(SHORT)*8) -1); //检查是否按下Ctrl键

if(pkbhs->vkCode==VK_ESCAPE&&bControlKeyDown) return 1; //禁止Ctrl+Esc

if(pkbhs->vkCode==VK_TAB&&pkbhs->flags&LLKHF_ALTDOWN) return 1; //禁止Alt+Tab

if(pkbhs->vkCode==VK_ESCAPE&&pkbhs->flags&LLKHF_ALTDOWN) return 1; break; //禁止Alt+Esc

default: break;

return CallNextHookEx(hhkLowLevelKybd,nCode,wParam,lParam); }

int WINAPIWinMain(HINSTANCE hinstExe,HINSTANCE,PTSTR pszCmdLine,int)

{ hhkLowLevelKybd=SetWindowsHookEx(WH_KEYBOARD_LL,LowLevelKeyboardProc,hinstExe,0); //安装钩子过程

MessageBox(NULL,TEXT("Alt+Esc,Ctrl+Esc,and Alt+Tab are now disabled.")

TEXT("Click"Ok" to terminate this application and re-enable these keys."),

TEXT("Disable Low-Level Keys"),MB_OK);

UnhookWindowsHookEx(hhkLowLevelKybd); return(0); }

(3)在WindowsNT环境下还有一种方法,通过枚举窗口,禁止除当前窗口以外的所有窗口,程序退出前,恢复这些窗

口为允许状态。这种方法同时适用于Windows9x和WindowsNT所有版本,只是无法禁止Ctrl+ESC键。

例4-2 WindowsNT环境下禁止任务切换。

#define _WIN32_WINNT0x0400

Typedef struct _DLONG