第 7 章 视窗报文 --- 7.6 键盘输入线程
本节深入剖析 Windows/ReactOS 中"键盘输入"(Keyboard Input)的内核态处理,包括 RIT(Raw Input Thread)架构、原始 KEYBOARD_INPUT_DATA 转换、VK 码处理、Numpad 翻译、LED 指示灯同步、字符消息生成、IME 与 LL 钩子等。
概述
键盘输入是 Windows GUI 系统最核心的 I/O 来源之一。Windows 的键盘处理需要解决以下问题:
- 原始数据 → 消息转换 :将键盘类驱动(KBDCLASS)产生的
KEYBOARD_INPUT_DATA转换为WM_KEYDOWN/WM_KEYUP/WM_CHAR/WM_DEADCHAR等消息; - VK 码处理:区分左右 Shift/Ctrl/Alt、处理扩展键标志;
- 键盘布局(Layout):根据当前语言(美式/法语/日语)将 VK 码翻译为字符;
- 状态维护:NumLock/CapsLock/ScrollLock 状态、按键 down/up 状态;
- 特殊输入:Alt+Numpad 字符输入(Unicode 直接输入)、VK_PACKET(来自 SendInput 的 Unicode);
- IME(输入法):将按键路由到 IME 进行中文/日文/韩文输入;
- 热键(HotKey):RegisterHotKey 注册的全局热键;
- LL 钩子:在原始数据级别拦截(WH_KEYBOARD_LL)。
键盘输入的本质是什么?
键盘输入是一种"原始数据 → 消息流"的转换过程。键盘类驱动(KBDCLASS)从硬件读取扫描码,包装为
KEYBOARD_INPUT_DATA投递到 RIT(Raw Input Thread,原始输入线程);RIT 在 win32k 内核态将原始数据转换为标准的窗口消息,并维护全局按键状态供GetAsyncKeyState/GetKeyState查询。
想象一个"国际邮件分拣中心"场景:
- 键盘硬件:各国的寄件人(说不同语言的);
- 扫描码(ScanCode):邮件上的"国家代码"(如 0x1C 是键盘 A 键的物理位置);
- VK 码:经过初步分拣后的"语言代码"(如 VK_A);
- KBDTABLES(键盘布局表):翻译字典,将 VK 码翻译为特定语言的字符;
- WM_KEYDOWN/WM_KEYUP:邮件本身的"按下/释放"状态;
- WM_CHAR:翻译后的"文字内容"(如 'a' 或 'A');
- IME:分拣中心的"特殊处理柜台"(把邮件路由到日语/中文处理);
- LL 钩子:分拣中心的"安检 X 光机"(可拦截并改派邮件);
- gafAsyncKeyState:分拣中心的"实时状态板"(记录哪个键当前是 down)。
本节内容概览
- 7.6.0 框架图:键盘输入子系统的整体架构;
- 7.6.1 键盘输入子系统架构(KBDCLASS → win32k → 窗口消息);
- 7.6.2 键盘初始化(InitKeyboardImpl、UserInitKeyboard);
- 7.6.3 VK 码转换:IntSimplifyVk / IntFixVk;
- 7.6.4 Numpad 翻译:IntTranslateNumpadKey;
- 7.6.5 LED 指示灯与锁定键;
- 7.6.6 gafAsyncKeyState 异步按键状态数组;
- 7.6.7 Alt+Numpad 字符输入;
- 7.6.8 co_IntProcessKeyboardMessage 核心处理;
- 7.6.9 字符消息生成(WM_CHAR / WM_DEADCHAR / ToUnicodeEx);
- 7.6.10 IME 处理与全局热键;
- 7.6.11 键盘 LL 钩子;
- 7.6.12 设计哲学问答:5 个关键设计问题解答。
学习目标
读完本节后,读者应当能够:
- 理解 RIT 原始输入线程的角色与数据流;
- 掌握 VK 码的左右手区分、扩展标志处理;
- 解释键盘布局(KBDTABLES)与字符翻译机制;
- 理解 NumLock/CapsLock/ScrollLock 状态同步与 LED 控制;
- 掌握 gafAsyncKeyState 的两位状态语义(down / locked);
- 分析 Alt+Numpad Unicode 字符输入状态机;
- 理解 IME 在键盘事件流中的角色;
- 掌握 WH_KEYBOARD_LL 钩子的实现。
涉及的内核子系统
| 子系统 | 职责 |
|---|---|
| ntoskrnl / kbdclass | 键盘类驱动:硬件扫描码 → KEYBOARD_INPUT_DATA |
| win32k.sys/ntuser/keyboard.c | 键盘消息处理、VK 转换、字符翻译、LL 钩子 |
| win32k.sys/ntuser/msgqueue.c | co_IntProcessKeyboardMessage:消息分派 |
| win32k.sys/ntuser/kbdlayout.c | KBDTABLES 加载与维护 |
| win32k.sys/ntuser/ime.c | IntImmProcessKey:IME 处理 |
| win32k.sys/ntuser/input.c | UserProcessKeyboardInput:原始输入入口 |
| user32.dll | ToUnicodeEx、GetKeyState、RegisterHotKey 用户态 API |
7.6.0 框架图
┌──────────────────────────────────────────────────────────────────────────────────────┐
│ 键盘输入子系统完整架构 │
├──────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 硬件层 │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ PS/2 / USB 键盘硬件 │ │
│ │ 产生中断(IRQ1 / USB HID) │ │
│ │ 键盘端口驱动(i8042prt / kbdhid)读取扫描码 │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 内核类驱动层 │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ kbdclass.sys (键盘类驱动) │ │
│ │ ├─► 维护 IRP_MJ_READ 队列 │ │
│ │ └─► 将扫描码包装为 KEYBOARD_INPUT_DATA 投递到 RIT │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ (RIT: \Device\KeyboardClass0 的读 IRP 完成) │
│ win32k 用户子系统(RIT 在 Win32k 内核态) │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ input.c UserProcessKeyboardInput(KEYBOARD_INPUT_DATA) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ keyboard.c │ │
│ │ ├─► 扫描码 → VK 码 (IntVkFromSc / pKbdTbl->pusVSCtoVK) │ │
│ │ ├─► 修正 VK (IntFixVk: 左右 Shift/Ctrl/Alt) │ │
│ │ ├─► 简化 VK (IntSimplifyVk) │ │
│ │ ├─► 更新 gafAsyncKeyState (UpdateAsyncKeyState) │ │
│ │ ├─► WH_KEYBOARD_LL 钩子 (co_CallLowLevelKeyboardHook) │ │
│ │ ├─► 更新 LED 灯 (IntKeyboardUpdateLeds) │ │
│ │ ├─► Alt+Numpad 状态机 (gAltNumPadState) │ │
│ │ └─► 投递硬件消息 (MsqPostHardwareMessage) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ msgqueue.c HardwareMessagesListHead ─► co_MsqPeekHardwareMessage │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ co_IntProcessKeyboardMessage │ │
│ │ ├─► 修正 VK 区分左右 (IntSimplifyVk) │ │
│ │ ├─► WH_JOURNALRECORD 钩子 │ │
│ │ ├─► F1 键特殊处理 (WM_HELP) │ │
│ │ ├─► Alt-Tab/ESC 系统键处理 (SC_NEXTWINDOW/SC_PREVWINDOW) │ │
│ │ ├─► WH_KEYBOARD 钩子 │ │
│ │ └─► IME 处理 (IntImmProcessKey) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ user32.dll TranslateMessage / DispatchMessage │ │
│ │ ├─► ToUnicodeEx 字符翻译 │ │
│ │ └─► WndProc 收到 WM_KEYDOWN / WM_CHAR / WM_DEADCHAR │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ 键盘状态与翻译表 │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ gafAsyncKeyState[64] : 256 个 VK 码 × 2 bit (down / locked) │ │
│ │ gafAsyncKeyStateRecentDown : 256 个 VK 码 × 1 bit (最近 down) │ │
│ │ gAltNumPadState : Alt+Numpad 状态机 │ │
│ │ gKeyboardInfo : 键盘类型/子类型/功能键数量 │ │
│ │ gIndicators : LED 状态 (Caps/Num/Scroll Lock) │ │
│ │ gpKeyboardIndicatorTrans : 指示灯翻译表 │ │
│ │ PKBDTABLES pKbdTbl : 当前线程的键盘布局表 │ │
│ │ ├─► pVkToWcharTable : VK → WCHAR 翻译 │ │
│ │ ├─► pusVSCtoVK : ScanCode → VK │ │
│ │ └─► pDeadKey : Dead Key 组合规则 │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────┘
7.6.1 键盘输入子系统架构
7.6.1.1 RIT(Raw Input Thread)原始输入线程
RIT 是 Windows 子系统启动时创建的特殊线程,专门负责处理来自设备的原始输入数据(键盘、鼠标、其他 HID 设备)。它的工作流程:
键盘硬件
│ 中断(IRQ1)
▼
i8042prt / kbdhid 端口驱动
│ 读取扫描码
▼
kbdclass 键盘类驱动
│ 包装为 KEYBOARD_INPUT_DATA
│ 完成 IRP_MJ_READ
▼
RIT 线程(等待 IRP 完成)
│ UserProcessKeyboardInput(data)
▼
win32k 键盘处理
为什么需要 RIT?
- 统一输入源:所有键盘设备(PS/2、USB、蓝牙)的输入都汇总到 RIT,避免每个设备驱动各自处理;
- 单线程输入上下文 :避免多线程并发修改
gafAsyncKeyState等全局状态; - 优先级控制:RIT 在高优先级运行,确保输入响应实时性。
7.6.1.2 UserProcessKeyboardInput
win32ss/user/ntuser/input.c(file:///d:/reactos/win32ss/user/ntuser/input.c) 中定义 UserProcessKeyboardInput:
c
VOID NTAPI
UserProcessKeyboardInput(KEYBOARD_INPUT_DATA *KeyData)
{
WORD wVk;
PKBDTABLES pKbdTbl;
DWORD dwFlags = 0;
BOOL bIsDown;
// 1. 获取当前线程的键盘布局
pKbdTbl = IntGetCurrentKbdTables();
// 2. 扫描码 → VK 码
wVk = IntVkFromSc(pKbdTbl, KeyData->MakeCode, KeyData->Flags & KEY_E0 ? 1 : 0);
// 3. 区分左右 Shift/Ctrl/Alt
if (KeyData->Flags & KEY_E0)
wVk = IntFixVk(wVk, TRUE);
else
wVk = IntFixVk(wVk, FALSE);
// 4. 设置 KeyEventF 标志
if (!(KeyData->Flags & KEY_E0)) dwFlags |= 0;
if (KeyData->Flags & KEY_E1) dwFlags |= KEYEVENTF_EXTENDEDKEY;
bIsDown = !(KeyData->Flags & KEY_BREAK);
// 5. 更新全局按键状态
UpdateAsyncKeyState(wVk, bIsDown);
// 6. 更新 LED 灯
if (wVk == VK_CAPITAL || wVk == VK_NUMLOCK || wVk == VK_SCROLL)
IntKeyboardUpdateLeds(...);
// 7. 处理 Alt+Numpad 字符输入
if (IntHandleAltNumpad(...))
return;
// 8. WH_KEYBOARD_LL 钩子
if (co_CallLowLevelKeyboardHook(wVk, ...))
return;
// 9. 投递硬件消息
MsqPostHardwareMessage(WM_KEYDOWN/WM_KEYUP, ...);
}
7.6.1.3 消息队列中的硬件消息
键盘消息进入硬件消息队列 (HardwareMessagesListHead),与普通投递消息(PostedMessagesListHead)分离:
c
// win32ss/user/ntuser/msgqueue.c
typedef struct _USER_MESSAGE_QUEUE
{
LIST_ENTRY HardwareMessagesListHead; // 硬件消息(键盘/鼠标)
LIST_ENTRY PostedMessagesListHead; // 普通投递消息
// ...
}
为何分离?
- 优先级:硬件消息优先级高于投递消息(避免输入延迟);
- 过滤机制 :
GetMessage可以选择只读取硬件消息(wMsgFilterMin/Max设置为WM_KEYFIRST...WM_KEYLAST); - 抢占 :通过
co_MsqPeekHardwareMessage中的ptiSysLock机制,确保同一时间只有一个线程处理硬件消息。
7.6.2 键盘初始化
7.6.2.1 InitKeyboardImpl
win32ss/user/ntuser/keyboard.c:45-57(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L45-L57):
c
CODE_SEG("INIT")
NTSTATUS NTAPI
InitKeyboardImpl(VOID)
{
RtlZeroMemory(&gafAsyncKeyState, sizeof(gafAsyncKeyState));
RtlZeroMemory(&gafAsyncKeyStateRecentDown, sizeof(gafAsyncKeyStateRecentDown));
// 初始化默认键盘信息
RtlZeroMemory(&gKeyboardInfo, sizeof(gKeyboardInfo));
gKeyboardInfo.KeyboardIdentifier.Type = 4; // AT-101
gKeyboardInfo.NumberOfFunctionKeys = 12; // 12 个 F 键
return STATUS_SUCCESS;
}
InitKeyboardImpl 在 win32k 启动时(DriverEntry)调用,清零全局状态并设置默认键盘类型。
7.6.2.2 UserInitKeyboard
win32ss/user/ntuser/keyboard.c:177-227(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L177-L227):
c
VOID NTAPI
UserInitKeyboard(HANDLE hKeyboardDevice)
{
NTSTATUS Status;
IO_STATUS_BLOCK Block;
// 1. 获取指示灯翻译表(哪个 ScanCode 控制哪个 LED)
IntKeyboardGetIndicatorTrans(hKeyboardDevice, &gpKeyboardIndicatorTrans);
// 2. 查询当前 LED 状态
Status = ZwDeviceIoControlFile(hKeyboardDevice, NULL, NULL, NULL, &Block,
IOCTL_KEYBOARD_QUERY_INDICATORS,
NULL, 0, &gIndicators, sizeof(gIndicators));
if (!NT_SUCCESS(Status))
{
WARN("NtDeviceIoControlFile() failed, ignored\n");
gIndicators.LedFlags = 0;
gIndicators.UnitId = 0;
}
// 3. 同步 gafAsyncKeyState 的 LED 锁定状态
SET_KEY_LOCKED(gafAsyncKeyState, VK_CAPITAL,
gIndicators.LedFlags & KEYBOARD_CAPS_LOCK_ON);
SET_KEY_LOCKED(gafAsyncKeyState, VK_NUMLOCK,
gIndicators.LedFlags & KEYBOARD_NUM_LOCK_ON);
SET_KEY_LOCKED(gafAsyncKeyState, VK_SCROLL,
gIndicators.LedFlags & KEYBOARD_SCROLL_LOCK_ON);
// 4. 查询键盘属性
Status = ZwDeviceIoControlFile(hKeyboardDevice, NULL, NULL, NULL, &Block,
IOCTL_KEYBOARD_QUERY_ATTRIBUTES,
NULL, 0, &gKeyboardInfo, sizeof(gKeyboardInfo));
// ...
}
UserInitKeyboard 在每个键盘设备打开时调用,与硬件同步初始状态。
7.6.2.3 IntKeyboardGetIndicatorTrans
win32ss/user/ntuser/keyboard.c:66-115(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L66-L115):
通过 IOCTL_KEYBOARD_QUERY_INDICATOR_TRANSLATION 查询"哪个 ScanCode 控制哪个 LED"。这是因为不同键盘的 LED 控制方式不同:
c
typedef struct _KEYBOARD_INDICATOR_TRANSLATION {
USHORT NumberOfIndicatorKeys;
struct {
USHORT MakeCode; // ScanCode
USHORT IndicatorFlags; // 对应的 LED 标志
} IndicatorList[];
} KEYBOARD_INDICATOR_TRANSLATION;
7.6.3 VK 码转换:IntSimplifyVk / IntFixVk
7.6.3.1 左右手键区分
Windows VK 码中,部分键区分左右手:
| 简化 VK | 左手 | 右手 | 备注 |
|---|---|---|---|
VK_SHIFT (0x10) |
VK_LSHIFT (0xA0) |
VK_RSHIFT (0xA1) |
物理上是两个键 |
VK_CONTROL (0x11) |
VK_LCONTROL (0xA2) |
VK_RCONTROL (0xA3) |
|
VK_MENU (0x12) |
VK_LMENU (0xA4) |
VK_RMENU (0xA5) |
右 Alt = AltGr |
两种语义:
- 简化 VK (
VK_SHIFT):用于GetAsyncKeyState、lParam中------只关心"是否按下了 Shift"; - 完整 VK (
VK_LSHIFT/VK_RSHIFT):用于WM_KEYDOWN的wParam------需要区分左右手(如游戏中 WASD 走位用左 Shift 蹲下、右 Shift 喷气)。
7.6.3.2 IntSimplifyVk
win32ss/user/ntuser/keyboard.c:234-255(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L234-L255):
c
static WORD IntSimplifyVk(WORD wVk)
{
switch (wVk)
{
case VK_LSHIFT:
case VK_RSHIFT:
return VK_SHIFT;
case VK_LCONTROL:
case VK_RCONTROL:
return VK_CONTROL;
case VK_LMENU:
case VK_RMENU:
return VK_MENU;
default:
return wVk;
}
}
将左右手 VK 合并为简化 VK。
7.6.3.3 IntFixVk
win32ss/user/ntuser/keyboard.c:262-280(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L262-L280):
c
static WORD IntFixVk(WORD wVk, BOOL bExt)
{
switch (wVk)
{
case VK_SHIFT:
return bExt ? VK_RSHIFT : VK_LSHIFT;
case VK_CONTROL:
return bExt ? VK_RCONTROL : VK_LCONTROL;
case VK_MENU:
return bExt ? VK_RMENU : VK_LMENU;
default:
return wVk;
}
}
bExt 表示扩展键标志 (来自扫描码的 E0/E1)。当用户按下右 Shift 时,硬件会发送 ScanCode=0x36 + Flags=KEY_E0,win32k 据此区分左右。
7.6.3.4 简化 vs 修正的语义差异
场景 :用户在键盘输入线程中接收到 WM_KEYDOWN,wParam 是哪个 VK?
wParam:经过IntFixVk修正,区分左右 (如VK_LSHIFT);lParam:包含KF_ALTDOWN、KF_EXTENDED等位标志;GetAsyncKeyState(VK_SHIFT):用IntSimplifyVk简化,不区分左右;GetKeyboardState():返回 256 字节的全局状态数组,每键 1 bit down + 1 bit locked。
7.6.4 Numpad 翻译:IntTranslateNumpadKey
7.6.4.1 NumLock 状态差异
数字小键盘(Numpad)在 NumLock 开启时是数字键,关闭时是方向键/翻页键:
| ScanCode | NumLock 开 | NumLock 关 |
|---|---|---|
| 0x52 (Insert) | VK_NUMPAD0 ('0') | VK_INSERT |
| 0x4F (End) | VK_NUMPAD1 ('1') | VK_END |
| 0x50 (Down) | VK_NUMPAD2 ('2') | VK_DOWN |
| ... | ... | ... |
7.6.4.2 IntTranslateNumpadKey
win32ss/user/ntuser/keyboard.c:287-306(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L287-L306):
c
static WORD IntTranslateNumpadKey(WORD wVk)
{
switch (wVk)
{
case VK_INSERT: return VK_NUMPAD0;
case VK_END: return VK_NUMPAD1;
case VK_DOWN: return VK_NUMPAD2;
case VK_NEXT: return VK_NUMPAD3;
case VK_LEFT: return VK_NUMPAD4;
case VK_CLEAR: return VK_NUMPAD5;
case VK_RIGHT: return VK_NUMPAD6;
case VK_HOME: return VK_NUMPAD7;
case VK_UP: return VK_NUMPAD8;
case VK_PRIOR: return VK_NUMPAD9;
case VK_DELETE: return VK_DECIMAL;
default: return wVk;
}
}
调用时机 :TranslateMessage 函数中,当 VK 在 Numpad 范围时调用,决定产生 WM_KEYDOWN 时是 VK_NUMPAD0 还是 VK_INSERT。
7.6.5 LED 指示灯与锁定键
7.6.5.1 IntKeyboardUpdateLeds
win32ss/user/ntuser/keyboard.c:122-170(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L122-L170):
c
static NTSTATUS APIENTRY
IntKeyboardUpdateLeds(HANDLE hKeyboardDevice, WORD wVk, WORD wScanCode)
{
NTSTATUS Status;
UINT i;
USHORT LedFlag = 0;
IO_STATUS_BLOCK Block;
if (!gpKeyboardIndicatorTrans)
return STATUS_NOT_SUPPORTED;
// 1. 根据 VK 码或 ScanCode 确定 LED 标志
switch (wVk)
{
case VK_CAPITAL: LedFlag = KEYBOARD_CAPS_LOCK_ON; break;
case VK_NUMLOCK: LedFlag = KEYBOARD_NUM_LOCK_ON; break;
case VK_SCROLL: LedFlag = KEYBOARD_SCROLL_LOCK_ON; break;
default:
for (i = 0; i < gpKeyboardIndicatorTrans->NumberOfIndicatorKeys; i++)
{
if (gpKeyboardIndicatorTrans->IndicatorList[i].MakeCode == wScanCode)
{
LedFlag = gpKeyboardIndicatorTrans->IndicatorList[i].IndicatorFlags;
break;
}
}
}
if (LedFlag)
{
// 2. 切换 LED 状态
gIndicators.LedFlags ^= LedFlag;
// 3. 发送 IOCTL 给键盘驱动
Status = ZwDeviceIoControlFile(hKeyboardDevice, NULL, NULL, NULL, &Block,
IOCTL_KEYBOARD_SET_INDICATORS,
&gIndicators, sizeof(gIndicators),
NULL, 0);
return Status;
}
return STATUS_SUCCESS;
}
7.6.5.2 LED 同步
LED 状态需要内核态 (gIndicators)与硬件 (键盘上的物理灯)以及用户态 (gafAsyncKeyState 的 locked 位)三方同步:
[用户按下 CapsLock 键]
│
▼
[键盘中断] ──► 扫描码 = 0x3A
│
▼
[kbdclass] ──► KEYBOARD_INPUT_DATA { MakeCode=0x3A, Flags=KEY_BREAK (这里假设是 release) }
│
▼
[RIT: UserProcessKeyboardInput]
│
├─► wVk = VK_CAPITAL
├─► UpdateAsyncKeyState(VK_CAPITAL, TRUE) ──► gafAsyncKeyState 翻转 locked 位
├─► IntKeyboardUpdateLeds(VK_CAPITAL)
│ ├─► gIndicators.LedFlags ^= KEYBOARD_CAPS_LOCK_ON
│ └─► IOCTL_KEYBOARD_SET_INDICATORS ──► 键盘驱动
│ └─► 硬件 LED 灯亮/灭
▼
[MsqPostHardwareMessage(WM_KEYDOWN, VK_CAPITAL, ...)]
│
▼
[WndProc 收到 WM_KEYDOWN VK_CAPITAL]
关键设计 :三个状态(gafAsyncKeyState / gIndicators / 硬件 LED)始终保持一致。任何修改都必须三处同时更新。
7.6.6 gafAsyncKeyState 异步按键状态数组
7.6.6.1 数据结构
win32ss/user/ntuser/keyboard.c:13-14(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L13-L14):
c
BYTE gafAsyncKeyState[256 * 2 / 8]; // 2 bits per key × 256 keys = 64 bytes
static BYTE gafAsyncKeyStateRecentDown[256 / 8]; // 1 bit per key × 256 keys = 32 bytes
gafAsyncKeyState:每键 2 bit
- bit 0 (0x01):key down state(当前是否按下)
- bit 1 (0x02):key locked state(是否处于 locked 状态,如 Caps Lock)
gafAsyncKeyStateRecentDown:每键 1 bit
- bit 0 (0x01):key was down since last call(自上次查询以来是否被按下过)
7.6.6.2 操作宏
c
#define IS_KEY_DOWN(kst, vk) ((kst)[(vk) * 2 / 8] & (1 << ((vk) & 7) * 2))
#define SET_KEY_DOWN(kst, vk, f) ...
#define IS_KEY_LOCKED(kst, vk) ((kst)[(vk) * 2 / 8] & (1 << (((vk) & 7) * 2 + 1)))
#define SET_KEY_LOCKED(kst, vk, f) ...
7.6.6.3 UpdateAsyncKeyState
win32ss/user/ntuser/keyboard.c:681-696(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L681-L696):
c
static VOID NTAPI
UpdateAsyncKeyState(WORD wVk, BOOL bIsDown)
{
if (bIsDown)
{
/* If it's first key down event, xor lock bit */
if (!IS_KEY_DOWN(gafAsyncKeyState, wVk))
SET_KEY_LOCKED(gafAsyncKeyState, wVk, !IS_KEY_LOCKED(gafAsyncKeyState, wVk));
SET_KEY_DOWN(gafAsyncKeyState, wVk, TRUE);
gafAsyncKeyStateRecentDown[wVk / 8] |= (1 << (wVk % 8));
}
else
SET_KEY_DOWN(gafAsyncKeyState, wVk, FALSE);
}
关键逻辑 :从"未按下"转为"按下"时(首次 down 事件),翻转 locked 位。这就是 CapsLock/NumLock/ScrollLock 的工作原理:
- 第一次按 CapsLock:down=TRUE,prev down=FALSE ──► 翻转 locked ──► locked=TRUE;
- 第二次按 CapsLock:down=TRUE,prev down=FALSE ──► 翻转 locked ──► locked=FALSE。
7.6.6.4 NtUserGetAsyncKeyState
win32ss/user/ntuser/keyboard.c:647-674(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L647-L674):
c
SHORT APIENTRY
NtUserGetAsyncKeyState(INT Key)
{
WORD wRet = 0;
if (Key >= 0x100 || Key < 0)
{
EngSetLastError(ERROR_INVALID_PARAMETER);
return 0;
}
UserEnterExclusive();
if (IS_KEY_DOWN(gafAsyncKeyState, Key))
wRet |= 0x8000; // 高位 = 0x8000 表示当前 down
if (gafAsyncKeyStateRecentDown[Key / 8] & (1 << (Key % 8)))
wRet |= 0x1; // 低位 = 0x1 表示自上次查询以来曾 down
gafAsyncKeyStateRecentDown[Key / 8] &= ~(1 << (Key % 8)); // 清除 recent down
UserLeave();
return wRet;
}
返回值语义:
0x8000位:当前键是否 down(实时状态);0x0001位:自上次调用GetAsyncKeyState以来该键是否被按下过(边沿触发,调用后清除);- 完整
SHORT状态:如0x8001表示"当前 down 且自上次查询以来曾 down"。
注意 :调用后 RecentDown 自动清除------这是"边沿触发"语义,用于检测"是否发生了按键"而非"键是否持续按下"。
7.6.7 Alt+Numpad 字符输入
7.6.7.1 功能描述
按住 Alt 键,在小键盘上输入数字,可以输入任意 Unicode 字符(包括 ASCII 控制字符)。例如:
Alt + 65(小键盘)→ 'A'Alt + 233(小键盘)→ 'é'Alt + 0x1F600(需要 HexNumpad,ReactOS 默认禁用)→ '😀'
7.6.7.2 状态机
win32ss/user/ntuser/keyboard.c:26-36(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L26-L36):
c
static enum _ALTNUM_STATE
{
ALTNUM_INACTIVE, // 非活动
ALTNUM_OEM, // Alt xxx (十进制 OEM)
ALTNUM_ACP, // Alt 0xxx (十进制 ANSI 代码页)
ALTNUM_HEX_ACP, // Alt .xxx (十六进制 ANSI 代码页)
ALTNUM_HEX_UTF // Alt +xxx (十六进制 UTF-16)
} gAltNumPadState = ALTNUM_INACTIVE;
static ULONG gAltNumPadValue = 0;
BOOL gbEnableHexNumpad = FALSE;
状态转换:
ALTNUM_INACTIVE
│ (Alt down)
▼
ALTNUM_OEM
│ (0xxx 在 Numpad)
▼
ALTNUM_ACP
│ (xxx 在 Numpad)
▼
ALTNUM_OEM (累积更多数字)
│ (Alt up)
▼ 产生 WM_CHAR
ALTNUM_INACTIVE
HexNumpad 扩展 (gbEnableHexNumpad = TRUE):
Alt +进入ALTNUM_HEX_UTF;Alt .进入ALTNUM_HEX_ACP;- 后续 Numpad 数字视为十六进制。
7.6.7.3 处理位置
IntHandleAltNumpad 函数在 UserProcessKeyboardInput 中调用:
c
if (IntHandleAltNumpad(KeyData))
return; // AltNumpad 字符已处理,不投递硬件消息
核心逻辑:
- 检查是否处于 Alt 按下状态;
- 检查 VK 是否在 Numpad 范围(VK_NUMPAD0-VK_NUMPAD9, VK_DECIMAL, VK_ADD, VK_SUBTRACT);
- 累积
gAltNumPadValue数值; - 当 Alt 释放时,调用
UserPostMessage(WM_CHAR, gAltNumPadValue, 0)。
7.6.7.4 设计意图
Alt+Numpad 是 Windows 提供的直接字符输入机制,绕过键盘布局:
- 不需要切换输入法;
- 输入生僻字符(如希腊字母、特殊符号);
- 兼容 DOS 时代的 Alt+数字输入方式。
7.6.8 co_IntProcessKeyboardMessage 核心处理
7.6.8.1 函数签名
win32ss/user/ntuser/msgqueue.c:1772-1895(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L1772-L1895):
c
BOOL co_IntProcessKeyboardMessage(MSG* Msg, BOOL* RemoveMessages);
调用时机 :co_MsqPeekHardwareMessage 检测到键盘消息时调用。
7.6.8.2 完整处理流程
c
BOOL co_IntProcessKeyboardMessage(MSG* Msg, BOOL* RemoveMessages)
{
EVENTMSG Event;
USER_REFERENCE_ENTRY Ref;
PWND pWnd;
UINT ImmRet;
BOOL Ret = TRUE, bKeyUpDown = FALSE;
PTHREADINFO pti = PsGetCurrentThreadWin32Thread();
const UINT uMsg = Msg->message;
// 1. VK_PACKET 是 SendInput 注入的 Unicode 字符
if (uMsg == VK_PACKET)
pti->wchInjected = HIWORD(Msg->wParam);
// 2. 简化 VK 区分(左右 Shift/Ctrl/Alt → VK_SHIFT/CONTROL/MENU)
if (uMsg == WM_KEYDOWN || uMsg == WM_SYSKEYDOWN ||
uMsg == WM_KEYUP || uMsg == WM_SYSKEYUP)
{
bKeyUpDown = TRUE;
switch (Msg->wParam)
{
case VK_LSHIFT: case VK_RSHIFT: Msg->wParam = VK_SHIFT; break;
case VK_LCONTROL: case VK_RCONTROL: Msg->wParam = VK_CONTROL; break;
case VK_LMENU: case VK_RMENU: Msg->wParam = VK_MENU; break;
}
}
// 3. 引用窗口对象
pWnd = ValidateHwndNoErr(Msg->hwnd);
if (pWnd) UserRefObjectCo(pWnd, &Ref);
// 4. WH_JOURNALRECORD 钩子(记录输入事件)
Event.message = uMsg;
Event.hwnd = Msg->hwnd;
Event.time = Msg->time;
Event.paramL = (Msg->wParam & 0xFF) | (HIWORD(Msg->lParam) << 8);
Event.paramH = Msg->lParam & 0x7FFF;
if (HIWORD(Msg->lParam) & 0x0100) Event.paramH |= 0x8000;
co_HOOK_CallHooks(WH_JOURNALRECORD, HC_ACTION, 0, (LPARAM)&Event);
// 5. 特殊键处理(仅 RemoveMessages=TRUE)
if (*RemoveMessages)
{
if (uMsg == WM_KEYDOWN)
{
// 5.1 F1 键 ──► WM_HELP
if (Msg->wParam == VK_F1)
UserPostMessage(Msg->hwnd, WM_KEYF1, 0, 0);
// 5.2 浏览器/启动键 ──► WM_APPCOMMAND
else if (Msg->wParam >= VK_BROWSER_BACK && Msg->wParam <= VK_LAUNCH_APP2)
co_IntSendMessage(Msg->hwnd, WM_APPCOMMAND, ...);
}
else if (uMsg == WM_KEYUP && Msg->wParam == VK_APPS)
{
// 5.3 Apps 键(菜单键)──► WM_CONTEXTMENU
UserPostMessage(Msg->hwnd, WM_CONTEXTMENU, (WPARAM)Msg->hwnd, -1);
}
}
// 6. 系统键处理(Alt+Tab/Alt+Esc)
if (*RemoveMessages && uMsg == WM_SYSKEYDOWN)
{
if (HIWORD(Msg->lParam) & KF_ALTDOWN)
{
if (Msg->wParam == VK_ESCAPE || Msg->wParam == VK_TAB)
{
WPARAM wParamTmp = UserGetKeyState(VK_SHIFT) & 0x8000
? SC_PREVWINDOW : SC_NEXTWINDOW;
co_IntSendMessage(Msg->hwnd, WM_SYSCOMMAND, wParamTmp, Msg->wParam);
Ret = FALSE;
goto Exit;
}
}
}
// 7. WH_KEYBOARD 钩子
if (co_HOOK_CallHooks(WH_KEYBOARD,
*RemoveMessages ? HC_ACTION : HC_NOREMOVE,
LOWORD(Msg->wParam), Msg->lParam))
{
// 钩子消费了消息
co_HOOK_CallHooks(WH_CBT, HCBT_KEYSKIPPED, LOWORD(Msg->wParam), Msg->lParam);
*RemoveMessages = TRUE;
Ret = FALSE;
}
// 8. IME 处理
if (pWnd && Ret && *RemoveMessages && bKeyUpDown && !(pti->TIF_flags & TIF_DISABLEIME))
{
ImmRet = IntImmProcessKey(pti->MessageQueue, pWnd, uMsg, Msg->wParam, Msg->lParam);
if (ImmRet)
{
if (ImmRet & (IPHK_HOTKEY|IPHK_SKIPTHISKEY))
ImmRet = 0;
if (ImmRet & IPHK_PROCESSBYIME)
Msg->wParam = VK_PROCESSKEY; // IME 处理中
}
}
Exit:
if (pWnd) UserDerefObjectCo(pWnd);
return Ret;
}
7.6.8.3 关键钩子与处理
| 步骤 | 处理对象 | 备注 |
|---|---|---|
| 1 | VK_PACKET 注入字符 | 来自 SendInput |
| 2 | 简化 VK 区分 | 便于 GetKeyState 查询 |
| 4 | WH_JOURNALRECORD 钩子 | 记录所有输入事件 |
| 5.1 | F1 键 → WM_HELP | 帮助键 |
| 5.2 | 浏览器/启动键 → WM_APPCOMMAND | 多媒体键 |
| 5.3 | Apps 键 → WM_CONTEXTMENU | 右键菜单 |
| 6 | Alt+Tab/Alt+Esc | 窗口切换 |
| 7 | WH_KEYBOARD 钩子 | 可消费消息 |
| 8 | IME | 输入法处理 |
7.6.9 字符消息生成
7.6.9.1 user32 TranslateMessage 流程
win32ss/user/user32/windows/message.c(file:///d:/reactos/win32ss/user/user32/windows/message.c) 中定义 TranslateMessage:
c
BOOL TranslateMessage(CONST MSG *lpMsg)
{
// 仅处理 WM_KEYDOWN
if (lpMsg->message != WM_KEYDOWN && lpMsg->message != WM_SYSKEYDOWN)
return FALSE;
// 1. 简化 VK
wVk = IntSimplifyVk(LOWORD(lpMsg->wParam));
// 2. Numpad 翻译
if (wVk >= VK_NUMPAD0 && wVk <= VK_DECIMAL)
wVk = IntTranslateNumpadKey(wVk);
// 3. 调用 ToUnicodeEx 翻译为字符
if (ToUnicodeEx(wVk, sc, afKeyState, wsz, 1, 0, pKbdTbl) > 0)
{
// 4. 投递 WM_CHAR
PostMessage(lpMsg->hwnd, WM_CHAR, wsz[0], lpMsg->lParam);
}
return TRUE;
}
7.6.9.2 ToUnicodeEx 字符翻译
win32ss/user/ntuser/keyboard.c:447-498(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L447-L498):
c
int APIENTRY
IntToUnicodeEx(UINT wVirtKey, UINT wScanCode, PBYTE pKeyState,
LPWSTR pwszBuff, int cchBuff, UINT wFlags, PKBDTABLES pKbdTbl)
{
WCHAR wchTranslatedChar;
BOOL bDead, bLigature;
static WCHAR wchDead = 0;
int iRet = 0;
// 1. 调用 IntTranslateChar 翻译当前 VK
if (!IntTranslateChar(wVirtKey, pKeyState, &bDead, &bLigature, &wchTranslatedChar, pKbdTbl))
return 0;
if (bLigature) return 0; // 连字暂不支持
// 2. Dead Key 组合
if (wchDead)
{
// 尝试将当前字符与之前的 dead char 组合
for (i = 0; pKbdTbl->pDeadKey[i].dwBoth; i++)
{
if (pKbdTbl->pDeadKey[i].dwBoth >> 16 == wchDead &&
pKbdTbl->pDeadKey[i].dwBoth & 0xFFFF == wchTranslatedChar)
{
wchTranslatedChar = pKbdTbl->pDeadKey[i].wchComposed;
wchDead = 0;
bDead = FALSE;
break;
}
}
// 组合失败:先投递之前 dead char 的字符,再保留当前字符
if (wchDead)
{
pwszBuff[iRet++] = wchDead;
wchDead = 0;
}
}
if (bDead)
{
// 当前字符是 dead key,记录等下次组合
wchDead = wchTranslatedChar;
}
else
{
pwszBuff[iRet++] = wchTranslatedChar;
}
return iRet;
}
7.6.9.3 IntTranslateChar
win32ss/user/ntuser/keyboard.c:335-440(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L335-L440):
复杂的字符翻译函数,根据 VK 码和修饰键状态查找 pKbdTbl->pVkToWcharTable 表:
c
static BOOL
IntTranslateChar(WORD wVirtKey, PBYTE pKeyState, PBOOL pbDead, PBOOL pbLigature,
PWCHAR pwcTranslatedChar, PKBDTABLES pKbdTbl)
{
DWORD i, dwModBits, dwVkModBits, dwModNumber = 0;
WCHAR wch;
// 1. 获取当前修饰键位(KBDCTRL/KBDALT/KBDSHIFT/KBDKANA)
dwModBits = pKeyState ? IntGetModBits(pKbdTbl, pKeyState) : 0;
// 2. 遍历 pVkToWcharTable 表
for (i = 0; pKbdTbl->pVkToWcharTable[i].pVkToWchars; i++)
{
PVK_TO_WCHARS10 pVkToVch = (PVK_TO_WCHARS10)(pKbdTbl->pVkToWcharTable[i].pVkToWchars);
while (pVkToVch->VirtualKey)
{
if (wVirtKey == (pVkToVch->VirtualKey & 0xFF))
{
// 3. 应用 CapsLock 翻转(仅对字母键)
if ((pVkToVch->Attributes & wCaplokAttr) && IS_KEY_LOCKED(pKeyState, VK_CAPITAL))
dwVkModBits ^= KBDSHIFT;
// 4. 查找当前修饰键对应的字符
dwModNumber = pKbdTbl->pCharModifiers->ModNumber[dwVkModBits];
wch = pVkToVch->wch[dwModNumber];
*pwcTranslatedChar = wch;
*pbDead = (wch == WCH_DEAD);
*pbLigature = (wch == WCH_LGTR);
return TRUE;
}
pVkToVch = (PVK_TO_WCHARS10)(((BYTE *)pVkToVch) + pVkToVchTbl->cbSize);
}
}
// 5. Ctrl+A-Z 翻译为 ASCII 控制字符
if (wVirtKey >= 'A' && wVirtKey <= 'Z' && IS_KEY_DOWN(pKeyState, VK_CONTROL))
{
*pwcTranslatedChar = (wVirtKey - 'A') + 1;
return TRUE;
}
return FALSE;
}
7.6.9.4 字符消息分类
| 消息 | 触发条件 | wParam | 用途 |
|---|---|---|---|
| WM_KEYDOWN | 任何键按下 | VK 码 | 通知应用"键被按下" |
| WM_KEYUP | 任何键释放 | VK 码 | 通知应用"键被释放" |
| WM_CHAR | 可打印字符 | Unicode 字符 | 通知应用"产生了字符" |
| WM_DEADCHAR | 死键(如 ´、`) | 死键字符 | 通知应用"死键已输入,等组合" |
| WM_SYSKEYDOWN | Alt + 键 | VK 码 | 系统键(菜单激活等) |
| WM_SYSCHAR | Alt + 字符 | Unicode 字符 | 系统字符 |
| WM_SYSDEADCHAR | Alt + 死键 | 死键字符 | 系统死键 |
为什么需要 WM_KEYDOWN + WM_CHAR 两套消息?
- WM_KEYDOWN:低层、按键为粒度(按下哪个 VK 键、Shift/Ctrl 是否按下);
- WM_CHAR:高层、字符为粒度('a' 还是 'A'、是否死键组合)。
应用可以根据需求选择处理:游戏通常只用 WM_KEYDOWN(需要 VK 码),文本编辑器只用 WM_CHAR(需要字符)。
7.6.10 IME 处理与全局热键
7.6.10.1 IME 在键盘处理中的位置
IME(输入法编辑器)将按键事件拦截并转换为"组合中"的字符:
用户按键 ──► co_IntProcessKeyboardMessage
│
▼
IntImmProcessKey (IME 处理)
│
├─► IPHK_HOTKEY (热键:Shift+Space 等)
├─► IPHK_SKIPTHISKEY (IME 消费此键)
└─► IPHK_PROCESSBYIME (VK_PROCESSKEY,WndProc 知道是 IME 处理中)
7.6.10.2 IntImmProcessKey
win32ss/user/ntuser/ime.c(file:///d:/reactos/win32ss/user/ntuser/ime.c) 中定义:
c
UINT IntImmProcessKey(PUSER_MESSAGE_QUEUE MessageQueue, PWND pWnd,
UINT uMsg, WPARAM wParam, LPARAM lParam)
{
// 1. 检查 IME 是否启用
if (!pWnd || !MessageQueue->spwndFocus)
return 0;
// 2. 检查热键
if (wParam == VK_SPACE && (GetAsyncKeyState(VK_SHIFT) & 0x8000))
{
// Shift+Space 切换中英文
return IPHK_HOTKEY;
}
// 3. 调用 IME 钩子
// 4. 转换结果
if (IME 想要处理此键)
return IPHK_PROCESSBYIME; // 替换为 VK_PROCESSKEY
return 0;
}
返回值:
IPHK_HOTKEY:识别为 IME 热键(如 Shift+Space 切换中英文),不发字符消息;IPHK_SKIPTHISKEY:IME 消费此键(不投递 WM_CHAR);IPHK_PROCESSBYIME:产生WM_KEYDOWN VK_PROCESSKEY(WndProc 应跳过,等待 IME 后续产生WM_IME_COMPOSITION)。
7.6.10.3 全局热键 RegisterHotKey
c
BOOL RegisterHotKey(HWND hWnd, int id, UINT fsModifiers, UINT vk)
{
// 1. 创建 HOTKEY_ITEM
// 2. 加入线程/桌面的热键链表
// 3. 任何按键事件都会检查热键链表
}
实现位置:win32ss/user/ntuser/hotkey.c(file:///d:/reactos/win32ss/user/ntuser/hotkey.c)(ReactOS 中存在)。
热键检查时机:UserProcessKeyboardInput 投递硬件消息之前,先扫描热键链表。
7.6.11 键盘 LL 钩子
7.6.11.1 co_CallLowLevelKeyboardHook
win32ss/user/ntuser/keyboard.c:703-732(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L703-L732):
c
static LRESULT
co_CallLowLevelKeyboardHook(WORD wVk, WORD wScanCode, DWORD dwFlags,
BOOL bInjected, DWORD dwTime, DWORD dwExtraInfo)
{
KBDLLHOOKSTRUCT KbdHookData;
UINT uMsg;
// 1. 填充 KBDLLHOOKSTRUCT
KbdHookData.vkCode = wVk;
KbdHookData.scanCode = wScanCode;
KbdHookData.flags = 0;
if (dwFlags & KEYEVENTF_EXTENDEDKEY) KbdHookData.flags |= LLKHF_EXTENDED;
if (IS_KEY_DOWN(gafAsyncKeyState, VK_MENU)) KbdHookData.flags |= LLKHF_ALTDOWN;
if (dwFlags & KEYEVENTF_KEYUP) KbdHookData.flags |= LLKHF_UP;
if (bInjected) KbdHookData.flags |= LLKHF_INJECTED;
KbdHookData.time = dwTime;
KbdHookData.dwExtraInfo = dwExtraInfo;
// 2. 决定 LL 钩子的 uMsg
if (dwFlags & KEYEVENTF_KEYUP)
uMsg = WM_KEYUP;
else if (IS_KEY_DOWN(gafAsyncKeyState, VK_MENU) && !IS_KEY_DOWN(gafAsyncKeyState, VK_CONTROL))
uMsg = WM_SYSKEYDOWN;
else
uMsg = WM_KEYDOWN;
// 3. 调用 WH_KEYBOARD_LL 钩子
return co_HOOK_CallHooks(WH_KEYBOARD_LL, HC_ACTION, uMsg, (LPARAM)&KbdHookData);
}
7.6.11.2 LL 钩子的特殊性
| 维度 | 普通 WH_KEYBOARD | WH_KEYBOARD_LL |
|---|---|---|
| 触发位置 | WndProc 之前 | 原始输入(RIT 阶段) |
| DLL 注入 | 全局钩子需要 | 不需要 |
| lParam | MSG 结构 | KBDLLHOOKSTRUCT |
| 可修改数据 | 不能 | 可以(修改 flags、vkCode) |
| 用途 | 应用程序消息过滤 | 全局热键、键位记录器 |
LL 钩子的关键优势 :不需要 DLL 注入 ,因为它在内核态实现并通过 KeUserModeCallback 切换到用户态调用钩子函数(参见 7.3 节)。
7.6.11.3 钩子返回值
co_HOOK_CallHooks 返回非 0 表示钩子消费了事件:
c
if (co_CallLowLevelKeyboardHook(...))
return; // 钩子消费,停止后续处理
这允许钩子函数阻止特定按键被处理(用于实现"屏蔽某些键"的应用)。
7.6.12 设计哲学问答
Q1:为什么 RIT 必须在专门的线程中处理输入?
A :统一输入源 + 避免多线程竞争。
- 多设备统一:所有键盘设备(PS/2、USB、蓝牙)的 IRP_MJ_READ 完成都汇入 RIT,避免每个设备驱动重复实现输入处理逻辑;
- 单线程状态修改 :
gafAsyncKeyState是全局状态,如果多线程并发修改需要锁,影响性能; - 优先级隔离:RIT 在较高优先级运行,确保输入响应实时(游戏、交互场景必需);
- 避免 UI 线程死锁:键盘处理可能涉及跨线程消息(同步发送),如果由 UI 线程处理可能导致嵌套死锁。
Q2:为什么需要区分 gafAsyncKeyState 的 down 位和 locked 位?
A :两种状态语义不同。
- down 位 :瞬时状态(按下时为 1,释放时为 0)------
GetAsyncKeyState(VK_LSHIFT) & 0x8000检测; - locked 位 :持续状态(CapsLock 按下后保持为 1,再次按下恢复为 0)------
GetKeyState(VK_CAPITAL) & 0x01检测。
典型应用:
- 游戏中检测"Shift 是否按住"------用 down 位;
- 文本编辑器中检测"CapsLock 是否开启"------用 locked 位。
通过两个独立位表示两个独立状态,简洁高效。
Q3:为什么 Alt+Numpad 输入绕过键盘布局(KBDTABLES)?
A :支持任意 Unicode 字符,不受语言限制。
普通字符输入需要 ToUnicodeEx 查表,受当前键盘布局限制(美式布局只能输入 ASCII 字符)。Alt+Numpad 直接输入字符码点:
- 不需要安装/切换输入法;
- 输入生僻字符(如希腊字母、数学符号);
- 兼容 DOS 时代的 Alt+数字输入。
实现方式:在 RIT 阶段识别"Alt + Numpad 数字",累积为数值,Alt 释放时直接产生 WM_CHAR 消息,跳过 TranslateMessage 和 ToUnicodeEx。
Q4:为什么 WH_KEYBOARD_LL 不需要 DLL 注入,而普通全局 WH_KEYBOARD 需要?
A :实现位置不同。
- 普通全局 WH_KEYBOARD :钩子函数在用户态。
SetWindowsHookEx时 win32k 需要将含钩子函数的 DLL 注入到所有 GUI 进程,使每个进程的线程都能调用钩子。 - WH_KEYBOARD_LL :钩子函数在用户态但调用点在内核态 。RIT 在内核态通过
co_HOOK_CallHooks(WH_KEYBOARD_LL, ...)触发钩子,co_HOOK_CallHooks内部使用KeUserModeCallback切换到当前进程的用户态调用钩子。
LL 钩子的实现避免了 DLL 注入:
- 钩子函数指针存储在内核的
gpsi->aphkStart[WH_KEYBOARD_LL],所有进程共享; - RIT 触发时,
KeUserModeCallback在当前 RIT 线程的用户态调用钩子; - 不需要把 DLL 映射到其他进程。
Q5:为什么键盘消息要进入"硬件消息队列"(HardwareMessagesListHead)而不是普通"投递队列"(PostedMessagesListHead)?
A :优先级 + 抢占机制。
- 优先级:硬件消息优先于投递消息,避免输入延迟(用户按键必须及时响应,不能被排在 WM_USER 之类的消息后);
- 抢占机制 :硬件消息的检查 (
co_MsqPeekHardwareMessage) 在co_IntPeekMessage中先于普通消息; - GetMessage 过滤 :应用程序可以通过
wMsgFilterMin=WM_KEYFIRST, wMsgFilterMax=WM_KEYLAST选择只读取硬件消息; - ptiSysLock:硬件消息的读取是"线程独占"的(同一时间只有一个线程读取硬件消息),避免鼠标移动消息在两个线程间分配(参见 7.7)。
总结
键盘输入子系统是 Windows GUI 最复杂的 I/O 路径之一。本节介绍了:
- RIT 架构:原始输入线程统一处理所有键盘设备;
- 键盘初始化 :
InitKeyboardImpl、UserInitKeyboard、IOCTL 查询指示灯与属性; - VK 码处理 :
IntSimplifyVk(简化左右手)、IntFixVk(修正扩展标志); - Numpad 翻译 :
IntTranslateNumpadKey(NumLock 切换数字/方向键); - LED 同步 :内核态
gIndicators、硬件 LED、gafAsyncKeyState三方同步; - 异步按键状态 :
gafAsyncKeyState[64](每键 2 bit:down/locked)、gafAsyncKeyStateRecentDown(边沿触发); - Alt+Numpad 字符输入:绕过键盘布局的 Unicode 直接输入;
co_IntProcessKeyboardMessage:处理 8 类特殊事件(F1、Apps 键、Alt+Tab、WH_KEYBOARD、IME 等);- 字符消息生成 :
TranslateMessage+ToUnicodeEx+ KBDTABLES 翻译表; - IME 与热键:IME 在按键流中的拦截和转换;
- LL 钩子:内核态实现的全局键盘钩子,无需 DLL 注入。
核心要点回顾:
- RIT 是所有键盘设备的统一处理点;
gafAsyncKeyState维护全局按键状态(down/locked/recent);- VK 码通过
IntSimplifyVk/IntFixVk在"区分左右"和"不区分左右"之间转换; - 字符消息通过
TranslateMessage+ KBDTABLES 翻译表生成; - WH_KEYBOARD_LL 在内核态实现,避免 DLL 注入。
本章代码索引
| 文件 | 内容 |
|---|---|
| win32ss/user/ntuser/keyboard.c(file:///d:/reactos/win32ss/user/ntuser/keyboard.c) | InitKeyboardImpl、UserInitKeyboard、IntKeyboardUpdateLeds、IntSimplifyVk、IntFixVk、IntTranslateNumpadKey、IntTranslateChar、IntToUnicodeEx、IntVkFromSc、UpdateAsyncKeyState、co_CallLowLevelKeyboardHook、IntHandleAltNumpad、NtUserGetAsyncKeyState |
| win32ss/user/ntuser/msgqueue.c(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c) | co_IntProcessKeyboardMessage、co_MsqPeekHardwareMessage、co_IntProcessHardwareMessage |
| win32ss/user/ntuser/input.c(file:///d:/reactos/win32ss/user/ntuser/input.c) | UserProcessKeyboardInput(RIT 原始输入入口) |
| win32ss/user/ntuser/kbdlayout.c(file:///d:/reactos/win32ss/user/ntuser/kbdlayout.c) | KBDTABLES 加载与维护、IntGetCurrentKbdTables |
| win32ss/user/ntuser/ime.c(file:///d:/reactos/win32ss/user/ntuser/ime.c) | IntImmProcessKey(IME 处理) |
| win32ss/user/ntuser/hotkey.c(file:///d:/reactos/win32ss/user/ntuser/hotkey.c) | 全局热键(RegisterHotKey) |
| win32ss/user/user32/windows/message.c(file:///d:/reactos/win32ss/user/user32/windows/message.c) | TranslateMessage、ToUnicode、ToAscii 用户态 API |
| ntoskrnl/kbdclass/kbdclass.c(file:///d:/reactos/ntoskrnl/kbdclass/kbdclass.c) | kbdclass 键盘类驱动(IRP_MJ_READ 队列) |