ReactOS 窗口系统分析(23):键盘 — keyboard.c + kbdlayout.c

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.hwin32ss/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 位取反 (见

UpdateAsyncKeyStateUpdateKeyState),这正是 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 提供,读取即清除)。
  • GetKeyStateUserGetKeyState(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=0KEY_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=0xA0VK_A='A'。内核内部把
    KBDEXT(0x100)/KBDMULTIVK(0x200)/KBDSPECIAL(0x400)/KBDNUMPAD(0x800) 这 4 个高位标志
    与 8 位 VK 合并使用(ndk/kbd.h L8-11),进入消息前再 & 0xFF 净化。
  • 扫描码(SC) :硬件物理键编号。SCANCODE_LSHIFT=0x2ASCANCODE_RSHIFT=0x36
    SCANCODE_CTRL=0x1DSCANCODE_ALT=0x38(ndk/kbd.h L128-131)。E0/E1 前缀扩展键的扫描码
    用高位标记:0xE000|vsc0xE100|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.AttributesCAPLOK(0x01)/CAPLOKALTGR(0x04) 表示该键受 CapsLock 影响。

修饰位常量:KBDSHIFT=0x001KBDCTRL=0x002KBDALT=0x004SHFT_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 指向当前激活的 KLpti->pClientInfo->hKL 是用户态
    GetKeyboardLayout 的数据源。
  • KLKBDFILE 是多对一:多个语言变体布局可以共享同一份布局 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,获取"哪些扫描码对应哪些指示灯"

的翻译表,存入全局 gpKeyboardIndicatorTransExAllocatePoolWithTag(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 后调用,做三件事:

  1. IntKeyboardGetIndicatorTrans 取指示灯翻译表;
  2. IOCTL_KEYBOARD_QUERY_INDICATORS 读当前 LED 状态,用 SET_KEY_LOCKED
    Caps/Num/Scroll 的 lock 位同步进 gafAsyncKeyState
  3. 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;

用于按键状态维护:gafAsyncKeyStateVK_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_NUMPAD0VK_END→VK_NUMPAD1......VK_DELETE→VK_DECIMAL

UserProcessKeyboardInput 中当扫描码带 KBDNUMPAD 标志且 NumLock 锁定时应用

IS_KEY_LOCKED(gafAsyncKeyState, VK_NUMLOCK))。

IntVscToVk(L579)与 IntVkToVsc(L542)

扫描码↔虚键的双向翻译(布局相关):

  • IntVscToVk(wScanCode, pKbdTbl):若高字节为 0xE000pVSCtoVK_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 复用。流程:

  1. dwModBits = IntGetModBits(pKbdTbl, pKeyState);计算 AltGr:
    bAltGr = (fLocaleFlags & KLLF_ALTGR) && IS_KEY_DOWN(pKeyState, VK_RMENU)
    wCaplokAttr = bAltGr ? CAPLOKALTGR : CAPLOK(AltGr 布局下 CapsLock 只影响左 Alt 组合)。
  2. 特例:单独 Alt(无 Ctrl)时去掉 KBDALT 位(dwModBits & (KBDALT|KBDCTRL)) == KBDALT
    dwModBits &= ~KBDALT),使 Alt+键与无修饰组合等价------这就是"Alt 弹出菜单栏时不应产生
    特殊字符"的根源。
  3. 遍历 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 直接放弃连字)。
  4. 布局表未命中时的兜底:ASCII 控制字符 ------wVirtKey ∈ ['A','Z'] 且 Ctrl 按下、Alt 未按下
    时返回 (wVirtKey - 'A') + 1(即 Ctrl+A → 0x01)。注释说明 Windows 也不在布局表中放这些项。
IntToUnicodeEx(L449)

