Windows应用程序的运行模式是基于消息驱动的,任何线程只要注册了窗口类都会有一个消息队列来接收用户的输入消息和系统消息。为了取得特定线程接收或发送的消息,就要用到Windows提供的钩子。
9.2.1 钩子的概念
钩子(Hook),是Windows消息处理机制中的一个监视点,应用程序可以在这里安装一个子程序(钩子函数)以监视指定窗口某种类型的消息,所监视的窗口可以是其他进程创建的。当消息到达后,在目标窗口处理函数处理之前,钩子机制允许应用程序截获它进行处理。
钩子函数是一个处理消息的程序段,通过调用相关API函数,把它挂入系统。每当特定的消息发出,在没有到达目的窗口前,钩子程序就先捕获该消息,亦即钩子函数先得到控制权。这时钩子函数既可以加工处理(改变)该消息,也可以不作处理而继续传递该消息。
总之,关于Windows钩子要知道以下几点:
(1)钩子是用来截获系统中的消息流的。利用钩子,可以处理任何感兴趣的消息,包括其他进程的消息。
(2)截获消息后,用于处理消息的子程序叫做钩子函数,它是应用程序自定义的一个函数,在安装钩子时要把这个函数的地址告诉Windows。
(3)系统中同一时间可能有多个进程安装了钩子,多个钩子函数在一起组成钩子链。所以在处理截获到的消息时,应该把消息事件传递下去,以便其他钩子也有机会处理这一消息。
钩子会使系统变慢,因为它增加了系统对每个消息的处理量。仅应该在必要时才安装钩子,而且在不需要时应尽快移除。
9.2.2 钩子的安装与卸载
1.安装钩子
SetWindowsHookEx函数可以把应用程序定义的钩子函数安装到系统中。函数用法如下。
SetWindowsHookEx(
int idHook, //指定钩子的类型
HOOKPROC lpfn, //钩子函数的地址。如果使用的是远程的钩子,钩子函数必须放在一个DLL中
HINSTANCE hmod, //钩子函数所在DLL的实例句柄。如果是一个局部的钩子,该参数的值为NULL
DWORD dwThreadId //指定要为哪个线程安装钩子。如果该值为0,那么该钩子将被解释成系统范围内的
);
(1)idHook参数指定了要安装的钩子类型,可以是下列取值之一:
WH_CALLWNDPROC 当目标线程调用SendMessage函数发送消息时,钩子函数被调用
WH_CALLWNDPROCRET 当SendMessage发送的消息返回时,钩子函数被调用
WH_GETMESSAGE 当目标线程调用GetMessage或PeekMessage时
WH_KEYBOARD 当从消息队列中查询WM_KEYUP或WM_KEYDOWN消息时
WH_MOUSE 当调用从消息队列中查询鼠标事件消息时
WH_MSGFILTER 当对话框、菜单或滚动条要处理一个消息时,钩子函数被调用。该钩子是局部的,它是为那些有自己消息处理过程的控件对象设计的
WH_SYSMSGFILTER 和WH_MSGFILTER一样,只不过是系统范围的
WH_JOURNALRECORD 当Windows从硬件队列中获得消息时
WH_JOURNALPLAYBACK 当一个事件从系统的硬件输入队列中被请求时
WH_SHELL 当关于Windows外壳事件发生时,譬如任务条需要重画它的按钮
WH_CBT 当基于计算机的训练(CBT)事件发生时
WH_FOREGROUNDIDLE Windows自己使用,一般的应用程序很少使用
WH_DEBUG 用来给钩子函数除错
(2)lpfn 参数是钩子函数的地址。 钩子安装后如果有相应的消息发生,Windows将调用此参数指向的函数。比如,idHook的值是WH_MOUSE,则当目标线程的消息队列中有鼠标消息取出时,lpfn函数就会被调用。
如果dwThreadId参数是0,或者指定一个由其他进程创建的线程ID,lpfn参数指向的钩子函数必须位于一个DLL中。这是因为进程的地址空间是相互隔离的,发生事件的进程不能调用其他进程地址空间的钩子函数。如果钩子函数的实现代码在DLL中,在相关事件发生时,系统会把这个DLL插入到发生事件的进程的地址空间,使它能够调用钩子函数。这种需要将钩子函数写入DLL以便挂钩其他进程事件的钩子称为远程钩子。
如果dwThreadId参数指定一个由自身进程创建的线程ID,lpfn参数指向的钩子函数只要在当前进程中即可,不必非写入DLL。这种仅钩挂属于自身进程事件的钩子称为局部钩子。
(3)hmod参数是钩子函数所在DLL的实例句柄,如果钩子函数不在DLL中,应将hmod的值设为NULL。
(4)dwThreadId参数指定要与钩子函数相关联的线程ID号。如果设为0,那么该钩子就是系统范围内的,即钩子函数将关联到系统内的所有线程。
2.钩子函数
钩子安装后如果有相应的消息发生,Windows将调用SetWindowHookEx函数指定的钩子函数lpfn。钩子函数的一般形式如下所示。
LRESULT CALLBACK HookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
//....处理该消息的代码
return ::CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
HookProc是应用程序定义的名字。nCode参数是Hook代码,钩子函数使用这个参数来确定任务,它的值依赖于Hook的类型。wParam和lParam参数的值依赖于Hook代码,但是它们典型的值是一些关于发送或者接收消息的信息。
因为系统中可能会有多个钩子存在,所以要调用CallNextHookEx函数把消息传到链中下一个钩子函数。hHook 参数是安装钩子时得到的钩子句柄(SetWindowsHookEx的返回值)。
3.卸载钩子
要卸载钩子,可以调用UnhookWindowsHookEx函数。
BOOL UnhookWindowsHookEx(HHOOK hhk); //hhk为要卸载的钩子的句柄
9.2.3 键盘钩子实例
程序在初始化时安装一个全局的键盘钩子,在运行期间将截拦用户所有的键盘输入。每当用户按键,程序把按键的名称显示在一个编辑框中,并发出"嘟"的一声。
为了能够安装全局钩子,必须创建一个DLL工程,在里面实现键盘钩子回调函数。安装钩子的代码可以在DLL模块中,也可以在主模块中,但是一般在DLL里实现它,主要是为了使程序更加模块化。
本节例子中的钩子函数是09KeyHookLib工程中的KeyHookProc。钩子成功安装上之后,每当有键盘输入,在将这个键盘消息投递给任何线程之前,系统会先调用钩子函数KeyHookProc。这个函数再向主窗口发送一个自定义消息HM_KEY通知主程序。09KeyHookApp程序接收到HM_KEY消息以后就知道有键盘输入了,它将根据消息的参数把具体信息显示到编辑框。
09KeyHookLib和09KeyHookApp工程下,一个是动态链接库工程,一个是Win32应用程序工程。
1.创建DLL工程
(1)使用键盘钩子。键盘钩子的钩子类型是WH_KEYBOARD。当应用程序调用GetMessage或PeekMessage函数,并且有键盘消息(WM_KEYUP或WM_KEYDOWN)将被处理时,系统调用键盘钩子函数。下面是09KeyHookLib工程中安装、卸载钩子的程序代码。
// 一个通过内存地址取得模块句柄的帮助函数
HMODULE WINAPI ModuleFromAddress(PVOID pv)
{
MEMORY_BASIC_INFORMATION mbi;
if(::VirtualQuery(pv, &mbi, sizeof(mbi)) != 0)
{
return (HMODULE)mbi.AllocationBase;
}
else
{
return NULL;
}
}
// 安装、卸载钩子的函数
BOOL WINAPI SetKeyHook(BOOL bInstall, DWORD dwThreadId, HWND hWndCaller)
{
BOOL bOk;
g_hWndCaller = hWndCaller;
if(bInstall)
{
g_hHook = ::SetWindowsHookEx(WH_KEYBOARD, KeyHookProc,
ModuleFromAddress(KeyHookProc), dwThreadId);
bOk = (g_hHook != NULL);
}
else
{
bOk = ::UnhookWindowsHookEx(g_hHook);
g_hHook = NULL;
}
return bOk;
}
自定义函数ModuleFromAddress通过虚拟内存管理函数VirtualQuery返回指定内存地址所处模块的模块句柄。VirtualQuery函数可以取得调用进程虚拟地址空间中指定内存页的状态,将这些信息返回到一个PMEMORY_BASIC_INFORMATION结构中
typedef struct _MEMORY_BASIC_INFORMATION {
PVOID BaseAddress; // 保留区域的基地址
PVOID AllocationBase; // VirtualAlloc函数分配的基地址
DWORD AllocationProtect; // 初次保留时设置的保护属性,可能是PAGE_EXECUTE、PAGE_READWRITE等
SIZE_T RegionSize; // 区域大小
DWORD State; // 状态(提交、保留或空闲)
DWORD Protect; // 当前访问保护属性
DWORD Type; // 页面类型
} MEMORY_BASIC_INFORMATION;
SetKeyHook是09KeyHookLib.dll模块惟一的导出函数,第一个参数bInstall说明是要安装钩子还是要卸载已安装的钩子,第二个参数dwThreadId是目标线程ID号,如果指定为0则说明要安装一个系统范围内的钩子,第三个参数hWndCaller指定主窗口的句柄,钩子函数会向这个窗口发送通知消息。
(2)使用共享数据段。由于此DLL将被映射到不同进程的地址空间,而在每个进程中,钩子函数都要使用钩子句柄和主窗口句柄,以便调用CallNextHookEx函数和向主窗口发送消息。09KeyHookApp程序先加载09KeyHookLib.dll,然后调用它的导出函数SetKeyHook安装钩子,SetKeyHook函数执行时设置钩子句柄和主窗口句柄。钩子成功安装后,Windows将此DLL加载到所有接受键盘消息的其他进程的地址空间,但是在这些进程中变量钩子句柄和主窗口句柄的值却没有正确设置,因为并没有线程为它们赋值。
共享数据段可以很好地解决这一问题。共享数据段中的数据在所有进程中共享一块内存,这意味着在一个进程中设置了共享数据段的数据,其他进程中同一数据段的数据也会随之改变。在程序中添加额外的数据段要使用#pragma data_seg()命令。
下面的代码是引入此命令之后的例子。共享数据段的数据必须初始化,否则它们将被安排到默认的数据段,#pragma data_seg()将不会起作用。
// 共享数据段
#pragma data_seg("YCIShared")
HWND g_hWndCaller = NULL; // 保存主窗口句柄
HHOOK g_hHook = NULL; // 保存钩子句柄
#pragma data_seg()
还要向DLL的DEF文件添加一个SECTIONS语句,以说明此数据段的属性为可读、可写、共享。
SECTIONS
YCIShared Read Write Shared
(3)创建过程。创建一个名称为09KeyHookLib的Win32 Dynamic-Link Library工程,在向导的第1步选择创建一个空的工程(第1个选项),避免VC++自动产生不必要的程序代码。
整个工程一共创建了KeyHookLib.h、KeyHookLib.cpp和KeyHookLib.def三个文件。下面是KeyHookLib.h文件中的程序代码。
// KeyHookLib.h文件
// 定义函数修饰宏,方便引用本DLL工程的导出函数
#ifdef KEYHOOKLIB_EXPORTS
#define KEYHOOKLIB_API __declspec(dllexport)
#else
#define KEYHOOKLIB_API __declspec(dllimport)
#endif
// 自定义与主程序通信的消息
#define HM_KEY WM_USER + 101
// 声明要导出的函数
BOOL KEYHOOKLIB_API WINAPI SetKeyHook(BOOL bInstall,
DWORD dwThreadId = 0, HWND hWndCaller = NULL);
KEYHOOKLIB_EXPORTS宏将定义在KeyHookLib.cpp文件。由于引用本模块的其他工程没有定义这个宏,所以在这些工程中KEYHOOKLIB_API宏被解释成__declspec(dllimport),而在本工程中被解释为__declspec(dllexport)。下面是KeyHookLib.cpp文件中的代码。
// KeyHookLib.cpp文件
#include <windows.h>
#define KEYHOOKLIB_EXPORTS
#include "KeyHookLib.h"
// 共享数据段
#pragma data_seg("YCIShared")
HWND g_hWndCaller = NULL; // 保存主窗口句柄
HHOOK g_hHook = NULL; // 保存钩子句柄
#pragma data_seg()
// 一个通过内存地址取得模块句柄的帮助函数
HMODULE WINAPI ModuleFromAddress(PVOID pv)
{
MEMORY_BASIC_INFORMATION mbi;
if(::VirtualQuery(pv, &mbi, sizeof(mbi)) != 0)
{
return (HMODULE)mbi.AllocationBase;
}
else
{
return NULL;
}
}
// 键盘钩子函数
LRESULT CALLBACK KeyHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if(nCode < 0 || nCode == HC_NOREMOVE)
return ::CallNextHookEx(g_hHook, nCode, wParam, lParam);
if(lParam & 0x40000000) // 消息重复就交给下一个hook链
{
return ::CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
// 通知主窗口。wParam参数为虚拟键码, lParam参数包含了此键的信息
::PostMessage(g_hWndCaller, HM_KEY, wParam, lParam);
return ::CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
// 安装、卸载钩子的函数
BOOL WINAPI SetKeyHook(BOOL bInstall, DWORD dwThreadId, HWND hWndCaller)
{
BOOL bOk;
g_hWndCaller = hWndCaller;
if(bInstall)
{
g_hHook = ::SetWindowsHookEx(WH_KEYBOARD, KeyHookProc,
ModuleFromAddress(KeyHookProc), dwThreadId);
bOk = (g_hHook != NULL);
}
else
{
bOk = ::UnhookWindowsHookEx(g_hHook);
g_hHook = NULL;
}
return bOk;
}
下面是KeyHookLib.def文件中的代码,它定义了导出函数SetKeyHook和共享代码段YCIShared。
EXPORTS
SetKeyHook
SECTIONS
YCIShared Read Write Shared
2.09KeyHookApp工程
主工程相当简单,在主窗口初始化时安装钩子,关闭时卸载钩子,工作期间不断处理钩子函数发来的HM_KEY自定义消息。下面是程序的关键代码。
// KeyHookApp.cpp文件
#include "resource.h"
#include "KeyHookApp.h"
#include "KeyHookLib.h"
#pragma comment(lib, "09KeyHookLib")
CMyApp theApp;
BOOL CMyApp::InitInstance()
{
CMainDialog dlg;
m_pMainWnd = &dlg;
dlg.DoModal();
return FALSE;
}
CMainDialog::CMainDialog(CWnd* pParentWnd):CDialog(IDD_MAIN, pParentWnd)
{
}
BEGIN_MESSAGE_MAP(CMainDialog, CDialog)
ON_MESSAGE(HM_KEY, OnHookKey)
END_MESSAGE_MAP()
BOOL CMainDialog::OnInitDialog()
{
CDialog::OnInitDialog();
SetIcon(theApp.LoadIcon(IDI_MAIN), FALSE);
::SetWindowPos(m_hWnd, HWND_TOPMOST, 0, 0,
0, 0, SWP_NOSIZE|SWP_NOREDRAW|SWP_NOMOVE);
// 安装钩子
if(!SetKeyHook(TRUE, 0, m_hWnd))
MessageBox("安装钩子失败!");
return TRUE;
}
void CMainDialog::OnCancel()
{
// 卸载钩子
SetKeyHook(FALSE);
CDialog::OnCancel();
return;
}
long CMainDialog::OnHookKey(WPARAM wParam, LPARAM lParam)
{
// 此时参数wParam为用户按键的虚拟键码,
// lParam参数包含按键的重复次数、扫描码、前一个按键状态等信息
char szKey[80];
::GetKeyNameText(lParam, szKey, 80);
CString strItem;
strItem.Format(" 用户按键:%s \r\n", szKey);
// 添加到编辑框中
CString strEdit;
GetDlgItem(IDC_KEYMSG)->GetWindowText(strEdit);
GetDlgItem(IDC_KEYMSG)->SetWindowText(strItem + strEdit);
::MessageBeep(MB_OK);
return 0;
}