ReactOS 窗口系统分析(24):输入法 — ime.c

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.cco_IntImmProcessKey 用户态回调)、

win32ss/user/imm32/(用户态 imm32.dll:ImmProcessKeyImmGetContext 等)、

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 键盘输入)。


目录

  1. 概述
  2. 设计动机
  3. 核心数据结构
  4. 全局状态与模块入口
  5. [IME 热键管理](#IME 热键管理)
  6. [按键拦截入口 IntImmProcessKey](#按键拦截入口 IntImmProcessKey)
  7. [用户态回调 co_IntImmProcessKey](#用户态回调 co_IntImmProcessKey)
  8. [键盘布局切换 NtUserSetThreadLayoutHandles](#键盘布局切换 NtUserSetThreadLayoutHandles)
  9. [HIMC 列表 NtUserBuildHimcList](#HIMC 列表 NtUserBuildHimcList)
  10. [IME 状态通知 NtUserNotifyIMEStatus](#IME 状态通知 NtUserNotifyIMEStatus)
  11. [线程/进程 IME 禁用与 AppImeLevel](#线程/进程 IME 禁用与 AppImeLevel)
  12. [IMEINFOEX 查询与设置](#IMEINFOEX 查询与设置)
  13. [IME 窗口的所有者与 Z 序管理](#IME 窗口的所有者与 Z 序管理)
  14. 输入上下文(IMC)对象生命周期
  15. [输入上下文关联 NtUserAssociateInputContext](#输入上下文关联 NtUserAssociateInputContext)
  16. [输入上下文更新与查询 NtUserQueryInputContext](#输入上下文更新与查询 NtUserQueryInputContext)
  17. [IME 窗口判定 IntWantImeWindow](#IME 窗口判定 IntWantImeWindow)
  18. [默认 IME 窗口创建 co_IntCreateDefaultImeWindow](#默认 IME 窗口创建 co_IntCreateDefaultImeWindow)
  19. [默认 IME 窗口销毁判定](#默认 IME 窗口销毁判定)
  20. [IME 显示状态(语言栏 ShowStatus)](#IME 显示状态(语言栏 ShowStatus))
  21. [IME 按键处理全流程](#IME 按键处理全流程)
  22. 输入上下文(IMC)生命周期总结
  23. [默认 IME 窗口机制总结](#默认 IME 窗口机制总结)
  24. 调用链总览(mermaid)
  25. [与 keyboard/msgqueue/window/imm32 的关系](#与 keyboard/msgqueue/window/imm32 的关系)
  26. 源码索引

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_* 消息。两者通过

KeUserModeCallbackUSER32_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(转换/句模式)、lfFontcfCompForm

(组合窗口位置)、cfCandForm[4](4 个候选窗位置)、hCompStr(组合字符串 HIMCC)、

hCandInfo(候选信息)、hGuideLine(引导行)、hPrivatehMsgBuf(消息缓冲)、

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_commonWM_NCCREATEHeapAlloc 分配,

SetWindowLongPtrW(hwnd, GWLP_IMEWND_PIMEUI, ...) 挂到窗口第 0 个 cbwndExtra 槽);

因此内核访问它时必须 ProbeForRead/ProbeForWrite + 必要时 KeAttachProcess(见

IntGetImeUIFromWndIntSendMessageToUIIntCheckImeShowStatus 的 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 窗口"有两种来源:

  1. 类样式带 CS_IME(0x10000,winuser.h L667)------IME UI 窗口的类注册时带此样式;
  2. 类名就是系统原子 ICLS_IME ------即用户态注册的内建 "IME" 类(user32/regcontrol.c
    IME_builtin_class,窗口过程 ImeWndProcA/W,cbwndExtra = sizeof(IMEWND)-sizeof(WND))。

内核 class.c 把 L"IME" 作为系统类名之一(ICLS_IME 索引),FNID_IMEICLS_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):hklIMEINFO ImeInfo(含

fdwPropertyIME_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);ImmGetDefaultIMEWndTHREADSTATE_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 在中文与日文布局下都注册过),需要一个打分机制

决定哪个优先:

  1. 热键语言为中性(LANGID_NEUTRAL)或与当前 hKL 低字相同 → 3 分(最高);
  2. 与当前线程 NtCurrentTeb()->CurrentLocale 的语言相同 → 2 分(SEH 保护读 TEB);
  3. 与系统默认 LCID(glcidSystem,惰性 ZwQueryDefaultLocale(FALSE,...))相同 → 1 分;
  4. 否则 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);
  • NtUserGetImeHotKeyUserEnterExclusive;对三个输出指针逐一 ProbeForWrite;按 ID
    查表后把字段拷回用户态(写入段再包一层 SEH,异常则视为失败)。
  • NtUserSetImeHotKeyUserEnterExclusiveIntSetImeHotKey
  • NtUserCheckImeHotKeyUserEnterExclusive要求前台队列存在且 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_IntProcessKeyboardMessageWH_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 吞掉原键。

兼容位 0x800000IntGetImeCompatFlags(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;                       // 无输入上下文 → 不处理

随后三组过滤:

  1. Up 键HIWORD(lParam) & KF_UP):若 IME_PROP_IGNORE_UPKEYS(IME 声称忽略
    抬键)→ 返回 0;
  2. DBE 键放行 (L690-700):VK_DBE_CODEINPUT/ENTERIMECONFIGMODE/ENTERWORDREGISTERMODE/ HIRAGANA/KATAKANA/NOCODEINPUT/NOROMAN/ROMAN 直接进入处理(这些是 IME 专用控制键);
  3. SysKey 与 Alt (L704-714):WM_SYSKEYDOWN/UP 只放行 VK_MENUVK_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 节流程图):

  1. ImmGetContext(hWnd) 取 IMC(imm.c L1066 → ImmGetSaveContext);
  2. ImmLockImeDpi(hKL) 取 IME 的导出表(IMEDPIImeProcessKeyImeEscape...);
  3. ImmLockIMCINPUTCONTEXTDX(含 fOpenbNeedsTransnVKey);
  4. VK_PACKET 宽虚键特判(非 Unicode IME 且 IME 打开 → IPHK_SKIPTHISKEY);
  5. GetKeyboardState + ImeProcessKey(hIMC, vk, lParam, KeyState):IME 本体决定
    "收下此键"则置 pIC->bNeedsTrans = TRUE; pIC->nVKey = vKey; ret |= IPHK_PROCESSBYIME
  6. 热键动作(非 Cicero 时直接跑 Imm32ProcessHotKey;Cicero 时先试
    CtfImeProcessCicHotkey);处理过 → ret |= IPHK_HOTKEYVK_KANJI +
    IME_JHOTKEY_CLOSE_OPEN 除外);
  7. 兼容标志 _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 性质发生变化时更新 hklPrevIS_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 校验两个输出缓冲后调 UserBuildHimcListdwCount < 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)派生平假名/片假名
    KATAKANA 0x2)、由 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_HANGULIME_CMODE_NATIVE(谚文开/关)、
    VK_JUNJAFULLSHAPEVK_HANJAIME_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 缓存状态并广播:

  1. IS_IMM_MODE() → ERR 退出;
  2. ValidateHwndNoErr(hwnd) 取窗口;pti 必须等于前台线程或其队列与前台队列相同
    (只关心前台线程的 IME 状态);
  3. 去重ghIMC == pwnd->hImc && gfImeOpen == !!fOpen && gdwImeConversion == dwConversion
    直接返回------ghIMC/gfImeOpen/gdwImeConversion 这三个全局就是状态缓存;
  4. 更新缓存并调 UserSetImeConversionKeyState(pti, fOpen ? dwConversion : IME_CMODE_ALPHANUMERIC)
    ------IME 关闭时把键状态映射为"字母数字模式"IME_CMODE_ALPHANUMERIC == 0);
  5. 若装了 WH_SHELL 钩子,调 co_HOOK_CallHooks(WH_SHELL, HSHELL_LANGUAGE, (WPARAM)hwnd, (LPARAM)hKL)
    ------通知 shell(如 explorer 的语言栏)语言发生变化;
  6. 末尾 // 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(含 fdwPropertywszImeFilewszUIClass)写回 win32k,

IntImmProcessKey 读取 IME_PROP_* 决策。


13. IME 窗口的所有者与 Z 序管理

IME 窗口必须跟随目标窗口的可见性/置顶状态变化(否则候选窗口会被普通窗口盖住,或

IME 窗口抢了目标窗口的 Z 序位置)。ime.c L1245-1522 的五个函数专门处理"IME 窗口的

owner 选择与置顶调整"。

13.1 IntImeSetFutureOwner(L1245-1308)--- 挑选首选 owner

为 IME 窗口选择"未来的 owner":

  1. 非法 owner 过滤!pwndOwner || (pwndOwner->style & WS_CHILD) 直接返回
    (子窗口不能作 owner);
  2. 沿 owner 链上溯到同线程顶层for (pwndNode = pwndOwner; ; pwndNode = pwndNextOwner)
    沿 spwndOwner 走到"非同线程 owner"为止------顶层 owner 链上第一个同线程窗口;
  3. 避开 IME-like 与 bottom-mostIS_WND_IMELIKE(pwndNode)
    (节点是 WNDS2_BOTTOMMOST 而目标不是)时退回 pwndOwner(SEH 保护读
    state2,跨进程窗口对象访问可能异常);
  4. 兄弟窗口择优 (L1278-1305):若节点有父窗口且目标就是节点本身,则在父窗口的
    子链(spwndChild/spwndNext)中找"同线程、非菜单(IS_WND_MENU)、非 IME-like、
    非销毁中(WNDS2_INDESTROY)、非 WS_CHILD、无跨线程 owner"的窗口替代;
  5. 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)并清零;
  • 初始化 THRDESKHEADpSelf = ObjHeadrpdeskpti
  • IntReferenceThreadInfo(pti)(线程引用 +1,防止对象存活期间线程被清理);
  • *HandleOwner = pti(句柄归属线程,跨线程访问时校验)、ppi->UserHandleCount++

这是 IMC 对象的通用构造骨架------注意它不创建 USER 句柄,句柄由

UserCreateObject 统一分配。

14.2 UserCreateInputContext(L1658-1703)--- 创建并挂链表

c 复制代码
PIMC FASTCALL UserCreateInputContext(ULONG_PTR dwClientImcData)
  1. 前置检查:!IS_IMM_MODE() || (pti->TIF_flags & TIF_DISABLEIME) → NULL(IME 被禁用);
    !pdesk → NULL;dwClientImcData 非零但线程还没有默认 IMC → NULL
    ("非首次创建"必须已有默认 IMC);
  2. UserCreateObject(gHandleTable, pdesk, pti, NULL, TYPE_INPUTCONTEXT, sizeof(IMC))
    创建对象(返回时已带 2 个引用);
  3. UserDereferenceObject(pIMC) 释放多余引用,断言 cLockObj == 1
  4. 链表挂接:
    • dwClientImcData 非零(非首次)→ 插到 spDefaultImc 之后的第二个位置
      pIMC->pImcNext = pti->spDefaultImc->pImcNext; spDefaultImc->pImcNext = pIMC;);
    • 首次 → UserAssignmentLock 设为 pti->spDefaultImc(默认 IMC),
      断言 cLockObj == 2------多出的引用在 ExitThreadCallback
      UserAssignmentUnlock 释放(注释 L1698 明示);
  5. 回填 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 清理,不在对象释放时处理);
  • DesktopHeapFreeUserHandleCount--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 的校验与动作:

  1. 属主校验pIMC->head.pti != gptiCurrentERROR_ACCESS_DENIED
    (只能销毁本线程的 IMC);
  2. 默认 IMC 保护pIMC == pti->spDefaultImcERROR_INVALID_PARAMETER
    (默认 IMC 不可销毁);
  3. 解除窗口关联IntBuildHwndList(rpdesk 桌面子链, IACE_LIST|IACE_CHILDREN, pti)
    枚举桌面上本线程的所有窗口,凡 pWnd->hImc == hIMC 者一律
    IntAssociateInputContext(pWnd, pti->spDefaultImc) 重新关联到默认 IMC------
    防止销毁后窗口悬空指向已失效句柄
  4. UserDeleteObject(hIMC, TYPE_INPUTCONTEXT) 删除句柄(引用归零后触发
    UserDestroyInputContextUserFreeInputContext)。

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 的 ImmAssociateContextExIACE_DEFAULT / IACE_CHILDREN /

IACE_IGNORENOCONTEXT(定义于 imm.h)。返回值约定:0 = 成功;1 = 焦点窗口被改绑
(imm32 据此刷新 IME 激活状态);2 = 失败

处理逻辑:

  1. IACE_DEFAULT → 用线程默认 IMC(pti->spDefaultImc)替代传入的 pIMC;
  2. 跨线程拒绝pIMC && pti != pIMC->head.pti → 返回 2;
  3. 跨进程/跨桌面拒绝pWnd->head.pti->ppi != GetW32ThreadInfo()->ppi || (pIMC && pIMC->head.rpdesk != pWnd->head.rpdesk) → 返回 2;
  4. IACE_CHILDREN 且窗口有子窗口:IntBuildHwndList(pWnd->spwndChild, IACE_CHILDREN|IACE_LIST, pti)
    枚举后代,逐个 IntAssociateInputContext;跳过"目标 IMC 相同"或
    IACE_IGNORENOCONTEXT 且目标无 IMC)的窗口;被改绑窗口恰为焦点窗口时
    ret = 1
  5. 处理窗口本体:!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_CLIENTIMCDATApIMC->dwClientImcData 已设置则失败(只能设置一次 ),
    否则回填------imm32 ImmCreateContext 后续调 NtUserUpdateInputContext(hImc, UIC_CLIENTIMCDATA, (DWORD_PTR)pClientImc)(imm.c L984)把用户态 CLIENTIMC 挂上;
  • UIC_IMEWINDOWValidateHwndNoErr((HWND)dwValue) 校验后写 pIMC->hImeWnd

16.2 NtUserQueryInputContext(L1901-1953)--- 查询 IMC 属性

c 复制代码
DWORD_PTR NTAPI NtUserQueryInputContext(HIMC hIMC, DWORD dwType)

UserEnterExclusiveUserGetObject(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_DEFAULTWINDOWIMEspwndDefaultIme 为 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)
  1. 登录进程兜底if (!(pti->spDefaultImc) && pid == gpidLogon) UserCreateInputContext(0);
    ------登录进程(winlogon)的首个窗口直接建默认 IMC(dwClientImcData=0,即"系统默认
    IMC"),其他进程的默认 IMC 由 imm32 的 ImmCreateContext 建立;
  2. 前置检查:无默认 IMC、目标是 IME-like、无桌面堆 → NULL;
  3. 跨线程子窗口特判 (L2092-2096):目标是不可见的跨线程子窗口 (父窗口属另一
    进程)→ 不建(子窗口不可见时不必为它准备 IME);
  4. 构造 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------禁用窗口不接收

键盘焦点,这正是"基础设施窗口"的定位;

  1. 标记扩展数据 (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):

  1. 跳过 pwndTarget 本身、不同线程窗口、菜单窗口(IS_WND_MENU);
  2. 非子窗口:若其 owner 链上任何一环是 IME-like → 跳过(它是 IME 的"服务对象");
  3. 子窗口:沿父链上溯,若存在同线程的 IME-like 祖先 → 跳过;上溯到非子/跨线程
    节点后,再沿 owner 链检查同线程 IME-like owner,有则跳过。

能找到返回 TRUE(还有"别人"在用这套 IME 设施,默认 IME 窗口不能拆);找不到返回

FALSE(这个线程只剩目标窗口依赖默认 IME 窗口,可以拆)。

19.2 IntImeCanDestroyDefIMEforChild(L2138-2182)--- 子窗口目标

c 复制代码
BOOL FASTCALL IntImeCanDestroyDefIMEforChild(PWND pImeWnd, PWND pwndTarget)
  1. pimeui(SEH):fChildThreadDef 必须为真 ,否则不拆(该 IME 窗口不是
    "为子线程目标"建的,可能是跨线程主窗口的默认 IME 窗口);
  2. 目标父窗口必须存在且与目标不同线程pwndTarget->head.pti != pwndTarget->spwndParent->head.pti
    否则返回 FALSE)------本函数只处理"跨线程子窗口"场景;
  3. 沿目标父链(L2172-2179)逐层调用 IntFindNonImeRelatedWndOfSameThread(pwndNode->spwndParent, pwndTarget)
    只要某一层还存在同线程非 IME 相关窗口 → 不拆(其它窗口还要用这个默认 IME 窗口)。

19.3 IntImeCanDestroyDefIME(L2185-2264)--- 顶层窗口目标

c 复制代码
BOOL FASTCALL IntImeCanDestroyDefIME(PWND pImeWnd, PWND pwndTarget)
  1. pimeui->fDestroy 为真 → 不拆(用户态已请求保留/正在销毁流程);
  2. owner 链可达性pImeWnd->spwndOwner 沿 owner 链必须能到达 pwndTarget
    pImeWnd 当前服务于 pwndTarget),否则不拆;
  3. 目标 owner 链不得有 IME-like (否则目标本身就是 IME 相关窗口,拆了默认窗口会
    影响它);
  4. 转移 owner 并检查IntImeSetFutureOwner(pImeWnd, pwndTarget) 重新挑选 owner,
    沿 spwndNext 链确认 pImeWnd 确实还在窗口列表里(L2250-2255,防已卸载),在则
    IntImeCheckTopmost 修正置顶状态;
  5. 终局检查pImeWnd->spwndOwner 必须是 NULL 或 pwndTarget,否则不拆;
  6. 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 指定时)同线程;
  • 跨进程访问用户态 IMEUIKeAttachProcess (L2311-2315 与 L2341-2342 成对,
    访问完即 KeDetachProcess);
  • 逻辑:若某兄弟 IME 窗口 fShowStatus == TRUE 且其 hwndIMC(拥有 IMC 的目标窗口)
    仍有效 → 清除 fShowStatusFALSE),并给该目标窗口发
    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 L1042 if (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)
  1. !IS_IMM_MODE() || (pImeWnd->state2 & WNDS2_INDESTROY) → 返回;
  2. 跨进程 KeAttachProcesspti != ptiIME 时)读/写 pimeui
  3. 计算显示意愿:bShow = (gfIMEShowStatus == TRUE) && SafeImeUI.fCtrlShowStatus;
    ------全局开关 ∧ 用户控制位
  4. 目标窗口:pWnd = ValidateHwndNoErr(SafeImeUI.hwndIMC),无效则回退
    ptiIME->MessageQueue->spwndFocus(IME 线程的焦点窗口);有效则
    pimeui->fShowStatus = bShow; bSendNotify = TRUE
  5. IntSendOpenStatusNotify(ptiIME, &SafeImeUI, pWnd, bShow) 通知开关状态;
  6. 末尾(窗口未销毁时)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_commonWM_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_NOTIFYImeWnd_OnImeNotify

WM_IME_SYSTEMImeWnd_OnImeSystem(含 IMS_LOADTHREADLAYOUT/IMS_ACTIVATELAYOUT

等,对应 18.1 节 window.c 的调用);WM_IME_CONTROLImeWnd_OnImeControl

重入保护(L1042-1078):nCntInIMEProc > 0 时除少数例外(WM_IME_NOTIFY

IMN_PRIVATE 起、WM_IME_SYSTEM 的 0x03/0x10/0x13)一律丢弃------与内核

IntSendMessageToUI 的计数配套。

组合完成后的 WM_CHARImmGenerateMessage/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)

关键不变量:

  1. 每线程至多一个默认 IMC (链表首节点,spDefaultImc);
  2. 默认 IMC 不可销毁(系统调用层拒绝);
  3. 窗口的 hImc 只可能指向本线程/本进程的 IMCIntAssociateInputContextEx
    的跨线程/跨进程/跨桌面三重校验);
  4. 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" 类窗口来承载 IMEUIWM_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->hklpiiex------布局对象
    由 kbdlayout.c 管理(UserHklToKblgspklBaseLayoutUserGetKeyboardLayout);
  • NtUserSetThreadLayoutHandles(ime.c)反过来写回线程布局指针,供 keyboard 管线下
    一次翻译使用。

