如果想要在Win32窗体程序中实现按键捕获,可以覆写PreTranslateMessage函数,但是有一个缺点,就是此方法只有当程序窗口获取到焦点时才可以捕获到键盘,如果在其它程序窗口中按下按键,是捕获不到的,为了解决这个问题,就需要使用到钩子
钩子简介
挂钩是一种机制,应用程序可以通过它截获事件,例如消息、鼠标操作和击键。截获特定类型事件的函数称为挂钩过程。挂钩过程可以对它接收的每个事件执行操作,然后修改或放弃该事件。
钩子分为线程钩子(局部钩子)、系统钩子(全局钩子)
线程钩子监视指定线程的事件消息。
系统钩子监视系统中的所有线程的事件消息。因为系统钩子会影响系统中所有的应用程序,所以钩子函数必须放在独立的动态链接库(DLL)中。这是系统钩子和线程钩子很大的不同之处。
钩子的类型
WH_CALLWNDPROC和WH_CALLWNDPROCRET
WH_CBT
WH_DEBUG
WH_FOREGROUNDIDLE
WH_GETMESSAGE
WH_JOURNALPLAYBACK
WH_JOURNALRECORD
WH_KEYBOARD_LL
WH_KEYBOARD
WH_MOUSE_LL
WH_MOUSE
WH_MSGFILTER和WH_SYSMSGFILTER
WH_SHELL
如对于WH_KEYBOARD,有对应的回调函数。回调函数形式
WH_KEYBOARD和WH_KEYBOARD_LL
WH_KEYBOARD_LL是一个Low-Level的钩子, 当Raw Input Thread(RIT)决定从系统消息队列中分发消息之前, 就已经截获了这个消息进行了处理. 所以WH_KEYBOARD_LL甚至会早于系统线程来处理消息, 比如ctrl + alt + del都可以截获, 并且WH_KEYBOARD_LL让系统不需要通过DLL来动态注入所有进程了,系统只会把消息发送到Hook线程. WH_KEYBOARD的层级比较高, 属于应用程序级别的, 所以系统线程会早于这个Hook响应, 并且只能截获系统发送到应用线程messag queue的消息, 但是优先执行该Hook的回调函数,如果Hook回调返回false才会继续处理当前应用的消息响应函数.
这两种钩子都可以作为全局钩子使用, 只需要设置钩子的时候注入线程设成 0
WH_KEYBOARD_LL 在消息发送之前就已经处理了, 而 WH_KEYBOARD是发送到注入线程以后
WH_KEYBOARD_LL 无需系统注入DLL执行, 而 WH_KEYBOARD 需要DLL来注入所有进程空间
使用钩子
使用VS2005新建一个项目TestKeyBoardHook,使用DLL注入的方式使用全局键盘钩子
对于TestKeyBoardHook中的CTestKeyBoardHookDlg类,覆写其中的PreTranslateMessage函数,使窗体接收到按键后能够显示对应的按键
BOOL CTestKeyBoardHookDlg::PreTranslateMessage(MSG* pMsg)
{
// TODO: 在此添加专用代码和/或调用基类
//按键按下会多次触发WM_KEYDOWN消息,因此使用WM_KEYUP消息
if (pMsg->message == WM_KEYUP)
{
switch (pMsg->wParam)
{
case VK_F5:
{
GetDlgItem(IDC_STATIC1)->SetWindowText(L"F5被按下");
}
break;
case VK_F11:
{
GetDlgItem(IDC_STATIC1)->SetWindowText(L"F11被按下");
}
break;
case VK_F12:
{
GetDlgItem(IDC_STATIC1)->SetWindowText(L"F12被按下");
}
break;
}
}
return CDialog::PreTranslateMessage(pMsg);
}
可以看到,当在窗体中按f5等键,能够显示出来,但是窗体失去焦点后,在其它程序中按键,窗体就不能捕获到了

生成KeyHook.dll
对解决方案右键,再添加一个新项目到解决方案,选择MFC DLL

MFC扩展DLL,点击完成

此时共有两个项目,一个KeyHook是要生成的dll,一个TestKeyBoardHook是测试程序

