ReactOS 窗口系统分析(23):键盘 --- keyboard.c + kbdlayout.c
本文是"窗口系统三大主线之鼠标控制"系列的第 23 册,配套总览文档《ReactOS 窗口系统架构分析》第 6.4 节。
覆盖源码:
win32ss/user/ntuser/keyboard.c(1991 行)、win32ss/user/ntuser/kbdlayout.c(1265 行)、
win32ss/user/ntuser/input.h、win32ss/user/ntuser/msgqueue.c(键盘状态与键盘消息处理部分)、
win32ss/user/user32/windows/input.c(用户态键盘 API 封装)。主线三(鼠标控制)分册:_21 输入子系统(input.c/event.c)、_22 鼠标(mouse.c)、_23 键盘(keyboard.c/kbdlayout.c,本文)、_24 输入法(ime.c)。
1. 概述
键盘是窗口系统最复杂的输入设备:一个键按下,既要维护"按键状态"(供 GetKeyState/GetAsyncKeyState 查询),
又要在键盘布局 (Keyboard Layout)的参与下翻译成虚键与字符,还要生成 WM_KEYDOWN/WM_KEYUP/
WM_SYSKEYDOWN/WM_SYSKEYUP 硬件消息,最终在用户态 TranslateMessage 阶段变成
WM_CHAR/WM_DEADCHAR/WM_SYSCHAR 送达焦点窗口的窗口过程。此外,键盘还要服务 IME(输入法)、
热键、语言/布局切换(Alt+Shift / Ctrl+Shift)、Alt+Numpad 字符输入、PrtSc 截屏、Alt+Tab 系统命令等旁路功能。
整个键盘管线可抽象为下图(对照总览 6.4 节):
┌──────────────────────────────────────────────────────────────────────────────┐
│ 键盘硬件中断 → 输入驱动(i8042prt/kbdclass)→ 原始数据 KEYBOARD_INPUT_DATA │
└──────────────────────────────────┬───────────────────────────────────────────┘
▼
RawInputThreadMain(input.c:ZwReadFile(\Device\KeyboardClass0))
│ UserEnterExclusive()
▼
UserProcessKeyboardInput(keyboard.c L1414)
├── 扫描码归一化:MakeCode & 0x7F,KEY_E0 → 0xE000,KEY_E1 → 0xE100
├── 取焦点队列 IntGetFocusMessageQueue(desktop.c L1324)
├── 取焦点线程布局 pFocusQueue->ptiKeyboard->KeyboardLayout(无则 W32kGetDefaultKeyLayout)
├── IntVscToVk:扫描码 → 虚键(KBDTABLES.pusVSCtoVK / pVSCtoVK_E0 / pVSCtoVK_E1)
├── 小键盘 NumLock 处理:IntTranslateNumpadKey
└── 构造 KEYBDINPUT → UserSendKeyboardInput → ProcessKeyEvent
▼
ProcessKeyEvent(keyboard.c L1106,真正的按键主处理)
├── IntSimplifyVk / IntFixVk:左右键虚键 ↔ 简化虚键
├── IntHandleAltNumpad:Alt+数字小键盘合成字符
├── UpdateAsyncKeyState:维护 gafAsyncKeyState(2 位/键:down+lock)
├── IntKeyboardUpdateLeds:Caps/Num/Scroll 指示灯
├── co_CallLowLevelKeyboardHook:WH_KEYBOARD_LL 低级钩子(可吞键)
├── co_UserProcessHotKeys:全局热键(RegisterHotKey)
├── SysKey 判定(F10 / Alt 按下 / 无焦点窗口)→ WM_SYSKEYDOWN/UP 或 WM_KEYDOWN/UP
├── IntCheckLanguageToggle:Alt+Shift / Ctrl+Shift 切换语言/布局 → WM_INPUTLANGCHANGEREQUEST
├── VK_SNAPSHOT:PrtSc 截屏到剪贴板(SnapWindow)
└── MsqPostMessage(..., QS_KEY):硬件消息入队(pt i=焦点窗口所属线程)
▼
消息循环 GetMessage(msgqueue.c co_IntProcessKeyboardMessage L1783)
├── WH_JOURNALRECORD 日志钩子
├── F1 → WM_KEYF1 / VK_APPS → WM_CONTEXTMENU / 浏览器键 → WM_APPCOMMAND
├── Alt+Tab/Alt+Esc → 切换窗口系统命令(WM_SYSCOMMAND SC_NEXTWINDOW/SC_PREVWINDOW)
├── WH_KEYBOARD 钩子(可吞键)
└── IntImmProcessKey(ime.c):IME 优先处理,可改写为 VK_PROCESSKEY
▼
用户态 TranslateMessage(user32 → NtUserTranslateMessage L2489)
└── IntTranslateKbdMessage(keyboard.c L1499)
├── IntToUnicodeEx:虚键+同步键状态 → 字符(含死键合成)
├── VK_PACKET:直接转 WM_CHAR/SYSCHAR
└── MsqPostMessage:WM_CHAR/WM_DEADCHAR/WM_SYSCHAR 入队
▼
DispatchMessage → 焦点窗口(spwndFocus)窗口过程收到字符
本册内容组织 :先讲设计动机(第 2 节),再讲核心数据结构(第 3 节),随后把 keyboard.c
中的每个核心函数逐一展开(第 4 节)、kbdlayout.c 的布局对象管理函数逐一展开(第 5 节),
再补充 msgqueue.c/message.c/user32 中与键盘相关的配合函数(第 6 节),
最后按主题梳理全流程(第 7~11 节)、绘制调用链(第 12 节)并给出源码索引(第 13 节)。
2. 设计动机
2.1 键状态的"集中管理"与"双轨并存"
Windows 键盘 API 存在两类键状态查询:
- 异步键状态 (
GetAsyncKeyState):反映"物理键盘此刻的真实状态",无论哪个线程查询都一致;
由内核全局位图gafAsyncKeyState[256*2/8](keyboard.c L13)维护,任何按键事件(真实硬件或
SendInput注入)都会更新它。 - 同步键状态 (
GetKeyState/GetKeyboardState):反映"本线程消息队列视角下的键状态",
由每队列位图USER_MESSAGE_QUEUE.afKeyState(msgqueue.h)维护。它与消息流绑定:
队列从硬件消息中更新自己的状态,GetKeyState返回的是"处理到当前消息时"该键的状态,
因此应用程序在窗口过程中读取的是与消息同步的历史状态。
MsqInitializeMessageQueue(msgqueue.c L2212)在创建消息队列时用 RtlCopyMemory 把
gafAsyncKeyState 快照进 afKeyState,作为新队列的初始同步状态------两条轨由此对齐起步。
2.2 键盘布局的解耦
字符产生方式因语言而异:英语按下 "A" 得 'a',法语布局按下同一键得 'q',希伯来语得另一个字母。
Windows 用**键盘布局(Keyboard Layout)**把"扫描码→虚键→字符"的映射从窗口系统核心逻辑中解耦:
- 布局的核心数据是
KBDTABLES(sdk/include/ndk/kbd.h L95),由布局 DLL (如 kbdus.dll、
kbdfr.dll)通过导出函数KbdLayerDescriptor()返回。 - 内核不解析 DLL 内容,而是把每个已加载布局包装成两个对象:
KBDFILE(一份布局文件的共享
描述,全局唯一)与KL(布局实例,挂在窗口站的布局链表中)。 - 线程通过
THREADINFO.KeyboardLayout(win32.h)指向当前激活布局;hkl(HKL,输入语言标识符)
是用户态可见的句柄形式,内核侧用UserHklToKbl/IntHKLtoPKL做双向换算。
这样的设计使"新增一种键盘语言"只需提供新的布局 DLL + 注册表条目,win32k 无需任何改动。
2.3 焦点定向
键盘消息不发给鼠标下的窗口,而发给焦点窗口 (USER_MESSAGE_QUEUE.spwndFocus)。
当焦点窗口不存在时退化为活动窗口(spwndActive)。键盘输入真正归属的"键盘线程"是焦点
窗口所属线程(ptiKeyboard),IntGetFocusMessageQueue(desktop.c L1324)返回活动桌面的
ActiveMessageQueue,即全局唯一的前台输入队列。IME 与键盘布局都挂在"焦点线程"上解析,
保证"谁的窗口有焦点,谁的语言和输入法生效"。
2.4 IME 优先
东方语言(中日韩)的文本输入需要输入法。键盘消息在到达窗口过程之前,co_IntProcessKeyboardMessage
(msgqueue.c L1783)会先调用 IntImmProcessKey(ime.c),让 IME 决定"这个键是我吃掉组合用的
(返回 IPHK_PROCESSBYIME,wParam 改写为 VK_PROCESSKEY),还是跳过(IPHK_SKIPTHISKEY)"。
IME 处理优先于普通字符翻译,是"键盘→字符"链路上的第一道闸门。
3. 核心数据结构
3.1 按键状态位图(2 位/键)
内核用紧凑位图表达 256 个虚键的状态,每键 2 位 :bit0 = 按下(down),bit1 = 锁定(lock)。
input.h 定义了一套位运算宏:
c
/* input.h L50-56 */
#define KS_DOWN_BIT 0x80 /* 用户态 GetKeyboardState 数组中的"按下"位 */
#define KS_LOCK_BIT 0x01 /* 用户态 GetKeyboardState 数组中的"锁定"位 */
#define SC_KEY_UP 0x8000 /* 扫描码高位表示释放 */
#define LP_DO_NOT_CARE_BIT (1<<25) /* GetKeyNameText 的"不在乎"标志 */
/* input.h L99-109:内核位图宏 */
#define GET_KS_BYTE(vk) ((vk) * 2 / 8) /* 每键 2 位 → 字节索引 */
#define GET_KS_DOWN_BIT(vk) (1 << (((vk) % 4)*2)) /* down 位 */
#define GET_KS_LOCK_BIT(vk) (1 << (((vk) % 4)*2 + 1)) /* lock 位 */
#define IS_KEY_DOWN(ks, vk) (((ks)[GET_KS_BYTE(vk)] & GET_KS_DOWN_BIT(vk)) ? TRUE : FALSE)
#define IS_KEY_LOCKED(ks, vk)(((ks)[GET_KS_BYTE(vk)] & GET_KS_LOCK_BIT(vk)) ? TRUE : FALSE)
#define SET_KEY_DOWN(ks, vk, down) /* 置/清 down 位 */
#define SET_KEY_LOCKED(ks, vk, down) /* 置/清 lock 位 */
两份位图:
| 位图 | 位置 | 大小 | 语义 |
|---|---|---|---|
gafAsyncKeyState |
keyboard.c 全局 | 256*2/8 = 64 字节 |
全局异步键状态(物理+注入) |
gafAsyncKeyStateRecentDown |
keyboard.c 静态 | 256/8 = 32 字节 |
"自上次查询以来曾按下"标志(GetAsyncKeyState 返回值 bit0) |
MessageQueue->afKeyState |
msgqueue.h | 64 字节 | 队列同步键状态(GetKeyState 数据源) |
MessageQueue->afKeyRecentDown |
msgqueue.h | 32 字节 | 队列"最近按下"标志 |
lock 位的翻转语义:每次"首次按下"(从 up 变 down 的沿)将 lock 位取反 (见
UpdateAsyncKeyState 与 UpdateKeyState),这正是 CapsLock/NumLock 的硬件行为。
lock 位也由 UserInitKeyboard 在初始化时从键盘驱动的 KEYBOARD_INDICATOR_PARAMETERS.LedFlags
同步(SET_KEY_LOCKED(gafAsyncKeyState, VK_CAPITAL, LedFlags & KEYBOARD_CAPS_LOCK_ON))。
用户态 API 返回约定(Windows 兼容):
GetAsyncKeyState:高位0x8000表示按下(keyboard.c 中NtUserGetAsyncKeyState置位),
低位0x1表示"自上次调用以来按下过"(由gafAsyncKeyStateRecentDown提供,读取即清除)。GetKeyState:UserGetKeyState(msgqueue.c L222)返回0xFF80(按下)或0x1(锁定),
注意按下位是 16 位的0xFF80而非0x8000------这是文档化的 Windows 行为。
3.2 KEYBOARD_INPUT_DATA(硬件输入记录)
驱动把每次按键(make/break)封装为一条 KEYBOARD_INPUT_DATA(sdk/include/psdk/ntddkbd.h L85):
c
typedef struct _KEYBOARD_INPUT_DATA {
USHORT UnitId; /* 键盘单元号(多键盘时区分) */
USHORT MakeCode; /* 扫描码(0xFF = 键盘溢出) */
USHORT Flags; /* KEY_MAKE(0)/KEY_BREAK(1)/KEY_E0(2)/KEY_E1(4) 等 */
USHORT Reserved;
ULONG ExtraInformation; /* 附加信息(SendInput 的 dwExtraInfo 会传到这里) */
} KEYBOARD_INPUT_DATA;
Flags 常量:KEY_MAKE=0、KEY_BREAK=1(释放)、KEY_E0=2(E0 前缀扩展键)、
KEY_E1=4(E1 前缀,如 Pause)。UserProcessKeyboardInput 据此把扫描码归一化为
带前缀形式:wScanCode = MakeCode & 0x7F,E0 键 |= 0xE000,E1 键 |= 0xE100。
3.3 KEYBDINPUT(SendInput 注入记录)
c
typedef struct tagKEYBDINPUT {
WORD wVk; /* 虚键码 */
WORD wScan; /* 扫描码 */
DWORD dwFlags; /* KEYEVENTF_EXTENDEDKEY / KEYEVENTF_KEYUP / KEYEVENTF_UNICODE /
KEYEVENTF_SCANCODE */
DWORD time; /* 事件时间戳(0 = 使用当前 tick) */
ULONG_PTR dwExtraInfo; /* 附加信息 */
} KEYBDINPUT;
UserSendKeyboardInput(keyboard.c L1332)是硬件键盘与 SendInput 注入两条路径的共同入口:
硬件路径由 UserProcessKeyboardInput 构造 KEYBDINPUT 后调用它;注入路径由 NtUserSendInput
(input.c L777 附近)构造后调用。两者都进入 ProcessKeyEvent 主处理。
3.4 虚键码与扫描码
- 虚键码(VK) :
VK_*常量(winuser.h),如VK_LSHIFT=0xA0、VK_A='A'。内核内部把
KBDEXT(0x100)/KBDMULTIVK(0x200)/KBDSPECIAL(0x400)/KBDNUMPAD(0x800)这 4 个高位标志
与 8 位 VK 合并使用(ndk/kbd.h L8-11),进入消息前再& 0xFF净化。 - 扫描码(SC) :硬件物理键编号。
SCANCODE_LSHIFT=0x2A、SCANCODE_RSHIFT=0x36、
SCANCODE_CTRL=0x1D、SCANCODE_ALT=0x38(ndk/kbd.h L128-131)。E0/E1 前缀扩展键的扫描码
用高位标记:0xE000|vsc、0xE100|vsc。 - 左右键区分 :
VK_LSHIFT/VK_RSHIFT等左/右细分虚键 ↔VK_SHIFT/VK_CONTROL/VK_MENU
简化虚键之间通过IntSimplifyVk/IntFixVk互转(见 4.4)。
3.5 KBDTABLES(布局翻译表)
布局 DLL 通过 KbdLayerDescriptor() 返回一个 KBDTABLES(ndk/kbd.h L95):
c
typedef struct _KBDTABLES {
PMODIFIERS pCharModifiers; /* 修饰键 → 修饰位(KBDSHIFT/KBDCTRL/KBDALT)映射 */
PVK_TO_WCHAR_TABLE pVkToWcharTable; /* 虚键 → 字符表(按修饰组合索引) */
PDEADKEY pDeadKey; /* 死键合成表(如 ' + e → é) */
VSC_LPWSTR *pKeyNames; /* 扫描码 → 键名(GetKeyNameText 用) */
VSC_LPWSTR *pKeyNamesExt; /* 扩展键名表 */
LPWSTR *pKeyNamesDead; /* 死键名表 */
USHORT *pusVSCtoVK; /* 普通扫描码 → 虚键数组(bMaxVSCtoVK 项) */
BYTE bMaxVSCtoVK; /* 普通扫描码表长度 */
PVSC_VK pVSCtoVK_E0; /* E0 扩展扫描码 → 虚键表 */
PVSC_VK pVSCtoVK_E1; /* E1 扩展扫描码 → 虚键表 */
DWORD fLocaleFlags; /* KLLF_ALTGR/KLLF_SHIFTLOCK/KLLF_LRM_RLM */
BYTE nLgMaxd; BYTE cbLgEntry; /* 连字(ligature)表信息 */
PLIGATURE1 pLigature; /* 连字表(如阿拉伯语多字符组合) */
} KBDTABLES;
配套结构:
c
/* 修饰键→修饰位 */
typedef struct _VK_TO_BIT { BYTE Vk; BYTE ModBits; } VK_TO_BIT;
typedef struct _MODIFIERS {
PVK_TO_BIT pVkToBit; /* 形如 {VK_SHIFT, KBDSHIFT}, {VK_CONTROL, KBDCTRL}, ... */
WORD wMaxModBits; /* 合法修饰位组合的最大值 */
BYTE ModNumber[]; /* 修饰位组合 → 表内序号(wch 数组下标) */
} MODIFIERS;
/* 虚键 → 字符(每键按修饰组合展开多个字符) */
#define TYPEDEF_VK_TO_WCHARS(i) \
typedef struct _VK_TO_WCHARS ## i { BYTE VirtualKey; BYTE Attributes; WCHAR wch[i]; } ...
typedef struct _VK_TO_WCHAR_TABLE {
PVK_TO_WCHARS1 pVkToWchars; /* 变长数组:若干 VK_TO_WCHARS(按 cbSize 步进) */
BYTE nModifications; /* 修饰组合数(即 wch[] 长度) */
BYTE cbSize; /* 每条记录字节数 */
} VK_TO_WCHAR_TABLE;
/* 死键合成:dwBoth 高 16 位=死字符,低 16 位=第二字符,wchComposed=合成结果 */
typedef struct _DEADKEY { DWORD dwBoth; WCHAR wchComposed; USHORT uFlags; } DEADKEY;
字符表特殊值:WCH_NONE=0xf000(无字符)、WCH_DEAD=0xf001(死键)、WCH_LGTR=0xf002(连字)。
VK_TO_WCHARS.Attributes 的 CAPLOK(0x01)/CAPLOKALTGR(0x04) 表示该键受 CapsLock 影响。
修饰位常量:KBDSHIFT=0x001、KBDCTRL=0x002、KBDALT=0x004;SHFT_INVALID=0x0F。
KLLF_ALTGR=0x1 表示布局把右 Alt 当 AltGr 用。
3.6 KBDFILE 与 KL(布局对象)
input.h L15-39:
c
typedef struct tagKBDFILE /* 布局文件对象:一份布局 DLL 的共享描述 */
{
HEAD head; /* USER 对象头(句柄/引用) */
struct tagKBDFILE *pkfNext; /* 全局 KBDFILE 链表(gpkfList) */
WCHAR awchKF[20]; /* 布局 ID(KLID)字符串,如 L"00000409" */
HANDLE hBase; /* EngLoadImage 加载的布局 DLL 模块句柄 */
struct _KBDTABLES *pKbdTbl; /* KbdLayerDescriptor() 返回的翻译表 */
ULONG Size;
PKBDNLSLAYER pKbdNlsTbl; /* NLS 层(OEM 标识/布局信息/功能键表) */
} KBDFILE, *PKBDFILE;
typedef struct tagKL /* 键盘布局对象:布局链表的节点 */
{
HEAD head; /* USER 对象头 */
struct tagKL *pklNext; /* 环形链表:下一个布局 */
struct tagKL *pklPrev; /* 环形链表:上一个布局 */
DWORD dwKL_Flags; /* KL_* 标志(KL_UNLOAD=待卸载等) */
HKL hkl; /* 输入语言标识符(HKL) */
PKBDFILE spkf; /* 指向共享的 KBDFILE */
DWORD dwFontSigs; /* 字体签名(FS_*,来自 CHARSETINFO) */
UINT iBaseCharset; /* 基础字符集(ANSI_CHARSET 等) */
USHORT CodePage; /* 代码页(ciACP) */
WCHAR wchDiacritic; /* 重音字符状态 */
PIMEINFOEX piiex; /* IME 信息(IME 布局才有) */
} KL, *PKL;
布局对象的组织:
- 全局
gpkfList(kbdlayout.c L23):所有KBDFILE的单向链表(新文件头插)。 - 全局
gspklBaseLayout(kbdlayout.c L22):默认布局KL,所有已加载KL构成环形双向链表 ,
gspklBaseLayout指向链表头部(FIXME 注释指出理想上应放窗口站pWinSta->spklList)。 - 线程
THREADINFO.KeyboardLayout指向当前激活的KL;pti->pClientInfo->hKL是用户态
GetKeyboardLayout的数据源。 KL与KBDFILE是多对一:多个语言变体布局可以共享同一份布局 DLL/翻译表
(例如不同的德语变体都映射到 kbdgr.dll),因此"布局文件"与"布局实例"分离。
相关全局量(keyboard.c L13-21 / kbdlayout.c L24-26):
c
BYTE gafAsyncKeyState[256*2/8]; /* 全局异步键状态 */
KEYBOARD_ATTRIBUTES gKeyboardInfo; /* 键盘属性:类型/子类型/功能键数 */
DWORD gdwLanguageToggleKey = 1; /* 语言切换键:1=左 Alt,2=Ctrl */
DWORD gdwLayoutToggleKey = 2; /* 布局切换键:1=左 Alt,2=Ctrl */
BOOL gbEnableHexNumpad = FALSE; /* 是否启用 Alt+ 十六进制小键盘输入 */
PKL gspklBaseLayout = NULL; /* 默认布局 */
PKBDFILE gpkfList = NULL; /* 布局文件链表 */
DWORD gSystemFS = 0; /* 系统字体签名(首个布局的 dwFontSigs) */
UINT gSystemCPCharSet = 0; /* 系统字符集(首个布局的 iBaseCharset) */
HKL ghKLSentToShell = NULL; /* 已通知 shell 的语言(去重) */
3.7 键盘属性与指示灯
c
typedef struct _KEYBOARD_ATTRIBUTES { /* IOCTL_KEYBOARD_QUERY_ATTRIBUTES 返回 */
KEYBOARD_ID KeyboardIdentifier; /* Type/Subtype */
USHORT KeyboardMode;
USHORT NumberOfFunctionKeys;
USHORT NumberOfIndicators;
USHORT NumberOfKeysTotal;
ULONG InputDataQueueLength;
KEYBOARD_TYPEMATIC_PARAMETERS KeyRepeatMinimum/Maximum;
} KEYBOARD_ATTRIBUTES;
typedef struct _KEYBOARD_INDICATOR_PARAMETERS { USHORT UnitId; USHORT LedFlags; } ...;
typedef struct _KEYBOARD_INDICATOR_TRANSLATION {
USHORT NumberOfIndicatorKeys;
INDICATOR_LIST IndicatorList[1]; /* {MakeCode, IndicatorFlags} 对 */
} KEYBOARD_INDICATOR_TRANSLATION;
/* LedFlags:KEYBOARD_SCROLL_LOCK_ON=1 / NUM_LOCK_ON=2 / CAPS_LOCK_ON=4 / KANA_LOCK_ON=8 */
4. keyboard.c 核心函数逐一展开
4.1 初始化与硬件交互
InitKeyboardImpl(L48)
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; /* 101 键 → 12 个 F 键 */
return STATUS_SUCCESS;
}
由 InitUserImpl(ntuser.c)在驱动加载早期调用,把全局异步键状态清零,并给出默认键盘属性
(真正的属性在 UserInitKeyboard 中从驱动查询覆盖)。CODE_SEG("INIT") 使该函数位于 INIT
段,初始化完成后可被丢弃。
IntKeyboardGetIndicatorTrans(L67)
向键盘驱动发送 IOCTL_KEYBOARD_QUERY_INDICATOR_TRANSLATION,获取"哪些扫描码对应哪些指示灯"
的翻译表,存入全局 gpKeyboardIndicatorTrans(ExAllocatePoolWithTag(PagedPool, USERTAG_KBDTABLE))。
处理 STATUS_BUFFER_TOO_SMALL 时扩大缓冲区重试。
IntKeyboardUpdateLeds(L124)
按下 CapsLock/NumLock/ScrollLock(或翻译表中有对应 MakeCode 的键)时,翻转
gIndicators.LedFlags 中对应位,并通过 IOCTL_KEYBOARD_SET_INDICATORS 写回硬件,点亮/熄灭 LED。
c
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: /* 在翻译表中按 MakeCode 查 IndicatorFlags */ ...
}
if (LedFlag) {
gIndicators.LedFlags ^= LedFlag; /* 翻转对应灯位 */
ZwDeviceIoControlFile(..., IOCTL_KEYBOARD_SET_INDICATORS, &gIndicators, ...);
}
在 ProcessKeyEvent 中于"按下"时调用(keyboard.c L1148)。
UserInitKeyboard(L178)
RawInputThreadMain 打开 \Device\KeyboardClass0 后调用,做三件事:
IntKeyboardGetIndicatorTrans取指示灯翻译表;IOCTL_KEYBOARD_QUERY_INDICATORS读当前 LED 状态,用SET_KEY_LOCKED把
Caps/Num/Scroll 的 lock 位同步进gafAsyncKeyState;IOCTL_KEYBOARD_QUERY_ATTRIBUTES读键盘属性到gKeyboardInfo(类型/子类型/功能键数)。
这保证系统刚启动、用户还没按键时,GetKeyState(VK_CAPITAL) 就能反映真实 CapsLock 灯状态。
4.2 虚键简化/还原与辅助翻译
IntSimplifyVk(L236)
把"区分左右"的虚键合并为"不区分左右"的虚键:
c
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;
用于按键状态维护:gafAsyncKeyState 中 VK_SHIFT 的 down 位代表"任一 Shift 按下"。
IntFixVk(L264)
IntSimplifyVk 的逆操作:根据是否扩展键(E0 前缀)把简化虚键还原为左右细分虚键:
c
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;
用于生成消息的 wParam 与更新细分虚键的异步状态(Shift 永远不算扩展键,ProcessKeyEvent
中专门处理:if (wSimpleVk == VK_SHIFT) bExt = FALSE;)。
IntTranslateNumpadKey(L289)
NumLock 开启时,把编辑键区(Insert/End/Down/PgDn/Left/Clear/Right/Home/Up/PgUp/Delete)的
虚键翻译为对应小键盘数字键:VK_INSERT→VK_NUMPAD0、VK_END→VK_NUMPAD1......VK_DELETE→VK_DECIMAL。
在 UserProcessKeyboardInput 中当扫描码带 KBDNUMPAD 标志且 NumLock 锁定时应用
(IS_KEY_LOCKED(gafAsyncKeyState, VK_NUMLOCK))。
IntVscToVk(L579)与 IntVkToVsc(L542)
扫描码↔虚键的双向翻译(布局相关):
IntVscToVk(wScanCode, pKbdTbl):若高字节为0xE000查pVSCtoVK_E0表,0xE100查
pVSCtoVK_E1表,否则查pusVSCtoVK[wScanCode](wScanCode < bMaxVSCtoVK时)。
0xFF/0x00视为非法虚键返回 0。IntVkToVsc(wVk, pKbdTbl):先在pusVSCtoVK中线性找(vsc & 0xFF) == wVk返回下标;
再查 E0/E1 表,命中则返回0xE000|Vsc/0xE100|Vsc(MapVirtualKey 的 VK_TO_VSC_EX 语义);
未找到返回 0。
IntGetModBits(L315)
遍历 pCharModifiers->pVkToBit[](如 {VK_SHIFT,KBDSHIFT}、{VK_CONTROL,KBDCTRL}、{VK_MENU,KBDALT}),
凡是 IS_KEY_DOWN(pKeyState, Vk) 就把对应 ModBits 累加进 dwModBits,得到当前"修饰位组合",
供 IntTranslateChar 索引 ModNumber[]。
IntTranslateChar(L337)
虚键→字符的核心翻译 ,被 IntToUnicodeEx/IntVkToChar/NtUserVkKeyScanEx 复用。流程:
dwModBits = IntGetModBits(pKbdTbl, pKeyState);计算 AltGr:
bAltGr = (fLocaleFlags & KLLF_ALTGR) && IS_KEY_DOWN(pKeyState, VK_RMENU);
wCaplokAttr = bAltGr ? CAPLOKALTGR : CAPLOK(AltGr 布局下 CapsLock 只影响左 Alt 组合)。- 特例:单独 Alt(无 Ctrl)时去掉 KBDALT 位 (
(dwModBits & (KBDALT|KBDCTRL)) == KBDALT
→dwModBits &= ~KBDALT),使 Alt+键与无修饰组合等价------这就是"Alt 弹出菜单栏时不应产生
特殊字符"的根源。 - 遍历
pVkToWcharTable[](每个条目是一张VK_TO_WCHARS变长表),外层按虚键匹配
VirtualKey & 0xFF:- CapsLock 处理:
if ((Attributes & wCaplokAttr) && IS_KEY_LOCKED(pKeyState, VK_CAPITAL)) dwVkModBits ^= KBDSHIFT;------字母类键(带 CAPLOK 属性)在 CapsLock 时反转 Shift 位; dwModNumber = pCharModifiers->ModNumber[dwVkModBits](修饰组合→字符下标),
若>= nModifications则无此组合;wch = pVkToVch->wch[dwModNumber];WCH_NONE表示无字符;WCH_DEAD死键:翻译表约定死键记录之后紧跟一条VirtualKey == 0xFF的 trailer 记录,
其中存放"死键+默认字符"的合成结果(*pwcTranslatedChar = trailer->wch[dwModNumber]);WCH_LGTR连字:置*pbLigature = TRUE(当前IntToUnicodeEx直接放弃连字)。
- CapsLock 处理:
- 布局表未命中时的兜底:ASCII 控制字符 ------
wVirtKey ∈ ['A','Z']且 Ctrl 按下、Alt 未按下
时返回(wVirtKey - 'A') + 1(即 Ctrl+A → 0x01)。注释说明 Windows 也不在布局表中放这些项。
IntToUnicodeEx(L449)
虚键+同步键状态 → 字符串 (ToUnicodeEx 的内核实现)。维护静态 wchDead 记录上一次的死键:
IntTranslateChar翻译当前键;bLigature时 WARN 并返回 0(未实现连字)。- 若上次有死键
wchDead:查pDeadKey[]表,把dwBoth拆成高 16 位(死键字符)与低 16 位
(当前字符),匹配则用wchComposed作为合成字符并清除wchDead。
还包含一个 GCC 特判:wchDead == 0x8000时忽略"GCC 发明的高位"(编译器把 0xf001 的符号位
符号扩展所致)。 - 死键未找到合成对象:把死键本身作为普通字符输出(
pwszBuff[iRet++] = wchDead),当前字符照常输出。 - 返回值为
iRet(正=普通字符数;负=死键,绝对值=已输出的字符数),并把
wchDead = bDead ? wchTranslatedChar : 0供下次调用合成。
IntTranslateKbdMessage 正是利用负返回值把消息类型从 WM_CHAR 改为 WM_DEADCHAR。
IntVkToChar(L622)
不带键状态的"虚键→字符"(pKeyState = NULL 传给 IntTranslateChar,IntGetModBits 返回 0),
供 MapVirtualKey(MAPVK_VK_TO_CHAR) 与 NtUserGetKeyNameText 兜底使用。
4.3 异步键状态 API
NtUserGetAsyncKeyState(L649)
c
SHORT APIENTRY NtUserGetAsyncKeyState(INT Key)
{
if (Key >= 0x100 || Key < 0) { EngSetLastError(ERROR_INVALID_PARAMETER); return 0; }
UserEnterExclusive();
if (IS_KEY_DOWN(gafAsyncKeyState, Key)) wRet |= 0x8000; /* 按下 */
if (gafAsyncKeyStateRecentDown[Key/8] & (1 << (Key%8))) wRet |= 0x1; /* 曾按下 */
gafAsyncKeyStateRecentDown[Key/8] &= ~(1 << (Key%8)); /* 读即清 */
UserLeave();
return wRet;
}
- 高位
0x8000:物理上正在按下(含注入); - 低位
0x1:自上次查询以来按下过,读取后清除; - 校验
Key范围,非法时ERROR_INVALID_PARAMETER(user32 的GetAsyncKeyState也先做
vKey < 0 || vKey > 256检查)。
UpdateAsyncKeyState(L683)
全局异步状态的更新者(ProcessKeyEvent 调用):
c
if (bIsDown) {
/* 首次按下(up→down 沿)翻转 lock 位 */
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);
注意 lock 位翻转只在第一次按下发生------按住 CapsLock 不放的自动重复不会反复翻转。
4.4 低级钩子与截屏
co_CallLowLevelKeyboardHook(L704)
构造 KBDLLHOOKSTRUCT(vkCode/scanCode/flags/time/dwExtraInfo),按
KEYEVENTF_EXTENDEDKEY→LLKHF_EXTENDED、KEYEVENTF_KEYUP→LLKHF_UP、注入→LLKHF_INJECTED,
以及当前 Alt 状态 (IS_KEY_DOWN(gafAsyncKeyState, VK_MENU)→LLKHF_ALTDOWN)填 flags;
再按"是释放→WM_KEYUP,Alt 按下且 Ctrl 未按→WM_SYSKEYDOWN,否则 WM_KEYDOWN"选消息号,
调用 co_HOOK_CallHooks(WH_KEYBOARD_LL, HC_ACTION, uMsg, &KbdHookData)。返回值非 0 表示
低级钩子吞掉了这个键(ProcessKeyEvent 中 bPostMsg = FALSE)。
SnapWindow(L740)
PrtSc 截屏:VK_SNAPSHOT 按下时,若 Alt 按住(且 Ctrl 未按)则截取活动窗口,否则截取整个桌面。
用 UserGetDCEx(pWnd, NULL, DCX_USESTYLE|DCX_WINDOW) 取窗口 DC → NtGdiCreateCompatibleBitmap
建位图 → NtGdiBitBlt(SRCCOPY) 拷贝 → UserOpenClipboard/UserEmptyClipboard/UserSetClipboardData(CF_BITMAP)
存入剪贴板。由 ProcessKeyEvent 在 bIsDown && wVk == VK_SNAPSHOT 时调用(keyboard.c L1255)。
4.5 语言/布局切换
IntGetNextKL(L813)
在布局环形链表中找下一个/上一个布局:
c
PKL pFirstKL = pKL; LANGID LangID = LOWORD(pKL->hkl);
do {
pKL = (bNext ? pKL->pklNext : pKL->pklPrev);
if (!(pKL->dwKL_Flags & KL_UNLOAD) && bSameLang == (LangID == LOWORD(pKL->hkl)))
return pKL; /* 未被卸载,且(若要求)同语言 */
} while (pKL != pFirstKL);
return pFirstKL; /* 找不到就留在原地 */
bSameLang 决定是"同语言不同布局"(布局切换)还是"任意语言"(语言切换)。
IntLanguageToggle(L834)
执行语言/布局切换动作:
c
pWnd = pFocusQueue->spwndFocus ? : pFocusQueue->spwndActive; /* 焦点/活动窗口 */
pti = pWnd->head.pti; pkl = pti->KeyboardLayout;
if (nKeyState == INPUTLANGCHANGE_FORWARD) pkl = IntGetNextKL(pkl, TRUE, bSameLang);
else if (nKeyState == INPUTLANGCHANGE_BACKWARD) pkl = IntGetNextKL(pkl, FALSE, bSameLang);
if (gSystemFS & pkl->dwFontSigs) wParam |= INPUTLANGCHANGE_SYSCHARSET;
UserPostMessage(UserHMGetHandle(pWnd), WM_INPUTLANGCHANGEREQUEST, wParam, (LPARAM)pkl->hkl);
向焦点窗口投递 WM_INPUTLANGCHANGEREQUEST,由窗口过程(通常 DefWindowProc 内部调用
ActivateKeyboardLayout)完成真正的切换------内核只发出请求,不直接改线程布局。
IntCheckLanguageToggle(L871)
识别 [左 Alt]+Shift(前向)或 [右 Alt]+Shift(后向)组合:按下阶段记录
*pKeyState = INPUTLANGCHANGE_FORWARD/BACKWARD(wVk==VK_LSHIFT 或"非 Shift 键按下但
左 Shift 按住"),释放阶段(*pKeyState != 0 时)调用 IntLanguageToggle 并清零状态。
返回值 TRUE 表示该组合被识别(调用者据此跳过布局切换的重复判断)。
切换接线(ProcessKeyEvent L1224-1253)
c
if (gdwLanguageToggleKey == 1 || gdwLanguageToggleKey == 2) {
if (wSimpleVk == VK_SHIFT) { /* Shift 按下/释放 */
UINT targetKey = (gdwLanguageToggleKey == 1) ? VK_LMENU : VK_CONTROL;
if (IS_KEY_DOWN(gafAsyncKeyState, targetKey))
bLangToggled = IntCheckLanguageToggle(pFocusQueue, bIsDown, wVk, &gLanguageToggleKeyState);
} else if ((wSimpleVk == VK_MENU && gdwLanguageToggleKey == 1) ||
(wSimpleVk == VK_CONTROL && gdwLanguageToggleKey == 2)) {
if (IS_KEY_DOWN(gafAsyncKeyState, VK_SHIFT))
bLangToggled = IntCheckLanguageToggle(pFocusQueue, bIsDown, 0, &gLanguageToggleKeyState);
}
}
/* 若语言切换未触发,再试布局切换(gdwLayoutToggleKey),逻辑同上但用 &gLayoutToggleKeyState */
gdwLanguageToggleKey/gdwLayoutToggleKey 是系统参数(SPI_SETLANGUAGETOGGLE 等),
由 UserSystemParametersInfo 设置,默认 1(左 Alt)与 2(Ctrl)。
4.6 Alt+Numpad 字符输入
IntHandleAltNumpad(L916)
实现 Windows 的 Alt+数字小键盘 字符输入,状态机 gAltNumPadState:
| 状态 | 触发 | 进制/代码页 |
|---|---|---|
ALTNUM_OEM |
Alt + 数字(首位非 0) | 十进制,OEM 代码页 |
ALTNUM_ACP |
Alt + 0 开头 | 十进制,ANSI 代码页(ACP) |
ALTNUM_HEX_ACP |
Alt + .(启用十六进制时) | 十六进制,ACP |
ALTNUM_HEX_UTF |
Alt + +(启用十六进制时) | 十六进制,UTF-16 |
流程:
- 仅当
Alt 按下 && Ctrl 未按下时进入;gbEnableHexNumpad控制是否接受./+引导十六进制。 - 状态建立后,后续小键盘数字(
VK_NUMPAD0..9)累积到gAltNumPadValue(*10+digit或*16+digit,
允许溢出截断,与 Windows 一致);这些按键不再走普通键盘处理(返回 TRUE)。 - Alt 释放时(
!bIsDown && wVk == VK_MENU)结算:ALTNUM_HEX_UTF直接& 0xFFFF取码点;
其余用RtlOemToUnicodeN/RtlMultiByteToUnicodeN按代码页转换(CJK 代码页保留 2 字节,
否则& 0xFF截断为 1 字节,与 Windows 的mod 256行为一致)。 - 转换成功则构造
WM_CHAR消息(wParam=字符,lParam=1,time/pt 从当前输入补齐),
MsqPostMessage(pFocusQueue->ptiKeyboard, &msgChar, FALSE, QS_KEY, 0, 0)投递给焦点队列。 - 复位状态;非 Alt 键打断(状态被重置)。返回 FALSE 表示"仍需常规键处理"。
4.7 ProcessKeyEvent(按键主处理)
ProcessKeyEvent(L1106)是所有键盘事件(硬件+注入)的统一处理核心,按序执行:
c
BOOL NTAPI ProcessKeyEvent(WORD wVk, WORD wScanCode, DWORD dwFlags,
BOOL bInjected, DWORD dwTime, DWORD dwExtraInfo)
{
bExt = (dwFlags & KEYEVENTF_EXTENDEDKEY); /* E0 扩展键 */
bIsDown = !(dwFlags & KEYEVENTF_KEYUP);
bPacket = (dwFlags & KEYEVENTF_UNICODE); /* VK_PACKET 模式 */
wSimpleVk = IntSimplifyVk(wVk);
/* 日语特例:Shift 按住时 VK_OEM_ATTN→VK_CAPITAL、VK_OEM_COPY→VK_OEM_FINISH */
if (PRIMARYLANGID(gusLanguageID) == LANG_JAPANESE && IS_KEY_DOWN(gafAsyncKeyState, VK_SHIFT)) {...}
/* 1. Alt+Numpad 组合 */
if (IntHandleAltNumpad(wSimpleVk, bIsDown, dwTime)) return TRUE;
bWasSimpleDown = IS_KEY_DOWN(gafAsyncKeyState, wSimpleVk);
/* 2. 维护简化虚键的异步状态(VK_SHIFT 等):
释放时若细分键仍按住,则简化键保持 down(wVk2 = IntFixVk(wSimpleVk, !bExt)) */
wVk2 = IntFixVk(wSimpleVk, !bExt);
bIsSimpleDown = bIsDown || IS_KEY_DOWN(gafAsyncKeyState, wVk2);
UpdateAsyncKeyState(wSimpleVk, bIsSimpleDown);
/* 3. 按下时更新指示灯 */
if (bIsDown) IntKeyboardUpdateLeds(ghKeyboardDevice, wSimpleVk, wScanCode);
/* 4. WH_KEYBOARD_LL 低级钩子(可吞键) */
if (co_CallLowLevelKeyboardHook(wVk, wScanCode, dwFlags, bInjected, dwTime, dwExtraInfo))
bPostMsg = FALSE;
/* 5. 全局热键 */
if (co_UserProcessHotKeys(wSimpleVk, bIsDown)) bPostMsg = FALSE;
wFixedVk = IntFixVk(wSimpleVk, bExt); /* LSHIFT + EXT → RSHIFT */
if (wSimpleVk == VK_SHIFT) bExt = FALSE;
pFocusQueue = IntGetFocusMessageQueue();
...
/* 6. SysKey 判定 */
if (wVk == VK_F10 ||
(wSimpleVk == VK_MENU && bMenuDownRecently) ||
(IS_KEY_DOWN(gafAsyncKeyState, VK_MENU) && !IS_KEY_DOWN(gafAsyncKeyState, VK_CONTROL)) ||
(pFocusQueue && !pFocusQueue->spwndFocus))
Msg.message = bIsDown ? WM_SYSKEYDOWN : WM_SYSKEYUP; /* 附带 bMenuDownRecently 维护 */
else
Msg.message = bIsDown ? WM_KEYDOWN : WM_KEYUP;
/* 7. 细分虚键的异步状态(GetKeyState winetest 依赖) */
UpdateAsyncKeyState(wFixedVk, bIsDown);
/* 8. 语言/布局切换检测(见 4.5) */
/* 9. VK_SNAPSHOT → SnapWindow 截屏 */
/* 10. 组装 MSG 并入队 */
Wnd = pFocusQueue->spwndFocus ? : pFocusQueue->spwndActive;
pti = Wnd ? Wnd->head.pti : pFocusQueue->ptiKeyboard;
Msg.hwnd = Wnd ? UserHMGetHandle(Wnd) : NULL;
Msg.wParam = wFixedVk & 0xFF;
Msg.lParam = MAKELPARAM(1, wScanCode);
Msg.time = dwTime; Msg.pt = gpsi->ptCursor;
/* lParam 高位标志(非 VK_PACKET 时):
KF_EXTENDED / KF_ALTDOWN / KF_REPEAT / KF_UP / KF_DLGMODE / KF_MENUMODE */
if (!bPacket) {
if (bExt) Msg.lParam |= KF_EXTENDED << 16;
if (IS_KEY_DOWN(gafAsyncKeyState, VK_MENU)) Msg.lParam |= KF_ALTDOWN << 16;
if (bWasSimpleDown) Msg.lParam |= KF_REPEAT << 16;
if (!bIsDown) Msg.lParam |= KF_UP << 16;
if (pFocusQueue->QF_flags & QF_DIALOGACTIVE) Msg.lParam |= KF_DLGMODE << 16;
if (pFocusQueue->MenuOwner) Msg.lParam |= KF_MENUMODE << 16;
}
/* 入队前先把滞留的鼠标移动合并,保持输入顺序 */
if (pFocusQueue->QF_flags & QF_MOUSEMOVED) IntCoalesceMouseMove(pti);
MsqPostMessage(pti, &Msg, TRUE, QS_KEY, 0, dwExtraInfo);
return TRUE;
}
关键点:
- SysKey 判定 :F10、Alt 系列(含"刚按过 Alt"的
bMenuDownRecently状态,保证只按 LAlt
也生成 SYSKEY 消息)、无焦点窗口时,都产生WM_SYSKEY*;否则普通WM_KEY*。 - lParam 编码 :低字 = 重复计数(1) + 扫描码;高字 =
KF_*位组合(KF_EXTENDED=0x0100、
KF_ALTDOWN=0x2000、KF_REPEAT=0x4000、KF_UP=0x8000、KF_DLGMODE=0x0800、KF_MENUMODE=0x1000)。 - wParam 用 wFixedVk (左右区分的虚键),但
co_IntProcessKeyboardMessage取出消息时又会
把VK_LSHIFT/VK_RSHIFT合并为VK_SHIFT------Windows 的键盘消息 wParam 按简化虚键传递。 - 硬件消息入队 :
MsqPostMessage(..., HardwareMessage=TRUE, QS_KEY),挂到
MessageQueue->HardwareMessagesListHead,置QS_KEY唤醒队列。
4.8 UserSendKeyboardInput 与 UserProcessKeyboardInput
UserSendKeyboardInput(L1332)
c
BOOL NTAPI UserSendKeyboardInput(KEYBDINPUT *pKbdInput, BOOL bInjected)
{
gppiInputProvider = ((PTHREADINFO)PsGetCurrentThreadWin32Thread())->ppi; /* 注入来源进程 */
/* 取焦点线程布局;无则退回默认布局 */
pFocusQueue = IntGetFocusMessageQueue();
if (pFocusQueue && pFocusQueue->ptiKeyboard)
pKl = pFocusQueue->ptiKeyboard->KeyboardLayout;
if (!pKl) pKl = W32kGetDefaultKeyLayout();
if (!pKl) return FALSE;
pKbdTbl = pKl->spkf->pKbdTbl;
wScanCode = pKbdInput->wScan; /* wScan 字段总是被使用 */
if (pKbdInput->dwFlags & KEYEVENTF_UNICODE) {
wVk = VK_PACKET; /* 产生 wParam==VK_PACKET、lParam 高字为 wchar 的消息 */
} else {
wScanCode &= 0x7F;
if (pKbdInput->dwFlags & KEYEVENTF_SCANCODE)
wVk = IntVscToVk(wScanCode | (bExt ? 0xE000 : 0), pKbdTbl) || 0xFF;
else
wVk = pKbdInput->wVk;
wVk &= 0xFF; /* 去掉 KBDEXT 等标志 */
}
dwTime = pKbdInput->time ? : EngGetTickCount32();
/* AltGr 布局:右 Alt 同时产生 Ctrl 事件(Windows 行为) */
if (wVk == VK_RMENU && (pKbdTbl->fLocaleFlags & KLLF_ALTGR))
ProcessKeyEvent(VK_LCONTROL, 0, pKbdInput->dwFlags & KEYEVENTF_KEYUP, bInjected, dwTime, 0);
return ProcessKeyEvent(wVk, wScanCode, pKbdInput->dwFlags, bInjected, dwTime, pKbdInput->dwExtraInfo);
}
UserProcessKeyboardInput(L1414)
硬件输入入口(由 RawInputThreadMain 调用,input.c L342):
c
VOID NTAPI UserProcessKeyboardInput(PKEYBOARD_INPUT_DATA pKbdInputData)
{
/* 扫描码归一化:MakeCode & 0x7F,KEY_E0 → 0xE000,KEY_E1 → 0xE100 */
wScanCode = pKbdInputData->MakeCode & 0x7F;
if (pKbdInputData->Flags & KEY_E0) wScanCode |= 0xE000;
if (pKbdInputData->Flags & KEY_E1) wScanCode |= 0xE100;
/* 焦点队列 → 焦点线程布局 → 默认布局兜底 */
pFocusQueue = IntGetFocusMessageQueue();
if (pFocusQueue && pFocusQueue->ptiKeyboard) pKl = pFocusQueue->ptiKeyboard->KeyboardLayout;
if (!pKl) pKl = W32kGetDefaultKeyLayout();
if (!pKl) return;
pKbdTbl = pKl->spkf->pKbdTbl;
wVk = IntVscToVk(wScanCode, pKbdTbl); /* 扫描码 → 虚键 */
if (wVk) {
/* NumLock 下小键盘数字键翻译 */
if ((wVk & KBDNUMPAD) && IS_KEY_LOCKED(gafAsyncKeyState, VK_NUMLOCK))
wVk = IntTranslateNumpadKey(wVk & 0xFF);
/* 组装 KEYBDINPUT 交给 UserSendKeyboardInput(统一走 ProcessKeyEvent) */
KbdInput.wVk = wVk & 0xFF;
KbdInput.wScan = wScanCode & 0x7F;
KbdInput.dwFlags = (pKbdInputData->Flags & KEY_BREAK) ? KEYEVENTF_KEYUP : 0;
if (wVk & KBDEXT) KbdInput.dwFlags |= KEYEVENTF_EXTENDEDKEY;
if (wVk == VK_RSHIFT) KbdInput.dwFlags |= KEYEVENTF_EXTENDEDKEY; /* 右 Shift 特殊 */
KbdInput.time = 0;
KbdInput.dwExtraInfo = pKbdInputData->ExtraInformation;
UserSendKeyboardInput(&KbdInput, FALSE);
/* E1 键没有独立 break 码,补发一次 KEYUP */
if (pKbdInputData->Flags & KEY_E1) {
KbdInput.dwFlags |= KEYEVENTF_KEYUP;
UserSendKeyboardInput(&KbdInput, FALSE);
}
}
}
4.9 IntTranslateKbdMessage(TranslateMessage 的内核实现)
NtUserTranslateMessage(message.c L2489)校验窗口后调用本函数(keyboard.c L1499),
把 WM_KEYDOWN/KEYUP/SYSKEYDOWN/SYSKEYUP 翻译为字符消息:
c
BOOL FASTCALL IntTranslateKbdMessage(LPMSG lpMsg, UINT flags)
{
switch (lpMsg->message) { /* 只处理 4 种键消息 */
case WM_KEYDOWN: case WM_KEYUP: case WM_SYSKEYDOWN: case WM_SYSKEYUP: break;
default: return FALSE;
}
pti = PsGetCurrentThreadWin32Thread();
/* 线程无布局时补挂默认布局并回写 pClientInfo->hKL */
if (!pti->KeyboardLayout) {
PKL pDefKL = W32kGetDefaultKeyLayout();
UserAssignmentLock((PVOID*)&pti->KeyboardLayout, pDefKL);
if (pDefKL) { pti->pClientInfo->hKL = pDefKL->hkl; pKbdTbl = pDefKL->spkf->pKbdTbl; }
else { pti->pClientInfo->hKL = NULL; pKbdTbl = NULL; }
} else
pKbdTbl = pti->KeyboardLayout->spkf->pKbdTbl;
if (!pKbdTbl) return FALSE;
if (lpMsg->message != WM_KEYDOWN && lpMsg->message != WM_SYSKEYDOWN)
return FALSE; /* 只翻译按下(KEYUP 无字符) */
/* VK_PACKET:lParam 高字即字符,直接转 WM_CHAR/WM_SYSCHAR */
if (lpMsg->wParam == VK_PACKET) {
NewMsg.message = (lpMsg->message == WM_KEYDOWN) ? WM_CHAR : WM_SYSCHAR;
NewMsg.wParam = HIWORD(lpMsg->lParam);
NewMsg.lParam = LOWORD(lpMsg->lParam);
MsqPostMessage(pti, &NewMsg, FALSE, QS_KEY, 0, 0);
return TRUE;
}
cch = IntToUnicodeEx(lpMsg->wParam, HIWORD(lpMsg->lParam) & 0xFF,
pti->MessageQueue->afKeyState, /* 同步键状态 */
wch, 3, 0, pKbdTbl);
if (cch) {
if (cch > 0) /* 普通字符 */
NewMsg.message = (lpMsg->message == WM_KEYDOWN) ? WM_CHAR : WM_SYSCHAR;
else { /* 死键:负数返回 */
cch = -cch;
NewMsg.message = (lpMsg->message == WM_KEYDOWN) ? WM_DEADCHAR : WM_SYSDEADCHAR;
}
NewMsg.lParam = lpMsg->lParam;
for (i = 0; i < cch; ++i) {
NewMsg.wParam = wch[i];
MsqPostMessage(pti, &NewMsg, FALSE, QS_KEY, 0, 0);
}
bResult = TRUE;
}
return bResult;
}
要点:
- 翻译使用队列同步键状态
pti->MessageQueue->afKeyState(而非全局异步状态),
与消息流严格同步------这是"TranslateMessage 必须与 GetMessage 同线程"的原因。 - 扫描码取
HIWORD(lParam) & 0xFF(含 E0 前缀信息,IntToUnicodeEx的 wScanCode 参数实际
只用于部分布局区分)。 - 字符消息(WM_CHAR 等)以投递消息 方式入队(
HardwareMessage=FALSE),带QS_KEY位。
4.10 MapVirtualKey / ToUnicodeEx / GetKeyNameText / VkKeyScan
IntMapVirtualKeyEx(L1616)与 NtUserMapVirtualKeyEx(L1662)
五种映射模式(IntMapVirtualKeyEx):
| Type | 宏 | 实现 |
|---|---|---|
| 0 | MAPVK_VK_TO_VSC |
IntFixVk(uCode, FALSE) → IntVkToVsc;带前缀的结果(>0xFF)置 0 |
| 1 | MAPVK_VSC_TO_VK |
IntVscToVk → IntSimplifyVk(去左右) |
| 2 | MAPVK_VK_TO_CHAR |
IntVkToChar(无 Shift 状态的字符) |
| 3 | MAPVK_VSC_TO_VK_EX |
IntVscToVk(保留左右区分) |
| 4 | MAPVK_VK_TO_VSC_EX |
IntVkToVsc(保留 E0/E1 前缀) |
NtUserMapVirtualKeyEx(L1662)在 UserEnterShared 下先按 dwhkl 取布局
(dwhkl 为空用当前线程布局,否则 UserHklToKbl),再调 IntMapVirtualKeyEx。
NtUserToUnicodeEx(L1702)
ToUnicodeEx 的内核实现:
- 参数校验:
wScanCode & SC_KEY_UP或wVirtKey >= 0x100时返回 0(ToUnicode 约定)。 - SEH 探针拷贝用户态 256 字节键状态为紧凑位图
afKeyState(KS_DOWN_BIT→SET_KEY_DOWN,
KS_LOCK_BIT→SET_KEY_LOCKED)。 - 分配
pwszBuff,UserEnterExclusive()(IntToUnicodeEx的wchDead静态变量需要独占)。 - 取布局(
dwhkl→UserHklToKbl,否则当前线程布局),调IntToUnicodeEx;
成功则MmCopyToCaller回拷结果。
NtUserGetKeyNameText(L1807)
GetKeyNameText 的内核实现:从 lParam 解出扫描码 (lParam >> 16) & 0xFF 与
KF_EXTENDED 标志;处理 LP_DO_NOT_CARE_BIT(把右 Shift 当作左 Shift、Ctrl/Alt 视为非扩展);
按扩展性选 pKeyNamesExt/pKeyNames 表线性查找 vsc;未找到时用 IntVscToVk +
IntVkToChar 兜底(返回该键的字符名);结果经 ProbeForWrite 写回用户缓冲,返回字符数
(不含结尾 NUL)。user32 的 GetKeyNameTextA 再经 WideCharToMultiByte 转 ANSI。
UserGetKeyboardType(L1907)
按 dwTypeFlag 返回 gKeyboardInfo 的字段:0=类型、1=子类型、2=功能键数。
由 NtUserxGetKeyboardType(simplecall.c L300 的 ONEPARAM_ROUTINE_GETKEYBOARDTYPE)分发。
NtUserVkKeyScanEx(L1932)
VkKeyScanEx 的内核实现------反查:给定字符,找能产生它的虚键+修饰组合:
c
for (i = 0; pKbdTbl->pVkToWcharTable[i].pVkToWchars; i++) {
pVkToWchTbl = &pKbdTbl->pVkToWcharTable[i];
pVkToWch = pVkToWchTbl->pVkToWchars;
while (pVkToWch->VirtualKey) {
for (dwModNumber = 0; dwModNumber < pVkToWchTbl->nModifications; dwModNumber++) {
if (pVkToWch->wch[dwModNumber] == wch) {
dwModBits = pKbdTbl->pCharModifiers->ModNumber[dwModNumber];
Ret = (dwModBits << 8) | (pVkToWch->VirtualKey & 0xFF);
goto Exit;
}
}
pVkToWch = (PVK_TO_WCHARS10)(((BYTE*)pVkToWch) + pVkToWchTbl->cbSize);
}
}
/* 返回:高字节 = Shift 状态(KBDSHIFT/KBDCTRL/KBDALT),低字节 = 虚键码;失败 = 0xFFFFFFFF */
bUsehKL 控制用传入 dwhkl 还是当前线程布局(VkKeyScanW 传 FALSE,VkKeyScanExW 传 TRUE)。
5. kbdlayout.c:键盘布局对象管理
5.1 查询辅助
IntHKLtoPKL(L36)
在当前线程布局链表中按 HKL 找 KL,支持四种 HKL 形态:
HKL_NEXT(UlongToHandle(HKL_NEXT)):沿pklNext找第一个未卸载(!KL_UNLOAD)的布局;HKL_PREV:沿pklPrev反向找;HIWORD(hKL)非零(完整输入语言标识符):按pKL->hkl == hKL精确匹配(不做 UNLOAD 检查);- 仅 LOWORD(纯语言 ID):按
LOWORD(pKL->hkl) == LOWORD(hKL)匹配语言。
均以 pFirstKL = pti->KeyboardLayout 为起点做环形遍历。
UserHklToKbl(L539)
全局版本:从 gspklBaseLayout 起环形遍历 pKL->hkl == hKl,返回 KL 或 NULL。
被 NtUserMapVirtualKeyEx/NtUserVkKeyScanEx/NtUserToUnicodeEx/UserSetDefaultInputLang 等大量使用。
W32kGetDefaultKeyLayout(L512)
返回"给新线程用的默认布局":从 gspklBaseLayout 起沿 pklPrev 找第一个未卸载的 KL
(注释注明"Confirmed on Win2k"------向链尾方向找更稳定),找不到返回 NULL。
IntGetKeyboardLayoutList(L96)
NtUserGetKeyboardLayoutList 的辅助:nBuff==0 时数出有效(非 KL_UNLOAD)布局个数;
否则把有效布局的 hkl 依次拷入 pHklBuff(最多 nBuff 个)。遍历从 gspklBaseLayout 开始环形走。
5.2 加载与卸载
UserLoadKbdDll(L248)
c
*phModule = EngLoadImage(pwszLayoutPath); /* 内核加载布局 DLL */
pfnKbdLayerDescriptor = EngFindImageProcAddress(*phModule, "KbdLayerDescriptor");
if (pfnKbdLayerDescriptor) *pKbdTables = pfnKbdLayerDescriptor(); /* 取翻译表 */
/* FIXME: Windows 是读文件而非执行 DLL!在内核态执行布局 DLL 有安全隐患,
将来应改为直接解析 kbd*.dll 中的 KBDTABLES 数据段 */
if (!pfnKbdLayerDescriptor || !*pKbdTables) {
EngUnloadImage(*phModule);
return FALSE;
}
return TRUE;
失败时卸载 DLL 并返回 FALSE。
UserLoadKbdFile(L296)
创建 KBDFILE 对象并填充内容:
UserCreateObject(gHandleTable, NULL, NULL, NULL, TYPE_KBDFILE, sizeof(KBDFILE))分配对象;_swprintf(pkf->awchKF, L"%wZ", pwszKLID)记录布局 ID(KLID,如 "00000409");- 打开注册表键
\REGISTRY\Machine\SYSTEM\CurrentControlSet\Control\Keyboard Layouts\<KLID>,
读Layout File值得到布局 DLL 路径(\SystemRoot\System32\<文件>); UserLoadKbdDll加载 DLL 并把pKbdTbl存入pkf->pKbdTbl、hBase存模块句柄;- 头插 进全局
gpkfList(pkf->pkfNext = gpkfList; gpkfList = pkf;)------同一布局 DLL
被多次加载时复用已有 KBDFILE; - 失败路径:
UserDereferenceObject后若未成功则UserDeleteObject(UserHMGetHandle(pkf), TYPE_KBDFILE)。
co_UserLoadKbdLayout(L375)
创建 KL 对象并绑定 KBDFILE:
c
pKl = UserCreateObject(gHandleTable, NULL, NULL, NULL, TYPE_KBDLAYOUT, sizeof(KL));
pKl->hkl = hKL; /* 输入语言标识符 */
pKl->spkf = UserLoadKbdFile(pustrKLID); /* 共享的布局文件对象 */
UserDereferenceObject(pKl);
if (!pKl->spkf) { ... UserDeleteObject(...); return NULL; }
/* 解析 KLID 为 LCID,取字符集信息填充 KL 字段 */
RtlUnicodeStringToInteger(pustrKLID, 16, &lCid);
if (co_IntGetCharsetInfo(lCid, &cs)) {
pKl->iBaseCharset = cs.ciCharset; /* 如 ANSI_CHARSET */
pKl->dwFontSigs = cs.fs.fsCsb[0]; /* 如 FS_LATIN1 */
pKl->CodePage = (USHORT)cs.ciACP; /* 代码页,如 1252 */
} else {
pKl->iBaseCharset = ANSI_CHARSET; pKl->dwFontSigs = FS_LATIN1; pKl->CodePage = CP_ACP;
}
/* 系统字符集/字体签名:以第一个加载的布局为准 */
if (gSystemFS == 0) { gSystemCPCharSet = pKl->iBaseCharset; gSystemFS = pKl->dwFontSigs; }
return pKl;
gSystemFS/gSystemCPCharSet 之后被 IntLanguageToggle 用于
INPUTLANGCHANGE_SYSCHARSET 标志判断。
UnloadKbdFile(L444)与 UserUnloadKbl(L471)
UnloadKbdFile:从gpkfList摘除 pkf(找前驱改pkfNext),EngUnloadImage(pkf->hBase)卸载
DLL,UserDeleteObject(TYPE_KBDFILE)删除对象。UserUnloadKbl:卸载一个 KL:- 若是
gspklBaseLayout且链上只有自己,返回 FALSE(默认布局不可卸载); - 否则把
gspklBaseLayout顺移到pklNext; pKl->head.cLockObj > 1(有其他线程使用)时仅置KL_UNLOAD标记(延迟卸载),返回 FALSE;- 否则摘链表、
UnloadKbdFile(pKl->spkf)、释放piiex(ExFreePoolWithTag(USERTAG_IME))、
UserDeleteObject(TYPE_KBDLAYOUT),返回 TRUE。
- 若是
KL_UNLOAD 标记的意义:布局仍挂在链表中(句柄仍可解析),但被所有"找有效布局"的遍历跳过
(IntGetKeyboardLayoutList/W32kGetDefaultKeyLayout/IntHKLtoPKL 的 NEXT/PREV 分支都检查它),
直到引用计数归零被真正删除------这与 Windows 的"UnloadKeyboardLayout 仅当无线程使用时才真正卸载"一致。
5.3 重排与默认布局
IntReorderKeyboardLayouts(L558)
把 pNewKL 移到环形链表头部并令其成为新的 gspklBaseLayout(即默认布局):
c
if ((pWinSta->Flags & WSS_NOIO) || pNewKL == pOldKL) return; /* 无输入窗口站/未变化 */
/* 摘除 pNewKL */
pNewKL->pklPrev->pklNext = pNewKL->pklNext;
pNewKL->pklNext->pklPrev = pNewKL->pklPrev;
/* 头插到 pOldKL 之前 */
pNewKL->pklNext = pOldKL; pNewKL->pklPrev = pOldKL->pklPrev;
pOldKL->pklPrev->pklNext = pNewKL; pOldKL->pklPrev = pNewKL;
gspklBaseLayout = pNewKL; /* 新默认布局 */
被 UserSetDefaultInputLang、co_IntActivateKeyboardLayout(KLF_REORDER)、
co_IntLoadKeyboardLayoutEx(KLF_REORDER)调用。
UserSetDefaultInputLang(L583)
c
BOOL NTAPI UserSetDefaultInputLang(HKL hKl)
{
pKl = UserHklToKbl(hKl);
if (!pKl) return FALSE;
IntReorderKeyboardLayouts(IntGetProcessWindowStation(NULL), pKl);
return TRUE;
}
由 UserSystemParametersInfo(SPI_SETDEFAULTINPUTLANG) 调用(user32 的
IntLoadPreloadKeyboardLayouts 在登录时也会调用),把系统默认输入语言设为 hKl 对应布局。
5.4 激活布局
IntImmActivateLayout(L596)
把某线程的键盘布局切换到 pKL(IME 感知版本):
c
if (pti->KeyboardLayout == pKL) return;
pImeWnd = pti->spwndDefaultIme; /* 线程默认 IME 窗口 */
if (pImeWnd) {
co_IntSendMessage(UserHMGetHandle(pImeWnd), WM_IME_SYSTEM, IMS_ACTIVATELAYOUT, (LPARAM)pKL->hkl);
} else {
/* 记住旧布局,中文 IME 需要切回 */
pti->hklPrev = pti->KeyboardLayout->hkl;
if (pti->spDefaultImc) pti->pClientInfo->CI_flags |= CI_IMMACTIVATE; /* 需要 IME 激活 */
}
UserAssignmentLock((PVOID*)&pti->KeyboardLayout, pKL); /* 原子切换线程布局指针 */
pti->pClientInfo->hKL = pKL->hkl; /* 用户态可见 HKL */
pti->pClientInfo->CodePage = pKL->CodePage; /* 代码页同步 */
UserAssignmentLock 是 win32k 的原子指针赋值辅助(对 pti->KeyboardLayout 这类共享指针做
互锁交换),防止与输入线程并发读写竞态。
co_IntSetKeyboardLayoutForProcess(L632)
遍历进程 ppi->ptiList 的全部线程,跳过已是指定布局或正在清理(TIF_INCLEANUP)的线程,
逐个调用 IntImmActivateLayout(IME 模式)或 UserAssignmentLock + 更新 pClientInfo
(非 IME 模式)。实现 KLF_SETFORPROCESS 的"整进程生效"语义。
co_UserActivateKeyboardLayout(L667)
ActivateKeyboardLayout 的内核核心,返回旧 HKL:
c
pOldKL = pti->KeyboardLayout; hOldKL = pOldKL ? pOldKL->hkl : NULL;
if (uFlags & KLF_RESET) FIXME("KLF_RESET\n"); /* 仅 winlogon 支持,未实现 */
if (!bSetForProcess && pKL == pti->KeyboardLayout) return hOldKL; /* 未变化 */
pKL->wchDiacritic = UNICODE_NULL; /* 重置布局的重音状态 */
if (pti->TIF_flags & TIF_CSRSSTHREAD) { /* CSRSS 线程:直接切 */
UserAssignmentLock(&pti->KeyboardLayout, pKL); ClientInfo->CodePage/ hKL = ...;
} else if (bSetForProcess) {
co_IntSetKeyboardLayoutForProcess(pti->ppi, pKL); /* 整进程切换 */
} else {
if (IS_IMM_MODE()) IntImmActivateLayout(pti, pKL); /* IME 模式 */
else UserAssignmentLock(&pti->KeyboardLayout, pKL);
ClientInfo->CodePage = pKL->CodePage; ClientInfo->hKL = pKL->hkl;
}
/* 前台进程切换语言时通知 shell(HSHELL_LANGUAGE,去重) */
if (gptiForeground && (gptiForeground->ppi == pti->ppi) && ISITHOOKED(WH_SHELL))
if (ghKLSentToShell != pKL->hkl) { co_IntShellHookNotify(HSHELL_LANGUAGE, 0, pKL->hkl); ghKLSentToShell = pKL->hkl; }
/* 向焦点/活动窗口发送 WM_INPUTLANGCHANGE(wParam=字符集,lParam=HKL) */
pTargetWnd = pti->MessageQueue->spwndFocus ?: pti->MessageQueue->spwndActive ?: pWnd;
if (pTargetWnd) co_IntSendMessage(UserHMGetHandle(pTargetWnd), WM_INPUTLANGCHANGE,
pKL->iBaseCharset, (LPARAM)pKL->hkl);
/* IME 布局:通过 WM_IME_SYSTEM:IMS_SENDNOTIFICATION 刷新 IME UI */
if (!(pti->TIF_flags & TIF_CSRSSTHREAD) && (IS_IME_HKL(pKL->hkl) || (IS_CICERO_MODE() && !IS_16BIT_MODE())))
if (pImeWnd = pti->spwndDefaultIme)
co_IntSendMessage(hImeWnd, WM_IME_SYSTEM, IMS_SENDNOTIFICATION, bSetForProcess & !IS_16BIT_MODE());
IS_IMM_MODE()/IS_IME_HKL()/IS_CICERO_MODE()/IS_16BIT_MODE() 是 ime.h 中依据
gpsi->dwSRVIFlags(SERVERINFO 的 SRVINFO_IMM32/SRVINFO_CICERO/SRVINFO_16BIT)判定的
输入法运行模式宏。
co_IntActivateKeyboardLayout(L782)
NtUserActivateKeyboardLayout 的内核辅助:先用 IntHKLtoPKL(pti, hKL) 把 HKL 解析为 KL
(支持 HKL_NEXT/HKL_PREV),KLF_REORDER 时先 IntReorderKeyboardLayouts,再委托
co_UserActivateKeyboardLayout。
5.5 卸载布局
co_IntUnloadKeyboardLayoutEx(L805)与 IntUnloadKeyboardLayout(L851)
c
static BOOL co_IntUnloadKeyboardLayoutEx(PWINSTATION_OBJECT pWinSta, PKL pKL, DWORD dwFlags)
{
if (pKL == gspklBaseLayout && !(dwFlags & UKL_NOACTIVATENEXT))
return FALSE; /* 默认布局且未要求激活下一个 */
UserRefObjectCo(pKL, &Ref1); /* 引用保护 */
UserMarkObjectDestroy(pKL); /* 标记销毁(句柄失效) */
pKL->dwKL_Flags |= KL_UNLOAD; /* 加入"待卸载"队列 */
/* 当前线程正用此布局:激活下一个有效布局 */
if (!(dwFlags & UKL_NOACTIVATENEXT) && pti->KeyboardLayout == pKL) {
pNextKL = IntHKLtoPKL(pti, UlongToHandle(HKL_NEXT));
if (pNextKL) { co_UserActivateKeyboardLayout(pNextKL, dwFlags, NULL); ... }
}
if (gspklBaseLayout == pKL && pKL != pKL->pklNext)
gspklBaseLayout = pKL->pklNext; /* 默认布局顺移 */
...
if (ISITHOOKED(WH_SHELL)) { co_IntShellHookNotify(HSHELL_LANGUAGE, 0, 0); ghKLSentToShell = NULL; }
return TRUE;
}
BOOL IntUnloadKeyboardLayout(PWINSTATION_OBJECT pWinSta, HKL hKL) /* 简单包装,dwFlags=0 */
UserMarkObjectDestroy 使该布局句柄立即失效(UserHklToKbl 仍能找到对象但用户侧句柄解析失败),
KL_UNLOAD 使其被有效布局遍历跳过;真正删除在引用计数归零时由 UserUnloadKbl 完成。
5.6 IME 布局加载
co_UserImmLoadLayout(L863)
调用用户态 imm32 的 ImmLoadLayout(经 co_ClientImmLoadLayout 回调)加载 IME 信息:
c
if (!IS_IME_HKL(hKL) && !IS_CICERO_MODE()) return NULL; /* 非 IME 布局无需 */
piiex = ExAllocatePoolWithTag(PagedPool, sizeof(IMEINFOEX), USERTAG_IME);
if (!co_ClientImmLoadLayout(hKL, piiex)) { ExFreePoolWithTag(piiex, USERTAG_IME); return NULL; }
return piiex;
结果存进 KL.piiex,供 IME 处理使用(见分册 _24)。
co_IntLoadKeyboardLayoutEx(L884)
LoadKeyboardLayout 的内核核心:
hNewKL == NULL或窗口站WSS_NOIO(无输入权限)时返回 NULL;hOldKL && hOldKL != hNewKL:先UserUnloadKbl卸载旧布局(KLF_UNLOADPREVIOUS语义);UserHklToKbl(hNewKL)已存在则复用;否则co_UserLoadKbdLayout创建新 KL 并挂入环形链表
(首个布局自环并成为gspklBaseLayout;后续布局插到"最后一个未卸载布局"之后),
再pNewKL->piiex = co_UserImmLoadLayout(hNewKL);- 清除
KL_UNLOAD(取消待卸载状态,允许重新激活已标记卸载的布局); KLF_REORDER→IntReorderKeyboardLayouts;KLF_ACTIVATE→co_UserActivateKeyboardLayout;- 非
KLF_NOTELLSHELL时co_IntShellHookNotify(HSHELL_LANGUAGE, 0, hNewKL)通知 shell; - 返回
hNewKL(FIXME:KLF_REPLACELANG未实现)。
IntVerifyKeyboardFileHandle(L962)
校验 NtUserLoadKeyboardLayoutEx 传入的文件句柄:ObReferenceObjectByHandle 验证句柄对应
文件对象后释放引用,返回句柄本身(或 NULL)。这是 ReactOS 相对 Windows 的安全设计差异------
Windows 的 NtUserLoadKeyboardLayoutEx 允许传入自定义布局文件句柄(曾被用作提权漏洞,
seclists 2012/Jul/137),ReactOS 的 NtUserLoadKeyboardLayoutEx 直接忽略
offTable/pTables 参数,只按 KLID 从注册表加载。
5.7 导出 API(NtUser* 系统调用)
UserGetKeyboardLayout(L995)
GetKeyboardLayout(idThread) 的内核实现(经 simplecall.c ONEPARAM_ROUTINE_GETKEYBOARDLAYOUT
分发为 NtUserxGetKeyboardLayout):idThread == 0 时返回当前线程布局的 hkl;
否则在当前桌面线程链表 (pti->rpdesk->PtiList)中按 PsGetThreadId == idThread 找目标线程,
返回其布局 hkl。找不到返回 NULL。
NtUserGetKeyboardLayoutList(L1038)
c
if (!pHklBuff) nBuff = 0;
UserEnterShared();
if (nBuff > MAXULONG/sizeof(HKL)) { SetLastNtError(ERROR_INVALID_PARAMETER); goto Quit; }
_SEH2_TRY { ProbeForWrite(pHklBuff, nBuff * sizeof(HKL), 1); } _SEH2_EXCEPT(...) {...}
pWinSta = IntGetProcessWindowStation(NULL);
ret = IntGetKeyboardLayoutList(pWinSta, nBuff, pHklBuff);
Quit: UserLeave(); return ret;
返回系统已加载布局数;pHklBuff 非空时把各布局 hkl 拷入(user32 的
CliImmInitializeHotKeys 用 NtUserGetKeyboardLayoutList(0, NULL) 先取个数再取列表)。
NtUserGetKeyboardLayoutName(L1092)
GetKeyboardLayoutName 的内核实现:取当前线程布局;IME 布局(IS_IME_HKL)时
RtlIntegerToUnicodeString(HandleToUlong(pKl->hkl), 16, ...) 输出十六进制 HKL;
普通布局输出 pKl->spkf->awchKF(KLID 字符串),要求缓冲 >= KL_NAMELENGTH*sizeof(WCHAR)。
全程 ProbeForWriteUnicodeString 探针保护。
NtUserLoadKeyboardLayoutEx(L1160)
c
/* 校验 Flags 只含允许位(KLF_ACTIVATE|NOTELLSHELL|REORDER|REPLACELANG|SUBSTITUTE_OK|
SETFORPROCESS|UNLOADPREVIOUS|RESET|SHIFTLOCK),否则 ERROR_INVALID_FLAGS */
/* SEH 拷贝 KLID 到内核缓冲 uszSafeKLID */
UserEnterExclusive();
hSafeFile = (hFile ? IntVerifyKeyboardFileHandle(hFile) : NULL);
pWinSta = IntGetProcessWindowStation(NULL);
hRetKL = co_IntLoadKeyboardLayoutEx(pWinSta, hSafeFile, hOldKL, &uszSafeKLID,
UlongToHandle(dwNewKL), Flags);
if (hSafeFile) ZwClose(hSafeFile);
UserLeave();
return hRetKL;
NtUserActivateKeyboardLayout(L1225)
UserEnterExclusive → co_IntActivateKeyboardLayout(IntGetProcessWindowStation(NULL), hKL, Flags, NULL)
→ 返回旧 HKL。
NtUserUnloadKeyboardLayout(L1250)
UserEnterExclusive → IntUnloadKeyboardLayout(pWinSta, hKl) → 返回是否成功。
user32 的 UnloadKeyboardLayout 成功后还会调用 CliImmInitializeHotKeys(SETIMEHOTKEY_DELETE) 清理 IME 热键。
6. 与 msgqueue.c / message.c / user32 的配合
6.1 同步键状态(msgqueue.c)
UserGetKeyState(msgqueue.c L222)
GetKeyState 的数据源:
c
DWORD FASTCALL UserGetKeyState(DWORD dwKey)
{
pti = PsGetCurrentThreadWin32Thread();
MessageQueue = pti->MessageQueue;
if (dwKey < 0x100) {
if (IS_KEY_DOWN(MessageQueue->afKeyState, dwKey)) dwRet |= 0xFF80; /* 按下 */
if (IS_KEY_LOCKED(MessageQueue->afKeyState, dwKey)) dwRet |= 0x1; /* 锁定 */
} else
EngSetLastError(ERROR_INVALID_PARAMETER);
return dwRet;
}
与异步版的差别:读队列 位图、按下位返回 0xFF80(16 位全高位)、锁定位 0x1、
不清除任何标志。加速键(accelerator.c L62)、默认窗口过程(defwnd.c L904 的
WM_SYSCOMMAND SC_PREVWINDOW/SC_NEXTWINDOW Shift 判定、WM_CONTEXTMENU、F10 状态)、
菜单(menu.c L2833 的 fFirstClick 鼠标键判定)等都直接调用它。
NtUserGetKeyState(L2573)与 NtUserGetKeyboardState(L2589)
NtUserGetKeyState:UserEnterShared→UserGetKeyState→ 返回 SHORT。NtUserGetKeyboardState:SEH 探针后把队列afKeyState展开为 256 字节用户态数组------
IS_KEY_DOWN → KS_DOWN_BIT(0x80)、IS_KEY_LOCKED → KS_LOCK_BIT(0x01)。
NtUserSetKeyboardState(L2627)
把用户态 256 字节数组写回队列 afKeyState:
c
UserEnterExclusive();
pti = PsGetCurrentThreadWin32Thread(); MessageQueue = pti->MessageQueue;
_SEH2_TRY {
ProbeForRead(pKeyState, 256 * sizeof(BYTE), 1);
for (i = 0; i < 256; ++i) {
SET_KEY_DOWN(MessageQueue->afKeyState, i, pKeyState[i] & KS_DOWN_BIT);
SET_KEY_LOCKED(MessageQueue->afKeyState, i, pKeyState[i] & KS_LOCK_BIT);
}
} _SEH2_EXCEPT(...) { SetLastNtError(...); bRet = FALSE; } _SEH2_END;
SetKeyboardState 只影响本线程队列的同步状态(常用于应用自己维护 CapsLock 逻辑)。
UpdateKeyState / UpdateKeyStateFromMsg / MsqReleaseModifierKeys(L247 / L266 / L2557)
UpdateKeyState(MessageQueue, wVk, bIsDown):队列版UpdateAsyncKeyState------首次按下翻转
lock 位、置 down 位、置afKeyRecentDown;释放清 down 位。UpdateKeyStateFromMsg(MessageQueue, msg):从键消息更新队列状态------按下消息把对应虚键置 down
(同时处理 VK_SHIFT/CONTROL/MENU 与左/右细分键的关系,即按下 LSHIFT 时 VK_SHIFT 与
VK_LSHIFT 都 down);释放消息清 down。MsqReleaseModifierKeys:把 9 个修饰键(LCONTROL/RCONTROL/CONTROL、LMENU/RMENU/MENU、
LSHIFT/RSHIFT/SHIFT)全部清 down,用于线程退出/队列清理时防止"卡键"。
MsqInitializeMessageQueue(L2212)
创建队列时 RtlCopyMemory(MessageQueue->afKeyState, gafAsyncKeyState, sizeof(gafAsyncKeyState)),
把全局异步状态快照为队列初始同步状态;并把 ptiMouse = ptiKeyboard = pti(新线程自己先接管
输入归属)。
6.2 键盘消息出队处理(msgqueue.c co_IntProcessKeyboardMessage,L1783)
co_IntProcessHardwareMessage(L1908)按 IS_KBD_MESSAGE 把硬件消息分派到本函数。处理链:
uMsg == VK_PACKET时把HIWORD(wParam)存入pti->wchInjected(注入字符缓存);- wParam 简化 :
VK_LSHIFT/VK_RSHIFT → VK_SHIFT、VK_LCONTROL/VK_RCONTROL → VK_CONTROL、
VK_LMENU/VK_RMENU → VK_MENU(消息中的 wParam 一律用简化虚键); - 记录
WH_JOURNALRECORD日志事件(EVENTMSG); - 特殊键旁路(
*RemoveMessages时):WM_KEYDOWN的VK_F1→UserPostMessage(hwnd, WM_KEYF1);- 浏览器键(
VK_BROWSER_BACK..VK_LAUNCH_APP2)→co_IntSendMessage(hwnd, WM_APPCOMMAND, MAKELPARAM(0, FAPPCOMMAND_KEY | (wParam - VK_BROWSER_BACK + 1))); WM_KEYUP的VK_APPS(菜单键)且无菜单 →UserPostMessage(hwnd, WM_CONTEXTMENU, hwnd, -1);
WM_SYSKEYDOWN+KF_ALTDOWN的VK_ESCAPE/VK_TAB:Alt+Tab / Alt+Esc 系统命令 ------
wParamTmp = UserGetKeyState(VK_SHIFT)&0x8000 ? SC_PREVWINDOW : SC_NEXTWINDOW,
co_IntSendMessage(hwnd, WM_SYSCOMMAND, wParamTmp, Msg->wParam),返回 FALSE(吞掉原消息);WH_KEYBOARD钩子:co_HOOK_CallHooks(WH_KEYBOARD, HC_ACTION|HC_NOREMOVE, LOWORD(wParam), lParam);
返回非 0 则WH_CBT的HCBT_KEYSKIPPED并*RemoveMessages = TRUE(吞键);- IME 优先 :
if (pWnd && Ret && *RemoveMessages && bKeyUpDown && !(pti->TIF_flags & TIF_DISABLEIME))
ImmRet = IntImmProcessKey(pti->MessageQueue, pWnd, uMsg, Msg->wParam, Msg->lParam);IPHK_HOTKEY|IPHK_SKIPTHISKEY→ 置 0(该键不再产生字符消息);IPHK_PROCESSBYIME→Msg->wParam = VK_PROCESSKEY(IME 吃掉按键,应用看到 VK_PROCESSKEY)。
VK_PROCESSKEY 与 TIF_DISABLEIME:IME 处理后 wParam 改写为 VK_PROCESSKEY(0xE5),
应用据此知道键已被输入法处理;TIF_DISABLEIME 线程标志可整体禁用 IME 拦截。
6.3 NtUserTranslateMessage(message.c L2489)
c
BOOL APIENTRY NtUserTranslateMessage(LPMSG lpMsg, UINT flags)
{
_SEH2_TRY { ProbeForRead(lpMsg, sizeof(MSG), 1); RtlCopyMemory(&SafeMsg, lpMsg, sizeof(MSG)); } ...
UserEnterExclusive();
pWnd = UserGetWindowObject(SafeMsg.hwnd);
if (pWnd) Ret = IntTranslateKbdMessage(&SafeMsg, flags); /* 必须要有窗口 */
...
UserLeave();
return Ret;
}
user32 的 TranslateMessage(windows/message.c)封装此调用;加速键翻译
TranslateAccelerator 也走独立路径(accelerator.c,读 UserGetKeyState 组装 FCONTROL 等掩码,
见分册 _14)。
6.4 用户态键盘 API(user32/windows/input.c)
| API | 实现 | 说明 |
|---|---|---|
GetAsyncKeyState |
范围校验后调 NtUserGetAsyncKeyState |
异步状态 |
GetKeyState |
NtUserGetKeyState |
同步状态 |
GetKeyboardLayout |
NtUserxGetKeyboardLayout(simplecall) |
线程 HKL |
GetKeyNameTextA/W |
NtUserGetKeyNameText(A 版再 WideCharToMultiByte) |
键名 |
GetKeyboardType |
NtUserxGetKeyboardType(simplecall) |
键盘属性 |
GetKeyboardLayoutNameA/W |
NtUserGetKeyboardLayoutName |
KLID |
MapVirtualKey(A/W) |
MapVirtualKeyExW → NtUserMapVirtualKeyEx(Ex 版传 GetKeyboardLayout(0)) |
键码互转 |
VkKeyScan(A/W) |
NtUserVkKeyScanEx(ch, 0, FALSE);Ex 版 (ch, hkl, TRUE) |
字符→虚键 |
ToAscii/ToAsciiEx |
先 ToUnicodeEx 再 WideCharToMultiByte(CP_ACP) |
字符转换 |
ToUnicode/ToUnicodeEx |
NtUserToUnicodeEx |
虚键→字符 |
LoadKeyboardLayoutA/W/Ex |
IntLoadKeyboardLayout:校验 KLID → 查注册表 |
|
Keyboard Layouts\<KLID> 的 Layout Id/IME File 等 → 组 HKL → |
||
NtUserLoadKeyboardLayoutEx → CliImmInitializeHotKeys(SETIMEHOTKEY_ADD) |
加载布局 | |
UnloadKeyboardLayout |
NtUserUnloadKeyboardLayout + 清理 IME 热键 |
卸载布局 |
keybd_event |
组 INPUT{type=INPUT_KEYBOARD} → NtUserSendInput |
模拟按键 |
GetLastInputInfo |
读 gpsi->dwLastRITEventTickCount |
最后输入时间 |
IntLoadPreloadKeyboardLayouts |
读 HKCU\Keyboard Layout\Preload 的 "1"..."n" 值逐个 |
|
LoadKeyboardLayoutW(第 1 项用 `KLF_ACTIVATE |
KLF_RESET`),最后 | |
SystemParametersInfoW(SPI_SETDEFAULTINPUTLANG);全失败兜底 LoadKeyboardLayoutW(L"00000409") |
登录时预载布局 |
IntLoadKeyboardLayout 的安全细节:IsValidKLID 只允许十六进制字符;KLF_SUBSTITUTE_OK 时
查 HKCU\Keyboard Layout\Substitutes 替换 KLID;IME 布局要求注册表存在有效 IME File
(禁止路径分隔符、必须存在于 System32),否则降级为非 IME 布局------防止通过布局加载提权。
7. 键盘输入处理全流程(按下/抬起)
以"用户按下并释放 'A' 键"为例串联完整链路:
【硬件路径】kbdclass 驱动产生 KEYBOARD_INPUT_DATA{MakeCode=0x1E, Flags=KEY_MAKE}
→ RawInputThreadMain 的 ZwReadFile(input.c)返回
→ IntLastInputTick(TRUE)(更新 gpsi->dwLastRITEventTickCount)
→ UserEnterExclusive(); UserProcessKeyboardInput(&KeyInput); UserLeave()
├─ wScanCode = 0x1E(无 E0/E1)
├─ 焦点队列 = IntGetFocusMessageQueue()(活动桌面 ActiveMessageQueue)
├─ 布局 = 焦点线程 ptiKeyboard->KeyboardLayout(首次为 W32kGetDefaultKeyLayout 兜底)
├─ wVk = IntVscToVk(0x1E) = 'A'(KBDTABLES.pusVSCtoVK[0x1E])
├─ 非小键盘键,跳过 NumLock 翻译
└─ UserSendKeyboardInput({wVk='A', wScan=0x1E, dwFlags=0})
└─ ProcessKeyEvent('A', 0x1E, 0, FALSE, tick, 0)
├─ wSimpleVk='A'(不变);IntHandleAltNumpad:Alt 未按下,直接 FALSE
├─ UpdateAsyncKeyState('A', TRUE):gafAsyncKeyState 置 down 位
├─ IntKeyboardUpdateLeds:'A' 无指示灯
├─ co_CallLowLevelKeyboardHook:WH_KEYBOARD_LL(未装钩子则直通)
├─ co_UserProcessHotKeys('A', down):非热键
├─ SysKey 判定:非 F10、Alt 未按下、有焦点窗口 → WM_KEYDOWN
├─ UpdateAsyncKeyState('A', down)(wFixedVk 同 'A')
├─ 语言/布局切换检测:非 Shift/Alt/Ctrl → 跳过
├─ 非 VK_SNAPSHOT
├─ Wnd = spwndFocus;pti = Wnd->head.pti
├─ Msg = {hwnd, wParam='A', lParam=MAKELPARAM(1,0x1E),
│ KF_* 位按需置位(重复/up/alt/扩展/对话框/菜单模式)}
├─ QF_MOUSEMOVED 时先 IntCoalesceMouseMove 合并鼠标移动
└─ MsqPostMessage(pti, &Msg, TRUE, QS_KEY, 0, 0)
└─ MsqCreateMessage 分配 USER_MESSAGE → 挂 HardwareMessagesListHead
→ MsqWakeQueue 置 QS_KEY 唤醒队列
→ 焦点线程消息循环 GetMessage(co_IntGetPeekMessage 取硬件消息)
└─ co_IntProcessKeyboardMessage(msgqueue.c L1783)
├─ wParam 简化('A' 无需)
├─ WH_JOURNALRECORD
├─ 非 F1/浏览器键/Apps 键
├─ 非 Alt+Tab(WM_SYSKEYDOWN 才查)
├─ WH_KEYBOARD 钩子(未装则直通)
└─ IntImmProcessKey:非 IME 布局/未启用时返回 0 → 消息原样交给应用
→ 应用窗口过程收到 WM_KEYDOWN(wParam='A',lParam 高字 KF_*)
→ 应用调用 TranslateMessage → NtUserTranslateMessage → IntTranslateKbdMessage
├─ 取线程布局;wParam 非 VK_PACKET
├─ IntToUnicodeEx('A', 0x1E, 队列 afKeyState, ...)
│ └─ IntTranslateChar:dwModBits=0 → ModNumber[0]=0 → wch[0]='a'(无 Shift)
├─ cch=1 > 0 → WM_CHAR
└─ MsqPostMessage(pti, &WM_CHAR{'a'}, FALSE, QS_KEY, 0, 0) /* 投递消息 */
→ 应用 GetMessage 取到 WM_CHAR → DispatchMessage → 窗口过程 WM_CHAR('a')
释放键 流程完全相同,但 KEY_BREAK 使 dwFlags |= KEYEVENTF_KEYUP:
bIsDown=FALSE → UpdateAsyncKeyState 清 down → Msg.message=WM_KEYUP、
lParam |= KF_UP → 队列收到 WM_KEYUP(TranslateMessage 对 KEYUP 不产生字符,
IntTranslateKbdMessage 只处理 KEYDOWN/SYSKEYDOWN)。
SysKey 情形 (Alt+字符 或 F10):Msg.message = WM_SYSKEYDOWN,lParam |= KF_ALTDOWN;
TranslateMessage 产生 WM_SYSCHAR;DefWindowProc 中 WM_SYSKEYDOWN 的 Alt+Tab/Esc 分支
在出队阶段(co_IntProcessKeyboardMessage)即被截获转成 WM_SYSCOMMAND SC_NEXTWINDOW/SC_PREVWINDOW
实现 Alt+Tab 窗口切换。
注入路径 (SendInput/keybd_event):NtUserSendInput → 校验 INPUT 结构 → 按类型
UserSendKeyboardInput(&SafeInput.ki, TRUE)(input.c L777)→ 同 ProcessKeyEvent,
但 bInjected=TRUE 会体现在低级钩子的 LLKHF_INJECTED 与 KEYBOARD_INPUT_DATA 注入位。
VK_PACKET 注入:dwFlags=KEYEVENTF_UNICODE 时 wVk=VK_PACKET、wScan=字符,消息 wParam
VK_PACKET & 0xFF,lParam 高字携带字符,TranslateMessage 直接转 WM_CHAR。
8. 按键状态表维护
8.1 更新时机(两类状态)
| 状态表 | 更新者 | 更新时机 | 读取者 |
|---|---|---|---|
gafAsyncKeyState(全局) |
ProcessKeyEvent → UpdateAsyncKeyState |
每次按键事件(硬件+注入), | |
| 在消息入队之前 | GetAsyncKeyState、低级钩子(LLKHF_ALTDOWN)、热键处理、 |
||
语言切换检测、MsqInitializeMessageQueue(快照) |
|||
MessageQueue->afKeyState(队列) |
co_IntProcessKeyboardMessage → UpdateKeyStateFromMsg |
键盘消息出队时 | GetKeyState、GetKeyboardState、TranslateMessage 的 IntToUnicodeEx |
两类状态的语义差异正是 Windows 文档中的经典区别:GetAsyncKeyState 反映物理键盘
(消息队列未处理也能查到);GetKeyState 反映消息流视角 (窗口过程里读取时,只包含
"已到达本线程消息队列"的键事件,因此能避开别的线程/注入的干扰,且与正在处理的消息同步)。
8.2 lock 位翻转的两次实现
UpdateAsyncKeyState(全局)与 UpdateKeyState(队列)都有相同的 lock 语义:
c
if (bIsDown) {
if (!IS_KEY_DOWN(ks, wVk)) /* 首次按下(up→down 沿) */
SET_KEY_LOCKED(ks, wVk, !IS_KEY_LOCKED(ks, wVk)); /* 翻转 */
SET_KEY_DOWN(ks, wVk, TRUE);
} else
SET_KEY_DOWN(ks, wVk, FALSE);
由于 ProcessKeyEvent 先更新全局态、入队后出队时 UpdateKeyStateFromMsg 再更新队列态,
同一次按键事件会对两份位图各翻转一次 lock 位 。这不是 bug:两份位图是独立维护的
(全局位反映"物理状态",供 GetAsyncKeyState/低级钩子/热键使用;队列位反映"消息流视角",
供 GetKeyState/TranslateMessage 使用),它们只要求各自语义自洽,不要求彼此相等。
Windows 的 SetKeyboardState/AttachThreadInput 会重置队列同步状态(input.c L660 注释:
"the state of the keyboard ... is reset after a call to AttachThreadInput"),
正是依赖这种独立性。
8.3 GetAsyncKeyState 的"读即清"位
gafAsyncKeyStateRecentDown[](32 字节,1 位/键)由 UpdateAsyncKeyState 置位、
NtUserGetAsyncKeyState 读取后清除。因此连续两次 GetAsyncKeyState(VK_X) 之间若没有新的
按下事件,第二次的低位为 0------这是文档化的"自上次调用以来是否按下"语义。
8.4 队列清理防卡键
MsqReleaseModifierKeys(msgqueue.c L2557)在线程退出(MsqDestroyMessageQueue 清理路径)时
把全部修饰键清 down,避免线程销毁后系统残留"Ctrl 一直按住"的脏状态。MsqCleanupThreadMsgs
(L2232)同时清空该线程的投递消息链表与 QS 计数。
9. 键盘布局机制(加载 / 激活 / 切换 / 对象链表)
9.1 布局对象的生命周期
启动:csrss 调用 user32(winsrv)→ IntLoadPreloadKeyboardLayouts(user32/input.c)
└─ HKCU\Keyboard Layout\Preload 的 "1".."n"(如 "00000409")
└─ LoadKeyboardLayoutW → IntLoadKeyboardLayout(user32)
└─ NtUserLoadKeyboardLayoutEx(win32k)
└─ co_IntLoadKeyboardLayoutEx
├─ co_UserLoadKbdLayout:UserCreateObject(TYPE_KBDLAYOUT)
│ ├─ UserLoadKbdFile:UserCreateObject(TYPE_KBDFILE)
│ │ ├─ 读注册表 Keyboard Layouts\<KLID>\Layout File → DLL 路径
│ │ ├─ EngLoadImage + KbdLayerDescriptor() → KBDTABLES
│ │ └─ 头插 gpkfList
│ ├─ co_IntGetCharsetInfo(LCID) → iBaseCharset/dwFontSigs/CodePage
│ └─ 挂入环形 KL 链表(首个成为 gspklBaseLayout)
├─ pNewKL->piiex = co_UserImmLoadLayout(IME 信息)
└─ KLF_ACTIVATE → co_UserActivateKeyboardLayout
└─ IntImmActivateLayout / UserAssignmentLock → pti->KeyboardLayout
9.2 激活语义(co_UserActivateKeyboardLayout)
- 线程级 (默认):只改
pti->KeyboardLayout+pClientInfo->hKL/CodePage; - 进程级 (
KLF_SETFORPROCESS):co_IntSetKeyboardLayoutForProcess遍历
ppi->ptiList改全部线程; - CSRSS 线程 (
TIF_CSRSSTHREAD):直接赋值(CSRSS 不参与 IME); - 附带动作:
WM_INPUTLANGCHANGE通知焦点窗口、HSHELL_LANGUAGE通知 shell(去重
ghKLSentToShell)、WM_IME_SYSTEM:IMS_SENDNOTIFICATION刷新 IME UI。
9.3 切换(语言/布局热键)
ProcessKeyEvent 检测 [左Alt]+Shift(gdwLanguageToggleKey==1)或 Ctrl+Shift
(gdwLanguageToggleKey==2)等组合 → IntCheckLanguageToggle 在 Shift 按下时记方向、
释放时 IntLanguageToggle 沿环形链表 IntGetNextKL 取下一布局 →
UserPostMessage(焦点窗口, WM_INPUTLANGCHANGEREQUEST, 标志, hkl) →
应用/DefWindowProc 收到后调用 ActivateKeyboardLayout(hkl, 0) 完成切换。
IntLanguageToggle 还会检查 gSystemFS & pkl->dwFontSigs 设置
INPUTLANGCHANGE_SYSCHARSET(切换涉及字符集变化,通知字体系统)。
9.4 卸载与延迟卸载
UnloadKeyboardLayout → NtUserUnloadKeyboardLayout → IntUnloadKeyboardLayout →
co_IntUnloadKeyboardLayoutEx:UserMarkObjectDestroy + KL_UNLOAD;若当前线程正用该布局,
自动激活 HKL_NEXT;默认布局顺移到下一节点;shell 收到 HSHELL_LANGUAGE(去重标记清空)。
实际内存回收等待引用计数归零后 UserUnloadKbl → UnloadKbdFile(EngUnloadImage)。
9.5 HKL 解析规则(IntHKLtoPKL / UserHklToKbl)
| 调用 | 范围 | 匹配规则 |
|---|---|---|
IntHKLtoPKL |
当前线程布局链表 | HKL_NEXT/HKL_PREV(跳过 KL_UNLOAD);完整 HKL 精确匹配;纯语言 ID 按 LOWORD 匹配 |
UserHklToKbl |
全局 gspklBaseLayout 链表 | pKl->hkl == hKl 精确匹配 |
co_IntActivateKeyboardLayout 用 IntHKLtoPKL(支持 ActivateKeyboardLayout(HKL_NEXT, ...)
的循环切换);其余 API(MapVirtualKeyEx/VkKeyScanEx/ToUnicodeEx)用 UserHklToKbl。
10. 虚键 → 字符翻译(VkKeyScan / MapVirtualKey / TranslateMessage)
10.1 三者的分工
| 函数 | 方向 | 键状态来源 | 布局来源 |
|---|---|---|---|
TranslateMessage(IntTranslateKbdMessage) |
消息 wParam → 字符(WM_CHAR) | 队列同步状态 afKeyState |
当前线程布局 |
ToUnicodeEx(NtUserToUnicodeEx) |
调用方虚键+键状态 → 字符串 | 调用方传入 256 字节数组 | dwhkl 或当前线程 |
MapVirtualKey(MAPVK_VK_TO_CHAR)(IntVkToChar) |
虚键 → 无修饰字符 | 无(ModBits=0) | dwhkl 或当前线程 |
VkKeyScanEx(NtUserVkKeyScanEx) |
字符 → 虚键+修饰(反查) | 无 | dwhkl 或当前线程 |
10.2 TranslateMessage 为什么必须和 GetMessage 同线程
IntTranslateKbdMessage 使用 pti->MessageQueue->afKeyState(队列同步状态)翻译字符。
消息队列是线程私有的,且 afKeyState 只随本队列的消息流更新------因此跨线程调用
TranslateMessage 会读到错误的 Shift 状态(这是 Windows 明确禁止"在其他线程的
MSG 上调用 TranslateMessage"的根本原因)。
10.3 死键与连字
- 死键(如法语 '^'):
IntTranslateChar返回WCH_DEAD,IntToUnicodeEx存入静态wchDead;
下一次按键时查pDeadKey[]表合成('^'+'e'→'ê');合成失败则先输出死键字符再输出当前字符。
IntTranslateKbdMessage依据负返回值发出WM_DEADCHAR/WM_SYSDEADCHAR(应用可自行绘制
等待合成的重音状态)。 - 连字(
WCH_LGTR,阿拉伯语等):IntToUnicodeEx当前直接 WARN 放弃(未实现),
表结构LIGATURE/nLgMaxd/cbLgEntry/pLigature已就绪但无翻译路径。
10.4 Alt+Numpad 与 VK_PACKET
Alt+小键盘数字:IntHandleAltNumpad状态机在按键层截获并累积数值,Alt 释放时按
OEM/ACP/UTF 代码页转换后直接投递WM_CHAR(不经过布局表)。SendInput(KEYEVENTF_UNICODE):wVk=VK_PACKET 走消息流,TranslateMessage 直接拆
HIWORD(lParam)为字符;出队时co_IntProcessKeyboardMessage把
HIWORD(wParam)存pti->wchInjected(IME 层可能用到)。
11. 焦点窗口键盘消息定向
11.1 定向规则
键盘事件到达(ProcessKeyEvent / UserProcessKeyboardInput)
→ 焦点队列 = IntGetFocusMessageQueue() = IntGetActiveDesktop()->ActiveMessageQueue
→ 布局 = 焦点队列 ptiKeyboard->KeyboardLayout ("谁的焦点谁的语言")
→ 目标窗口 Wnd = spwndFocus ?: spwndActive
→ 目标线程 pti = Wnd->head.pti ?: ptiKeyboard
→ Msg.hwnd = Wnd 的句柄(无窗口时 NULL------此时仍投递到 ptiKeyboard 队列,
应用层按 WM_SYSKEY* 与 NULL hwnd 处理,如无焦点时的 Alt+Tab)
→ MsqPostMessage(pti, Msg, HardwareMessage=TRUE, QS_KEY, ...)
spwndFocus为空 时(例如刚启动、焦点还没建立):按键仍按 SysKey 处理
(ProcessKeyEvent的(pFocusQueue && !pFocusQueue->spwndFocus)分支),目标退化为
spwndActive,符合 Windows"无焦点时键盘消息发给活动窗口"的行为。IntGetFocusMessageQueue(desktop.c L1324)返回活动桌面的ActiveMessageQueue,
由IntSetFocusMessageQueue(L1336)在焦点切换时维护(旧队列摘除、新队列挂接并
互锁交换指针)。focus.c的NtUserSetForegroundWindow/SetActiveWindow等通过它
驱动键盘归属。- AttachThreadInput :共享队列时
ptiKeyboard指向同一队列,键盘输入一并定向
(input.c L511 注释:键盘/鼠标事件都归idAttachTo线程处理)。
11.2 与焦点相关的辅助机制
- 菜单/对话框模式:
ProcessKeyEvent根据QF_DIALOGACTIVE/MenuOwner设置
KF_DLGMODE/KF_MENUMODE(lParam 高字),DefWindowProc/菜单代码据此区分
Alt 键在对话框/菜单中的含义。 - Alt+Tab:出队阶段(
co_IntProcessKeyboardMessage)由KF_ALTDOWN+VK_TAB/VK_ESCAPE
截获 →WM_SYSCOMMAND SC_NEXTWINDOW/SC_PREVWINDOW(Shift 决定方向)→ 激活切换
(focus.c 的co_IntSwitchWindow系列,见分册 _16)。 WM_INPUTLANGCHANGE目标窗口同样取spwndFocus ?: spwndActive ?: pWnd三级回退
(co_UserActivateKeyboardLayout)。
12. 调用链(mermaid)
12.1 硬件按键全链路
#mermaid-svg-mIWpm2i35QasEHnO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mIWpm2i35QasEHnO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mIWpm2i35QasEHnO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mIWpm2i35QasEHnO .error-icon{fill:#552222;}#mermaid-svg-mIWpm2i35QasEHnO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mIWpm2i35QasEHnO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mIWpm2i35QasEHnO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mIWpm2i35QasEHnO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mIWpm2i35QasEHnO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mIWpm2i35QasEHnO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mIWpm2i35QasEHnO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mIWpm2i35QasEHnO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mIWpm2i35QasEHnO .marker.cross{stroke:#333333;}#mermaid-svg-mIWpm2i35QasEHnO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mIWpm2i35QasEHnO p{margin:0;}#mermaid-svg-mIWpm2i35QasEHnO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mIWpm2i35QasEHnO .cluster-label text{fill:#333;}#mermaid-svg-mIWpm2i35QasEHnO .cluster-label span{color:#333;}#mermaid-svg-mIWpm2i35QasEHnO .cluster-label span p{background-color:transparent;}#mermaid-svg-mIWpm2i35QasEHnO .label text,#mermaid-svg-mIWpm2i35QasEHnO span{fill:#333;color:#333;}#mermaid-svg-mIWpm2i35QasEHnO .node rect,#mermaid-svg-mIWpm2i35QasEHnO .node circle,#mermaid-svg-mIWpm2i35QasEHnO .node ellipse,#mermaid-svg-mIWpm2i35QasEHnO .node polygon,#mermaid-svg-mIWpm2i35QasEHnO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mIWpm2i35QasEHnO .rough-node .label text,#mermaid-svg-mIWpm2i35QasEHnO .node .label text,#mermaid-svg-mIWpm2i35QasEHnO .image-shape .label,#mermaid-svg-mIWpm2i35QasEHnO .icon-shape .label{text-anchor:middle;}#mermaid-svg-mIWpm2i35QasEHnO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mIWpm2i35QasEHnO .rough-node .label,#mermaid-svg-mIWpm2i35QasEHnO .node .label,#mermaid-svg-mIWpm2i35QasEHnO .image-shape .label,#mermaid-svg-mIWpm2i35QasEHnO .icon-shape .label{text-align:center;}#mermaid-svg-mIWpm2i35QasEHnO .node.clickable{cursor:pointer;}#mermaid-svg-mIWpm2i35QasEHnO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mIWpm2i35QasEHnO .arrowheadPath{fill:#333333;}#mermaid-svg-mIWpm2i35QasEHnO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mIWpm2i35QasEHnO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mIWpm2i35QasEHnO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mIWpm2i35QasEHnO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mIWpm2i35QasEHnO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mIWpm2i35QasEHnO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mIWpm2i35QasEHnO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mIWpm2i35QasEHnO .cluster text{fill:#333;}#mermaid-svg-mIWpm2i35QasEHnO .cluster span{color:#333;}#mermaid-svg-mIWpm2i35QasEHnO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mIWpm2i35QasEHnO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mIWpm2i35QasEHnO rect.text{fill:none;stroke-width:0;}#mermaid-svg-mIWpm2i35QasEHnO .icon-shape,#mermaid-svg-mIWpm2i35QasEHnO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mIWpm2i35QasEHnO .icon-shape p,#mermaid-svg-mIWpm2i35QasEHnO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mIWpm2i35QasEHnO .icon-shape .label rect,#mermaid-svg-mIWpm2i35QasEHnO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mIWpm2i35QasEHnO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mIWpm2i35QasEHnO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mIWpm2i35QasEHnO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} KEYBOARD_INPUT_DATA
ZwReadFile + IntLastInputTick
IntVscToVk 扫描码→虚键
键盘中断/驱动 kbdclass
RawInputThreadMain
input.c
UserProcessKeyboardInput
keyboard.c L1414
NumLock 翻译
IntTranslateNumpadKey
UserSendKeyboardInput
keyboard.c L1332
ProcessKeyEvent
keyboard.c L1106
IntSimplifyVk/IntFixVk
左右键虚键
IntHandleAltNumpad
Alt+小键盘
UpdateAsyncKeyState
gafAsyncKeyState
IntKeyboardUpdateLeds
指示灯
co_CallLowLevelKeyboardHook
WH_KEYBOARD_LL
co_UserProcessHotKeys
全局热键
IntCheckLanguageToggle
Alt/Ctrl+Shift 切语言
SnapWindow
PrtSc 截屏
Msg 组装 wParam/lParam KF_*
MsqPostMessage QS_KEY
GetMessage
co_IntProcessKeyboardMessage msgqueue.c L1783
WH_JOURNALRECORD / WH_KEYBOARD 钩子
Alt+Tab → WM_SYSCOMMAND
IntImmProcessKey
ime.c IME 优先
应用 WM_KEYDOWN/WM_SYSKEYDOWN
TranslateMessage
NtUserTranslateMessage message.c L2489
IntTranslateKbdMessage
keyboard.c L1499
IntToUnicodeEx
虚键+afKeyState→字符
死键合成 pDeadKey
MsqPostMessage WM_CHAR/WM_DEADCHAR/WM_SYSCHAR
DispatchMessage → 焦点窗口窗口过程
12.2 布局管理调用链
#mermaid-svg-nPaTAIHunH3to95n{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nPaTAIHunH3to95n .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nPaTAIHunH3to95n .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nPaTAIHunH3to95n .error-icon{fill:#552222;}#mermaid-svg-nPaTAIHunH3to95n .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nPaTAIHunH3to95n .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nPaTAIHunH3to95n .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nPaTAIHunH3to95n .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nPaTAIHunH3to95n .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nPaTAIHunH3to95n .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nPaTAIHunH3to95n .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nPaTAIHunH3to95n .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nPaTAIHunH3to95n .marker.cross{stroke:#333333;}#mermaid-svg-nPaTAIHunH3to95n svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nPaTAIHunH3to95n p{margin:0;}#mermaid-svg-nPaTAIHunH3to95n .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-nPaTAIHunH3to95n .cluster-label text{fill:#333;}#mermaid-svg-nPaTAIHunH3to95n .cluster-label span{color:#333;}#mermaid-svg-nPaTAIHunH3to95n .cluster-label span p{background-color:transparent;}#mermaid-svg-nPaTAIHunH3to95n .label text,#mermaid-svg-nPaTAIHunH3to95n span{fill:#333;color:#333;}#mermaid-svg-nPaTAIHunH3to95n .node rect,#mermaid-svg-nPaTAIHunH3to95n .node circle,#mermaid-svg-nPaTAIHunH3to95n .node ellipse,#mermaid-svg-nPaTAIHunH3to95n .node polygon,#mermaid-svg-nPaTAIHunH3to95n .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nPaTAIHunH3to95n .rough-node .label text,#mermaid-svg-nPaTAIHunH3to95n .node .label text,#mermaid-svg-nPaTAIHunH3to95n .image-shape .label,#mermaid-svg-nPaTAIHunH3to95n .icon-shape .label{text-anchor:middle;}#mermaid-svg-nPaTAIHunH3to95n .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nPaTAIHunH3to95n .rough-node .label,#mermaid-svg-nPaTAIHunH3to95n .node .label,#mermaid-svg-nPaTAIHunH3to95n .image-shape .label,#mermaid-svg-nPaTAIHunH3to95n .icon-shape .label{text-align:center;}#mermaid-svg-nPaTAIHunH3to95n .node.clickable{cursor:pointer;}#mermaid-svg-nPaTAIHunH3to95n .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-nPaTAIHunH3to95n .arrowheadPath{fill:#333333;}#mermaid-svg-nPaTAIHunH3to95n .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nPaTAIHunH3to95n .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nPaTAIHunH3to95n .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nPaTAIHunH3to95n .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-nPaTAIHunH3to95n .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nPaTAIHunH3to95n .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-nPaTAIHunH3to95n .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nPaTAIHunH3to95n .cluster text{fill:#333;}#mermaid-svg-nPaTAIHunH3to95n .cluster span{color:#333;}#mermaid-svg-nPaTAIHunH3to95n div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-nPaTAIHunH3to95n .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nPaTAIHunH3to95n rect.text{fill:none;stroke-width:0;}#mermaid-svg-nPaTAIHunH3to95n .icon-shape,#mermaid-svg-nPaTAIHunH3to95n .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nPaTAIHunH3to95n .icon-shape p,#mermaid-svg-nPaTAIHunH3to95n .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-nPaTAIHunH3to95n .icon-shape .label rect,#mermaid-svg-nPaTAIHunH3to95n .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nPaTAIHunH3to95n .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nPaTAIHunH3to95n .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nPaTAIHunH3to95n :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} IntLoadKeyboardLayout
查注册表+组 HKL
KLF_ACTIVATE
LoadKeyboardLayoutW
user32/input.c
NtUserLoadKeyboardLayoutEx
kbdlayout.c L1160
co_IntLoadKeyboardLayoutEx L884
co_UserLoadKbdLayout L375
创建 KL
UserLoadKbdFile L296
创建 KBDFILE
UserLoadKbdDll L248
EngLoadImage+KbdLayerDescriptor
co_UserImmLoadLayout L863
IME 信息
IntReorderKeyboardLayouts L558
置为默认
co_UserActivateKeyboardLayout L667
IntImmActivateLayout L596
IME 窗口通知
UserAssignmentLock
pti->KeyboardLayout
WM_INPUTLANGCHANGE
→ 焦点窗口
HSHELL_LANGUAGE
→ shell 钩子
ActivateKeyboardLayoutW
NtUserActivateKeyboardLayout L1225
co_IntActivateKeyboardLayout L782
IntHKLtoPKL 解析 HKL
UnloadKeyboardLayoutW
NtUserUnloadKeyboardLayout L1250
IntUnloadKeyboardLayout L851
co_IntUnloadKeyboardLayoutEx L805
KL_UNLOAD+激活下一布局
UserUnloadKbl L471
引用归零后真卸载
UnloadKbdFile L444
EngUnloadImage+删对象
13. 与 input / msgqueue / ime / focus 的关系
| 模块 | 文件 | 与键盘的关系 |
|---|---|---|
| input.c(分册 _21) | RawInputThreadMain |
键盘设备打开(\Device\KeyboardClass0)、 |
ZwReadFile 读 KEYBOARD_INPUT_DATA、UserInitKeyboard 初始化硬件状态、 |
||
NtUserSendInput 分发注入(键盘分支调 UserSendKeyboardInput) |
||
| msgqueue.c(分册 _8) | USER_MESSAGE_QUEUE.afKeyState |
队列同步键状态; |
UserGetKeyState/NtUserGetKeyState/GetKeyboardState/SetKeyboardState; |
||
UpdateKeyState/UpdateKeyStateFromMsg;co_IntProcessKeyboardMessage |
||
(钩子/Alt+Tab/IME 调用点);MsqPostMessage 入队 |
||
| ime.c(分册 _24) | IntImmProcessKey |
键盘消息出队后、窗口过程前的 IME 拦截; |
VK_PROCESSKEY 改写;spwndDefaultIme/spDefaultImc 与布局激活联动 |
||
(IntImmActivateLayout、IMS_ACTIVATELAYOUT/IMS_SENDNOTIFICATION) |
||
| focus.c(分册 _16) | spwndFocus/spwndActive/spwndForeground |
键盘消息目标窗口; |
IntGetFocusMessageQueue 的 ActiveMessageQueue 由焦点系统维护; |
||
Alt+Tab 触发 WM_SYSCOMMAND 后由 focus.c 完成窗口切换 |
||
| desktop.c(分册 _18) | IntGetFocusMessageQueue/IntSetFocusMessageQueue |
活动桌面与前台队列 |
| winsta.c(分册 _18) | WINSTATION_OBJECT |
FIXME 目标:布局链表 spklList 应在窗口站上 |
(当前是全局 gspklBaseLayout);WSS_NOIO 阻止无输入权限窗口站加载布局 |
||
| hotkey.c(分册 _14) | co_UserProcessHotKeys |
按键路径上的全局热键匹配(WM_HOTKEY) |
| hook.c(分册 _15) | co_CallLowLevelKeyboardHook/co_HOOK_CallHooks |
WH_KEYBOARD_LL / WH_KEYBOARD / |
| WH_JOURNALRECORD / WH_CBT(HCBT_KEYSKIPPED) | ||
| message.c(分册 _9) | NtUserTranslateMessage |
TranslateMessage 系统调用入口 |
| simplecall.c | ONEPARAM_ROUTINE_GETKEYBOARDTYPE/GETKEYBOARDLAYOUT |
NtUserxGetKeyboardType/ |
NtUserxGetKeyboardLayout 的单参数快速分发 |
||
| sysparams.c | SPI_SETDEFAULTINPUTLANG/SPI_SETLANGUAGETOGGLE 等 |
默认输入语言 |
(UserSetDefaultInputLang)与切换键配置(gdwLanguageToggleKey/gdwLayoutToggleKey) |
||
| user32/input.c | 全部用户态键盘 API | 见 6.4 表格 |
14. 源码索引
14.1 keyboard.c(win32ss/user/ntuser/keyboard.c,1991 行)
| 函数 | 行号 | 职责 |
|---|---|---|
InitKeyboardImpl |
L48 | 清零全局键状态、设置默认键盘属性 |
IntKeyboardGetIndicatorTrans |
L67 | 查询指示灯翻译表 |
IntKeyboardUpdateLeds |
L124 | 更新硬件 LED |
UserInitKeyboard |
L178 | 初始化指示灯状态与键盘属性 |
IntSimplifyVk |
L236 | 左右键虚键 → 简化虚键 |
IntFixVk |
L264 | 简化虚键 → 左右细分虚键 |
IntTranslateNumpadKey |
L289 | NumLock 下编辑键 → 小键盘数字 |
IntGetModBits |
L315 | 键状态 → 修饰位组合 |
IntTranslateChar |
L337 | 虚键 → 字符(核心翻译) |
IntToUnicodeEx |
L449 | 虚键+键状态 → 字符串(含死键) |
IntVkToVsc |
L542 | 虚键 → 扫描码 |
IntVscToVk |
L579 | 扫描码 → 虚键 |
IntVkToChar |
L622 | 虚键 → 无修饰字符 |
NtUserGetAsyncKeyState |
L649 | 异步键状态查询 |
UpdateAsyncKeyState |
L683 | 全局异步键状态更新 |
co_CallLowLevelKeyboardHook |
L704 | WH_KEYBOARD_LL 低级钩子 |
SnapWindow |
L740 | PrtSc 截屏 |
IntGetNextKL |
L813 | 环形布局链表取下一个/上一个 |
IntLanguageToggle |
L834 | 语言/布局切换动作 |
IntCheckLanguageToggle |
L871 | Alt/Ctrl+Shift 组合识别 |
IntHandleAltNumpad |
L916 | Alt+小键盘字符输入 |
ProcessKeyEvent |
L1106 | 按键主处理(核心) |
UserSendKeyboardInput |
L1332 | SendInput 注入入口 |
UserProcessKeyboardInput |
L1414 | 硬件键盘输入入口 |
IntTranslateKbdMessage |
L1499 | TranslateMessage 内核实现 |
IntMapVirtualKeyEx |
L1616 | 虚键/扫描码映射 |
NtUserMapVirtualKeyEx |
L1662 | MapVirtualKeyEx 系统调用 |
NtUserToUnicodeEx |
L1702 | ToUnicodeEx 系统调用 |
NtUserGetKeyNameText |
L1807 | GetKeyNameText 系统调用 |
UserGetKeyboardType |
L1907 | GetKeyboardType 内核实现 |
NtUserVkKeyScanEx |
L1932 | VkKeyScanEx 系统调用(字符反查) |
14.2 kbdlayout.c(win32ss/user/ntuser/kbdlayout.c,1265 行)
| 函数 | 行号 | 职责 |
|---|---|---|
IntHKLtoPKL |
L36 | 线程布局链表按 HKL 解析 KL |
IntGetKeyboardLayoutList |
L96 | 收集有效布局 HKL |
UserLoadKbdDll |
L248 | 加载布局 DLL 并取 KBDTABLES |
UserLoadKbdFile |
L296 | 创建 KBDFILE 对象 |
co_UserLoadKbdLayout |
L375 | 创建 KL 对象并绑定 KBDFILE |
UnloadKbdFile |
L444 | 卸载布局文件对象 |
UserUnloadKbl |
L471 | 卸载布局(延迟卸载逻辑) |
W32kGetDefaultKeyLayout |
L512 | 返回默认布局 |
UserHklToKbl |
L539 | 全局按 HKL 解析 KL |
IntReorderKeyboardLayouts |
L558 | 布局链表重排为默认 |
UserSetDefaultInputLang |
L583 | 设置系统默认输入语言 |
IntImmActivateLayout |
L596 | IME 感知的线程布局切换 |
co_IntSetKeyboardLayoutForProcess |
L632 | 进程级布局切换 |
co_UserActivateKeyboardLayout |
L667 | 激活布局核心(返回旧 HKL) |
co_IntActivateKeyboardLayout |
L782 | ActivateKeyboardLayout 内核辅助 |
co_IntUnloadKeyboardLayoutEx |
L805 | 卸载布局核心 |
IntUnloadKeyboardLayout |
L851 | 卸载布局包装 |
co_UserImmLoadLayout |
L863 | 加载 IME 信息 |
co_IntLoadKeyboardLayoutEx |
L884 | 加载布局核心 |
IntVerifyKeyboardFileHandle |
L962 | 校验布局文件句柄 |
UserGetKeyboardLayout |
L995 | GetKeyboardLayout 内核实现 |
NtUserGetKeyboardLayoutList |
L1038 | 布局列表系统调用 |
NtUserGetKeyboardLayoutName |
L1092 | 布局名系统调用 |
NtUserLoadKeyboardLayoutEx |
L1160 | 加载布局系统调用 |
NtUserActivateKeyboardLayout |
L1225 | 激活布局系统调用 |
NtUserUnloadKeyboardLayout |
L1250 | 卸载布局系统调用 |
14.3 其他文件
| 文件 | 位置 | 内容 |
|---|---|---|
input.h(ntuser) |
L15-39、L50-56、L99-109 | KBDFILE/KL 结构、KS_* 常量、位图宏 |
msgqueue.h(ntuser) |
USER_MESSAGE_QUEUE | afKeyState/afKeyRecentDown/ptiKeyboard/spwndFocus/spwndActive |
msgqueue.c(ntuser) |
L222、L247、L266、L1783、L2212、L2557、L2573、L2589、L2627 | 同步键状态与键盘消息出队 |
message.c(ntuser) |
L2489 | NtUserTranslateMessage |
desktop.c(ntuser) |
L1324 | IntGetFocusMessageQueue |
input.c(ntuser) |
L140-362、L777 | RawInputThreadMain、SendInput 键盘分发 |
simplecall.c(ntuser) |
L299-305 | GetKeyboardType/GetKeyboardLayout 分发 |
user32/windows/input.c |
全文件 | 用户态键盘 API 封装 |
sdk/include/ndk/kbd.h |
L8-131 | KBDTABLES 及配套结构/常量 |
sdk/include/psdk/ntddkbd.h |
L29-91、L85-120 | 键盘 IOCTL、KEYBOARD_INPUT_DATA/属性/指示灯 |
15. 小结
keyboard.c + kbdlayout.c 共同实现了 Windows 键盘输入的内核半边:
- 输入入口 :硬件数据
KEYBOARD_INPUT_DATA经RawInputThreadMain交给
UserProcessKeyboardInput,与SendInput注入在UserSendKeyboardInput汇合,
统一进入ProcessKeyEvent主处理。 - 状态双轨 :
gafAsyncKeyState(全局异步,物理真实)与MessageQueue->afKeyState
(队列同步,消息流视角)各用 2 位/键位图表达 down+lock,lock 位在"首次按下"沿翻转,
支撑GetAsyncKeyState/GetKeyState/GetKeyboardState/SetKeyboardState四件套。 - 布局解耦 :
KBDTABLES由布局 DLL 提供,KBDFILE共享、KL实例挂环形链表,
LoadKeyboardLayout/ActivateKeyboardLayout/UnloadKeyboardLayout与
GetKeyboardLayout(List/Name)完整覆盖布局生命周期;Alt/Ctrl+Shift 热键切换、
WM_INPUTLANGCHANGE(REQUEST)、HSHELL_LANGUAGE构成切换通知链。 - 字符翻译 :
IntTranslateChar/IntToUnicodeEx处理修饰位、CapsLock、AltGr、死键合成,
用户态TranslateMessage(IntTranslateKbdMessage)用队列同步状态把键消息变为
WM_CHAR/WM_DEADCHAR/WM_SYSCHAR;MapVirtualKey/VkKeyScan/ToUnicodeEx提供
按需的键码↔字符互转。 - 定向与旁路 :消息定向焦点窗口(
spwndFocus→spwndActive回退),
IME(IntImmProcessKey)、钩子(WH_KEYBOARD_LL/WH_KEYBOARD)、热键、Alt+Tab、
PrtSc 截屏、Alt+Numpad、AltGr(右 Alt 补发 Ctrl)等旁路在恰当阶段介入。
键盘子系统的下一层是输入法(ime.c,分册 _24):IntImmProcessKey 在
co_IntProcessKeyboardMessage 中的调用点已在本册点明,IME 布局的 piiex 加载与
IntImmActivateLayout 的联动也已就位,二者共同构成东方语言输入的完整内核支持。