虚键+同步键状态 → 字符串 (ToUnicodeEx 的内核实现)。维护静态 wchDead 记录上一次的死键:

  1. IntTranslateChar 翻译当前键;bLigature 时 WARN 并返回 0(未实现连字)。
  2. 若上次有死键 wchDead:查 pDeadKey[] 表,把 dwBoth 拆成高 16 位(死键字符)与低 16 位
    (当前字符),匹配则用 wchComposed 作为合成字符并清除 wchDead
    还包含一个 GCC 特判:wchDead == 0x8000 时忽略"GCC 发明的高位"(编译器把 0xf001 的符号位
    符号扩展所致)。
  3. 死键未找到合成对象:把死键本身作为普通字符输出(pwszBuff[iRet++] = wchDead),当前字符照常输出。
  4. 返回值为 iRet(正=普通字符数;负=死键,绝对值=已输出的字符数),并把
    wchDead = bDead ? wchTranslatedChar : 0 供下次调用合成。

IntTranslateKbdMessage 正是利用负返回值把消息类型从 WM_CHAR 改为 WM_DEADCHAR

IntVkToChar(L622)

不带键状态的"虚键→字符"(pKeyState = NULL 传给 IntTranslateCharIntGetModBits 返回 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_EXTENDEDKEYEVENTF_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 表示

低级钩子吞掉了这个键(ProcessKeyEventbPostMsg = FALSE)。

SnapWindow(L740)

PrtSc 截屏:VK_SNAPSHOT 按下时,若 Alt 按住(且 Ctrl 未按)则截取活动窗口,否则截取整个桌面。

UserGetDCEx(pWnd, NULL, DCX_USESTYLE|DCX_WINDOW) 取窗口 DC → NtGdiCreateCompatibleBitmap

建位图 → NtGdiBitBlt(SRCCOPY) 拷贝 → UserOpenClipboard/UserEmptyClipboard/UserSetClipboardData(CF_BITMAP)

存入剪贴板。由 ProcessKeyEventbIsDown && 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/BACKWARDwVk==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

流程:

  1. 仅当 Alt 按下 && Ctrl 未按下 时进入;gbEnableHexNumpad 控制是否接受 ./+ 引导十六进制。
  2. 状态建立后,后续小键盘数字(VK_NUMPAD0..9)累积到 gAltNumPadValue*10+digit*16+digit
    允许溢出截断,与 Windows 一致);这些按键不再走普通键盘处理(返回 TRUE)。
  3. Alt 释放时(!bIsDown && wVk == VK_MENU)结算:ALTNUM_HEX_UTF 直接 & 0xFFFF 取码点;
    其余用 RtlOemToUnicodeN/RtlMultiByteToUnicodeN 按代码页转换(CJK 代码页保留 2 字节,
    否则 & 0xFF 截断为 1 字节,与 Windows 的 mod 256 行为一致)。
  4. 转换成功则构造 WM_CHAR 消息(wParam=字符,lParam=1,time/pt 从当前输入补齐),
    MsqPostMessage(pFocusQueue->ptiKeyboard, &msgChar, FALSE, QS_KEY, 0, 0) 投递给焦点队列。
  5. 复位状态;非 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=0x2000KF_REPEAT=0x4000KF_UP=0x8000KF_DLGMODE=0x0800KF_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 IntVscToVkIntSimplifyVk(去左右)
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 的内核实现:

  1. 参数校验:wScanCode & SC_KEY_UPwVirtKey >= 0x100 时返回 0(ToUnicode 约定)。
  2. SEH 探针拷贝用户态 256 字节键状态为紧凑位图 afKeyStateKS_DOWN_BIT→SET_KEY_DOWN
    KS_LOCK_BIT→SET_KEY_LOCKED)。
  3. 分配 pwszBuffUserEnterExclusive()IntToUnicodeExwchDead 静态变量需要独占)。
  4. 取布局(dwhklUserHklToKbl,否则当前线程布局),调 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_NEXTUlongToHandle(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 对象并填充内容:

  1. UserCreateObject(gHandleTable, NULL, NULL, NULL, TYPE_KBDFILE, sizeof(KBDFILE)) 分配对象;
  2. _swprintf(pkf->awchKF, L"%wZ", pwszKLID) 记录布局 ID(KLID,如 "00000409");
  3. 打开注册表键 \REGISTRY\Machine\SYSTEM\CurrentControlSet\Control\Keyboard Layouts\<KLID>
    Layout File 值得到布局 DLL 路径(\SystemRoot\System32\<文件>);
  4. UserLoadKbdDll 加载 DLL 并把 pKbdTbl 存入 pkf->pKbdTblhBase 存模块句柄;
  5. 头插 进全局 gpkfListpkf->pkfNext = gpkfList; gpkfList = pkf;)------同一布局 DLL
    被多次加载时复用已有 KBDFILE;
  6. 失败路径: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)、释放 piiexExFreePoolWithTag(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;   /* 新默认布局 */

UserSetDefaultInputLangco_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 的内核核心:

  1. hNewKL == NULL 或窗口站 WSS_NOIO(无输入权限)时返回 NULL;
  2. hOldKL && hOldKL != hNewKL:先 UserUnloadKbl 卸载旧布局(KLF_UNLOADPREVIOUS 语义);
  3. UserHklToKbl(hNewKL) 已存在则复用;否则 co_UserLoadKbdLayout 创建新 KL 并挂入环形链表
    (首个布局自环并成为 gspklBaseLayout;后续布局插到"最后一个未卸载布局"之后),
    pNewKL->piiex = co_UserImmLoadLayout(hNewKL)
  4. 清除 KL_UNLOAD(取消待卸载状态,允许重新激活已标记卸载的布局);
  5. KLF_REORDERIntReorderKeyboardLayoutsKLF_ACTIVATEco_UserActivateKeyboardLayout
  6. KLF_NOTELLSHELLco_IntShellHookNotify(HSHELL_LANGUAGE, 0, hNewKL) 通知 shell;
  7. 返回 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 的

CliImmInitializeHotKeysNtUserGetKeyboardLayoutList(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)

UserEnterExclusiveco_IntActivateKeyboardLayout(IntGetProcessWindowStation(NULL), hKL, Flags, NULL)

→ 返回旧 HKL。

NtUserUnloadKeyboardLayout(L1250)

UserEnterExclusiveIntUnloadKeyboardLayout(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)
  • NtUserGetKeyStateUserEnterSharedUserGetKeyState → 返回 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 把硬件消息分派到本函数。处理链:

  1. uMsg == VK_PACKET 时把 HIWORD(wParam) 存入 pti->wchInjected(注入字符缓存);
  2. wParam 简化VK_LSHIFT/VK_RSHIFT → VK_SHIFTVK_LCONTROL/VK_RCONTROL → VK_CONTROL
    VK_LMENU/VK_RMENU → VK_MENU(消息中的 wParam 一律用简化虚键);
  3. 记录 WH_JOURNALRECORD 日志事件(EVENTMSG);
  4. 特殊键旁路(*RemoveMessages 时):
    • WM_KEYDOWNVK_F1UserPostMessage(hwnd, WM_KEYF1)
    • 浏览器键(VK_BROWSER_BACK..VK_LAUNCH_APP2)→ co_IntSendMessage(hwnd, WM_APPCOMMAND, MAKELPARAM(0, FAPPCOMMAND_KEY | (wParam - VK_BROWSER_BACK + 1)))
    • WM_KEYUPVK_APPS(菜单键)且无菜单 → UserPostMessage(hwnd, WM_CONTEXTMENU, hwnd, -1)
  5. WM_SYSKEYDOWN + KF_ALTDOWNVK_ESCAPE/VK_TABAlt+Tab / Alt+Esc 系统命令 ------
    wParamTmp = UserGetKeyState(VK_SHIFT)&0x8000 ? SC_PREVWINDOW : SC_NEXTWINDOW
    co_IntSendMessage(hwnd, WM_SYSCOMMAND, wParamTmp, Msg->wParam),返回 FALSE(吞掉原消息);
  6. WH_KEYBOARD 钩子:co_HOOK_CallHooks(WH_KEYBOARD, HC_ACTION|HC_NOREMOVE, LOWORD(wParam), lParam)
    返回非 0 则 WH_CBTHCBT_KEYSKIPPED*RemoveMessages = TRUE(吞键);
  7. 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_PROCESSBYIMEMsg->wParam = VK_PROCESSKEY(IME 吃掉按键,应用看到 VK_PROCESSKEY)。

VK_PROCESSKEYTIF_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) MapVirtualKeyExWNtUserMapVirtualKeyEx(Ex 版传 GetKeyboardLayout(0) 键码互转
VkKeyScan(A/W) NtUserVkKeyScanEx(ch, 0, FALSE);Ex 版 (ch, hkl, TRUE) 字符→虚键
ToAscii/ToAsciiEx ToUnicodeExWideCharToMultiByte(CP_ACP) 字符转换
ToUnicode/ToUnicodeEx NtUserToUnicodeEx 虚键→字符
LoadKeyboardLayoutA/W/Ex IntLoadKeyboardLayout:校验 KLID → 查注册表
Keyboard Layouts\<KLID>Layout Id/IME File 等 → 组 HKL →
NtUserLoadKeyboardLayoutExCliImmInitializeHotKeys(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=FALSEUpdateAsyncKeyState 清 down → Msg.message=WM_KEYUP

lParam |= KF_UP → 队列收到 WM_KEYUP(TranslateMessage 对 KEYUP 不产生字符,

IntTranslateKbdMessage 只处理 KEYDOWN/SYSKEYDOWN)。

SysKey 情形 (Alt+字符 或 F10):Msg.message = WM_SYSKEYDOWNlParam |= 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_INJECTEDKEYBOARD_INPUT_DATA 注入位。

VK_PACKET 注入:dwFlags=KEYEVENTF_UNICODE 时 wVk=VK_PACKET、wScan=字符,消息 wParam

VK_PACKET & 0xFF,lParam 高字携带字符,TranslateMessage 直接转 WM_CHAR。


8. 按键状态表维护

8.1 更新时机(两类状态)

状态表 更新者 更新时机 读取者
gafAsyncKeyState(全局) ProcessKeyEventUpdateAsyncKeyState 每次按键事件(硬件+注入),
在消息入队之前 GetAsyncKeyState、低级钩子(LLKHF_ALTDOWN)、热键处理、
语言切换检测、MsqInitializeMessageQueue(快照)
MessageQueue->afKeyState(队列) co_IntProcessKeyboardMessageUpdateKeyStateFromMsg 键盘消息出队时 GetKeyStateGetKeyboardStateTranslateMessageIntToUnicodeEx

两类状态的语义差异正是 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]+ShiftgdwLanguageToggleKey==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 卸载与延迟卸载

UnloadKeyboardLayoutNtUserUnloadKeyboardLayoutIntUnloadKeyboardLayout

co_IntUnloadKeyboardLayoutExUserMarkObjectDestroy + KL_UNLOAD;若当前线程正用该布局,

自动激活 HKL_NEXT;默认布局顺移到下一节点;shell 收到 HSHELL_LANGUAGE(去重标记清空)。

实际内存回收等待引用计数归零后 UserUnloadKblUnloadKbdFile(EngUnloadImage)。

9.5 HKL 解析规则(IntHKLtoPKL / UserHklToKbl)

调用 范围 匹配规则
IntHKLtoPKL 当前线程布局链表 HKL_NEXT/HKL_PREV(跳过 KL_UNLOAD);完整 HKL 精确匹配;纯语言 ID 按 LOWORD 匹配
UserHklToKbl 全局 gspklBaseLayout 链表 pKl->hkl == hKl 精确匹配

co_IntActivateKeyboardLayoutIntHKLtoPKL(支持 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_DEADIntToUnicodeEx 存入静态 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.cNtUserSetForegroundWindow/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)、
ZwReadFileKEYBOARD_INPUT_DATAUserInitKeyboard 初始化硬件状态、
NtUserSendInput 分发注入(键盘分支调 UserSendKeyboardInput
msgqueue.c(分册 _8) USER_MESSAGE_QUEUE.afKeyState 队列同步键状态;
UserGetKeyState/NtUserGetKeyState/GetKeyboardState/SetKeyboardState
UpdateKeyState/UpdateKeyStateFromMsgco_IntProcessKeyboardMessage
(钩子/Alt+Tab/IME 调用点);MsqPostMessage 入队
ime.c(分册 _24) IntImmProcessKey 键盘消息出队后、窗口过程前的 IME 拦截;
VK_PROCESSKEY 改写;spwndDefaultIme/spDefaultImc 与布局激活联动
IntImmActivateLayoutIMS_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 键盘输入的内核半边:

  1. 输入入口 :硬件数据 KEYBOARD_INPUT_DATARawInputThreadMain 交给
    UserProcessKeyboardInput,与 SendInput 注入在 UserSendKeyboardInput 汇合,
    统一进入 ProcessKeyEvent 主处理。
  2. 状态双轨gafAsyncKeyState(全局异步,物理真实)与 MessageQueue->afKeyState
    (队列同步,消息流视角)各用 2 位/键位图表达 down+lock,lock 位在"首次按下"沿翻转,
    支撑 GetAsyncKeyState/GetKeyState/GetKeyboardState/SetKeyboardState 四件套。
  3. 布局解耦KBDTABLES 由布局 DLL 提供,KBDFILE 共享、KL 实例挂环形链表,
    LoadKeyboardLayout/ActivateKeyboardLayout/UnloadKeyboardLayout
    GetKeyboardLayout(List/Name) 完整覆盖布局生命周期;Alt/Ctrl+Shift 热键切换、
    WM_INPUTLANGCHANGE(REQUEST)HSHELL_LANGUAGE 构成切换通知链。
  4. 字符翻译IntTranslateChar/IntToUnicodeEx 处理修饰位、CapsLock、AltGr、死键合成,
    用户态 TranslateMessageIntTranslateKbdMessage)用队列同步状态把键消息变为
    WM_CHAR/WM_DEADCHAR/WM_SYSCHAR;MapVirtualKey/VkKeyScan/ToUnicodeEx 提供
    按需的键码↔字符互转。
  5. 定向与旁路 :消息定向焦点窗口(spwndFocusspwndActive 回退),
    IME(IntImmProcessKey)、钩子(WH_KEYBOARD_LL/WH_KEYBOARD)、热键、Alt+Tab、
    PrtSc 截屏、Alt+Numpad、AltGr(右 Alt 补发 Ctrl)等旁路在恰当阶段介入。

键盘子系统的下一层是输入法(ime.c,分册 _24):IntImmProcessKey

co_IntProcessKeyboardMessage 中的调用点已在本册点明,IME 布局的 piiex 加载与

IntImmActivateLayout 的联动也已就位,二者共同构成东方语言输入的完整内核支持。

相关推荐
Emily156853598701 小时前
Emerson 1C31129G03 Analog Input Module
java·开发语言·前端·plc·emerson·1c31129g03·input module
就叫_这个吧6 小时前
Java递归方法实现面包屑导航
java·开发语言
fīɡЙtīиɡ ℡6 小时前
AI 应用系统设计
java·开发语言·人工智能
今天AI了吗8 小时前
Python 基础语法(一):常量、变量、输入输出与运算符
开发语言·数据库·人工智能·python·sql·深度学习·机器学习
TELL5219 小时前
selenium webdriver 第二次初始化的异常
开发语言·python
动词ing9 小时前
【C语言】自定义函数+指针入门
c语言·开发语言·算法
重生之后端学习9 小时前
283. 移动零[简单]✅
开发语言·数据结构·算法·leetcode·职场和发展
动词ing10 小时前
【C语言】结构体+文件基础
c语言·开发语言·数据结构
caimouse10 小时前
ReactOS 窗口系统分析(7):分层窗口与绘制辅助 — layered.c + draw.c
c语言·开发语言