在KeyHook的KeyHook.cpp中定义全局数据段,实现共享数据
#pragma data_seg("myData") //定义全局数据段
HHOOK g_hHook=NULL; //钩子句柄
HINSTANCE g_hInstance=NULL;//dll实例句柄
HWND g_hookWindow=NULL;//窗体句柄
#pragma data_seg()
在KeyHook.def文件中定义段属性
SECTIONS
myData READ WRITE SHARED
在DllMain函数中
extern "C" int APIENTRY
DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
// 如果使用 lpReserved,请将此移除
UNREFERENCED_PARAMETER(lpReserved);
if (dwReason == DLL_PROCESS_ATTACH)
{
TRACE0("KeyHook.DLL 正在初始化!\n");
// 扩展 DLL 一次性初始化
if (!AfxInitExtensionModule(KeyHookDLL, hInstance))
return 0;
// 将此 DLL 插入到资源链中
// 注意: 如果此扩展 DLL 由
// MFC 规则 DLL (如 ActiveX 控件)隐式链接到,
// 而不是由 MFC 应用程序链接到,则需要
// 将此行从 DllMain 中移除并将其放置在一个
// 从此扩展 DLL 导出的单独的函数中。使用此扩展 DLL 的
// 规则 DLL 然后应显式
// 调用该函数以初始化此扩展 DLL。否则,
// CDynLinkLibrary 对象不会附加到
// 规则 DLL 的资源链,并将导致严重的
// 问题。
new CDynLinkLibrary(KeyHookDLL);//把DLL加入动态MFC类库中
g_hInstance=hInstance;//插入保存DLL实例句柄
}
else if (dwReason == DLL_PROCESS_DETACH)
{
TRACE0("KeyHook.DLL 正在终止!\n");
// 在调用析构函数之前终止该库
AfxTermExtensionModule(KeyHookDLL);
}
return 1; // 确定
}
添加按键回调函数
LRESULT CALLBACK KeyboardProc(int code// hook code
,WPARAM wParam// virtual-key code
,LPARAM lParam// keystroke-message information
)
{
if (code >= 0)
{
//如果正在抬起按键,lParam的第31位置1,正在按下第31位置0,这里只检测按键抬起后发送消息给目标窗体
if(lParam&0x80000000){
PostMessage(g_hookWindow,WM_KEYUP,wParam,lParam);
}
}
return CallNextHookEx(g_hHook, code, wParam, lParam);
}
添加安装和卸载dll的函数,为了使函数符号命名按照C语言的形式,使用extern "C"修饰
extern "C" void install_dll(HWND hookWindow){
g_hHook=SetWindowsHookEx(WH_KEYBOARD,KeyboardProc,g_hInstance,0);
g_hookWindow = hookWindow;
}
extern "C" void uninstall_dll()
{
UnhookWindowsHookEx(g_hHook);
g_hHook = NULL;
}
在KeyHook.def文件中导出函数install_dll和uninstall_dll
EXPORTS
; 此处可以是显式导出
install_dll @1
uninstall_dll @2
之后右键KeyHook项目,生成,得到了KeyHook.dll和KeyHook.lib文件

加载KeyHook.dll
对于动态库的加载,有静态加载和动态加载两种方式,对于这里使用动态加载的方式载入
在CTestKeyBoardHookDlg.cpp文件中定义函数指针类型
定义全局变量,函数指针和dll句柄
在CTestKeyBoardHookDlg::OnInitDialog函数中动态载入dll,安装钩子
BOOL CTestKeyBoardHookDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// 将“关于...”菜单项添加到系统菜单中。
// IDM_ABOUTBOX 必须在系统命令范围内。
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != NULL)
{
CString strAboutMenu;
strAboutMenu.LoadString(IDS_ABOUTBOX);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
// 设置此对话框的图标。当应用程序主窗口不是对话框时,框架将自动
// 执行此操作
SetIcon(m_hIcon, TRUE); // 设置大图标
SetIcon(m_hIcon, FALSE); // 设置小图标
// TODO: 在此添加额外的初始化代码
//动态加载dll
hDll=LoadLibrary(L"KeyHook.dll");
installDll = (InstallDll)GetProcAddress(hDll,"install_dll");
unInstallDll = (UnInstallDll)GetProcAddress(hDll,"uninstall_dll");
//安装钩子
installDll(GetSafeHwnd());
return TRUE; // 除非将焦点设置到控件,否则返回 TRUE
}
在析构函数中卸载钩子
之后再次编译运行TestKeyBoardHook,发现在窗口失去焦点,其它程序中按键,程序也可以捕获到按键并显示

注意事项:
编译的dll和测试程序都对应debug版本或者release版本,都是win32位
窗体接收两次按键消息的问题
由于在钩子回调函数中使用PostMessage向窗体发送了一次按键消息,而之后按键消息会再次被传递到窗体,因此窗体接收了两次按键消息,解决办法是在回调函数中向窗体发送消息后直接返回ture,这样就会拦截该消息,不再向下传递
参考
MSDN
https://learn.microsoft.com/en-us/windows/win32/winmsg/about-hooks#wh_keyboard