25.2 与 msgqueue.c(分册 _8)

msgqueue.c 是 IME 的挂载点消费者co_IntProcessKeyboardMessage

IntImmProcessKey 并按返回值吞消息/改 VK_PROCESSKEY。同时

MessageQueue->afKeyStateIntCheckImeHotKey(组合修饰键)与

UserSetImeConversionKeyState(写 DBE 键状态)共享的键状态数组。

25.3 与 window.c / winpos.c

  • window.c :创建窗口时(L2441)触发默认 IME 窗口创建;销毁窗口时(L3029)触发
    销毁判定;IntImeSetTopMostIntUnlinkWindow/IntLinkHwnd/HWND_TOP 操作
    Z 序(与 winpos.c 的窗口链管理共用原语);
  • simplecall.cHWNDLOCK_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 Imm32BuildHimcListImmEnumInputContext 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 内反复出现:

  1. ProbeForRead/ProbeForWrite + _SEH2_TRY/_SEH2_EXCEPT :防止访问无效用户指针
    引发内核崩溃(如 IntGetImeHotKeyLanguageScore 读 TEB、co_IntCreateDefaultImeWindow
    写 pimeui、IntCheckImeShowStatus 读写 pimeui);
  2. KeAttachProcess / KeDetachProcess :跨进程访问前切换到目标进程的地址空间
    IntSendMessageToUIIntCheckImeShowStatusIntNotifyImeShowStatus 均成对使用);
  3. 引用保护 :跨进程窗口/对象先 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 的"输入三驾

马车"(鼠标/键盘/输入法)即全部打通。

相关推荐
ly76892 小时前
C 语言从入门到进阶:语法、指针、内存管理与工程实践详解
c语言·开发语言
Chester_19993 小时前
CSP202209.B何以包邮?
c语言·开发语言
caimouse3 小时前
ReactOS 窗口系统分析(13):滚动条 — scrollbar.c + scrollex.c
c语言·开发语言·reactos
caimouse6 小时前
ReactOS 窗口系统分析(23):键盘 — keyboard.c + kbdlayout.c
c语言·开发语言·计算机外设
动词ing14 小时前
【C语言】自定义函数+指针入门
c语言·开发语言·算法
动词ing15 小时前
【C语言】结构体+文件基础
c语言·开发语言·数据结构
caimouse16 小时前
ReactOS 窗口系统分析(7):分层窗口与绘制辅助 — layered.c + draw.c
c语言·开发语言
不灭的黄金瞳12319 小时前
预处理详解
c语言
小雪崩20 小时前
嵌入式学习 day25:哈希表及排序与查找
linux·c语言·数据结构·学习·排序算法