ReactOS 窗口系统分析(24):输入法 --- ime.c
本文是"窗口系统三大主线之鼠标控制"系列的第 24 册(最后一册),核心回答:窗口系统如何接入输入法(IME) 。
即:键盘按键如何在进入窗口过程之前被输入法拦截、预组合(precomposition)、再以
WM_IME_*消息与WM_CHAR的形式交给应用程序;输入上下文(HIMC/IMC)如何创建、关联、销毁;每个目标窗口的"默认 IME 窗口"如何诞生与消亡;语言栏显示状态如何广播。
核心源码:
win32ss/user/ntuser/ime.c(2556 行,内核态 IME/IMM 支持主文件)、
win32ss/user/ntuser/msgqueue.c(键盘消息落地,IntImmProcessKey调用点)、
win32ss/user/ntuser/callback.c(co_IntImmProcessKey用户态回调)、
win32ss/user/imm32/(用户态 imm32.dll:ImmProcessKey、ImmGetContext等)、
win32ss/user/user32/misc/imm.c("IME" 类窗口过程ImeWndProc_common)。关联分册:_4(kbdlayout.c 键盘布局)、_5(class.c 窗口类)、_8(msgqueue.c 消息队列)、_21(input.c/event.c 输入子系统)、_22(mouse.c 鼠标)、_23(keyboard.c 键盘输入)。
目录
- 概述
- 设计动机
- 核心数据结构
- 全局状态与模块入口
- [IME 热键管理](#IME 热键管理)
- [按键拦截入口 IntImmProcessKey](#按键拦截入口 IntImmProcessKey)
- [用户态回调 co_IntImmProcessKey](#用户态回调 co_IntImmProcessKey)
- [键盘布局切换 NtUserSetThreadLayoutHandles](#键盘布局切换 NtUserSetThreadLayoutHandles)
- [HIMC 列表 NtUserBuildHimcList](#HIMC 列表 NtUserBuildHimcList)
- [IME 状态通知 NtUserNotifyIMEStatus](#IME 状态通知 NtUserNotifyIMEStatus)
- [线程/进程 IME 禁用与 AppImeLevel](#线程/进程 IME 禁用与 AppImeLevel)
- [IMEINFOEX 查询与设置](#IMEINFOEX 查询与设置)
- [IME 窗口的所有者与 Z 序管理](#IME 窗口的所有者与 Z 序管理)
- 输入上下文(IMC)对象生命周期
- [输入上下文关联 NtUserAssociateInputContext](#输入上下文关联 NtUserAssociateInputContext)
- [输入上下文更新与查询 NtUserQueryInputContext](#输入上下文更新与查询 NtUserQueryInputContext)
- [IME 窗口判定 IntWantImeWindow](#IME 窗口判定 IntWantImeWindow)
- [默认 IME 窗口创建 co_IntCreateDefaultImeWindow](#默认 IME 窗口创建 co_IntCreateDefaultImeWindow)
- [默认 IME 窗口销毁判定](#默认 IME 窗口销毁判定)
- [IME 显示状态(语言栏 ShowStatus)](#IME 显示状态(语言栏 ShowStatus))
- [IME 按键处理全流程](#IME 按键处理全流程)
- 输入上下文(IMC)生命周期总结
- [默认 IME 窗口机制总结](#默认 IME 窗口机制总结)
- 调用链总览(mermaid)
- [与 keyboard/msgqueue/window/imm32 的关系](#与 keyboard/msgqueue/window/imm32 的关系)
- 源码索引
1. 概述
"输入法"这条主线要回答的问题:在拉丁字母键盘上如何输入 CJK(中日韩)等表意文字 。
Win32 的答案是:把"键盘事件流"先交给一个叫 IMM(Input Method Manager,输入法管理器)
的中间层,由它把按键翻译成"组合字符串",组合完成后以 WM_CHAR 提交给应用程序。ReactOS
的 IMM 实现横跨内核(win32k 的 ntuser/ime.c)与用户态(imm32.dll),中间靠
KeUserModeCallback 桥接。
从物理按键到应用窗口过程,IME 处理的流水线如下(本册讨论范围是加粗部分):
键盘硬件中断
→ RawInput 线程(input.c: RawInputThreadMain)
→ UserProcessKeyboardInput(keyboard.c,见分册 _23)
→ 键状态更新 + 布局翻译(kbdlayout.c,见分册 _4)
→ WM_KEYDOWN/WM_KEYUP 等键消息入队(HardwareMessagesListHead)
→ 应用 GetMessage / PeekMessage
→ co_IntProcessKeyboardMessage(msgqueue.c L1888)【本册:调用点】
→ IntImmProcessKey(ime.c L602)【本册:按键交给 IME 的入口】
├── IntCheckImeHotKey:先查 IME 热键(切换语言/形状等)
├── 过滤:非 IMM 模式 / 无 IMC / SysKey / Alt 组合 / Up 键
└── co_IntImmProcessKey(callback.c L1252)【本册】
→ KeUserModeCallback(USER32_CALLBACK_IMMPROCESSKEY)
→ user32: User32CallImmProcessKeyFromKernel(dllmain.c L703)
→ imm32: ImmProcessKey(imm32/keymsg.c L740)【用户态】
├── ImmGetContext(hWnd) → 取输入上下文
├── ImeProcessKey() → 把按键交给 IME 本体(组合/候选)
├── Imm32ProcessHotKey() → 处理 IME 热键动作
└── 返回 IPHK_* 标志(PROCESSBYIME / HOTKEY / SKIPTHISKEY)
→ 回到 IntImmProcessKey,返回标志给 msgqueue.c
├── IPHK_PROCESSBYIME → 键消息 wParam 改为 VK_PROCESSKEY
└── IPHK_HOTKEY / IPHK_SKIPTHISKEY → 吞掉原键消息
→ IME 本体(IME DLL,imm32 加载)处理按键
├── 组合状态机:WM_IME_STARTCOMPOSITION → WM_IME_COMPOSITION(组合串更新)
├── 候选窗口:WM_IME_NOTIFY(IMN_OPENCANDIDATE) + 候选列表
└── 组合完成:ImmGenerateMessage → WM_IME_ENDCOMPOSITION + WM_CHAR 提交
→ 应用程序窗口过程收到 WM_IME_* 与 WM_CHAR
一次按键的"可见结果"有三个:消息 (原键消息或被替换为 VK_PROCESSKEY 的键消息、
IME 组合消息、最终 WM_CHAR)、输入上下文状态 (组合串/候选列表的 HIMCC 内存块)、
IME 窗口状态(默认 IME 窗口 / UI 窗口的可见性与 Z 序)。
2. 设计动机
2.1 从"键消息"到"文字"的翻译需要中间层
WM_KEYDOWN + wParam=VK_A 只告诉应用"用户按了一个键",而要输入汉字,用户通常要连按
多个键(拼音/注音/仓颉/五笔),这些键的组合 才有意义。Windows 在键盘消息与应用之间
插入 IMM 层:它先把键消息发给 IME(ImmProcessKey),IME 决定"这个键我自己收下用于组合"
(返回 IPHK_PROCESSBYIME)还是"放行给应用"(返回 0)。被收下的键不再以原样到达应用,
而是驱动 IME 内部的组合状态机,最终生成 WM_IME_COMPOSITION(更新组合串)与 WM_CHAR
(提交文字)。
2.2 为什么需要"输入上下文"(Input Context)
IME 的状态(开/关、转换模式、组合串、候选列表、字体、状态窗口位置)必须跟窗口绑定 :
用户在记事本里打了一段中文,切到浏览器地址栏时,IME 不应继续沿用记事本的组合串。Win32
用 HIMC(Input Method Context) 对象承载这些状态,每个窗口通过 hImc 字段指向它。
窗口之间切换焦点时,HIMC 也随之切换------这就是 IntAssociateInputContext /
NtUserAssociateInputContext 存在的意义。
2.3 为什么需要"IME 窗口 / UI 窗口"分离
IME 的界面有两层:IME 窗口 ("IME" 类的隐形窗口,内核/用户态用它接收 WM_IME_SYSTEM
等系统消息、持有 IMEUI 扩展数据)与 IME UI 窗口 (候选窗口、状态窗口等可见 UI)。
分离的好处:
- IME 窗口是每个线程一个 的隐式基础设施窗口(
pti->spwndDefaultIme),随线程存在,
不抢焦点、不画界面; - UI 窗口是每个 IME 实现自己创建 的可见窗口(
IMEUI.hwndUI),按需显示/隐藏; - 这样键盘输入管线只跟"IME 窗口"打交道,UI 的绘制细节被完全封装在 IME DLL 内部。
2.4 为什么需要"默认 IME 窗口"
ImmGetContext 要求"任意窗口都能查到输入上下文"。若每个窗口都显式创建 IMC,资源开销大
且应用要自己维护。Windows 的折中:每个线程维护一个默认 IMC(spDefaultImc) ,
并给"第一个需要 IME 的窗口"创建一个默认 IME 窗口(spwndDefaultIme)。窗口的
hImc == NULL 时,imm32 会回退到线程默认 IMC(NtUserGetThreadState(THREADSTATE_DEFAULTINPUTCONTEXT))。
默认 IME 窗口在目标窗口销毁时按需销毁(IntImeCanDestroyDefIME),避免悬挂。
2.5 为什么需要"热键"与"显示状态"两个旁路
- IME 热键 :用户按 Ctrl+Space / Alt+
等组合切换中英文、全半角。这些按键不能走普通 IME 组合流程(否则会被 IME 当普通键吞掉),必须由 win32k 的gpImeHotKeyList表 预先匹配,命中后由 imm32 的Imm32ProcessHotKey执行动作(IME_CHOTKEY_IME_NONIME_TOGGLE` 等)。 - 显示状态(ShowStatus) :语言栏(language bar)需要知道"哪个 IME 的状态窗口该显示"。
win32k 用全局gfIMEShowStatus+ 每 IME 窗口的IMEUI.fShowStatus/fCtrlShowStatus
位组合,配合WM_IME_NOTIFY(IMN_OPENSTATUSWINDOW/IMN_CLOSESTATUSWINDOW)通知。
2.6 为什么内核只做"一半"
IME 的本体 (组合算法、候选词库、用户词典)是重量级用户态代码,且不同 IME 实现不同,
不可能放进 win32k。内核负责:按键拦截时机、输入上下文对象管理、IME 窗口生命周期、热键
匹配、显示状态广播;用户态 imm32 负责:加载 IME DLL、调 ImeProcessKey、维护
INPUTCONTEXT/COMPOSITIONSTRING 等共享内存块、发送 WM_IME_* 消息。两者通过
KeUserModeCallback(USER32_CALLBACK_IMMPROCESSKEY)在按键路径上回合。
3. 核心数据结构
3.1 输入上下文内核对象 tagIMC --- win32ss/include/ntuser.h L197
c
typedef struct tagIMC
{
THRDESKHEAD head; // 对象头:h 句柄 / cLockObj 引用 / pti 属主线程 / rpdesk 桌面
struct tagIMC *pImcNext; // 同一线程内的下一个 IMC(首节点即默认 IMC spDefaultImc)
ULONG_PTR dwClientImcData;// 用户态 CLIENTIMC 指针(跨内核/用户态传递)
HWND hImeWnd; // 与本 IMC 关联的 IME 窗口句柄
} IMC, *PIMC;
32 位布局断言:head.h 偏移 0x0、cLockObj 0x4、pti 0x8、pImcNext 0x14、
dwClientImcData 0x18、hImeWnd 0x1c。注意:内核 IMC 只是"登记册" ------真正的内容
(INPUTCONTEXT:fOpen、fdwConversion、hCompStr、hCandInfo...)在用户态 imm32 的
CLIENTIMC 结构里,通过 dwClientImcData 指回。这与 ReactOS 的 USER 对象设计一致:
内核对象小而稳定,用户态数据走共享堆。
INPUTCONTEXT(sdk/include/ddk/immdev.h L106)的完整字段包括:hWnd(关联窗口)、
fOpen(IME 开)、fdwConversion/fdwSentence(转换/句模式)、lfFont、cfCompForm
(组合窗口位置)、cfCandForm[4](4 个候选窗位置)、hCompStr(组合字符串 HIMCC)、
hCandInfo(候选信息)、hGuideLine(引导行)、hPrivate、hMsgBuf(消息缓冲)、
fdwInit 等------这解释了为什么内核 tagIMC 如此精简。
3.2 IME 窗口扩展数据 IMEUI / IMEWND --- ntuser.h L1218
c
typedef struct tagIMEUI
{
PWND spwnd; // 所属 IME 窗口(内核对象指针)
HIMC hIMC; // 关联的输入上下文
HWND hwndIMC; // 拥有 IMC 的目标窗口
HKL hKL; // 当前键盘布局
HWND hwndUI; // IME UI 窗口(候选/状态等可见窗口)
LONG nCntInIMEProc; // IME 过程重入计数(防 WM_IME_* 重入风暴)
struct {
UINT fShowStatus:1; // 本 IME 窗口应显示状态窗口
UINT fActivate:1; // IME 已激活
UINT fDestroy:1; // 请求销毁
UINT fDefault:1; // 是默认 IME 窗口
UINT fChildThreadDef:1; // 跨线程子窗口的默认 IME 窗口
UINT fCtrlShowStatus:1; // 用户要求显示状态窗口
UINT fFreeActiveEvent:1; // 释放激活事件
};
DWORD dwLastStatus; // 上次状态
} IMEUI, *PIMEUI;
typedef struct tagIMEWND
{
WND wnd; // 普通窗口对象在前
PIMEUI pimeui; // 扩展指针在 wnd 的 cbwndExtra 区
} IMEWND, *PIMEWND;
#define GWLP_IMEWND_PIMEUI 0 // 用户态用 SetWindowLongPtr(hwnd, 0, pimeui) 存取
IMEUI 是用户态内存 (ImeWndProc_common 在 WM_NCCREATE 时 HeapAlloc 分配,
SetWindowLongPtrW(hwnd, GWLP_IMEWND_PIMEUI, ...) 挂到窗口第 0 个 cbwndExtra 槽);
因此内核访问它时必须 ProbeForRead/ProbeForWrite + 必要时 KeAttachProcess(见
IntGetImeUIFromWnd、IntSendMessageToUI、IntCheckImeShowStatus 的 SEH/Attach 套路)。
IntGetImeUIFromWnd(ime.c L38)直接断言 cbwndExtra >= sizeof(PIMEUI) 后强转读取。
3.3 IS_WND_IMELIKE 与 IME 类窗口 --- window.h L111
c
// The IME-like windows are the IME windows and the IME UI windows.
// The IME window's class name is "IME".
#define IS_WND_IMELIKE(pWnd) \
(((pWnd)->pcls->style & CS_IME) || \
((pWnd)->pcls->atomClassName == gpsi->atomSysClass[ICLS_IME]))
判定"像不像 IME 窗口"有两种来源:
- 类样式带
CS_IME(0x10000,winuser.h L667)------IME UI 窗口的类注册时带此样式; - 类名就是系统原子
ICLS_IME------即用户态注册的内建 "IME" 类(user32/regcontrol.c
IME_builtin_class,窗口过程ImeWndProcA/W,cbwndExtra =sizeof(IMEWND)-sizeof(WND))。
内核 class.c 把 L"IME" 作为系统类名之一(ICLS_IME 索引),FNID_IME ↔ ICLS_IME
映射在 FnidToiCls[](class.c L126)。IS_WND_IMELIKE 在 ime.c 中被广泛用于:
IntImeSetFutureOwner(不为 IME 窗口选 IME-like owner)、IntGetLastTopMostWindowNoIME、
IntWantImeWindow(IME 窗口不需要再配默认 IME 窗口)、NtUserSetImeOwnerWindow、
IntImeCanDestroyDefIME 等------一句话,IME 窗口不得是另一个 IME 窗口的"目标"。
3.4 线程输入上下文字段 --- win32.h L130
c
struct _WND* spwndDefaultIme; // 线程默认 IME 窗口
struct tagIMC* spDefaultImc; // 线程默认输入上下文(IMC 链表首节点)
HKL hklPrev; // 上一个键盘布局(IME↔非 IME 切换记忆)
这三个字段是所有 IME 线程级状态的挂载点:
spDefaultImc:默认 IMC 链表(首节点),UserBuildHimcList遍历它;spwndDefaultIme:为线程创建的默认 IME 窗口,co_IntCreateDefaultImeWindow产出,
window.c 创建窗口时挂载;hklPrev:布局切换时若 IME↔非 IME 变化则记录旧布局,供"中/英切换"热键恢复用
(IntGetImeHotKeyByKey检查pti->hklPrev)。
3.5 窗口 hImc 字段 --- ntuser.h L740
c
HIMC hImc; // Input context associated with this window.
每个窗口对象内嵌一个 HIMC 句柄,IntAssociateInputContext 负责赋值/更新;imm32 的
ImmGetSaveContext(imm.c L1022)直接读 pWnd->hImc,为空时回退查询
NtUserQueryWindow(hWnd, QUERY_WINDOW_DEFAULT_ICONTEXT) 或线程默认 IMC。
3.6 键盘布局对象 KL 与 IMEINFOEX --- input.h L26 / imm32_undoc.h L72
c
typedef struct tagKL
{
HEAD head;
struct tagKL *pklNext, *pklPrev; // 窗口站布局环链(首节点 gspklBaseLayout)
DWORD dwKL_Flags;
HKL hkl; // 布局句柄(低字=语言 ID)
PKBDFILE spkf; // 键盘表文件
DWORD dwFontSigs; // 字体特征(IntImmProcessKey 用来算 INPUTLANGCHANGE_SYSCHARSET)
UINT iBaseCharset;
USHORT CodePage;
WCHAR wchDiacritic;
PIMEINFOEX piiex; // ← IME 信息扩展(本 IME 布局的完整描述)
} KL, *PKL;
piiex 指向 IMEINFOEX(imm32_undoc.h L72):hkl、IMEINFO ImeInfo(含
fdwProperty:IME_PROP_IGNORE_UPKEYS/IME_PROP_NEED_ALTKEY/IME_PROP_ACCEPT_WIDE_VKEY...)、
wszUIClass[16](IME UI 窗口类名)、fdwInitConvMode(初始转换模式)、fInitOpen、
fLoadFlag(已加载)、wszImeFile[80](IME DLL 路径)。IntImmProcessKey 大量读取
pKL->piiex->ImeInfo.fdwProperty 来决定按键是否交给 IME。
3.7 IME 热键节点 IMEHOTKEY --- ime.c L26
c
typedef struct tagIMEHOTKEY
{
struct tagIMEHOTKEY *pNext;
DWORD dwHotKeyId; // IME_CHOTKEY_* / IME_JHOTKEY_* / IME_KHOTKEY_* / IME_THOTKEY_* / IME_HOTKEY_DSWITCH_*
UINT uVirtualKey; // 虚键
UINT uModifiers; // MOD_CONTROL|MOD_SHIFT|MOD_ALT|MOD_WIN|MOD_LEFT|MOD_RIGHT|MOD_IGNORE_ALL_MODIFIER|MOD_ON_KEYUP
HKL hKL; // 目标布局(语言切换类热键)
} IMEHOTKEY, *PIMEHOTKEY;
全局链表 gpImeHotKeyList,由 NtUserSetImeHotKey(imm32 的
ImmSetImeHotKey/ImmGetImeHotKey 系统调用)维护。
4. 全局状态与模块入口
ime.c 顶部(L16-36)定义了模块级全局:
| 全局变量 | 初值 | 含义 |
|---|---|---|
HIMC ghIMC |
NULL | 最近一次 NtUserNotifyIMEStatus 记录的输入上下文句柄 |
BOOL gfImeOpen |
-1 | 缓存的 IME 打开标志(-1 表示未初始化) |
DWORD gdwImeConversion |
-1 | 缓存的转换模式 |
BOOL gfIMEShowStatus |
-1 | 全局语言栏显示状态 |
PIMEHOTKEY gpImeHotKeyList |
NULL | IME 热键链表 |
LCID glcidSystem |
0 | 系统默认 LCID(ZwQueryDefaultLocale 惰性填充) |
常量:INVALID_THREAD_ID ((ULONG)-1)、INVALID_HOTKEY ((UINT)-1)、
MOD_KEYS (MOD_CONTROL|MOD_SHIFT|MOD_ALT|MOD_WIN)、MOD_LEFT_RIGHT (MOD_LEFT|MOD_RIGHT)。
NtUserGetThreadState(misc.c L239)虽然不是 ime.c 的函数,却是 IME 用户态访问内核
线程 IME 状态的主入口,与本模块强相关,其 IME 分支:
c
case THREADSTATE_DEFAULTIMEWINDOW: // 线程默认 IME 窗口句柄(pti->spwndDefaultIme)
case THREADSTATE_DEFAULTINPUTCONTEXT:// 线程默认 IMC 句柄(pti->spDefaultImc)
case THREADSTATE_IMECOMPATFLAGS: // pti->ppi->dwImeCompatFlags(同 IntGetImeCompatFlags)
case THREADSTATE_OLDKEYBOARDLAYOUT: // pti->hklPrev
case THREADSTATE_CHECKCONIME: // 控制台 IME 线程判定
imm32 的 ImmGetSaveContext(hWnd=NULL) 正是靠 THREADSTATE_DEFAULTINPUTCONTEXT 取默认
IMC(imm.c L1038);ImmGetDefaultIMEWnd 靠 THREADSTATE_DEFAULTIMEWINDOW(imm.c L893)。
5. IME 热键管理
IME 热键管理是 ime.c 前半部分(L45-513)的主题,包含约 15 个函数:从"热键 ID 的语言
语义"到"链表维护"再到"三个系统调用"。
5.1 IntGetImeHotKeyLanguageScore(L54)--- 语言匹配打分
c
UINT FASTCALL IntGetImeHotKeyLanguageScore(HKL hKL, LANGID HotKeyLangId)
热键 ID(如 IME_CHOTKEY_IME_NONIME_TOGGLE=0x10)带有语言属性:0x10/0x20 简体中文、
0x30/0x40 日文、0x50/0x60 韩文、0x70/0x80 繁体中文(IntGetImeHotKeyLangId L102 用
静态表 s_array[(dwHotKeyId & 0xF0) >> 4] 映射)。当用户按下一个键时,可能命中多个
"同键不同语言"的热键(例如 Ctrl+Space 在中文与日文布局下都注册过),需要一个打分机制
决定哪个优先:
- 热键语言为中性(
LANGID_NEUTRAL)或与当前hKL低字相同 → 3 分(最高); - 与当前线程
NtCurrentTeb()->CurrentLocale的语言相同 → 2 分(SEH 保护读 TEB); - 与系统默认 LCID(
glcidSystem,惰性ZwQueryDefaultLocale(FALSE,...))相同 → 1 分; - 否则 0 分。
glcidSystem 的惰性填充(L78-79)是本文件唯一的"系统级惰性初始化"模式。
5.2 IntGetActiveKeyboardLayout(L87)
优先取前台队列活动窗口所属线程的布局:gpqForeground->spwndActive->head.pti->KeyboardLayout->hkl;
前台不可用时回退 UserGetKeyboardLayout(0)(当前线程,kbdlayout.c L995)。它是
IntGetImeHotKeyByKey 判断"当前是韩文布局吗"的依据。
5.3 链表维护:IntAddImeHotKey / IntGetImeHotKeyById / IntDeleteImeHotKey(L128/151/200)
纯单向链表操作:IntAddImeHotKey 尾插;IntGetImeHotKeyById 按 ID 线性查找;
IntDeleteImeHotKey 摘除并 ExFreePoolWithTag(..., USERTAG_IMEHOTKEY)。USERTAG_IMEHOTKEY
是专用池标签,便于泄漏排查。
5.4 IntGetImeHotKeyByKeyAndLang(L165)--- 按"键+语言"查重
用于 IntSetImeHotKey 添加前的查重:虚键相同、语言相同(IntGetImeHotKeyLangId 结果
等于目标语言或为 0),修饰键完全一致(MOD_IGNORE_ALL_MODIFIER 特判,或
(uModifiers & MOD_KEYS) == uModKeys 且左右位匹配)。找到即返回------添加时直接覆盖该节点
的修饰键/虚键/布局(见 5.6)。
5.5 IntGetImeHotKeyByKey(L225)--- 按键匹配 + 语言打分择优
运行时匹配:遍历全链表,虚键相同且修饰键匹配(同样处理
MOD_IGNORE_ALL_MODIFIER 与左右位),然后用 IntGetImeHotKeyLanguageScore 打分:
- 3 分(当前布局语言)直接返回;
- 韩文布局(
fKorean)下跳过 0 分项------韩文 IME 不允许注册跨语言热键(L267-268); - 0 分且是
IME_CHOTKEY_IME_NONIME_TOGGLE/IME_THOTKEY_IME_NONIME_TOGGLE(中/英切换)
时,若LOWORD(pti->hklPrev)与热键语言一致也直接返回(记忆"上次布局"用于中英切换恢复); - 否则记录最高分节点,循环结束后返回。
5.6 IntCheckImeHotKey(L290)--- 队列键状态 + KeyUp 语义
c
static PIMEHOTKEY IntCheckImeHotKey(const USER_MESSAGE_QUEUE *MessageQueue,
UINT uVirtualKey, LPARAM lParam)
- 从
MessageQueue->afKeyState读取左右 Shift/Ctrl/Alt 的 down 状态,合成
uModifiers(带MOD_LEFT/MOD_RIGHT左右位); - 处理
MOD_ON_KEYUP(抬键触发)热键:静态变量s_uKeyUpVKey记录"按下时匹配
的键",抬键时同一虚键才命中(L300-311)------保证"先按下后抬起"的完整手势; - 调
IntGetImeHotKeyByKey得到节点;KeyDown 且节点要求 OnKeyUp → 记住虚键等待抬起;
KeyUp 且节点要求 OnKeyUp → 命中。
5.7 IntFreeImeHotKeys(L344)
遍历链表释放全部节点、gpImeHotKeyList = NULL。调用点:IntSetImeHotKey 的
SETIMEHOTKEY_INITIALIZE 动作(L419)与 main.c L183 (win32k 清理阶段的
IntFreeImeHotKeys())。
5.8 IntSetImeHotKey(L356)--- 增删改查的总闸
dwAction 三种:
SETIMEHOTKEY_DELETE:按 ID 找,找不到报 ERR 返回 FALSE;找到则IntDeleteImeHotKey;SETIMEHOTKEY_ADD:拒绝VK_PACKET(LOWORD 判断);拒绝韩文热键(LangId == LANGID_KOREAN
→ FALSE,韩文不能注册);先按"键+语言"查重、再按 ID 查重,命中则原地更新
uModifiers/uVirtualKey/hKL;否则ExAllocatePoolWithTag(PagedPool, sizeof(IMEHOTKEY), USERTAG_IMEHOTKEY)
分配并IntAddImeHotKey;SETIMEHOTKEY_INITIALIZE:清空链表。
5.9 三个系统调用:Get / Set / CheckImeHotKey(L428/476/492)
c
BOOL NTAPI NtUserGetImeHotKey(DWORD dwHotKeyId, PUINT lpuModifiers, PUINT lpuVirtualKey, LPHKL lphKL);
BOOL NTAPI NtUserSetImeHotKey(DWORD dwHotKeyId, UINT uModifiers, UINT uVirtualKey, HKL hKL, DWORD dwAction);
DWORD NTAPI NtUserCheckImeHotKey(UINT uVirtualKey, LPARAM lParam);
NtUserGetImeHotKey:UserEnterExclusive;对三个输出指针逐一ProbeForWrite;按 ID
查表后把字段拷回用户态(写入段再包一层 SEH,异常则视为失败)。NtUserSetImeHotKey:UserEnterExclusive转IntSetImeHotKey。NtUserCheckImeHotKey:UserEnterExclusive;要求前台队列存在且IS_IMM_MODE()
(gpsi->dwSRVIFlags & SRVINFO_IMM32);IntCheckImeHotKey(gpqForeground, ...)命中
返回dwHotKeyId,未命中返回INVALID_HOTKEY。imm32 的ImmGetImeHotKey/
ImmSetImeHotKey/ImmCheckImeHotKey直接透传这三个调用(keymsg.c L656 等)。
6. 按键拦截入口 IntImmProcessKey
IntImmProcessKey(ime.c L602-729)是整个 IME 管线在 win32k 侧的唯一入口,被
msgqueue.c 的 co_IntProcessKeyboardMessage 在 WH_KEYBOARD 钩子之后调用(详见第 21 节)。
其逻辑分五段:
6.1 消息类型过滤(L620-630)
只处理 WM_KEYDOWN / WM_KEYUP / WM_SYSKEYDOWN / WM_SYSKEYUP 四类键消息,其余直接
返回 0(不干预)。
6.2 取布局与热键匹配(L632-649)
c
pKL = pWnd->head.pti->KeyboardLayout;
if (!pKL) return 0; // 线程无布局,无从谈起
uVirtualKey = LOBYTE(wParam); // 虚键
pImeHotKey = IntCheckImeHotKey(MessageQueue, uVirtualKey, lParam); // 热键匹配
命中热键则取 dwHotKeyId 与目标 hKL,否则 dwHotKeyId = INVALID_HOTKEY。
6.3 语言切换(DSWITCH)热键分支(L651-664)
c
if (IME_HOTKEY_DSWITCH_FIRST <= dwHotKeyId && dwHotKeyId <= IME_HOTKEY_DSWITCH_LAST)
{
if (pKL->hkl != hKL)
UserPostMessage(hWnd, WM_INPUTLANGCHANGEREQUEST,
((pKL->dwFontSigs & gSystemFS) ? INPUTLANGCHANGE_SYSCHARSET : 0),
(LPARAM)hKL);
if (IntGetImeCompatFlags(pWnd->head.pti) & 0x800000)
return 0; // 兼容标志:语言切换热键交给应用自行处理
return IPHK_HOTKEY; // 通知 msgqueue:此键已被 IME 层消费
}
IME_HOTKEY_DSWITCH_*(0x100 起)是"直接切换语言"类热键:内核发
WM_INPUTLANGCHANGEREQUEST 给目标窗口请求切换布局(dwFontSigs & gSystemFS 判断系统
字符集是否需要 INPUTLANGCHANGE_SYSCHARSET 标志),并返回 IPHK_HOTKEY 吞掉原键。
兼容位 0x800000 在 IntGetImeCompatFlags(L45,读 pti->ppi->dwImeCompatFlags)
中出现两次(此处与 6.5),作用是让老应用自行处理这类热键。
6.4 非 IMM 模式短路(L666-672)
!IS_IMM_MODE() 时警告并返回 0------没有 imm32 的纯键盘系统里 IME 不存在。
6.5 普通键:决定"要不要交给 IME"(L674-718)
无热键(dwHotKeyId == INVALID_HOTKEY)时,进入"普通键过滤":
c
if (!pKL->piiex) return 0; // 非 IME 布局(无 IME 信息扩展)
if (pWnd->hImc) pIMC = UserGetObject(gHandleTable, pWnd->hImc, TYPE_INPUTCONTEXT);
if (!pIMC) return 0; // 无输入上下文 → 不处理
随后三组过滤:
- Up 键 (
HIWORD(lParam) & KF_UP):若IME_PROP_IGNORE_UPKEYS(IME 声称忽略
抬键)→ 返回 0; - DBE 键放行 (L690-700):
VK_DBE_CODEINPUT/ENTERIMECONFIGMODE/ENTERWORDREGISTERMODE/ HIRAGANA/KATAKANA/NOCODEINPUT/NOROMAN/ROMAN直接进入处理(这些是 IME 专用控制键); - SysKey 与 Alt (L704-714):
WM_SYSKEYDOWN/UP只放行VK_MENU与VK_F10;
若 IME 不 要求 Alt 键(无IME_PROP_NEED_ALTKEY),则VK_MENU或带
KF_ALTDOWN的键一律不放行(Alt 组合留给系统菜单)。
6.6 VK_PACKET 宽虚键合成(L720-721)
c
if (LOBYTE(uVirtualKey) == VK_PACKET)
uVirtualKey = MAKELONG(wParam, GetW32ThreadInfo()->wchInjected);
VK_PACKET(0xE7)是"注入任意字符"用的虚键,其高字携带 Unicode 字符
(wchInjected)。这里合成宽虚键传给 IME,imm32 侧再用
IME_PROP_ACCEPT_WIDE_VKEY 决定取低字还是全字(keymsg.c L764-778)。
6.7 进入用户态并归一化返回(L723-728)
c
ret = co_IntImmProcessKey(hWnd, pKL->hkl, uVirtualKey, lParam, dwHotKeyId);
if (IntGetImeCompatFlags(pWnd->head.pti) & 0x800000)
ret &= ~IPHK_HOTKEY; // 兼容位下不吞键
return ret;
返回值是 IPHK_* 标志组合,语义由调用方 msgqueue.c 解释(第 21 节)。
7. 用户态回调 co_IntImmProcessKey
co_IntImmProcessKey(callback.c L1250-1272)是 win32k → user32/imm32 的桥:
c
DWORD APIENTRY co_IntImmProcessKey(HWND hWnd, HKL hKL, UINT vKey,
LPARAM lParam, DWORD dwHotKeyID)
{
IMMPROCESSKEY_CALLBACK_ARGUMENTS Common = { hWnd, hKL, vKey, lParam, dwHotKeyID };
UserLeaveCo(); // 回调期间释放 CO 引用锁(防死锁)
Status = KeUserModeCallback(USER32_CALLBACK_IMMPROCESSKEY,
&Common, sizeof(Common), &ResultPointer, &ResultLength);
UserEnterCo();
if (NT_SUCCESS(Status)) ret = *(LPDWORD)ResultPointer; // 返回 IPHK_* 标志
return ret;
}
回调参数包 IMMPROCESSKEY_CALLBACK_ARGUMENTS(callback.h L167):hWnd / hKL / vKey / lParam / dwHotKeyID,正好与 ImmProcessKey 签名一一对应。回调序号 18 在 u32cb.h L27
注册为 User32CallImmProcessKeyFromKernel。
user32 侧(dllmain.c L702-713):
c
NTSTATUS WINAPI User32CallImmProcessKeyFromKernel(PVOID Arguments, ULONG ArgumentLength)
{
PIMMPROCESSKEY_CALLBACK_ARGUMENTS Common = Arguments;
DWORD Result = IMM_FN(ImmProcessKey)(Common->hWnd, Common->hKL, Common->vKey,
Common->lParam, Common->dwHotKeyID);
return ZwCallbackReturn(&Result, sizeof(DWORD), STATUS_SUCCESS);
}
IMM_FN 是 imm32 的导出函数指针表------即按键路径最终落到 imm32.dll 的 ImmProcessKey
(keymsg.c L740),其内部(见第 21 节流程图):
ImmGetContext(hWnd)取 IMC(imm.c L1066 →ImmGetSaveContext);ImmLockImeDpi(hKL)取 IME 的导出表(IMEDPI:ImeProcessKey、ImeEscape...);ImmLockIMC取INPUTCONTEXTDX(含fOpen、bNeedsTrans、nVKey);VK_PACKET宽虚键特判(非 Unicode IME 且 IME 打开 →IPHK_SKIPTHISKEY);GetKeyboardState+ImeProcessKey(hIMC, vk, lParam, KeyState):IME 本体决定
"收下此键"则置pIC->bNeedsTrans = TRUE; pIC->nVKey = vKey; ret |= IPHK_PROCESSBYIME;- 热键动作(非 Cicero 时直接跑
Imm32ProcessHotKey;Cicero 时先试
CtfImeProcessCicHotkey);处理过 →ret |= IPHK_HOTKEY(VK_KANJI+
IME_JHOTKEY_CLOSE_OPEN除外); - 兼容标志
_IME_APP_COMPAT_PROCESS_BY_IME:韩文以外生成
ImmTranslateMessage(hWnd, WM_KEYDOWN, VK_PROCESSKEY, lParam)并改为
ret = (ret & ~IPHK_PROCESSBYIME) | IPHK_SKIPTHISKEY------即把"IME 收下的键"变成
"VK_PROCESSKEY 键消息"投给应用(应用可借此得知键被 IME 消费)。
Imm32ProcessHotKey(keymsg.c L252-307)按热键 ID 分发到具体动作:中/英切换
Imm32CImeNonImeToggle、全半角 Imm32CShapeToggle、符号 Imm32CSymbolToggle、
日文开闭 Imm32JCloseOpen、韩文形状/汉字转换/英文 Imm32KShapeToggle/
Imm32KHanjaConvert/Imm32KEnglish;私有热键(IME_HOTKEY_PRIVATE_FIRST..LAST)走
pImeDpi->ImeEscape(hIMC, IME_ESC_PRIVATE_HOTKEY, &dwHotKeyID) 交给 IME 本体。
8. 键盘布局切换 NtUserSetThreadLayoutHandles
NtUserSetThreadLayoutHandles(ime.c L535-564)不是严格意义上的"IME 函数",但它维护
hklPrev 与线程布局指针,是热键语言记忆(5.5 节)与 IntGetActiveKeyboardLayout 的
数据基础:
c
DWORD NTAPI NtUserSetThreadLayoutHandles(HKL hNewKL, HKL hOldKL)
{
pti = GetW32ThreadInfo();
pOldKL = pti->KeyboardLayout;
if (pOldKL && pOldKL->hkl != hOldKL) goto Quit; // 旧布局不匹配 → 拒绝
pNewKL = UserHklToKbl(hNewKL);
if (!pNewKL) goto Quit;
if (IS_IME_HKL(hNewKL) != IS_IME_HKL(hOldKL))
pti->hklPrev = hOldKL; // IME↔非 IME 切换,记下旧布局
UserAssignmentLock((PVOID*)&pti->KeyboardLayout, pNewKL); // 原子换指针
pti->pClientInfo->hKL = pNewKL->hkl; // 同步用户态共享信息
return 0;
}
要点:只在 IME/非 IME 性质发生变化时更新 hklPrev (IS_IME_HKL 判定高字语言
是否 CJK)。UserAssignmentLock 是 win32k 的指针原子替换工具(带引用计数管理),
pClientInfo->hKL 是 USER 共享内存里用户态可见的当前布局。
9. HIMC 列表 NtUserBuildHimcList
9.1 UserBuildHimcList(L566-600)--- 内核侧枚举
c
DWORD FASTCALL UserBuildHimcList(PTHREADINFO pti, DWORD dwCount, HIMC *phList)
{
if (pti) // 指定线程:遍历该线程 spDefaultImc 链表
for (pIMC = pti->spDefaultImc; pIMC; pIMC = pIMC->pImcNext) { ... }
else // pti == NULL:遍历整个进程的所有线程
for (pti = gptiCurrent->ppi->ptiList; pti; pti = pti->ptiSibling)
for (pIMC = pti->spDefaultImc; pIMC; pIMC = pIMC->pImcNext) { ... }
return dwRealCount; // 真实数量(phList 最多填 dwCount 个)
}
注意 pti==NULL 的语义是"当前进程的全部线程 "(从 gptiCurrent->ppi->ptiList
兄弟链出发),而"指定线程"则只走单链表。这是 imm32 ImmEnumInputContext 枚举
IMC 的内核支撑。
9.2 NtUserBuildHimcList(L731-788)--- 系统调用
c
NTSTATUS NTAPI NtUserBuildHimcList(DWORD dwThreadId, DWORD dwCount,
HIMC *phList, PDWORD pdwCount)
UserEnterExclusive;非IS_IMM_MODE()→ERROR_CALL_NOT_IMPLEMENTED;dwThreadId解析:0→ 当前线程;INVALID_THREAD_ID (0xFFFFFFFF)→ NULL(进程级);
其他 →IntTID2PTI(必须pti->rpdesk非空);ProbeForWrite校验两个输出缓冲后调UserBuildHimcList;dwCount < dwRealCount
→ 返回STATUS_BUFFER_TOO_SMALL(真实数量已写回*pdwCount),否则STATUS_SUCCESS。
imm32 侧 Imm32BuildHimcList(utils.c L338-375)用倍增重试 消费这个协议:先按
INITIAL_COUNT (0x40) 个 HIMC 分配,调用 NtUserBuildHimcList,遇
STATUS_BUFFER_TOO_SMALL 就按返回数量重新分配,最多重试 10 次(MAX_RETRY)。
ImmEnumInputContext(imm.c L1123)随后遍历这份列表逐个调用回调。
10. IME 状态通知 NtUserNotifyIMEStatus
10.1 UserSetImeConversionKeyState(L790-846)--- 转换模式 → 键状态映射
把"IME 转换模式"翻译成键盘状态(pti->MessageQueue->afKeyState),使
GetKeyState(VK_DBE_*) 能反映当前输入模式。按语言分三路:
- 日文 (
LANG_JAPANESE):由IME_CMODE_NATIVE(0x1)派生平假名/片假名
(KATAKANA0x2)、由FULLSHAPE(0x8)派生全角/半角、由ROMAN(0x10)派生
罗马字/假名输入、由CHARCODE(0x20)派生代码输入:
c
bAlphaNumeric = !(dwConversion & IME_CMODE_NATIVE);
bKatakana = !bAlphaNumeric && (dwConversion & IME_CMODE_KATAKANA);
bHiragana = !bAlphaNumeric && !(dwConversion & IME_CMODE_KATAKANA);
SET_KEY_DOWN(KeyState, VK_DBE_ALPHANUMERIC, bAlphaNumeric);
SET_KEY_LOCKED(KeyState, VK_DBE_HIRAGANA, bHiragana);
SET_KEY_LOCKED(KeyState, VK_DBE_KATAKANA, bKatakana);
SET_KEY_LOCKED(KeyState, VK_DBE_DBCSCHAR, bFullShape); // 全角
SET_KEY_LOCKED(KeyState, VK_DBE_SBCSCHAR, !bFullShape); // 半角
SET_KEY_LOCKED(KeyState, VK_DBE_ROMAN, bRoman);
SET_KEY_LOCKED(KeyState, VK_DBE_CODEINPUT, bCharCode);
- 韩文 (
LANG_KOREAN):VK_HANGUL←IME_CMODE_NATIVE(谚文开/关)、
VK_JUNJA←FULLSHAPE、VK_HANJA←IME_CMODE_HANJACONVERT(0x40,汉字转换)。 - 其他语言:不处理。
SET_KEY_DOWN/SET_KEY_LOCKED 是 2 位/键的键状态宏(input.h:KS_DOWN_BIT 0x80 /
KS_LOCK_BIT 0x01),与 NtUserGetKeyState 的返回格式一致------因此应用调用
GetKeyState(VK_HANGUL) 就能画出"谚文/英文"状态灯。
10.2 NtUserNotifyIMEStatus(L848-901)
c
DWORD NTAPI NtUserNotifyIMEStatus(HWND hwnd, BOOL fOpen, DWORD dwConversion)
imm32 在 IME 打开状态/转换模式变化后调用(imm.c L1247、ime.c L1053/L1582、
keymsg.c L363 等),通知 win32k 缓存状态并广播:
- 非
IS_IMM_MODE()→ ERR 退出; ValidateHwndNoErr(hwnd)取窗口;pti必须等于前台线程或其队列与前台队列相同
(只关心前台线程的 IME 状态);- 去重 :
ghIMC == pwnd->hImc && gfImeOpen == !!fOpen && gdwImeConversion == dwConversion
直接返回------ghIMC/gfImeOpen/gdwImeConversion这三个全局就是状态缓存; - 更新缓存并调
UserSetImeConversionKeyState(pti, fOpen ? dwConversion : IME_CMODE_ALPHANUMERIC)
------IME 关闭时把键状态映射为"字母数字模式" (IME_CMODE_ALPHANUMERIC == 0); - 若装了
WH_SHELL钩子,调co_HOOK_CallHooks(WH_SHELL, HSHELL_LANGUAGE, (WPARAM)hwnd, (LPARAM)hKL)
------通知 shell(如 explorer 的语言栏)语言发生变化; - 末尾
// TODO:标注尚未完成的后续动作。
11. 线程/进程 IME 禁用与 AppImeLevel
11.1 NtUserDisableThreadIme(L903-969)--- 禁用指定线程/整个进程的 IME
c
BOOL NTAPI NtUserDisableThreadIme(DWORD dwThreadID)
dwThreadID == INVALID_THREAD_ID:进程级禁用 ------置ppi->W32PF_flags |= W32PF_DISABLEIME,
然后遍历ppi->ptiList兄弟链给每个线程置TIF_DISABLEIME并销毁其
spwndDefaultIme;销毁会改动ptiList,所以用goto Retry重新遍历(L927-938
注释明确说明)。- 其他值:
0→ 当前线程;否则IntTID2PTI查,必须与当前线程同进程
(pti->ppi != ptiCurrent->ppi拒绝)。只置TIF_DISABLEIME+ 销毁默认 IME 窗口。
TIF_DISABLEIME 是 IME 管线的总开关:IntImmProcessKey 的调用点
(msgqueue.c L1888 的 !(pti->TIF_flags & TIF_DISABLEIME) 条件)、
IntWantImeWindow(L2039)、UserCreateInputContext(L1665)都检查它。
11.2 NtUserGetAppImeLevel / NtUserSetAppImeLevel(L971-1002 / L1134-1167)
"Application IME Level"是韩国 IME 专有概念,源码注释(L1120-1133)给出等级表:
| 等级 | 含义 |
|---|---|
| 1 | 无 IME 支持,忽略所有 IME 专用消息 |
| 2 | 部分支持:组合/候选窗口位置、输入模式与状态 |
| 3 | 完整 IME 支持 |
| 4/5 | (未知) |
实现上,等级存为窗口属性(UserSetProp/UserGetProp(pWnd, AtomImeLevel, ...)),
仅同进程 窗口可读写(pWnd->head.pti->ppi == pti->ppi 校验)。imm32 侧
ImmGetAppImeLevel/ImmSetAppImeLevel(win3send.c L330/L337)调用它们;韩文
IME 组合行为(keymsg.c L925/L978 的 NtUserGetAppImeLevel(...) == 3)依赖该值。
12. IMEINFOEX 查询与设置
IMEINFOEX 是"某个键盘布局的 IME 完整描述"(3.6 节)。内核侧有两对函数:
UserGetImeInfoEx/UserSetImeInfoEx(内核内实现)与 NtUserGetImeInfoEx/
NtUserSetImeInfoEx(系统调用)。
12.1 UserGetImeInfoEx(L1004-1065)--- 在布局环链中查 IME 信息
c
BOOL FASTCALL UserGetImeInfoEx(PWINSTATION_OBJECT pWinSta, PIMEINFOEX pInfoEx,
IMEINFOEXCLASS SearchType)
- 前置条件:
pWinSta与全局gspklBaseLayout都非空(gspklBaseLayout是窗口站
键盘布局环链首节点,kbdlayout.c L22 定义,注释FIXME: Please move this to pWinSta->spklList); SearchType == ImeInfoExKeyboardLayout:沿pkl->pklNext环链找hkl相等的 KL,
取*pInfoEx = *pkl->piiex整块拷贝;SearchType == ImeInfoExImeFileName:按wszImeFile文件名比较
(_wcsnicmp,忽略大小写,比较长度RTL_NUMBER_OF(wszImeFile));- 找不到打印 ERR("HKL not found"/"wszImeFile not found")。
12.2 NtUserGetImeInfoEx(L1067-1118)
UserEnterShared(读操作);ProbeForRead 拷入栈上 IMEINFOEX ImeInfoEx;
IntGetProcessWindowStation(NULL) 取进程窗口站;调 UserGetImeInfoEx;
成功后 ProbeForWrite 拷回。imm32 的 ImmGetImeInfoEx 用它查询布局的
IMEINFOEX(如 wszImeFile 判断是否为 IME 布局)。
12.3 UserSetImeInfoEx(L1169-1206)--- 更新布局的 IME 信息
按 hkl 在环链上定位 KL;fLoadFlag 为真(已加载)时不覆盖
(if (!pkl->piiex->fLoadFlag) *pkl->piiex = *pImeInfoEx;)------防止布局已加载后
被外部改写关键信息。返回 TRUE 表示更新完成。
12.4 NtUserSetImeInfoEx(L1208-1242)
UserEnterExclusive + ProbeForRead + 转 UserSetImeInfoEx。imm32 的
ImmSetImeInfoEx(IME DLL 通过 ImeInquire 注册自身信息)调用它------IME 加载时
把 IMEINFOEX(含 fdwProperty、wszImeFile、wszUIClass)写回 win32k,
供 IntImmProcessKey 读取 IME_PROP_* 决策。
13. IME 窗口的所有者与 Z 序管理
IME 窗口必须跟随目标窗口的可见性/置顶状态变化(否则候选窗口会被普通窗口盖住,或
IME 窗口抢了目标窗口的 Z 序位置)。ime.c L1245-1522 的五个函数专门处理"IME 窗口的
owner 选择与置顶调整"。
13.1 IntImeSetFutureOwner(L1245-1308)--- 挑选首选 owner
为 IME 窗口选择"未来的 owner":
- 非法 owner 过滤 :
!pwndOwner || (pwndOwner->style & WS_CHILD)直接返回
(子窗口不能作 owner); - 沿 owner 链上溯到同线程顶层 :
for (pwndNode = pwndOwner; ; pwndNode = pwndNextOwner)
沿spwndOwner走到"非同线程 owner"为止------顶层 owner 链上第一个同线程窗口; - 避开 IME-like 与 bottom-most :
IS_WND_IMELIKE(pwndNode)或
(节点是WNDS2_BOTTOMMOST而目标不是)时退回pwndOwner(SEH 保护读
state2,跨进程窗口对象访问可能异常); - 兄弟窗口择优 (L1278-1305):若节点有父窗口且目标就是节点本身,则在父窗口的
子链(spwndChild/spwndNext)中找"同线程、非菜单(IS_WND_MENU)、非 IME-like、
非销毁中(WNDS2_INDESTROY)、非 WS_CHILD、无跨线程 owner"的窗口替代; WndSetOwner(pImeWnd, pwndNode)定案。
13.2 IntGetLastTopMostWindowNoIME(L1311-1345)
从桌面(UserGetDesktopWindow())的子链中,沿 WS_EX_TOPMOST 段找最后一个
非 IME-like 的置顶窗口 。IME-like 窗口若其 owner 链可达 pImeWnd 则跳过
(它是"自己人",不计入插入基准)。返回值作为 IntImeSetTopMost 非置顶时的
插入基准点。
13.3 IntImeSetTopMost(L1348-1434)--- 重排 IME 窗口的 Z 序
c
static VOID FASTCALL IntImeSetTopMost(PWND pImeWnd, BOOL bTopMost, PWND pwndInsertBefore)
bTopMost == FALSE时先算插入点:IntGetLastTopMostWindowNoIME得到基准;
若给了pwndInsertBefore则沿链找到它;若 IME 窗口的 owner 是 bottom-most
(WNDS2_BOTTOMMOST),插入点回退到 owner 之前最后一个非 IME-like 窗口------
保证 IME 窗口夹在"普通窗口"与"owner"之间;- 遍历父窗口子链,把同线程 的 IME-like 窗口(owner 链可达
pImeWnd)逐个
IntUnlinkWindow摘除,按bTopMost设置/清除WS_EX_TOPMOST,再
IntLinkHwnd重新插入到插入点之后(HWND_TOP或指定窗口后)------注意
pwndInsertAfterSave快照避免重复遍历(L1429-1432)。
一句话:IME 窗口总是贴着 owner 窗口走------owner 置顶它置顶,owner 沉底它沉底。
13.4 IntImeCheckTopmost(L1437-1451)
bTopMost = !!(pwndOwner->ExStyle & WS_EX_TOPMOST);IME 窗口线程不是前台线程时,
pwndInsertBefore = pwndOwner(把 IME 窗口排到 owner 之前)。然后调
IntImeSetTopMost。
13.5 NtUserSetImeOwnerWindow(L1453-1522)--- 系统调用
imm32 的 ImmSetImeWindow/ImmAssociateIME 场景调用(设置 IME 窗口与目标窗口的
owner 关系):
hImeWnd必须是FNID_IME窗口(否则 ERR "Not IME window");- 有焦点窗口(
hwndFocus非空):- 焦点窗口不能是 IME-like;
IntGetTopLevelWindow(pwndFocus)取顶层,且顶层窗口的 owner 链上不得有
IME 类窗口(atomClassName == gpsi->atomSysClass[ICLS_IME],L1487);WndSetOwner(pImeWnd, pwndTopLevel)+IntImeCheckTopmost;
- 无焦点窗口:
- 线程活动窗口仍是当前 owner → 不动;
- 活动窗口同线程且非 IME-like →
WndSetOwner(pImeWnd, pwndActive); - 否则
IntImeSetFutureOwner(pImeWnd, pImeWnd->spwndOwner)走"择优"路径; - 最后
IntImeCheckTopmost。
14. 输入上下文(IMC)对象生命周期
IMC 是 win32k 的 USER 对象之一(TYPE_INPUTCONTEXT),走标准对象管理:
UserCreateObject / UserDeleteObject / UserHMGetHandle,外加 IMC 特有的
"默认 IMC 链表"与"窗口 hImc 关联"两条边。本节的六个函数(L1524-1734)构成完整
生命周期。
14.1 AllocInputContextObject(L1524-1553)--- 对象体分配
c
PVOID AllocInputContextObject(PDESKTOP pDesk, PTHREADINFO pti, SIZE_T Size, PVOID* HandleOwner)
- 桌面堆分配(
DesktopHeapAlloc,无桌面则用pti->rpdesk)并清零; - 初始化
THRDESKHEAD:pSelf = ObjHead、rpdesk、pti; IntReferenceThreadInfo(pti)(线程引用 +1,防止对象存活期间线程被清理);*HandleOwner = pti(句柄归属线程,跨线程访问时校验)、ppi->UserHandleCount++。
这是 IMC 对象的通用构造骨架------注意它不创建 USER 句柄,句柄由
UserCreateObject 统一分配。
14.2 UserCreateInputContext(L1658-1703)--- 创建并挂链表
c
PIMC FASTCALL UserCreateInputContext(ULONG_PTR dwClientImcData)
- 前置检查:
!IS_IMM_MODE() || (pti->TIF_flags & TIF_DISABLEIME)→ NULL(IME 被禁用);
!pdesk→ NULL;dwClientImcData 非零但线程还没有默认 IMC→ NULL
("非首次创建"必须已有默认 IMC); UserCreateObject(gHandleTable, pdesk, pti, NULL, TYPE_INPUTCONTEXT, sizeof(IMC))
创建对象(返回时已带 2 个引用);UserDereferenceObject(pIMC)释放多余引用,断言cLockObj == 1;- 链表挂接:
dwClientImcData非零(非首次)→ 插到spDefaultImc之后的第二个位置
(pIMC->pImcNext = pti->spDefaultImc->pImcNext; spDefaultImc->pImcNext = pIMC;);- 首次 →
UserAssignmentLock设为pti->spDefaultImc(默认 IMC),
断言cLockObj == 2------多出的引用在ExitThreadCallback时
UserAssignmentUnlock释放(注释 L1698 明示);
- 回填
pIMC->dwClientImcData = dwClientImcData。
"非首次"分支对应 imm32 的 ImmCreateContext(imm.c L623
NtUserCreateInputContext((ULONG_PTR)pClientImc)):每次 ImmCreateContext 都会
新增一个非默认 IMC,dwClientImcData 指向用户态 CLIENTIMC。
14.3 NtUserCreateInputContext(L1705-1734)--- 系统调用
要求 dwClientImcData 非零 (ERROR_INVALID_PARAMETER)------从系统调用层面杜绝
"裸创建无客户端数据的 IMC";成功后返回 UserHMGetHandle(pIMC)。
14.4 UserFreeInputContext(L1555-1581)--- 对象体释放
c
VOID UserFreeInputContext(PVOID Object)
- 先把自身从线程 IMC 链表中摘除:遍历
pti->spDefaultImc链找
pNode->pImcNext == pIMC,改链(注释 L1566:除 spDefaultImc 首节点外 ------
默认 IMC 的摘除走UserAssignmentLock清理,不在对象释放时处理); DesktopHeapFree、UserHandleCount--、IntDereferenceThreadInfo(与
AllocInputContextObject对称)。
14.5 UserDestroyInputContext(L1583-1593)--- 销毁钩子
c
BOOLEAN UserDestroyInputContext(PVOID Object)
{
UserMarkObjectDestroy(pIMC); // 打"销毁中"标记
UserDeleteObject(UserHMGetHandle(pIMC), TYPE_INPUTCONTEXT); // 从句柄表删除
return TRUE;
}
这是 USER 对象管理器在句柄引用归零时调用的析构例程(类似 window/desktop 的
UserDestroy* 模式)。
14.6 IntDestroyInputContext + NtUserDestroyInputContext(L1595-1656)
c
static BOOL IntDestroyInputContext(PIMC pIMC)
BOOL NTAPI NtUserDestroyInputContext(HIMC hIMC)
ImmDestroyContext(imm.c L747)的落点。IntDestroyInputContext 的校验与动作:
- 属主校验 :
pIMC->head.pti != gptiCurrent→ERROR_ACCESS_DENIED
(只能销毁本线程的 IMC); - 默认 IMC 保护 :
pIMC == pti->spDefaultImc→ERROR_INVALID_PARAMETER
(默认 IMC 不可销毁); - 解除窗口关联 :
IntBuildHwndList(rpdesk 桌面子链, IACE_LIST|IACE_CHILDREN, pti)
枚举桌面上本线程的所有窗口,凡pWnd->hImc == hIMC者一律
IntAssociateInputContext(pWnd, pti->spDefaultImc)重新关联到默认 IMC------
防止销毁后窗口悬空指向已失效句柄; UserDeleteObject(hIMC, TYPE_INPUTCONTEXT)删除句柄(引用归零后触发
UserDestroyInputContext→UserFreeInputContext)。
15. 输入上下文关联 NtUserAssociateInputContext
15.1 IntAssociateInputContext(L527-533)--- 单窗口原子换绑
c
static HIMC FASTCALL IntAssociateInputContext(PWND pWnd, PIMC pImc)
{
HIMC hOldImc = pWnd->hImc;
pWnd->hImc = (pImc ? UserHMGetHandle(pImc) : NULL);
return hOldImc;
}
仅更新 WND.hImc(允许解绑:pImc == NULL 时置空),返回旧句柄。
15.2 IntAssociateInputContextEx(L1736-1800)--- 递归关联
c
static DWORD FASTCALL IntAssociateInputContextEx(PWND pWnd, PIMC pIMC, DWORD dwFlags)
dwFlags 来自 imm32 的 ImmAssociateContextEx:IACE_DEFAULT / IACE_CHILDREN /
IACE_IGNORENOCONTEXT(定义于 imm.h)。返回值约定:0 = 成功;1 = 焦点窗口被改绑
(imm32 据此刷新 IME 激活状态);2 = 失败。
处理逻辑:
IACE_DEFAULT→ 用线程默认 IMC(pti->spDefaultImc)替代传入的 pIMC;- 跨线程拒绝 :
pIMC && pti != pIMC->head.pti→ 返回 2; - 跨进程/跨桌面拒绝 :
pWnd->head.pti->ppi != GetW32ThreadInfo()->ppi || (pIMC && pIMC->head.rpdesk != pWnd->head.rpdesk)→ 返回 2; IACE_CHILDREN且窗口有子窗口:IntBuildHwndList(pWnd->spwndChild, IACE_CHILDREN|IACE_LIST, pti)
枚举后代,逐个IntAssociateInputContext;跳过"目标 IMC 相同"或
(IACE_IGNORENOCONTEXT且目标无 IMC)的窗口;被改绑窗口恰为焦点窗口时
ret = 1;- 处理窗口本体:
!bIgnoreNullImc || pWnd->hImc条件满足才改绑(IACE_IGNORENOCONTEXT
语义:不动那些没有上下文的窗口 ,L1787);本体是焦点窗口也置ret = 1。
15.3 NtUserAssociateInputContext(L1802-1834)
ValidateHwndNoErr + UserGetObjectNoErr(TYPE_INPUTCONTEXT)(hIMC 为 NULL 合法,
表示解绑)后转 IntAssociateInputContextEx。imm32 ImmAssociateContext(imm.c L528)
与 ImmAssociateContextEx(imm.c L578)都走它。
16. 输入上下文更新与查询 NtUserQueryInputContext
16.1 UserUpdateInputContext / NtUserUpdateInputContext(L1836-1899)
c
static BOOL UserUpdateInputContext(PIMC pIMC, DWORD dwType, DWORD_PTR dwValue)
BOOL NTAPI NtUserUpdateInputContext(HIMC hIMC, DWORD dwType, DWORD_PTR dwValue)
跨进程校验(pti->ppi != ptiIMC->ppi → FALSE)后按 dwType 分支:
UIC_CLIENTIMCDATA:pIMC->dwClientImcData已设置则失败(只能设置一次 ),
否则回填------imm32ImmCreateContext后续调NtUserUpdateInputContext(hImc, UIC_CLIENTIMCDATA, (DWORD_PTR)pClientImc)(imm.c L984)把用户态 CLIENTIMC 挂上;UIC_IMEWINDOW:ValidateHwndNoErr((HWND)dwValue)校验后写pIMC->hImeWnd。
16.2 NtUserQueryInputContext(L1901-1953)--- 查询 IMC 属性
c
DWORD_PTR NTAPI NtUserQueryInputContext(HIMC hIMC, DWORD dwType)
UserEnterExclusive;UserGetObject(TYPE_INPUTCONTEXT)(失败即 0)。dwType 四种:
| dwType | 返回值 | imm32 用途示例 |
|---|---|---|
QIC_INPUTPROCESSID |
PsGetThreadProcessId(ptiIMC->pEThread) |
ImmGetProcessId(utils.c L264) |
QIC_INPUTTHREADID |
PsGetThreadId(ptiIMC->pEThread) |
ImmGetThreadId(utils.c L27)、win3send.c L281 |
QIC_DEFAULTWINDOWIME |
ptiIMC->spwndDefaultIme 句柄 |
ImmGetDefaultIMEWnd(imemenu.c L680) |
QIC_DEFAULTIMC |
ptiIMC->spDefaultImc 句柄 |
取默认 IMC |
| 其他 | FIXME 提示 + 0 |
--- |
注意 QIC_DEFAULTWINDOWIME 在 spwndDefaultIme 为 NULL 时返回 0------imm32 的
ImmGetDefaultIMEWnd(imm.c L893)据此判定"线程还没有默认 IME 窗口"并触发创建。
17. IME 窗口判定 IntWantImeWindow
IntWantImeWindow(ime.c L2032-2069)回答:"这个窗口需不需要 配一个默认 IME 窗口?"
window.c 创建窗口时用它把关(第 18 节)。判定为 FALSE 的七种情况:
c
1. gptiCurrent->TIF_flags & TIF_DISABLEIME // 线程已禁用 IME
2. IS_WND_IMELIKE(pwndTarget) // 目标本身是 IME 窗口/UI 窗口
3. pwndTarget->fnid == FNID_DESKTOP || FNID_MESSAGEWND // 桌面/消息窗口
4. pwndTarget->state & WNDS_SERVERSIDEWINDOWPROC // 服务端窗口过程(内核窗口)
5. !rpdesk // 无桌面
6. !rpwinstaParent || (rpwinstaParent->Flags & WSS_NOIO) // 窗口站禁止 I/O(如服务会话)
7. 父链中存在 rpdesk->spwndMessage // 目标是消息窗口的后代
其余情况返回 TRUE。这七个条件保证了默认 IME 窗口只出现在"普通应用窗口"上:
服务进程(Session 0 无交互)不建、内核内部窗口不建、IME 自身不建、禁用 IME 的线程不建。
18. 默认 IME 窗口创建 co_IntCreateDefaultImeWindow
co_IntCreateDefaultImeWindow(ime.c L2072-2135)为"第一个需要 IME 的窗口"创建线程
的默认 IME 窗口,被 window.c 的 co_UserCreateWindowEx 在窗口创建流程中调用(见下)。
18.1 window.c 的调用点(window.c L2440-2465)
c
/* Create the IME window for pWnd */
if (IS_IMM_MODE() && !pti->spwndDefaultIme && IntWantImeWindow(Window))
{
PWND pwndDefaultIme = co_IntCreateDefaultImeWindow(Window, Window->hModule);
UserAssignmentLock((PVOID*)&pti->spwndDefaultIme, pwndDefaultIme);
if (pwndDefaultIme)
{
co_IntSendMessage(hImeWnd, WM_IME_SYSTEM, IMS_LOADTHREADLAYOUT, 0);
if (pti->pClientInfo->CI_flags & CI_IMMACTIVATE)
{
co_IntSendMessage(hImeWnd, WM_IME_SYSTEM, IMS_ACTIVATELAYOUT, (LPARAM)hKL);
pti->pClientInfo->CI_flags &= ~CI_IMMACTIVATE;
}
}
}
触发条件三合一:IS_IMM_MODE()(系统启用 IMM)、!pti->spwndDefaultIme(线程尚未
创建过)、IntWantImeWindow(Window)(目标窗口合格)。创建成功后立即给默认 IME 窗口
发 WM_IME_SYSTEM / IMS_LOADTHREADLAYOUT(加载线程布局),若用户态留有
CI_IMMACTIVATE(等待激活)请求,则补发 IMS_ACTIVATELAYOUT 并清除标志。
18.2 函数本体(L2072-2135)
c
PWND FASTCALL co_IntCreateDefaultImeWindow(PWND pwndTarget, HINSTANCE hInst)
- 登录进程兜底 :
if (!(pti->spDefaultImc) && pid == gpidLogon) UserCreateInputContext(0);
------登录进程(winlogon)的首个窗口直接建默认 IMC(dwClientImcData=0,即"系统默认
IMC"),其他进程的默认 IMC 由 imm32 的ImmCreateContext建立; - 前置检查:无默认 IMC、目标是 IME-like、无桌面堆 → NULL;
- 跨线程子窗口特判 (L2092-2096):目标是不可见的跨线程子窗口 (父窗口属另一
进程)→ 不建(子窗口不可见时不必为它准备 IME); - 构造 CREATESTRUCTW:
c
RtlInitLargeUnicodeString(&WindowName, L"Default IME", 0);
ClassName.Buffer = UlongToPtr(gpsi->atomSysClass[ICLS_IME]); // "IME" 类原子
Cs.style = WS_POPUP | WS_DISABLED; // 弹窗 + 禁用:不抢焦点、不参与输入
Cs.hInstance = hInst;
Cs.hwndParent = UserHMGetHandle(pwndTarget); // 目标窗口是父窗口
Cs.lpszName = WindowName.Buffer;
Cs.lpszClass = ClassName.Buffer;
pImeWnd = co_UserCreateWindowEx(&Cs, &ClassName, (PLARGE_STRING)&WindowName, NULL, WINVER);
窗口名固定为 "Default IME" ,类用系统原子 ICLS_IME(用户态注册的 "IME" 类,
ImeWndProc_common 处理,见 21.4),样式 WS_POPUP|WS_DISABLED------禁用窗口不接收
键盘焦点,这正是"基础设施窗口"的定位;
- 标记扩展数据 (L2116-2131):成功后
pimeui = IntGetImeUIFromWnd(pImeWnd),
置 pimeui->fDefault = TRUE(我是默认 IME 窗口);若目标是跨线程子窗口
(IS_WND_CHILD(pwndTarget) && pwndTarget->spwndParent->head.pti != pti)再置
pimeui->fChildThreadDef = TRUE(我是"子线程的默认 IME 窗口",销毁判定用);
写用户态内存包 SEH(ProbeForWrite + 异常处理)。
创建过程中 UserRefObjectCo(pwndTarget, &Ref) / UserDerefObjectCo 保护目标窗口
引用(L2104/L2133)。
19. 默认 IME 窗口销毁判定
目标窗口销毁时(window.c IntDestroyWindow L3029-3044),若满足"系统还运行在 IMM 模式、
线程未清理、默认 IME 窗口存在且不是正在销毁的窗口、目标非 IME-like",则按目标是否为
子窗口选择判定函数,返回 TRUE 就 co_UserDestroyWindow(ti->spwndDefaultIme)。
19.1 IntFindNonImeRelatedWndOfSameThread(L1957-2029)--- 找"同线程非 IME 相关窗口"
这是两个销毁判定的公共工具:在 pwndParent 的子链中,找是否存在一个与 pwndTarget
同线程 且与 IME 无关的窗口。三阶段过滤(源码注释 L1955-2023):
- 跳过
pwndTarget本身、不同线程窗口、菜单窗口(IS_WND_MENU); - 非子窗口:若其 owner 链上任何一环是 IME-like → 跳过(它是 IME 的"服务对象");
- 子窗口:沿父链上溯,若存在同线程的 IME-like 祖先 → 跳过;上溯到非子/跨线程
节点后,再沿 owner 链检查同线程 IME-like owner,有则跳过。
能找到返回 TRUE(还有"别人"在用这套 IME 设施,默认 IME 窗口不能拆);找不到返回
FALSE(这个线程只剩目标窗口依赖默认 IME 窗口,可以拆)。
19.2 IntImeCanDestroyDefIMEforChild(L2138-2182)--- 子窗口目标
c
BOOL FASTCALL IntImeCanDestroyDefIMEforChild(PWND pImeWnd, PWND pwndTarget)
- 读
pimeui(SEH):fChildThreadDef必须为真 ,否则不拆(该 IME 窗口不是
"为子线程目标"建的,可能是跨线程主窗口的默认 IME 窗口); - 目标父窗口必须存在且与目标不同线程 (
pwndTarget->head.pti != pwndTarget->spwndParent->head.pti
否则返回 FALSE)------本函数只处理"跨线程子窗口"场景; - 沿目标父链(L2172-2179)逐层调用
IntFindNonImeRelatedWndOfSameThread(pwndNode->spwndParent, pwndTarget):
只要某一层还存在同线程非 IME 相关窗口 → 不拆(其它窗口还要用这个默认 IME 窗口)。
19.3 IntImeCanDestroyDefIME(L2185-2264)--- 顶层窗口目标
c
BOOL FASTCALL IntImeCanDestroyDefIME(PWND pImeWnd, PWND pwndTarget)
pimeui->fDestroy为真 → 不拆(用户态已请求保留/正在销毁流程);- owner 链可达性 :
pImeWnd->spwndOwner沿 owner 链必须能到达pwndTarget
(pImeWnd当前服务于 pwndTarget),否则不拆; - 目标 owner 链不得有 IME-like (否则目标本身就是 IME 相关窗口,拆了默认窗口会
影响它); - 转移 owner 并检查 :
IntImeSetFutureOwner(pImeWnd, pwndTarget)重新挑选 owner,
沿spwndNext链确认pImeWnd确实还在窗口列表里(L2250-2255,防已卸载),在则
IntImeCheckTopmost修正置顶状态; - 终局检查 :
pImeWnd->spwndOwner必须是 NULL 或pwndTarget,否则不拆; WndSetOwner(pImeWnd, NULL)解除 owner 关系,返回 TRUE(允许调用方销毁)。
销毁语义总结:默认 IME 窗口在"不再被任何同线程窗口需要"时才销毁 ------子窗口目标
看父链是否还有同线程窗口;顶层目标看 owner 转移后是否只剩自己。
20. IME 显示状态(语言栏 ShowStatus)
IME 显示状态(IMEUI.fShowStatus)决定"状态窗口/语言栏图标要不要显示"。涉及六个函数
(L2267-2556),构成一条完整通知链。
20.1 IntCheckImeShowStatus(L2267-2362)--- 清理兄弟 IME 窗口的显示状态
c
BOOL FASTCALL IntCheckImeShowStatus(PWND pwndIme, PTHREADINFO pti)
pwndIme销毁中(WNDS2_INDESTROY)→ FALSE;IntBuildHwndList(pwndIme->spwndParent->spwndChild, IACE_LIST, NULL)枚举兄弟窗口
(同一父窗口下的所有窗口);- 逐个检查:必须是 IME 类(
atomClassName == gpsi->atomSysClass[ICLS_IME])、未销毁、
(pti指定时)同线程; - 跨进程访问用户态
IMEUI要KeAttachProcess(L2311-2315 与 L2341-2342 成对,
访问完即KeDetachProcess); - 逻辑:若某兄弟 IME 窗口
fShowStatus == TRUE且其hwndIMC(拥有 IMC 的目标窗口)
仍有效 → 清除fShowStatus(FALSE),并给该目标窗口发
co_IntSendMessage(hwndIMC, WM_IME_NOTIFY, IMN_CLOSESTATUSWINDOW, 0)(线程不在
TIF_INCLEANUP时,L2345)------同一时刻只允许一个 IME 窗口显示状态 ,新窗口显示时
旧的必须关闭。
20.2 IntSendMessageToUI(L2365-2448)--- 向 IME UI 窗口发消息(带重入保护)
c
LRESULT FASTCALL IntSendMessageToUI(PTHREADINFO ptiIME, PIMEUI pimeui,
UINT uMsg, WPARAM wParam, LPARAM lParam)
ptiIME非当前线程 →KeAttachProcess(进 IME 线程的进程上下文读用户态 pimeui);- 读
pimeui->hwndUI(SEH),ValidateHwndNoErr校验; InterlockedIncrement(&pimeui->nCntInIMEProc)(SEH 写保护)------重入计数,
与 user32 的ImeWndProc_common(imm.c L1042if (pimeui->nCntInIMEProc > 0))
配合:IME 过程重入时丢弃大部分WM_IME_*消息,防止递归风暴(注释 L2403-2404
"See also ImeWndProc_common of user32");UserRefObjectCo(pwndUI)+co_IntSendMessage(...)+UserDerefObjectCo;- 收尾
InterlockedDecrement并在必要时KeDetachProcess。
20.3 IntSendOpenStatusNotify(L2451-2468)--- 打开/关闭状态通知的分发
c
VOID FASTCALL IntSendOpenStatusNotify(PTHREADINFO ptiIME, PIMEUI pimeui, PWND pWnd, BOOL bOpen)
wParam = bOpen ? IMN_OPENSTATUSWINDOW : IMN_CLOSESTATUSWINDOW;- 目标窗口是 WinNT4 及以上且有输入上下文 (
pWnd->dwExpWinVer >= WINVER_WINNT4 && pWnd->hImc)
→ 直接给目标窗口发WM_IME_NOTIFY(新式路径,通知到应用自己); - 否则 →
IntSendMessageToUI(ptiIME, pimeui, WM_IME_NOTIFY, wParam, 0)(旧式路径,
通知到 IME UI 窗口)。
20.4 IntNotifyImeShowStatus(L2471-2536)--- 核心状态更新
c
VOID FASTCALL IntNotifyImeShowStatus(PWND pImeWnd)
!IS_IMM_MODE() || (pImeWnd->state2 & WNDS2_INDESTROY)→ 返回;- 跨进程
KeAttachProcess(pti != ptiIME时)读/写pimeui; - 计算显示意愿:
bShow = (gfIMEShowStatus == TRUE) && SafeImeUI.fCtrlShowStatus;
------全局开关 ∧ 用户控制位; - 目标窗口:
pWnd = ValidateHwndNoErr(SafeImeUI.hwndIMC),无效则回退
ptiIME->MessageQueue->spwndFocus(IME 线程的焦点窗口);有效则
pimeui->fShowStatus = bShow; bSendNotify = TRUE; IntSendOpenStatusNotify(ptiIME, &SafeImeUI, pWnd, bShow)通知开关状态;- 末尾(窗口未销毁时)
IntCheckImeShowStatus(pImeWnd, NULL)清理兄弟 IME 窗口。
20.5 IntBroadcastImeShowStatusChange(L2538-2549)--- 全局广播入口
c
BOOL FASTCALL IntBroadcastImeShowStatusChange(PWND pImeWnd, BOOL bShow)
{
if (gfIMEShowStatus == bShow || !IS_IMM_MODE()) return TRUE; // 无变化
gfIMEShowStatus = bShow;
IntNotifyImeShowStatus(pImeWnd);
return TRUE;
}
调用点:simplecall.c L918(NtUserCallOneParam 的 IME 显示状态例程,语言栏切换
"显示/隐藏"时触发)。
20.6 IntCheckImeShowStatusInThread(L2551-2556)--- 线程内收尾
c
VOID FASTCALL IntCheckImeShowStatusInThread(PWND pImeWnd)
{
if (IS_IMM_MODE() && !(pImeWnd->state2 & WNDS2_INDESTROY))
IntCheckImeShowStatus(pImeWnd, pImeWnd->head.pti); // 限定本线程
}
调用点:simplecall.c L672(HWNDLOCK_ROUTINE_CHECKIMESHOWSTATUSINTHRD,窗口锁例程)
------如焦点切换等时机收一下同线程的 IME 显示状态。
21. IME 按键处理全流程
把第 6/7 节与 msgqueue.c、imm32 串起来,得到完整按键流水线。
21.1 消息出队时的拦截(msgqueue.c L1888-1902)
co_IntProcessKeyboardMessage(键盘硬件消息从队列取出、即将交给应用时):
c
if (pWnd && Ret && *RemoveMessages && bKeyUpDown && !(pti->TIF_flags & TIF_DISABLEIME))
{
ImmRet = IntImmProcessKey(pti->MessageQueue, pWnd, uMsg, Msg->wParam, Msg->lParam);
if (ImmRet)
{
if (ImmRet & (IPHK_HOTKEY|IPHK_SKIPTHISKEY))
ImmRet = 0; // 热键/跳过:消息被吞,不再分发
if (ImmRet & IPHK_PROCESSBYIME)
Msg->wParam = VK_PROCESSKEY; // IME 收下:键消息改写为 VK_PROCESSKEY
}
}
前置条件:窗口有效、消息未被钩子/系统命令消费(Ret)、是按下/抬起键(bKeyUpDown)、
线程未禁用 IME(TIF_DISABLEIME)。WH_KEYBOARD 钩子在此之前 已经跑过
(L1870-1886),钩子若吞键则 IME 不再参与。
21.2 IPHK_* 标志的三种命运(undocuser.h L140-143)
| 标志 | 值 | msgqueue.c 的处理 | 最终效果 |
|---|---|---|---|
IPHK_HOTKEY |
0x0001 | ImmRet = 0(与 SKIPTHISKEY 合并判断) |
键消息从队列删除,应用收不到 |
IPHK_PROCESSBYIME |
0x0002 | Msg->wParam = VK_PROCESSKEY |
应用收到 WM_KEYDOWN:VK_PROCESSKEY |
IPHK_CHECKCTRL |
0x0004 | (本调用点未使用) | 由调用方自定 |
IPHK_SKIPTHISKEY |
0x0010 | ImmRet = 0 |
键消息被吞 |
21.3 全流程时序
应用线程 GetMessage 取到 WM_KEYDOWN
→ co_IntProcessKeyboardMessage(msgqueue.c)
→ IntImmProcessKey(ime.c L602) [win32k,见第 6 节]
├─ IntCheckImeHotKey(ime.c L290) 热键预检
├─ DSWITCH 语言切换热键 → WM_INPUTLANGCHANGEREQUEST + IPHK_HOTKEY(直接结束)
├─ 普通键过滤(piiex/IMC/Up 键/SysKey/Alt/DBE 键)
└─ co_IntImmProcessKey(callback.c L1252)
→ KeUserModeCallback(USER32_CALLBACK_IMMPROCESSKEY) [进入用户态]
→ User32CallImmProcessKeyFromKernel(user32 dllmain.c L703)
→ imm32!ImmProcessKey(keymsg.c L740)
├─ ImmGetContext(hWnd) → HIMC(WND.hImc 或默认 IMC)
├─ ImmLockImeDpi(hKL) → IMEDPI(IME 导出表)
├─ ImmLockIMC(hIMC) → INPUTCONTEXTDX
├─ VK_PACKET 宽虚键特判
├─ ImeProcessKey(hIMC, vk, lParam, KeyState)
│ → IME DLL 的 ImeProcessKey 入口
│ 决定:收下 → bNeedsTrans=TRUE, nVKey=vKey, ret|=IPHK_PROCESSBYIME
│ 不收 → ret=0(按键放行给应用)
├─ Imm32ProcessHotKey(keymsg.c L252)→ IPHK_HOTKEY
└─ 返回 DWORD(IPHK_* 组合)
← ZwCallbackReturn 回到内核,co_IntImmProcessKey 返回 ret
← IntImmProcessKey 归一化(0x800000 兼容位)后返回
← msgqueue.c 按标志:吞消息 or 改 VK_PROCESSKEY
→ 应用窗口过程收到(或收不到)WM_KEYDOWN
21.4 IME 收下按键之后:组合消息的产生(user32 misc/imm.c)
IME 的 ImeProcessKey 返回"收下"后,imm32 维护 INPUTCONTEXT 的组合状态:
组合开始/更新/结束时通过 IME 窗口("IME" 类,ImeWndProc_common,imm.c L989)
向目标窗口 发送 WM_IME_* 消息。ImeWndProc_common 的 WM_NCCREATE 分支
(L1012-1023)在堆上分配 IMEUI 并通过 SetWindowLongPtrW(hwnd, GWLP_IMEWND_PIMEUI, ...)
挂到 cbwndExtra 第 0 槽------这与内核 IntGetImeUIFromWnd 读的是同一块内存
(cbwndExtra >= sizeof(PIMEUI) 断言成立的前提是类注册时 extra = sizeof(IMEWND)-sizeof(WND))。
消息分发(L1094-1142):WM_IME_STARTCOMPOSITION/COMPOSITION/ENDCOMPOSITION →
User32SendImeUIMessage(转发给 UI 窗口);WM_IME_NOTIFY → ImeWnd_OnImeNotify;
WM_IME_SYSTEM → ImeWnd_OnImeSystem(含 IMS_LOADTHREADLAYOUT/IMS_ACTIVATELAYOUT
等,对应 18.1 节 window.c 的调用);WM_IME_CONTROL → ImeWnd_OnImeControl。
重入保护(L1042-1078):nCntInIMEProc > 0 时除少数例外(WM_IME_NOTIFY 的
IMN_PRIVATE 起、WM_IME_SYSTEM 的 0x03/0x10/0x13)一律丢弃------与内核
IntSendMessageToUI 的计数配套。
组合完成后的 WM_CHAR 由 ImmGenerateMessage/ImmTranslateMessage 投递
(imm32/compstr.c),最终以普通 WM_CHAR 进入应用消息队列。
22. 输入上下文(IMC)生命周期总结
线程诞生
├─ 首个需要 IME 的窗口创建(window.c L2441)
│ └─ co_IntCreateDefaultImeWindow → 登录进程时顺带 UserCreateInputContext(0)
│ (ime.c L2086,默认 IMC 诞生)
└─ imm32 ImmCreateContext(应用显式创建)
└─ NtUserCreateInputContext → UserCreateInputContext(ime.c L1658)
└─ UserCreateObject(TYPE_INPUTCONTEXT, sizeof(IMC))
├─ 首次 → pti->spDefaultImc = pIMC(UserAssignmentLock)
└─ 非首次 → 插入 spDefaultImc 之后第 2 位(pImcNext)
└─ dwClientImcData = CLIENTIMC(NtUserUpdateInputContext UIC_CLIENTIMCDATA)
窗口绑定(任意时刻)
└─ ImmAssociateContext/ImmAssociateContextEx
└─ NtUserAssociateInputContext → IntAssociateInputContextEx(ime.c L1736)
├─ 单窗口:IntAssociateInputContext 写 WND.hImc
├─ IACE_CHILDREN:递归绑定子窗口
└─ 返回 1 表示焦点窗口被改绑(imm32 刷新激活)
使用
├─ 按键路径:IntImmProcessKey 读 pWnd->hImc → UserGetObject(TYPE_INPUTCONTEXT)
├─ 状态查询:NtUserQueryInputContext(进程/线程 ID、默认 IME 窗口、默认 IMC)
├─ 状态更新:NtUserUpdateInputContext(UIC_IMEWINDOW)
└─ 状态通知:NtUserNotifyIMEStatus(缓存 ghIMC/gfImeOpen/gdwImeConversion)
销毁
├─ ImmDestroyContext → NtUserDestroyInputContext → IntDestroyInputContext(ime.c L1595)
│ ├─ 拒绝销毁默认 IMC(ERROR_INVALID_PARAMETER)
│ ├─ 桌面上所有 hImc==本 IMC 的窗口改绑到默认 IMC
│ └─ UserDeleteObject → UserDestroyInputContext → UserFreeInputContext
│ (线程 IMC 链表摘除 + DesktopHeapFree + 线程引用释放)
└─ 线程退出:ExitThreadCallback 释放 spDefaultImc 的额外引用(cLockObj 2→1)
关键不变量:
- 每线程至多一个默认 IMC (链表首节点,
spDefaultImc); - 默认 IMC 不可销毁(系统调用层拒绝);
- 窗口的 hImc 只可能指向本线程/本进程的 IMC (
IntAssociateInputContextEx
的跨线程/跨进程/跨桌面三重校验); - IMC 销毁前必须解除所有窗口关联 (
IntDestroyInputContext的改绑循环)。
23. 默认 IME 窗口机制总结
条件:IS_IMM_MODE() && !pti->spwndDefaultIme && IntWantImeWindow(Window)
└─ co_IntCreateDefaultImeWindow(ime.c L2072)
├─ 类:原子 ICLS_IME(用户态 "IME" 类,ImeWndProc_common)
├─ 名称:"Default IME";样式:WS_POPUP|WS_DISABLED
├─ 父窗口:目标窗口;owner:目标窗口(经 NtUserSetImeOwnerWindow 调整)
├─ pimeui->fDefault = TRUE;跨线程子窗口额外 fChildThreadDef = TRUE
└─ 挂载:pti->spwndDefaultIme(UserAssignmentLock,window.c L2444)
└─ 初始化:WM_IME_SYSTEM IMS_LOADTHREADLAYOUT [+ IMS_ACTIVATELAYOUT]
运行期
├─ 目标窗口销毁 → IntImeCanDestroyDefIME / IntImeCanDestroyDefIMEforChild(window.c L3036/L3041)
│ ├─ 子窗口:fChildThreadDef + 父链无其他同线程非 IME 相关窗口 → 可拆
│ └─ 顶层:fDestroy 为假 + owner 链可达 + 目标 owner 链无 IME-like
│ + owner 转移后只剩自己 → WndSetOwner(NULL) 后拆
├─ 线程禁用 IME → NtUserDisableThreadIme 直接 co_UserDestroyWindow 拆
├─ Z 序/置顶:IntImeCheckTopmost/IntImeSetTopMost 贴着 owner 走
└─ 显示状态:IntNotifyImeShowStatus/IntCheckImeShowStatus 维护 fShowStatus
句柄查询
└─ NtUserGetThreadState(THREADSTATE_DEFAULTIMEWINDOW)(misc.c L324)
└─ imm32 ImmGetDefaultIMEWnd(imm.c L893)
设计要点回顾:默认 IME 窗口是线程级基础设施 ,不参与焦点/输入(WS_DISABLED),
它的存在让 imm32 始终有一个可通信的 "IME" 类窗口来承载 IMEUI 与 WM_IME_SYSTEM
消息;目标窗口的生死决定它的生死,且销毁判定刻意保守(宁可多留不可悬空)。
24. 调用链总览(mermaid)
24.1 按键处理主链
#mermaid-svg-06D4shrVMTodJD2L{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-06D4shrVMTodJD2L .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-06D4shrVMTodJD2L .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-06D4shrVMTodJD2L .error-icon{fill:#552222;}#mermaid-svg-06D4shrVMTodJD2L .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-06D4shrVMTodJD2L .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-06D4shrVMTodJD2L .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-06D4shrVMTodJD2L .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-06D4shrVMTodJD2L .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-06D4shrVMTodJD2L .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-06D4shrVMTodJD2L .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-06D4shrVMTodJD2L .marker{fill:#333333;stroke:#333333;}#mermaid-svg-06D4shrVMTodJD2L .marker.cross{stroke:#333333;}#mermaid-svg-06D4shrVMTodJD2L svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-06D4shrVMTodJD2L p{margin:0;}#mermaid-svg-06D4shrVMTodJD2L .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-06D4shrVMTodJD2L .cluster-label text{fill:#333;}#mermaid-svg-06D4shrVMTodJD2L .cluster-label span{color:#333;}#mermaid-svg-06D4shrVMTodJD2L .cluster-label span p{background-color:transparent;}#mermaid-svg-06D4shrVMTodJD2L .label text,#mermaid-svg-06D4shrVMTodJD2L span{fill:#333;color:#333;}#mermaid-svg-06D4shrVMTodJD2L .node rect,#mermaid-svg-06D4shrVMTodJD2L .node circle,#mermaid-svg-06D4shrVMTodJD2L .node ellipse,#mermaid-svg-06D4shrVMTodJD2L .node polygon,#mermaid-svg-06D4shrVMTodJD2L .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-06D4shrVMTodJD2L .rough-node .label text,#mermaid-svg-06D4shrVMTodJD2L .node .label text,#mermaid-svg-06D4shrVMTodJD2L .image-shape .label,#mermaid-svg-06D4shrVMTodJD2L .icon-shape .label{text-anchor:middle;}#mermaid-svg-06D4shrVMTodJD2L .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-06D4shrVMTodJD2L .rough-node .label,#mermaid-svg-06D4shrVMTodJD2L .node .label,#mermaid-svg-06D4shrVMTodJD2L .image-shape .label,#mermaid-svg-06D4shrVMTodJD2L .icon-shape .label{text-align:center;}#mermaid-svg-06D4shrVMTodJD2L .node.clickable{cursor:pointer;}#mermaid-svg-06D4shrVMTodJD2L .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-06D4shrVMTodJD2L .arrowheadPath{fill:#333333;}#mermaid-svg-06D4shrVMTodJD2L .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-06D4shrVMTodJD2L .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-06D4shrVMTodJD2L .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-06D4shrVMTodJD2L .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-06D4shrVMTodJD2L .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-06D4shrVMTodJD2L .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-06D4shrVMTodJD2L .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-06D4shrVMTodJD2L .cluster text{fill:#333;}#mermaid-svg-06D4shrVMTodJD2L .cluster span{color:#333;}#mermaid-svg-06D4shrVMTodJD2L 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-06D4shrVMTodJD2L .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-06D4shrVMTodJD2L rect.text{fill:none;stroke-width:0;}#mermaid-svg-06D4shrVMTodJD2L .icon-shape,#mermaid-svg-06D4shrVMTodJD2L .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-06D4shrVMTodJD2L .icon-shape p,#mermaid-svg-06D4shrVMTodJD2L .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-06D4shrVMTodJD2L .icon-shape .label rect,#mermaid-svg-06D4shrVMTodJD2L .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-06D4shrVMTodJD2L .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-06D4shrVMTodJD2L .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-06D4shrVMTodJD2L :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} WM_KEYDOWN/UP 出队后
DSWITCH 热键
普通键
通过
KeUserModeCallback #18
取上下文
调 IME 本体
收下
热键动作
HOTKEY/SKIP
PROCESSBYIME
co_IntProcessKeyboardMessage
msgqueue.c L1888
IntImmProcessKey
ime.c L602
IntCheckImeHotKey
ime.c L290
UserPostMessage WM_INPUTLANGCHANGEREQUEST
返回 IPHK_HOTKEY
过滤
piiex/IMC/Up/Sys/Alt
co_IntImmProcessKey
callback.c L1252
User32CallImmProcessKeyFromKernel
user32 dllmain.c L703
ImmProcessKey
imm32 keymsg.c L740
ImmGetContext
imm.c L1066
ImeProcessKey
IME DLL
bNeedsTrans=TRUE + IPHK_PROCESSBYIME
Imm32ProcessHotKey
keymsg.c L252
ZwCallbackReturn 回内核
IntImmProcessKey 归一化
msgqueue.c 解释 IPHK_*
吞掉键消息
wParam = VK_PROCESSKEY
24.2 默认 IME 窗口生命周期链
#mermaid-svg-24RrIR09Y8aA57rC{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-24RrIR09Y8aA57rC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-24RrIR09Y8aA57rC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-24RrIR09Y8aA57rC .error-icon{fill:#552222;}#mermaid-svg-24RrIR09Y8aA57rC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-24RrIR09Y8aA57rC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-24RrIR09Y8aA57rC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-24RrIR09Y8aA57rC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-24RrIR09Y8aA57rC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-24RrIR09Y8aA57rC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-24RrIR09Y8aA57rC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-24RrIR09Y8aA57rC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-24RrIR09Y8aA57rC .marker.cross{stroke:#333333;}#mermaid-svg-24RrIR09Y8aA57rC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-24RrIR09Y8aA57rC p{margin:0;}#mermaid-svg-24RrIR09Y8aA57rC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-24RrIR09Y8aA57rC .cluster-label text{fill:#333;}#mermaid-svg-24RrIR09Y8aA57rC .cluster-label span{color:#333;}#mermaid-svg-24RrIR09Y8aA57rC .cluster-label span p{background-color:transparent;}#mermaid-svg-24RrIR09Y8aA57rC .label text,#mermaid-svg-24RrIR09Y8aA57rC span{fill:#333;color:#333;}#mermaid-svg-24RrIR09Y8aA57rC .node rect,#mermaid-svg-24RrIR09Y8aA57rC .node circle,#mermaid-svg-24RrIR09Y8aA57rC .node ellipse,#mermaid-svg-24RrIR09Y8aA57rC .node polygon,#mermaid-svg-24RrIR09Y8aA57rC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-24RrIR09Y8aA57rC .rough-node .label text,#mermaid-svg-24RrIR09Y8aA57rC .node .label text,#mermaid-svg-24RrIR09Y8aA57rC .image-shape .label,#mermaid-svg-24RrIR09Y8aA57rC .icon-shape .label{text-anchor:middle;}#mermaid-svg-24RrIR09Y8aA57rC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-24RrIR09Y8aA57rC .rough-node .label,#mermaid-svg-24RrIR09Y8aA57rC .node .label,#mermaid-svg-24RrIR09Y8aA57rC .image-shape .label,#mermaid-svg-24RrIR09Y8aA57rC .icon-shape .label{text-align:center;}#mermaid-svg-24RrIR09Y8aA57rC .node.clickable{cursor:pointer;}#mermaid-svg-24RrIR09Y8aA57rC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-24RrIR09Y8aA57rC .arrowheadPath{fill:#333333;}#mermaid-svg-24RrIR09Y8aA57rC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-24RrIR09Y8aA57rC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-24RrIR09Y8aA57rC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-24RrIR09Y8aA57rC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-24RrIR09Y8aA57rC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-24RrIR09Y8aA57rC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-24RrIR09Y8aA57rC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-24RrIR09Y8aA57rC .cluster text{fill:#333;}#mermaid-svg-24RrIR09Y8aA57rC .cluster span{color:#333;}#mermaid-svg-24RrIR09Y8aA57rC 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-24RrIR09Y8aA57rC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-24RrIR09Y8aA57rC rect.text{fill:none;stroke-width:0;}#mermaid-svg-24RrIR09Y8aA57rC .icon-shape,#mermaid-svg-24RrIR09Y8aA57rC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-24RrIR09Y8aA57rC .icon-shape p,#mermaid-svg-24RrIR09Y8aA57rC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-24RrIR09Y8aA57rC .icon-shape .label rect,#mermaid-svg-24RrIR09Y8aA57rC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-24RrIR09Y8aA57rC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-24RrIR09Y8aA57rC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-24RrIR09Y8aA57rC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} IS_IMM_MODE && !spwndDefaultIme && IntWantImeWindow
目标窗口销毁
TRUE
co_UserCreateWindowEx
window.c L2441
co_IntCreateDefaultImeWindow
ime.c L2072
co_UserCreateWindowEx
IME 类 + 'Default IME' + WS_POPUP|WS_DISABLED
WM_IME_SYSTEM IMS_LOADTHREADLAYOUT
spwndDefaultIme 挂载
UserAssignmentLock
IntImeCanDestroyDefIME / forChild
ime.c L2138/L2185
co_UserDestroyWindow
window.c L3037/L3042
24.3 显示状态通知链
#mermaid-svg-lbErzybw4IvFWSxP{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-lbErzybw4IvFWSxP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lbErzybw4IvFWSxP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lbErzybw4IvFWSxP .error-icon{fill:#552222;}#mermaid-svg-lbErzybw4IvFWSxP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lbErzybw4IvFWSxP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lbErzybw4IvFWSxP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lbErzybw4IvFWSxP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lbErzybw4IvFWSxP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lbErzybw4IvFWSxP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lbErzybw4IvFWSxP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lbErzybw4IvFWSxP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lbErzybw4IvFWSxP .marker.cross{stroke:#333333;}#mermaid-svg-lbErzybw4IvFWSxP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lbErzybw4IvFWSxP p{margin:0;}#mermaid-svg-lbErzybw4IvFWSxP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lbErzybw4IvFWSxP .cluster-label text{fill:#333;}#mermaid-svg-lbErzybw4IvFWSxP .cluster-label span{color:#333;}#mermaid-svg-lbErzybw4IvFWSxP .cluster-label span p{background-color:transparent;}#mermaid-svg-lbErzybw4IvFWSxP .label text,#mermaid-svg-lbErzybw4IvFWSxP span{fill:#333;color:#333;}#mermaid-svg-lbErzybw4IvFWSxP .node rect,#mermaid-svg-lbErzybw4IvFWSxP .node circle,#mermaid-svg-lbErzybw4IvFWSxP .node ellipse,#mermaid-svg-lbErzybw4IvFWSxP .node polygon,#mermaid-svg-lbErzybw4IvFWSxP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lbErzybw4IvFWSxP .rough-node .label text,#mermaid-svg-lbErzybw4IvFWSxP .node .label text,#mermaid-svg-lbErzybw4IvFWSxP .image-shape .label,#mermaid-svg-lbErzybw4IvFWSxP .icon-shape .label{text-anchor:middle;}#mermaid-svg-lbErzybw4IvFWSxP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lbErzybw4IvFWSxP .rough-node .label,#mermaid-svg-lbErzybw4IvFWSxP .node .label,#mermaid-svg-lbErzybw4IvFWSxP .image-shape .label,#mermaid-svg-lbErzybw4IvFWSxP .icon-shape .label{text-align:center;}#mermaid-svg-lbErzybw4IvFWSxP .node.clickable{cursor:pointer;}#mermaid-svg-lbErzybw4IvFWSxP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lbErzybw4IvFWSxP .arrowheadPath{fill:#333333;}#mermaid-svg-lbErzybw4IvFWSxP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lbErzybw4IvFWSxP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lbErzybw4IvFWSxP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lbErzybw4IvFWSxP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lbErzybw4IvFWSxP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lbErzybw4IvFWSxP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lbErzybw4IvFWSxP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lbErzybw4IvFWSxP .cluster text{fill:#333;}#mermaid-svg-lbErzybw4IvFWSxP .cluster span{color:#333;}#mermaid-svg-lbErzybw4IvFWSxP 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-lbErzybw4IvFWSxP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lbErzybw4IvFWSxP rect.text{fill:none;stroke-width:0;}#mermaid-svg-lbErzybw4IvFWSxP .icon-shape,#mermaid-svg-lbErzybw4IvFWSxP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lbErzybw4IvFWSxP .icon-shape p,#mermaid-svg-lbErzybw4IvFWSxP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lbErzybw4IvFWSxP .icon-shape .label rect,#mermaid-svg-lbErzybw4IvFWSxP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lbErzybw4IvFWSxP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lbErzybw4IvFWSxP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lbErzybw4IvFWSxP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} NtUserCallOneParam
NT4+ 且有 hImc
旧路径
语言栏切换显示/隐藏
IntBroadcastImeShowStatusChange
ime.c L2538
gfIMEShowStatus = bShow
IntNotifyImeShowStatus
ime.c L2471
bShow = gfIMEShowStatus && fCtrlShowStatus
IntSendOpenStatusNotify
ime.c L2451
目标窗口 WM_IME_NOTIFY IMN_OPEN/CLOSESTATUSWINDOW
IntSendMessageToUI(nCntInIMEProc++)
ime.c L2365
IntCheckImeShowStatus
ime.c L2267
兄弟 IME 窗口 fShowStatus=FALSE + IMN_CLOSESTATUSWINDOW
25. 与 keyboard/msgqueue/window/imm32 的关系
25.1 与 keyboard.c / kbdlayout.c(分册 _23 / _4)
UserProcessKeyboardInput(keyboard.c)只负责把硬件输入变成WM_KEYDOWN等键消息
入队;IME 判断发生在消息出队时(msgqueue.c),两者由消息队列解耦;IntImmProcessKey读取pWnd->head.pti->KeyboardLayout->hkl与piiex------布局对象
由 kbdlayout.c 管理(UserHklToKbl、gspklBaseLayout、UserGetKeyboardLayout);NtUserSetThreadLayoutHandles(ime.c)反过来写回线程布局指针,供 keyboard 管线下
一次翻译使用。
25.2 与 msgqueue.c(分册 _8)
msgqueue.c 是 IME 的挂载点 与消费者 :co_IntProcessKeyboardMessage 调
IntImmProcessKey 并按返回值吞消息/改 VK_PROCESSKEY。同时
MessageQueue->afKeyState 是 IntCheckImeHotKey(组合修饰键)与
UserSetImeConversionKeyState(写 DBE 键状态)共享的键状态数组。
25.3 与 window.c / winpos.c
- window.c :创建窗口时(L2441)触发默认 IME 窗口创建;销毁窗口时(L3029)触发
销毁判定;IntImeSetTopMost用IntUnlinkWindow/IntLinkHwnd/HWND_TOP操作
Z 序(与 winpos.c 的窗口链管理共用原语); - simplecall.c :
HWNDLOCK_ROUTINE_CHECKIMESHOWSTATUSINTHRD(L672)与 IME 显示
状态广播(L918)两个"快速系统调用"路径。
25.4 与 imm32(用户态)的协议面
| 内核(ime.c / misc.c) | imm32 消费方 | 用途 |
|---|---|---|
NtUserGetImeHotKey / SetImeHotKey / CheckImeHotKey |
ImmGetImeHotKey/ImmSetImeHotKey/ImmCheckImeHotKey |
热键注册与匹配 |
co_IntImmProcessKey(callback) |
ImmProcessKey |
按键交给 IME 本体 |
NtUserCreate/DestroyInputContext |
ImmCreateContext/ImmDestroyContext |
IMC 生命周期 |
NtUserAssociateInputContext |
ImmAssociateContext(Ex) |
窗口↔IMC 绑定 |
NtUserQueryInputContext |
ImmGetThreadId/ImmGetProcessId/ImmGetDefaultIMEWnd |
属性查询 |
NtUserUpdateInputContext |
ImmCreateContext(UIC_CLIENTIMCDATA) |
CLIENTIMC 挂接 |
NtUserNotifyIMEStatus |
状态变化通知 | 键状态/Shell 钩子同步 |
NtUserBuildHimcList |
Imm32BuildHimcList→ImmEnumInputContext |
IMC 枚举 |
NtUserGet/SetImeInfoEx |
ImmGet/SetImeInfoEx |
IMEINFOEX 读写 |
NtUserGet/SetAppImeLevel |
ImmGet/SetAppImeLevel |
韩文 IME 等级 |
NtUserSetImeOwnerWindow |
IME 窗口 owner 管理 | Z 序/置顶跟随 |
NtUserGetThreadState(DEFAULTIMEWINDOW/DEFAULTINPUTCONTEXT 等) |
ImmGetSaveContext/ImmGetDefaultIMEWnd |
线程默认资源回退 |
IntBroadcastImeShowStatusChange(simplecall) |
语言栏 | 显示状态广播 |
25.5 跨进程访问惯例
IMEUI 是用户态堆内存,内核函数访问它有三道防线,ime.c 内反复出现:
ProbeForRead/ProbeForWrite+_SEH2_TRY/_SEH2_EXCEPT:防止访问无效用户指针
引发内核崩溃(如IntGetImeHotKeyLanguageScore读 TEB、co_IntCreateDefaultImeWindow
写 pimeui、IntCheckImeShowStatus读写 pimeui);KeAttachProcess/KeDetachProcess:跨进程访问前切换到目标进程的地址空间
(IntSendMessageToUI、IntCheckImeShowStatus、IntNotifyImeShowStatus均成对使用);- 引用保护 :跨进程窗口/对象先
UserRefObjectCo/UserDerefObjectCo
(co_IntSendMessage前),防并发销毁。
26. 源码索引
26.1 ime.c 全部函数(win32ss/user/ntuser/ime.c,共 2556 行)
| 行号 | 函数 | 类别 |
|---|---|---|
| L38 | IntGetImeUIFromWnd |
基础设施(取 IME 窗口扩展数据) |
| L45 | IntGetImeCompatFlags |
兼容标志 |
| L54 | IntGetImeHotKeyLanguageScore |
热键语言打分 |
| L87 | IntGetActiveKeyboardLayout |
前台布局 |
| L102 | IntGetImeHotKeyLangId |
热键 ID→语言 |
| L128 | IntAddImeHotKey |
热键链表 |
| L151 | IntGetImeHotKeyById |
热键链表 |
| L165 | IntGetImeHotKeyByKeyAndLang |
热键查重 |
| L200 | IntDeleteImeHotKey |
热键链表 |
| L225 | IntGetImeHotKeyByKey |
按键匹配+打分 |
| L290 | IntCheckImeHotKey |
队列键状态+KeyUp |
| L344 | IntFreeImeHotKeys |
释放热键(main.c L183 清理) |
| L356 | IntSetImeHotKey |
增删改查总闸 |
| L428 | NtUserGetImeHotKey |
系统调用 |
| L476 | NtUserSetImeHotKey |
系统调用 |
| L492 | NtUserCheckImeHotKey |
系统调用 |
| L515 | IntGetTopLevelWindow |
取顶层窗口 |
| L527 | IntAssociateInputContext |
窗口 IMC 换绑 |
| L535 | NtUserSetThreadLayoutHandles |
布局切换(hklPrev) |
| L566 | UserBuildHimcList |
IMC 枚举 |
| L602 | IntImmProcessKey |
按键拦截入口 |
| L731 | NtUserBuildHimcList |
系统调用 |
| L790 | UserSetImeConversionKeyState |
转换模式→键状态 |
| L848 | NtUserNotifyIMEStatus |
状态通知(ghIMC 缓存) |
| L903 | NtUserDisableThreadIme |
禁用 IME |
| L971 | NtUserGetAppImeLevel |
韩文 IME 等级 |
| L1004 | UserGetImeInfoEx |
IMEINFOEX 查询 |
| L1067 | NtUserGetImeInfoEx |
系统调用 |
| L1134 | NtUserSetAppImeLevel |
韩文 IME 等级 |
| L1169 | UserSetImeInfoEx |
IMEINFOEX 更新 |
| L1208 | NtUserSetImeInfoEx |
系统调用 |
| L1245 | IntImeSetFutureOwner |
IME 窗口 owner 择优 |
| L1311 | IntGetLastTopMostWindowNoIME |
置顶窗口定位 |
| L1348 | IntImeSetTopMost |
Z 序重排 |
| L1437 | IntImeCheckTopmost |
置顶跟随 |
| L1453 | NtUserSetImeOwnerWindow |
系统调用 |
| L1524 | AllocInputContextObject |
IMC 对象分配 |
| L1555 | UserFreeInputContext |
IMC 对象释放 |
| L1583 | UserDestroyInputContext |
IMC 析构 |
| L1595 | IntDestroyInputContext |
IMC 销毁(窗口改绑) |
| L1635 | NtUserDestroyInputContext |
系统调用 |
| L1658 | UserCreateInputContext |
IMC 创建(默认/非默认) |
| L1705 | NtUserCreateInputContext |
系统调用 |
| L1736 | IntAssociateInputContextEx |
递归关联 |
| L1802 | NtUserAssociateInputContext |
系统调用 |
| L1836 | UserUpdateInputContext |
IMC 字段更新 |
| L1869 | NtUserUpdateInputContext |
系统调用 |
| L1901 | NtUserQueryInputContext |
IMC 查询 |
| L1957 | IntFindNonImeRelatedWndOfSameThread |
销毁判定辅助 |
| L2032 | IntWantImeWindow |
IME 窗口资格判定 |
| L2072 | co_IntCreateDefaultImeWindow |
默认 IME 窗口创建 |
| L2138 | IntImeCanDestroyDefIMEforChild |
子窗口销毁判定 |
| L2185 | IntImeCanDestroyDefIME |
顶层销毁判定 |
| L2267 | IntCheckImeShowStatus |
显示状态清理 |
| L2365 | IntSendMessageToUI |
UI 消息(重入保护) |
| L2451 | IntSendOpenStatusNotify |
开关通知分发 |
| L2471 | IntNotifyImeShowStatus |
显示状态核心 |
| L2538 | IntBroadcastImeShowStatusChange |
全局广播 |
| L2551 | IntCheckImeShowStatusInThread |
线程内收尾 |
26.2 相关外部函数与定义
| 位置 | 符号 | 作用 |
|---|---|---|
| callback.c L1252 | co_IntImmProcessKey |
KeUserModeCallback 封装 |
| callback.h L167 | IMMPROCESSKEY_CALLBACK_ARGUMENTS |
回调参数包 |
| u32cb.h L27 | USER32_CALLBACK_IMMPROCESSKEY(#18) |
回调注册 |
| user32 misc/dllmain.c L703 | User32CallImmProcessKeyFromKernel |
用户态入口 |
| user32 misc/imm.c L989 | ImeWndProc_common |
"IME" 类窗口过程 |
| user32 misc/imm.c L1183 | IME_builtin_class |
"IME" 类描述(extra=IMEWND-WND) |
| user32 controls/regcontrol.c L68/L85 | RegisterSystemControls |
"IME" 类注册(IS_IMM_MODE 条件) |
| ntuser class.c L31/L126 | L"IME" / FNID_IME→ICLS_IME |
系统类名/映射 |
| msgqueue.c L1888-1902 | IntImmProcessKey 调用点 |
IPHK_* 消费 |
| window.c L2440-2465 | 默认 IME 窗口创建点 | IMS_LOADTHREADLAYOUT |
| window.c L3029-3044 | 默认 IME 窗口销毁点 | IntImeCanDestroyDefIME* |
| simplecall.c L672/L918 | 显示状态快速调用 | IntCheckImeShowStatusInThread / IntBroadcastImeShowStatusChange |
| misc.c L239-356 | NtUserGetThreadState |
THREADSTATE_DEFAULTIMEWINDOW 等 |
| imm32 keymsg.c L740 | ImmProcessKey |
用户态按键处理 |
| imm32 keymsg.c L252 | Imm32ProcessHotKey |
热键动作分发 |
| imm32 imm.c L1066/L1110 | ImmGetContext/ImmReleaseContext |
上下文取/还 |
| imm32 imm.c L1022 | ImmGetSaveContext |
hImc 回退链 |
| imm32 utils.c L338 | Imm32BuildHimcList |
HIMC 列表(倍增重试) |
| ntuser.h L197 | tagIMC |
内核 IMC 对象 |
| ntuser.h L740 | WND.hImc |
窗口↔IMC 绑定 |
| ntuser.h L1218/L1238 | IMEUI/IMEWND |
扩展数据 |
| window.h L111-116 | IS_WND_IMELIKE |
IME-like 判定 |
| win32.h L130-133 | spwndDefaultIme/spDefaultImc/hklPrev |
线程 IME 状态 |
| input.h L26-39 | tagKL(piiex) |
布局对象 |
| imm32_undoc.h L72 | IMEINFOEX |
IME 信息扩展 |
| immdev.h L20/L106 | IMEINFO/INPUTCONTEXT |
IME 属性/用户态上下文 |
| undocuser.h L140-143 | IPHK_* |
返回标志 |
| imm.h L195-L212 | IME_CHOTKEY_*/IME_HOTKEY_DSWITCH_FIRST |
热键 ID |
26.3 关于 NtUserGetOpenInputContextIds 的说明
任务清单中提到的 NtUserGetOpenInputContextIds 在当前源码树中不存在 (全树检索
无匹配,已在 ReactOS 演进中移除/从未落地)。当前 IMC 枚举职责由
NtUserBuildHimcList(本册 9.2 节)+ 用户态 Imm32BuildHimcList 承担,
ImmEnumInputContext 是面向应用的完整枚举接口。特此说明以避免读者误找。
结语
本册是"窗口系统三大主线之鼠标控制"系列的收官之作。输入法子系统与前面的键盘
(_23/_4)、消息队列(_8)、窗口创建(_5)、输入状态(21)形成闭环:**按键从
keyboard.c 的硬件事件出发,在 msgqueue.c 出队时被 ime.c 拦截,经
KeUserModeCallback 下沉到 imm32 与 IME DLL,组合结果以 WM_IME * 与 WM_CHAR 回流**
------而输入上下文(HIMC)、默认 IME 窗口、IMEUI 扩展数据这三个内核/用户态共用的载体,
则回答了"状态放哪、窗口谁建、界面谁管"三个根本问题。掌握本册,ReactOS 的"输入三驾
马车"(鼠标/键盘/输入法)即全部打通。