Windows下使用键盘钩子实现自动化

108 阅读3分钟

Windows下使用键盘钩子实现自动化

键盘钩子是Windows操作系统提供的强大机制,允许应用程序监视和处理系统中的键盘输入事件。
通过拦截和分析键盘消息,我们可以实现各种自动化键盘操作。

实战

打印键盘输入

下面是一个最简单的例子,用于记录用户的键盘输入:

  • 定义键盘钩子的回调函数 PrintKeyboardInputProc 用于打印键盘扫描码、虚拟码和状态;扫描码和虚拟码的区别后文会给出。
  • SetWindowsHookEx设置键盘钩子,第一个参数为WH_KEYBOARD_LL,表示低级键盘钩子,用于捕获所有窗口的键盘事件;
    第二个参数即我们实现的回调函数。
  • while (GetMessage(&msg, NULL, 0, 0)) {} 消息循环是必须的,用于捕获windows窗口消息。
  • 返回 CallNextHookEx(NULL, code, wParam, lParam); 让其他钩子程序也能捕获该事件;如果返回任意大于1的数,其他窗口都无法获取键盘输入,这个逻辑可以用于劫持或重写按键的输入。
#include <stdio.h>
#include <windows.h>

static LRESULT PrintKeyboardInputProc(int code, WPARAM wParam, LPARAM lParam) {
  KBDLLHOOKSTRUCT* ks = (KBDLLHOOKSTRUCT*)lParam;
  printf("scan_code:%lu vk_code:%lu press:%d\n", ks->scanCode, ks->vkCode,
         wParam);
  return CallNextHookEx(NULL, code, wParam, lParam);
}

int main() {
  SetWindowsHookEx(WH_KEYBOARD_LL, PrintKeyboardInputProc, NULL, 0);
  MSG msg;
  while (GetMessage(&msg, NULL, 0, 0)) {
  }
}

编译后执行,输入123abc:
image.png
打印每一次的扫描码、虚拟码和按键状态值。
press:256表示按键按下,press:257表示按键释放。

键盘输入截获并重写

以下代码实现了按键的的重定向,F7音量减,F8音量加:

  • 匹配到F7F8时,返回1,这样其他程序就无法获取F7F8的输入,相当于物理键盘重映射。
#include <stdio.h>
#include <windows.h>

static LRESULT KeyboardProc(int code, WPARAM wParam, LPARAM lParam)
{
    KBDLLHOOKSTRUCT* ks = (KBDLLHOOKSTRUCT*)lParam;
    if (ks->vkCode == VK_F7) {
        DWORD dwFlags = wParam == WM_KEYDOWN ? 0 : KEYEVENTF_KEYUP;
        keybd_event(VK_VOLUME_UP, 0, dwFlags, 0);
        return 1;
    }
    if (ks->vkCode == VK_F8) {
        DWORD dwFlags = wParam == WM_KEYDOWN ? 0 : KEYEVENTF_KEYUP;
        keybd_event(VK_VOLUME_DOWN, 0, dwFlags, 0);
        return 1;
    }
    return CallNextHookEx(NULL, code, wParam, lParam);
}

int main()
{
    SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, NULL, 0);
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) {
    }
}

扫描码与虚拟码

win32 api的键盘事件中有两种键盘码,他们的对比如下:

对比维度扫描码 (Scan Code)虚拟键码 (Virtual-Key Code)
本质硬件原始数据操作系统标准化标识
来源键盘控制器直接生成Windows系统定义
稳定性因键盘而异固定不变
目的标识物理按键位置标识逻辑功能
API转换MapVirtualKey(MAPVK_VSC_TO_VK)MapVirtualKey(MAPVK_VK_TO_VSC)
典型用途键盘驱动、热键工具、游戏外设应用程序快捷键、UI交互

常规的win32窗体程序,使用 keybd_event 或者 SendInput 发送虚拟键码已足够,但在一些涉及更底层的场景中,比如基于DirectX编写的Win32游戏,是无法捕捉到虚拟键码的事件的,它们通常是直接获取底层输入,此时必须使用扫描码。

下面给出一个例子,将F7映射为键盘A输入,不同的是使用的是扫描码输入:

  • keybd_event的第二个参数为扫描码,使用 MapVirtualKey('A', MAPVK_VK_TO_VSC) 来获取扫描码。
  • keybd_event的第三个参数需要相与 KEYEVENTF_SCANCODE
#include <stdio.h>
#include <windows.h>

static LRESULT KeyboardProc(int code, WPARAM wParam, LPARAM lParam)
{
    KBDLLHOOKSTRUCT* ks = (KBDLLHOOKSTRUCT*)lParam;
    if (ks->vkCode == VK_F7) {
        DWORD dwFlags = (wParam == WM_KEYDOWN ? 0 : KEYEVENTF_KEYUP) | KEYEVENTF_SCANCODE;
        keybd_event(0, MapVirtualKey('A', MAPVK_VK_TO_VSC), dwFlags, 0);
        return 1;
    }
    return CallNextHookEx(NULL, code, wParam, lParam);
}

int main()
{
    SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, NULL, 0);
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) {
    }
}