1189 lines
55 KiB
Markdown
1189 lines
55 KiB
Markdown
# 新增功能模块 / 模块重构
|
||
|
||
> 适用场景:新增模块、重大模块重构、核心架构能力演进。
|
||
> 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。
|
||
|
||
## 基本信息
|
||
|
||
- 模块 ID:Module-20261001-0001
|
||
- 模块名称:通用输入采集、焦点与键盘路由系统
|
||
- 状态:设计中(v0.13;0C 当前可用环境基线通过,0B 阶段 1 前置基线已关闭;微软拼音 Forward/Consume 有效,搜狗 Consume 请求按 `ime_condition_not_met` fail-open 并形成 `MustForward`;阶段 1 尚未开始)
|
||
- 类型:新增模块 / 架构演进
|
||
- 所属系统 / 子系统:StellarX Core / Window / Control / Input / Focus / TextBox
|
||
- 版本 / 分支:`master`,阶段 0A 实现基线 `c218c25`
|
||
- 环境:Windows + Visual Studio 2022 + MSVC v143 + EasyX + MBCS 工程配置
|
||
- 负责人:Codex 协作设计
|
||
|
||
## 背景与目标
|
||
|
||
- 背景:
|
||
- 当前框架已经具备 Window、Control、Canvas、Button、TextBox、Table、TabControl、Dialog 等基础能力,但尚未形成统一的输入采集、逻辑焦点和键盘定向路由体系。
|
||
- 当前 TextBox 仍依赖 EasyX 输入能力,焦点也不能继续作为 TextBox 独有状态;Button、Table 分页按钮、TabControl 页签按钮和 Dialog 都需要进入同一套键盘交互模型。
|
||
- 阶段 0 已确认 EasyX HWND/WndProc 与宿主 `peekmessage` 分属两个线程,当前窗口为 ANSI;微软拼音与搜狗的提交/回声顺序已经取得真实输入法证据;宽字符 GDI 绘制链与当前 96/144 DPI 坐标基线也已取得自动证据。
|
||
- 当前痛点:
|
||
- 缺少“哪个控件接收键盘”的统一规则。
|
||
- 键盘物理按键、字符输入和 IME 提交结果没有明确分层。
|
||
- Table、TabControl 等复合控件的内部 Button 缺少统一的 Tab 顺序和事件边界。
|
||
- Dialog 默认按钮、Escape 关闭、TextBox 编辑和 IME 候选操作可能争用同一按键。
|
||
- 控件删除、隐藏、禁用、切页和模态重入可能使裸指针焦点失效。
|
||
- 工程仍为 MBCS,现有文本链路不能直接承担完整 Unicode 编辑内核。
|
||
- 目标:
|
||
- 建立“输入采集与规范化 → 通用焦点 → 键盘定向路由 → 控件接入”的稳定分层。
|
||
- 让 Button、TextBox、Table 内部分页 Button、TabControl 页签 Button 等交互控件使用同一焦点系统。
|
||
- 建立不依赖控件裸指针的焦点和路由生命周期。
|
||
- 为 TextBox 自绘、UTF-16 编辑内核、剪贴板和 IME 提交提供统一输入基础。
|
||
- 在框架接管输入前,通过阶段 0 探测 EasyX 和 Win32 的真实消息链路。
|
||
- 非目标:
|
||
- v1 不自绘 IME 拼音组合串和候选列表,继续使用系统 IME UI。
|
||
- v1 不重写现有全部鼠标采集链;现有 EasyX 鼠标消息只在框架入口规范化为 `PointerEvent`。
|
||
- v1 不承诺完整 Unicode grapheme cluster 编辑,只保证不拆分 UTF-16 代理对。
|
||
- 本设计稿不代表框架代码已经实现,也不构成运行验证结果。
|
||
|
||
## 模块边界
|
||
|
||
- 职责:
|
||
- WndProc 同步窗口协议的就地应答。
|
||
- 键盘、字符、IME 和窗口激活消息的异步采集与规范化。
|
||
- 每个 Window 的 NativeInputQueue、单一 drainer 和嵌套模态循环输入所有权。
|
||
- 通用逻辑焦点、焦点作用域、Tab 顺序和焦点回退。
|
||
- 焦点目标键盘路由、祖先冒泡和 Dialog/Window 默认动作。
|
||
- Button、TextBox、Table、TabControl、Dialog 的统一键盘行为。
|
||
- UTF-16 文本输入、ANSI/DBCS 回退和 IME 正式提交去重。
|
||
- 不负责什么:
|
||
- 不替代操作系统键盘布局、IMM32 或系统候选窗口。
|
||
- 不让绘制控件变成原生 HWND 子控件。
|
||
- 不在路由层回溯修改已经结束的 WndProc 返回值。
|
||
- 不把非活动 Tab 页面作为可访问焦点树的一部分。
|
||
- 不在 v1 中把所有指针输入迁移到原生 WndProc 采集。
|
||
- 外部依赖:
|
||
- Win32 窗口消息和旧窗口过程。
|
||
- EasyX 消息循环和工作图像。
|
||
- IMM32:`ImmGetCompositionStringW`、`ImmSetCompositionWindow`、`ImmSetCandidateWindow`、`ImmNotifyIME`。
|
||
- 宽字符 GDI:`TextOutW` / `DrawTextW` / `GetTextExtentPoint32W`。
|
||
- 对外能力 / API:
|
||
- 本稿只确定职责和接口形状,公共 API 名称在阶段 0 完成后再落地。
|
||
- 控件通过焦点能力、键盘处理、文本输入和 Tab stop 收集接口接入。
|
||
- 关键数据 / 状态:
|
||
- `NativeInputEvent`、`KeyEvent`、`TextInputEvent`、`CompositionEvent`、`WindowActivationEvent`。
|
||
- `ControlHandle`、`FocusScope`、逻辑焦点句柄和窗口键盘激活状态。
|
||
- 每个 Window 的输入队列、drainer 所有者、dispatch/modal 深度。
|
||
- 每个 HWND 的 IME 状态、`compositionId`、结果回声和 ANSI 解码状态。
|
||
|
||
## 设计说明
|
||
|
||
### 1. 总体建设顺序
|
||
|
||
```text
|
||
阶段 0:输入链路探测
|
||
↓
|
||
阶段 1:输入采集与规范化
|
||
↓
|
||
阶段 2:通用焦点系统
|
||
↓
|
||
阶段 3:键盘定向路由
|
||
↓
|
||
阶段 4:Button / Table / TabControl / Dialog 接入
|
||
↓
|
||
阶段 5:TextBox 编辑内核与自绘
|
||
↓
|
||
阶段 6:IME、Unicode、DPI 完整接入
|
||
```
|
||
|
||
阶段 0 是实现门槛,不是可选调研。阶段 0A 的详细规格见:
|
||
|
||
- [`Module-20261001-0002-阶段0A输入链路被动探测规格.md`](./Module-20261001-0002-阶段0A输入链路被动探测规格.md)
|
||
|
||
#### 1.1 阶段 0 已确认事实(更新至 2026-10-06)
|
||
|
||
独立 `EasyX.ThreadProbe` 不链接 StellarX,只执行裸 `initgraph`、WndProc 子类化、固定键投递和 `peekmessage`。实测:
|
||
|
||
- main 在 `initgraph` 前后保持同一线程。
|
||
- EasyX HWND 属于另一个线程。
|
||
- 裸 WndProc 和固定键 WndProc 都在 HWND 属主线程执行。
|
||
- `peekmessage` 仍由 main/宿主线程调用。
|
||
- 固定键同时经过 WndProc 和 EasyX `EX_KEY` 两个观察面。
|
||
- `IsWindowUnicode(hwnd) == false`,当前 EasyX 窗口类为 ANSI;活动中文布局代码页实测为 936。
|
||
|
||
因此,双线程不是 UiFixture 或 StellarX 钩子制造的假象,而是当前 EasyX `initgraph` 的固有运行模型。阶段 1 不得再使用“WndProc 与宿主循环同线程”的假设。
|
||
|
||
微软拼音与搜狗真实人工日志进一步确认:
|
||
|
||
- 两者均以 `WM_IME_COMPOSITION + GCS_RESULTSTR` 提供正式提交结果。
|
||
- 当前 ANSI/CP936 窗口中,随后出现与结果完整匹配的 `WM_IME_CHAR` 回声;两种输入法均未形成可匹配的 `WM_CHAR` 回声。
|
||
- 微软拼音 3 次结果均为 2 个 scalar;搜狗 3 次结果分别为 7、2、6 个 scalar,证明去重不能依赖固定长度或文本内容。
|
||
- 两次“中英文/输入法切换”会话均未观察到 `WM_INPUTLANGCHANGE`。IME 内部模式、TSF 输入配置文件与 HKL/代码页不得混为同一状态。
|
||
- 原探测器遗漏了 `WM_INPUTLANGCHANGEREQUEST` 默认观察,v0.6 已补入 request/change 分层:request 只观察和转发,change 才更新活动代码页。
|
||
- v0.7 补观测后的 Win+Space 专用短测仍未出现 request/change;当前系统仅安装 `zh-Hans-CN` 语言标签,实际切换发生在同一中文 HKL/CP936 下的 TSF 输入配置文件之间,因此该结果确认“TSF 配置文件切换不保证产生 HKL 消息”,但不代表跨语言 HKL 切换也不会产生。
|
||
|
||
2026-10-05 的独立 `EasyX.VisualProbe` 进一步确认:
|
||
|
||
- EasyX 25.9.10 的 `TextOutW`、`DrawTextW`、`GetTextExtentPoint32W` 可直接写入主画面与 `SetWorkingImage` 工作图像,覆盖批次内外、BMP 中文、代理对、NFD 组合字符和混排;无需因当前 MBCS 工程默认降级 CP936 绘制。
|
||
- 主画面批次内绘制在 Flush 前不出现在客户区,Flush/End 后出现;工作图像不会自行上屏,必须显式 `putimage` 合成。
|
||
- 批次必须在主画面进入/退出;`SetWorkingImage(&working)` 后直接调用 `BeginBatchDraw()` 会触发 `0xC0000005`。安全顺序是“主画面 Begin → 批次内切工作图像 → 回主画面 → Flush/End”。
|
||
- 当前应用继承模式为 DPI-unaware,窗口 DPI 为 96;同一台 150% 显示器上的独立 Per-Monitor-V2 进程窗口 DPI 为 144。两种模式下 EasyX、原生 GDI 与 HWND 客户区使用相同数值坐标,未观察到 EasyX 额外隐式缩放,客户区/屏幕往返误差为 0。
|
||
- 上述坐标结论只覆盖当前显示器的 96 虚拟化视图和 144 物理视图;100%/125%、跨屏及真实候选框视觉位置仍不得宣称完成。
|
||
- 微软拼音三个合成 anchor 的真实截图未观察到相对 caret 的整体偏移;两轮人工操作实际均为微软拼音,脚本中的预期名称不能代替输入法身份事实。
|
||
- 搜狗真实运行证明候选 UI 可能已经出现,但 EasyX HWND 尚未收到探测器原先依赖的 `WM_IME_STARTCOMPOSITION/WM_IME_COMPOSITION`。因此位置设置不能只挂在 IME 生命周期消息上;必须在焦点/caret/anchor 变化时预设,并在生命周期消息到达时再校准。
|
||
- 修复后的搜狗运行中,F1/F2/F3 的 anchor 预设全部成功,F3 生命周期二次校准成功;F3 提交后回到 F1 的截图与初始 F1 裁剪字节一致。当前 96 DPI 虚拟化视图下,微软拼音与搜狗 0C-3 均通过人工视觉验收。
|
||
|
||
2026-10-06 的 0B 首轮 Composition 人工运行进一步确认:
|
||
|
||
- 操作者按 Escape 时,组合态 WndProc 观察到的是 `WM_KEYDOWN + VK_PROCESSKEY(229) + scanCode 1`,组合结束后的 KeyUp 才是 `VK_ESCAPE(27)`;因此原生采集规则必须允许用 message、VK 与 scan code 三元组精确描述,不能把 WndProc 层的 `VK_PROCESSKEY` 伪装成已恢复的逻辑 Escape。
|
||
- `ImmGetVirtualKey` 只允许应用在自身消息循环调用 `TranslateMessage` 前恢复原 VK;当前钩子已经位于 EasyX WndProc,不能依赖该 API 补救时机。
|
||
- 0B 夹具必须自己引用 0C 已验证的客户区 caret/line rect,并在焦点/IME context 与组合生命周期双时机设置组合窗和候选窗位置;否则“候选框异常”只证明夹具缺少定位,不能证明消费动作破坏 IME。
|
||
- 宿主安全点复制整批记录会与 WndProc 争用状态锁。0B 修正为双固定缓冲、锁内常数时间交换、锁外写盘;诊断采集不再用 `try-lock` 丢弃 WndProc 观察。
|
||
- 固定 IME 定位 setter 可能同步重入 WndProc。任何探测或生产输入实现都不得持有状态/输入队列锁调用 IMM32 setter;0B 使用“锁内提交父 enter 和不可变请求 → 锁外 IMM32 → 锁内校验会话并提交 `ime_position` 结果”的三阶段协议,并以线程局部标志阻止递归定位。
|
||
|
||
### 2. 分层架构
|
||
|
||
```text
|
||
Win32 / EasyX
|
||
│
|
||
▼
|
||
InputCollector
|
||
同步应答、异步采集、Unicode/DBCS 解码、IME 状态与去重
|
||
│
|
||
▼
|
||
NativeInputQueue
|
||
SPSC 语义的跨线程通道:EasyX 窗口线程生产,宿主线程消费
|
||
│
|
||
▼
|
||
InputDispatcher
|
||
单 drainer、模态重入、逐条出队
|
||
├───────────────┐
|
||
▼ ▼
|
||
FocusManager KeyboardRouter
|
||
└───────┬───────┘
|
||
▼
|
||
Button / TextBox / Table / TabControl / Dialog
|
||
```
|
||
|
||
鼠标 v1 暂不在 WndProc 重新采集。现有 EasyX 鼠标事件进入控件分发前转换成框架 `PointerEvent`,由 PointerDown 在命中焦点叶子时申请焦点。
|
||
|
||
### 3. 关键不变量
|
||
|
||
1. NativeInputQueue 是键盘、文本和 IME 的唯一业务输入源。
|
||
2. EasyX `EX_KEY` 只排空和观察,不得再次触发业务路由。
|
||
3. 同步窗口协议不得进入异步输入队列。
|
||
4. 异步路由只能阻止框架默认动作,不能倒过来改变已经结束的 WndProc 返回值。
|
||
5. 队列、焦点、Tab 顺序、路由路径和待删除列表长期只保存 `ControlHandle`。
|
||
6. 同一 Window 同时只能由最内层消息循环 drain 输入队列。
|
||
7. 每次只从队列取一条事件,不预取整个批次到局部容器。
|
||
8. WndProc 只在 EasyX 窗口线程采集;焦点、路由和控件操作只在宿主分发线程执行。
|
||
9. NativeInputQueue 必须提供跨线程发布/获取的 happens-before,不能使用无锁保护的普通 `std::deque`。
|
||
10. `sequence` 提供全局因果编号,但不能代替队列同步。
|
||
|
||
### 4. Win32 语义边界
|
||
|
||
#### 4.1 `TranslateMessage` 与 WndProc
|
||
|
||
标准 Win32 循环通常先执行 `TranslateMessage`,再通过 `DispatchMessage` 进入 WndProc。WndProc 对当前 `WM_KEYDOWN` 返回 0 不能回溯撤销已经产生的字符消息。
|
||
|
||
真正需要探测的是 EasyX 是否调用 `TranslateMessage`、调用顺序是否标准、普通循环和模态循环是否一致。因此 v1 默认采用“采集 + 转发”,只允许对实测确认后的窄白名单消费。
|
||
|
||
#### 4.2 `WM_UNICHAR`
|
||
|
||
设计不假设 `WM_UNICHAR` 只会到达某一种窗口:
|
||
|
||
- `UNICODE_NOCHAR` 是同步能力查询,支持真实 `WM_UNICHAR` 时立即返回 `TRUE`。
|
||
- 真实 `WM_UNICHAR` 使用 UTF-32,收到后统一校验并转换为 UTF-16。
|
||
- 真实字符若由框架处理,则返回 0,防止 DefWindowProc 再产生 `WM_CHAR`。
|
||
- 阶段 0 记录其实际来源,不把未被微软文档承诺的系统投递规律写成框架契约。
|
||
|
||
#### 4.3 `WM_IME_SETCONTEXT`
|
||
|
||
IME UI 由“是否转发、转发时保留哪些 `ISC_SHOWUI*` 标志、被调用窗口过程的返回值”共同决定。
|
||
|
||
v1 使用系统 IME UI:
|
||
|
||
1. 不修改 `lParam`。
|
||
2. 调用旧窗口过程。
|
||
3. 返回旧窗口过程结果。
|
||
|
||
未来自绘组合串时,才在转发前清除 `ISC_SHOWUICOMPOSITIONWINDOW` 等由框架接管的标志。
|
||
|
||
### 5. WndProc 消息分类
|
||
|
||
#### 5.1 同步窗口协议
|
||
|
||
以下消息必须就地处理,不进入 NativeInputQueue:
|
||
|
||
| 消息 | v1 行为 |
|
||
|---|---|
|
||
| `WM_GETDLGCODE` | 根据当前框架焦点能力,选择性合并并返回 `DLGC_WANTTAB/WANTARROWS/WANTCHARS` 等 |
|
||
| `WM_UNICHAR + UNICODE_NOCHAR` | 支持真实 `WM_UNICHAR` 时立即返回 `TRUE` |
|
||
| `WM_IME_SETCONTEXT` | 原样转发旧窗口过程并返回其结果 |
|
||
| `WM_SETCURSOR` | 框架拥有命中区域光标时就地设置,否则转发 |
|
||
| `WM_MOUSEACTIVATE` | 根据激活策略同步返回 |
|
||
| `WM_GETMINMAXINFO` | 保留现有同步处理 |
|
||
| `WM_SIZING` | 保留现有同步处理 |
|
||
| `WM_ERASEBKGND` | 保留现有同步处理 |
|
||
| `WM_NCHITTEST` 等 | 返回值直接影响当前系统行为的消息保持同步 |
|
||
|
||
`WM_GETDLGCODE` 不得无条件返回 `DLGC_WANTALLKEYS`,必须结合当前焦点能力、`wParam` 和可选 `MSG*` 选择性声明。
|
||
|
||
#### 5.2 异步输入消息
|
||
|
||
| 消息 | 规范化结果 | 初始处置 |
|
||
|---|---|---|
|
||
| `WM_KEYDOWN/UP` | `KeyEvent` | 采集后转发 |
|
||
| `WM_SYSKEYDOWN/UP` | `KeyEvent` | 采集后转发,保留系统组合键 |
|
||
| `WM_CHAR` | `TextInputEvent` 候选来源 | 解码、去重后转发 |
|
||
| 真实 `WM_UNICHAR` | UTF-32 转 UTF-16 | 采集并返回 0 |
|
||
| `WM_DEADCHAR` | 只记录状态 | 不产生文本,继续转发 |
|
||
| `WM_SYSCHAR/SYSDEADCHAR` | 通常不进入控件 | 继续转发 |
|
||
| `WM_IME_STARTCOMPOSITION` | `CompositionEvent::Start` | 采集后转发 |
|
||
| `WM_IME_COMPOSITION` | Update / Commit / Cancel | 采集后转发 |
|
||
| `WM_IME_ENDCOMPOSITION` | `CompositionEvent::End` | 采集后转发 |
|
||
| `WM_IME_CHAR` | IME 回声或兼容来源 | 观察、去重、转发 |
|
||
| `WM_SETFOCUS/KILLFOCUS` | `WindowActivationEvent` | 采集后转发 |
|
||
| `WM_INPUTLANGCHANGEREQUEST` | 输入布局切换请求 | 观察请求方向与目标 HKL,原样转发,不提前更新代码页 |
|
||
| `WM_INPUTLANGCHANGE` | 更新输入代码页状态 | 同步更新状态并转发 |
|
||
|
||
#### 5.3 消费策略
|
||
|
||
初始策略:
|
||
|
||
```text
|
||
KeyDown/KeyUp:采集 + 转发
|
||
WM_CHAR:采集 + 转发
|
||
IME 消息:采集 + 转发
|
||
真实 WM_UNICHAR:采集 + 消费
|
||
```
|
||
|
||
只有阶段 0 证明转发会产生重复行为或原生副作用后,才允许建立窄白名单,例如:
|
||
|
||
- 非组合态 Tab。
|
||
- 非组合态且框架明确拥有的方向键。
|
||
- 非组合态 Button 的 Enter/Space。
|
||
- 框架明确拥有的 Ctrl 快捷键。
|
||
|
||
硬规则:`imeComposingAtCapture == true` 或原始 `virtualKey == VK_PROCESSKEY` 的键不能进入消费白名单,也不得进入 Button、Dialog、快捷键或焦点导航等业务路由。后者不依赖标准 Composition 生命周期,因为搜狗实测已证明候选 UI 可见时目标 HWND 仍可能保持 `imeState=idle`。
|
||
|
||
### 6. 输入事件模型
|
||
|
||
#### 6.1 `KeyEvent`
|
||
|
||
```cpp
|
||
struct KeyEvent
|
||
{
|
||
KeyAction action; // Down / Up
|
||
|
||
UINT virtualKey; // 消息原始 VK
|
||
UINT physicalVirtualKey; // 区分左右修饰键后的 VK
|
||
UINT scanCode;
|
||
|
||
uint16_t repeatCount;
|
||
bool extended;
|
||
bool wasDown;
|
||
bool altContext;
|
||
bool systemKey;
|
||
|
||
bool imeComposingAtCapture;
|
||
|
||
KeyModifiers modifiers; // left/right Ctrl/Shift/Alt
|
||
};
|
||
```
|
||
|
||
规则:
|
||
|
||
```cpp
|
||
bool isAutoRepeat = action == KeyAction::Down && wasDown;
|
||
```
|
||
|
||
- `modifiers.alt()` 是业务逻辑依据。
|
||
- `altContext` 只保留 Win32 lParam bit 29 原始值。
|
||
- 左右 Ctrl/Alt 使用 extended 位,Shift 使用 scan code 区分。
|
||
- Button 判断首次按下使用 `wasDown`,不使用 `repeatCount > 1` 代替。
|
||
|
||
#### 6.2 `TextInputEvent`
|
||
|
||
```cpp
|
||
enum class TextSource
|
||
{
|
||
ImeResult,
|
||
UnicodeChar,
|
||
UnicodeScalar,
|
||
AnsiCharFallback,
|
||
Clipboard
|
||
};
|
||
|
||
struct TextInputEvent
|
||
{
|
||
std::wstring text;
|
||
TextSource source;
|
||
uint64_t compositionId; // 非 IME 路径为 0
|
||
};
|
||
```
|
||
|
||
该事件只表示已经形成的文字,不表示物理按键。
|
||
|
||
#### 6.3 `CompositionEvent`
|
||
|
||
```cpp
|
||
enum class CompositionStage
|
||
{
|
||
Start,
|
||
Update,
|
||
Commit,
|
||
Cancel,
|
||
End
|
||
};
|
||
|
||
struct CompositionEvent
|
||
{
|
||
CompositionStage stage;
|
||
uint64_t compositionId;
|
||
std::wstring compositionText;
|
||
uint32_t caretOffset;
|
||
};
|
||
```
|
||
|
||
`Commit` 只描述 IME 状态变化。真正插入 TextBox 的内容仍然只通过一条 `TextInputEvent` 发送。
|
||
|
||
#### 6.4 `WindowActivationEvent`
|
||
|
||
```cpp
|
||
struct WindowActivationEvent
|
||
{
|
||
bool keyboardFocusActive;
|
||
HWND otherWindow;
|
||
};
|
||
```
|
||
|
||
窗口失活时可以保留逻辑焦点句柄,但 `keyboardFocusActive == false` 时不路由键盘。窗口重新激活后先验证句柄,失效则执行焦点回退。
|
||
|
||
#### 6.5 队列外壳
|
||
|
||
```cpp
|
||
struct NativeInputEvent
|
||
{
|
||
uint64_t sequence;
|
||
DWORD messageTime;
|
||
HWND hwnd;
|
||
|
||
std::variant<
|
||
KeyEvent,
|
||
TextInputEvent,
|
||
CompositionEvent,
|
||
WindowActivationEvent
|
||
> payload;
|
||
};
|
||
```
|
||
|
||
键盘、文本和 IME 事件不合并、不静默丢弃。
|
||
|
||
#### 6.6 跨线程队列模型
|
||
|
||
当前拓扑是单生产者/单消费者:
|
||
|
||
```text
|
||
EasyX HWND/WndProc 线程
|
||
└─ InputCollector / push
|
||
↓
|
||
NativeInputQueue
|
||
↓
|
||
宿主 main/runEventLoop 线程
|
||
└─ 最内层 drainer / pop one
|
||
```
|
||
|
||
阶段 1 的正确性基线采用 `mutex + std::deque`:
|
||
|
||
1. WndProc 每次只在短临界区内移动一条完整事件入队。
|
||
2. 宿主每次只在短临界区内弹出一条事件,释放锁后再执行路由、回调和控件 mutation。
|
||
3. 禁止在持锁期间调用旧窗口过程、IMM、用户回调、控件方法或文件 I/O。
|
||
4. 当前宿主循环持续轮询,不额外增加唤醒;未来改成阻塞等待时再使用 Event/条件变量。
|
||
5. 队列不得静默丢输入;若以后改为固定容量 lock-free SPSC,必须先定义文本负载所有权、满队列和分配失败策略。
|
||
6. 若改为 lock-free SPSC,生产者写完 slot 后以 release 发布写索引,消费者以 acquire 读取;回收索引反向使用同等内存序。
|
||
7. 全局 `sequence` 使用 atomic 分配,但事件可见性由互斥锁或 release/acquire 队列协议保证。
|
||
|
||
探测器使用的固定双缓冲和 `records_lost` 失效判定只适用于诊断日志,不能照搬为生产输入队列;生产输入事件比探测记录具有更强的不丢失要求。诊断路径同样禁止持锁调用旧窗口过程、IMM32 setter、用户回调或文件 I/O。
|
||
|
||
### 7. Unicode 和文字来源
|
||
|
||
#### 7.1 窗口属性
|
||
|
||
初始化时记录:
|
||
|
||
```cpp
|
||
bool unicodeWindow = IsWindowUnicode(hwnd);
|
||
```
|
||
|
||
窗口类 A/W 属性决定 `WM_CHAR` 是 UTF-16 还是当前进程代码页数据,但不用于拒绝真实 `WM_UNICHAR`。
|
||
|
||
#### 7.2 正式提交优先级
|
||
|
||
```text
|
||
1. ImmGetCompositionStringW(GCS_RESULTSTR)
|
||
2. Unicode HWND 的 WM_CHAR
|
||
3. 真实 WM_UNICHAR
|
||
4. ANSI HWND 的状态化 WM_CHAR / WM_IME_CHAR 回退
|
||
```
|
||
|
||
`ImmGetCompositionStringW` 返回的是字节数,即使使用 W 版本也必须换算为 UTF-16 code unit 数量。
|
||
|
||
#### 7.3 IME 结果去重
|
||
|
||
收到 `GCS_RESULTSTR` 时:
|
||
|
||
1. 立即产生唯一的 `TextInputEvent`。
|
||
2. 记录当前 `compositionId`。
|
||
3. 保存预期 UTF-16 或 ANSI 回声序列。
|
||
4. 后续紧邻且匹配的 `WM_IME_CHAR/WM_CHAR` 只观察和转发,不再次入队。
|
||
5. 遇到不匹配字符、新组合、焦点切换或越过本次结果边界时清空回声状态。
|
||
|
||
不能使用“字符内容相同就去重”,否则连续输入两个相同汉字时会误删第二个。具体回声顺序以阶段 0 日志决定。
|
||
|
||
#### 7.4 ANSI / DBCS 回退
|
||
|
||
```cpp
|
||
struct AnsiDecoderState
|
||
{
|
||
UINT codePage;
|
||
std::optional<BYTE> pendingLead;
|
||
};
|
||
```
|
||
|
||
处理规则:
|
||
|
||
1. 使用当前实际 ANSI 代码页。
|
||
2. 使用 `IsDBCSLeadByteEx` 判断 lead byte。
|
||
3. lead byte 暂存,不立即形成文本。
|
||
4. 下一字节作为 trail。
|
||
5. 使用 `MultiByteToWideChar` 严格转换。
|
||
6. 转换失败时清除状态并记录诊断。
|
||
7. 焦点切换、窗口失活、IME 开始时清除残留 `pendingLead`。
|
||
8. 收到 `WM_INPUTLANGCHANGEREQUEST` 时只记录目标布局并转发,不提前改变解码状态。
|
||
9. 收到 `WM_INPUTLANGCHANGE` 时,必须先清除 `pendingLead`,再更新活动代码页,禁止跨代码页拼接字节。
|
||
10. 窗口挂接和重新获得键盘焦点时,按 HWND 属主线程调用 `GetKeyboardLayout` 做防御性快照;若快照与当前 HKL/代码页不同,同样先清 `pendingLead` 再更新,并记录状态来源。该快照只用于修复消息遗漏或失焦期间变化,不把 TSF 配置文件误建模为代码页变化。
|
||
|
||
`WM_IME_CHAR` 在 ANSI 窗口可能携带一个双字节字符;转发给 DefWindowProc 后又可能拆成两个 `WM_CHAR`,因此必须纳入结果去重状态机。
|
||
|
||
### 8. IME 状态机
|
||
|
||
#### 8.1 v1 策略
|
||
|
||
- 保留系统组合串和候选框 UI。
|
||
- TextBox 自绘已提交文本、选区和光标。
|
||
- 框架设置系统 IME UI 的位置。
|
||
- `WM_IME_SETCONTEXT` 原样转发。
|
||
- v1 不清除 `ISC_SHOWUICOMPOSITIONWINDOW`。
|
||
|
||
#### 8.2 `compositionId`
|
||
|
||
每个 Window 维护独立的单调递增 `uint64_t nextCompositionId`,0 保留给非 IME 输入。
|
||
|
||
```text
|
||
WM_IME_STARTCOMPOSITION:
|
||
compositionId = ++nextCompositionId
|
||
```
|
||
|
||
如果在 Idle 状态直接收到 `WM_IME_COMPOSITION`,必须惰性分配新的 `compositionId`。该 ID 不得沿用上一次 Commit 或 Cancel 的组合,避免迟到或乱序消息污染已经结束的状态。
|
||
|
||
#### 8.3 状态流
|
||
|
||
```text
|
||
Idle
|
||
└─ WM_IME_STARTCOMPOSITION → 分配 id → Composing
|
||
|
||
Composing
|
||
├─ GCS_COMPSTR → Update
|
||
├─ GCS_RESULTSTR → Commit + TextInputEvent
|
||
├─ 无任何 GCS_* → Cancel
|
||
└─ WM_IME_ENDCOMPOSITION → End → Idle
|
||
```
|
||
|
||
#### 8.4 Cancel 来源
|
||
|
||
`CompositionStage::Cancel` 由以下情况合成:
|
||
|
||
1. 活跃组合中收到无 `GCS_*` 的 `WM_IME_COMPOSITION`。
|
||
2. 框架调用 `ImmNotifyIME(..., NI_COMPOSITIONSTR, CPS_CANCEL, 0)` 成功。
|
||
3. 收到 `WM_IME_ENDCOMPOSITION`,但本次组合没有 Commit。
|
||
|
||
每个 `compositionId` 最多产生一次 Cancel。主动取消成功后,后续系统消息不能再次发出 Cancel。
|
||
|
||
#### 8.5 组合期间按键
|
||
|
||
组合开始后:
|
||
|
||
1. 取消 Button 等控件残留的键盘按压态。
|
||
2. KeyEvent 继续采集并转发旧窗口过程。
|
||
3. KeyEvent 不发送给控件命令处理器。
|
||
4. 不执行 Tab 导航。
|
||
5. 不执行 Dialog 的 Enter/Escape。
|
||
6. 不切换 TabControl 页签。
|
||
7. 文本只接受 IME 正式提交结果。
|
||
|
||
判断依据是事件自己的 `imeComposingAtCapture`,不是稍后读取的全局状态。
|
||
|
||
### 9. IME 位置与 DPI
|
||
|
||
TextBox 提供:
|
||
|
||
```cpp
|
||
struct TextInputAnchor
|
||
{
|
||
POINT caretTopLeft;
|
||
RECT caretLineRect;
|
||
};
|
||
```
|
||
|
||
v1 使用:
|
||
|
||
```text
|
||
COMPOSITIONFORM:
|
||
CFS_POINT
|
||
ptCurrentPos = caretBottom
|
||
|
||
CANDIDATEFORM:
|
||
CFS_EXCLUDE
|
||
ptCurrentPos = caretBottom
|
||
rcArea = 当前文字行矩形
|
||
```
|
||
|
||
DPI 规则:
|
||
|
||
1. 记录进程 DPI awareness 和 `GetDpiForWindow(hwnd)`。
|
||
2. 验证 EasyX 绘制坐标是否与目标 HWND 坐标空间一致。
|
||
3. 一致时直接使用,不手工乘 DPI 比例。
|
||
4. 只有实测发现坐标空间不同才做明确转换。
|
||
5. 验证 100%、125%、150% 和跨显示器移动。
|
||
|
||
当前 0C-2 证据允许把第 3 条落实为当前默认:在已验证的 DPI-unaware 96 视图和 Per-Monitor-V2 144 视图中,`caretBottom` 与 `caretLineRect` 直接使用目标 HWND 客户区坐标,不再手工乘 DPI 比例。该默认必须在 0C-3 真实 IME 和尚缺的 100%/125%/跨屏矩阵中继续验证;发现反例时只能增加带 awareness 适用范围的显式换算,不能全局补乘比例。
|
||
|
||
位置刷新规则:
|
||
|
||
1. TextBox 获得焦点、caret 移动、选择变化导致 caret 改变、滚动改变可视 caret,或输入法/输入配置发生可观察变化时,立即把最新 anchor 写入当前 IME context。
|
||
2. `WM_IME_STARTCOMPOSITION/WM_IME_COMPOSITION` 到达时再次写入,校准 IME 在组合开始时重置位置的实现差异。
|
||
3. 两类写入使用同一份 `TextInputAnchor` 和 HWND 客户区坐标,不建立两套换算公式。
|
||
4. 不把“未收到 IME 生命周期消息”等同于“没有候选 UI”;搜狗实测已经否定该假设。
|
||
|
||
### 10. 通用焦点系统
|
||
|
||
#### 10.1 焦点能力
|
||
|
||
```cpp
|
||
enum class FocusPolicy
|
||
{
|
||
None,
|
||
Programmatic,
|
||
Click,
|
||
TabAndClick
|
||
};
|
||
```
|
||
|
||
| 控件 | 焦点角色 |
|
||
|---|---|
|
||
| Label | 不可聚焦 |
|
||
| Canvas | 透明焦点容器 |
|
||
| Button | 焦点叶子 |
|
||
| TextBox | 焦点叶子 |
|
||
| Table | 焦点容器,暴露内部翻页 Button |
|
||
| TabControl | 页签焦点组和活动页面容器 |
|
||
| Dialog | 焦点作用域 |
|
||
| Window | 根焦点作用域 |
|
||
|
||
#### 10.2 `ControlHandle`
|
||
|
||
```cpp
|
||
struct ControlHandle
|
||
{
|
||
WindowId owner;
|
||
uint32_t slot;
|
||
uint32_t generation;
|
||
};
|
||
```
|
||
|
||
- slot 重用必须递增 generation。
|
||
- 失效句柄解析为空。
|
||
- `collectTabStops()` 返回 `ControlHandle`。
|
||
- `Control*` 只允许作为当前函数内立即使用的借用视图,不得跨事件、回调、mutation transaction 或模态调用保存。
|
||
|
||
#### 10.3 焦点有效条件
|
||
|
||
```text
|
||
句柄有效
|
||
&& effectiveVisible
|
||
&& effectiveEnabled
|
||
&& FocusPolicy 允许
|
||
&& 位于最上层活动 FocusScope
|
||
&& 位于活动 Tab 页面
|
||
&& 不在 pending removal
|
||
```
|
||
|
||
#### 10.4 焦点切换
|
||
|
||
```cpp
|
||
FocusChangeResult requestFocus(
|
||
ControlHandle target,
|
||
FocusReason reason);
|
||
```
|
||
|
||
顺序:
|
||
|
||
1. 验证目标。
|
||
2. 保存旧句柄。
|
||
3. 更新 FocusManager。
|
||
4. 向旧目标发送 `FocusLost`。
|
||
5. 向新目标发送 `FocusGained`。
|
||
6. 使相关区域失效并重绘。
|
||
7. 新目标为 TextBox 时更新 IME 锚点。
|
||
|
||
#### 10.5 焦点失效回退
|
||
|
||
焦点控件被隐藏、禁用、删除或移入非活动页时:
|
||
|
||
```text
|
||
同一作用域下一个可聚焦项
|
||
→ 上一个可聚焦项
|
||
→ 作用域 initialFocus
|
||
→ 无焦点
|
||
```
|
||
|
||
控件进入 pending removal 前,必须先让焦点系统中的对应句柄失效。
|
||
|
||
#### 10.6 Dialog 作用域
|
||
|
||
Dialog 打开:
|
||
|
||
1. 压入新 FocusScope。
|
||
2. 保存外层焦点句柄。
|
||
3. 选择 `initialFocus`,否则选择第一个 Tab stop。
|
||
4. 完成作用域建立后才允许模态循环 drain。
|
||
|
||
Dialog 关闭:
|
||
|
||
1. 停止内层 drain。
|
||
2. 弹出作用域。
|
||
3. 验证并恢复原焦点。
|
||
4. 原焦点失效时执行外层回退。
|
||
|
||
### 11. Tab 顺序与复合控件
|
||
|
||
#### 11.1 Tab 顺序
|
||
|
||
顺序由显式 `tabIndex` 和控件树稳定顺序决定,过滤:
|
||
|
||
- 隐藏控件。
|
||
- 禁用控件。
|
||
- pending removal 控件。
|
||
- 非活动页面控件。
|
||
- FocusPolicy 不包含 Tab 的控件。
|
||
|
||
Tab 只在当前最上层 FocusScope 内循环。
|
||
|
||
#### 11.2 Table
|
||
|
||
Table 本身不是焦点叶子,通过 `collectTabStops()` 暴露上一页和下一页 Button。
|
||
|
||
- 第一页时上一页 Button 禁用并退出 Tab 顺序。
|
||
- 最后一页时下一页 Button 禁用并退出 Tab 顺序。
|
||
- 翻页后当前焦点 Button 若被禁用,立即执行焦点回退。
|
||
- 翻页 Button 只向 Table 和外层祖先冒泡,不能把按键转发到表格内容。
|
||
|
||
#### 11.3 TabControl
|
||
|
||
页签使用 roving-tab-stop:
|
||
|
||
- 只有当前选中页签位于外部 Tab 顺序。
|
||
- Tab 进入 TabControl 时先到当前页签。
|
||
- Left/Right 在页签组内移动并激活页签。
|
||
- Tab 从页签进入活动页面第一个焦点项。
|
||
- Shift+Tab 从页面第一个焦点项回到活动页签。
|
||
- 非活动页面完全退出焦点树。
|
||
|
||
页签 Button 与页面是兄弟关系:
|
||
|
||
```text
|
||
页签 Button → TabControl → 外层容器
|
||
```
|
||
|
||
禁止:
|
||
|
||
```text
|
||
页签 Button → 活动页面
|
||
```
|
||
|
||
活动页面控件可以向祖先 TabControl 冒泡,但 TabControl 只有在原始焦点是页签 Button 时才处理 Left/Right。
|
||
|
||
### 12. 键盘路由
|
||
|
||
#### 12.1 总流程
|
||
|
||
```text
|
||
取出一条事件
|
||
→ 验证窗口激活状态
|
||
→ IME 预处理
|
||
→ Tab 导航
|
||
→ 锁定原始焦点句柄
|
||
→ 构造句柄路由路径
|
||
→ 焦点叶子
|
||
→ 父容器冒泡
|
||
→ FocusScope 默认动作
|
||
→ Window 默认动作
|
||
→ 提交当前事件产生的 mutation
|
||
```
|
||
|
||
`TextInputEvent` 只发送给实现文本输入接口的当前焦点叶子,不冒泡。
|
||
|
||
#### 12.2 路由结果
|
||
|
||
```cpp
|
||
struct KeyRouteResult
|
||
{
|
||
bool stopBubble;
|
||
bool preventFrameworkDefault;
|
||
};
|
||
```
|
||
|
||
- `stopBubble`:不再交给父容器。
|
||
- `preventFrameworkDefault`:冒泡结束后不再执行 Dialog/Window 默认动作。
|
||
- 两者均不影响已经完成的原生 WndProc 返回值。
|
||
|
||
#### 12.3 路由路径与 mutation
|
||
|
||
事件开始时使用句柄构造路径快照:
|
||
|
||
```text
|
||
FocusedLeaf → Parent → ... → FocusScope → Window
|
||
```
|
||
|
||
每个节点使用前重新解析。处理过程中删除控件时,无效节点被跳过;当前事件仍使用原始路径,下一事件才使用新焦点和新树结构。
|
||
|
||
### 13. 按键归属
|
||
|
||
#### 13.1 Tab
|
||
|
||
- IME 组合中交给 IME,不移动焦点。
|
||
- 非组合态由 FocusManager 处理。
|
||
- Shift+Tab 反向移动。
|
||
- 普通 Button/TextBox 不直接接收 Tab。
|
||
|
||
#### 13.2 Space
|
||
|
||
Button:
|
||
|
||
```text
|
||
首次 KeyDown → armed + pressed
|
||
重复 KeyDown → 忽略
|
||
KeyUp 且仍聚焦、仍 armed → 触发一次 click
|
||
焦点丢失或控件禁用 → 取消 armed
|
||
```
|
||
|
||
TextBox 不从 KeyEvent 插入空格,只接受 `TextInputEvent(U+0020)`。
|
||
|
||
#### 13.3 Enter
|
||
|
||
```text
|
||
IME
|
||
→ 聚焦 Button
|
||
→ 聚焦控件自定义动作
|
||
→ Dialog 默认按钮
|
||
→ Window 默认动作
|
||
```
|
||
|
||
- 单行 TextBox 不插入换行,默认不阻止 Dialog 默认按钮。
|
||
- Button 只在首次 KeyDown 激活,自动重复不能重复点击。
|
||
|
||
#### 13.4 Escape
|
||
|
||
```text
|
||
IME 取消
|
||
→ Button 清除按压态
|
||
→ 子控件局部取消
|
||
→ Dialog 关闭
|
||
```
|
||
|
||
- Button 清除 pressed 只恢复视觉状态,不阻止 Dialog 默认动作。
|
||
- 子控件存在真正局部取消状态时,可以设置 `preventFrameworkDefault=true`。
|
||
- 普通文本选区不建议定义为 Escape 可取消状态。
|
||
|
||
#### 13.5 方向键
|
||
|
||
```text
|
||
TextBox 光标/选区
|
||
→ 原始焦点为页签 Button 时由 TabControl 切页
|
||
→ 其他容器自定义导航
|
||
```
|
||
|
||
#### 13.6 Ctrl 快捷键与控制字符
|
||
|
||
TextBox v1 处理 `Ctrl+A/C/X/V`。这些命令走 KeyEvent;相应 `WM_CHAR 0x01–0x1A` 即使形成 TextInputEvent 也不得插入文本。
|
||
|
||
#### 13.7 Button 触发时机注释
|
||
|
||
Button 的 Space 在 KeyUp 确认、Enter 在首次 KeyDown 确认是有意设计:
|
||
|
||
- Space 对应按压释放语义,允许按住后因焦点移走或状态变化而放弃。
|
||
- Enter 对应默认按钮即时确认语义。
|
||
|
||
禁止为了表面一致把两者统一到同一个触发时机。
|
||
|
||
### 14. TextBox 编辑内核与自绘
|
||
|
||
#### 14.1 内部模型
|
||
|
||
```cpp
|
||
class TextEditModel
|
||
{
|
||
std::wstring text;
|
||
size_t caret;
|
||
size_t selectionAnchor;
|
||
size_t selectionActive;
|
||
size_t horizontalScroll;
|
||
};
|
||
```
|
||
|
||
最小 Unicode 保证:
|
||
|
||
- 光标不能落在代理项中间。
|
||
- Backspace/Delete 不能删除半个代理对。
|
||
- UTF-32 输入拒绝大于 `U+10FFFF` 和代理区值。
|
||
- v1 暂不承诺完整 grapheme cluster 删除语义。
|
||
|
||
#### 14.2 KeyEvent 与 TextInputEvent 分工
|
||
|
||
KeyEvent 处理:
|
||
|
||
- Left/Right、Home/End。
|
||
- Shift 选区。
|
||
- Ctrl+A/C/X/V。
|
||
- Backspace/Delete。
|
||
|
||
TextInputEvent 处理:
|
||
|
||
- 普通字符。
|
||
- 空格。
|
||
- IME 正式结果。
|
||
- 剪贴板文字。
|
||
|
||
#### 14.3 单行控制字符
|
||
|
||
单行 TextBox 默认拒绝 `U+0000–U+001F` 和 `U+007F`。Backspace、Tab、Enter、Escape 走 KeyEvent,不通过 TextInputEvent 插入。
|
||
|
||
粘贴建议规范:
|
||
|
||
```text
|
||
CRLF / CR / LF → 一个空格
|
||
Tab → 一个空格
|
||
其他控制字符 → 删除
|
||
```
|
||
|
||
#### 14.4 自绘顺序
|
||
|
||
```text
|
||
背景
|
||
→ 边框
|
||
→ 选区背景
|
||
→ 文本
|
||
→ 光标
|
||
→ 焦点边框
|
||
```
|
||
|
||
必须支持内容裁剪、水平滚动、光标可见、选区颜色、焦点/窗口失活样式和 IME 锚点刷新。
|
||
|
||
#### 14.5 宽字符绘制
|
||
|
||
首选:
|
||
|
||
```text
|
||
TextOutW / DrawTextW
|
||
GetTextExtentPoint32W
|
||
```
|
||
|
||
直接操作 EasyX 当前工作图像对应的 HDC。MBCS 工程配置不等于必须把内部文本降级成 CP936。
|
||
|
||
阶段 0 必须验证:
|
||
|
||
- W API 是否实际画入 EasyX 当前 working image。
|
||
- `SetWorkingImage` 后 HDC 是否指向正确图像。
|
||
- 中文、代理对和字体回退。
|
||
- 宽字符测量与实际绘制是否一致。
|
||
|
||
#### 14.6 CP936 显示降级
|
||
|
||
只有宽字符 GDI 实测不可用时才使用 `WideCharToMultiByte(CP936, WC_NO_BEST_FIT_CHARS, ...)`。
|
||
|
||
- 无法表示的字符显示 `?`。
|
||
- 检查 `usedDefaultChar`。
|
||
- 内部 `std::wstring` 原文不变。
|
||
- 维护 UTF-16 索引到绘制字节偏移的映射。
|
||
- 这是显示降级,不是文本存储降级。
|
||
|
||
旧 `std::string` API 可保留兼容,同时增加宽字符接口;旧接口在明确的 CP936 边界转换,不能污染内部模型。
|
||
|
||
### 15. 模态循环与单 drainer
|
||
|
||
不能使用简单的 `bool managedDispatchActive` 表示输入调度状态,需改为深度和所有者:
|
||
|
||
```cpp
|
||
struct DispatchState
|
||
{
|
||
uint32_t dispatchDepth;
|
||
uint32_t modalDepth;
|
||
DrainToken activeDrainer;
|
||
};
|
||
```
|
||
|
||
规则:
|
||
|
||
1. 外层循环持有 drainer。
|
||
2. Dialog 进入模态前先压入 FocusScope。
|
||
3. 最内层模态循环接管 drainer。
|
||
4. 外层停止取下一条事件。
|
||
5. 内层退出后归还 drainer。
|
||
6. 外层继续剩余队列。
|
||
7. 每次只弹一条事件。
|
||
8. 禁止一次性预取整个队列。
|
||
|
||
模态窗口可能在 Button 回调中打开,因此 pending removal 中的句柄立即失效,但对象内存直到最外层 `dispatchDepth == 0` 才回收。任何借用裸指针都不得跨越 `showModal()`。
|
||
|
||
### 16. 控件行为矩阵
|
||
|
||
| 控件 | 鼠标焦点 | Tab 焦点 | Enter | Space | Escape | 方向键 |
|
||
|---|---:|---:|---|---|---|---|
|
||
| Button | 是 | 是 | 首次 KeyDown 激活 | KeyUp 激活 | 清 pressed,默认动作可继续 | 默认不处理 |
|
||
| TextBox | 是 | 是 | 单行不插入 | TextInput 插入 | 局部状态决定 | 光标/选区 |
|
||
| Table | 否 | 否 | 内部 Button | 内部 Button | 冒泡 | 可扩展 |
|
||
| Table 翻页 Button | 是 | 是 | 翻页 | 翻页 | 清 pressed | 不处理 |
|
||
| Tab 页签 Button | 是 | roving | 激活页签 | 激活页签 | 冒泡 | 切换页签 |
|
||
| Canvas | 否 | 否 | 冒泡 | 冒泡 | 冒泡 | 冒泡 |
|
||
| Dialog | 作用域 | 作用域 | 默认按钮 | 无 | 关闭 | 无 |
|
||
| Label | 否 | 否 | 无 | 无 | 无 | 无 |
|
||
|
||
### 17. 阶段 0 门槛
|
||
|
||
在实现 FocusManager 前必须验证:
|
||
|
||
1. `IsWindowUnicode(hwnd)` 实际结果。
|
||
2. EasyX 主循环是否经过当前 WndProc。
|
||
3. WndProc 和 EasyX `EX_KEY` 是否重复报告。
|
||
4. EasyX 是否调用 `TranslateMessage`。
|
||
5. `GCS_RESULTSTR`、`WM_IME_CHAR`、`WM_CHAR` 的真实顺序。
|
||
6. 微软拼音和搜狗输入法差异。
|
||
7. 消费 `WM_KEYDOWN` 是否影响 EasyX 或 IME 状态。
|
||
8. 是否收到 `WM_GETDLGCODE`。
|
||
9. ANSI 中文是否拆成 lead/trail `WM_CHAR`。
|
||
10. 鼠标、键盘、IME、窗口焦点消息顺序。
|
||
11. 模态循环是否仍经过同一 WndProc。
|
||
12. 宽字符 GDI 是否画入当前 EasyX 工作图像。(归属阶段 0C)
|
||
13. 100%、125%、150% 候选框定位。(归属阶段 0C)
|
||
14. 跨显示器后的 DPI 和候选框坐标。(归属阶段 0C)
|
||
15. 接管后 EasyX 原有鼠标、缩放和退出行为是否正常。
|
||
|
||
其中第 5、6、9 项在当前微软拼音/搜狗 + ANSI EasyX + CP936 环境下已有结论:正式提交来自 `GCS_RESULTSTR`,`WM_IME_CHAR` 是完整匹配回声,未观察到匹配的 `WM_CHAR` lead/trail 回声。第 12 项的自动绘制门槛已通过;第 13 项已取得当前 150% 显示器上的 DPI-unaware 96 与 Per-Monitor-V2 144 坐标基线,并完成微软拼音/搜狗真实候选框视觉验收;100%/125% 仍受实机条件限制。第 14 项仍待跨屏环境。第 7 项仍属于 0B 消费实验。
|
||
|
||
阶段 0A 仅被动观察;阶段 0C 采集宽字符绘制与 IME 坐标证据;阶段 0B 才允许在测试开关下按消息类型、虚拟键和 IME 状态执行隔离消费实验。三者是能力分类,编号不代表执行顺序,建议顺序为 0A → 0C → 0B。
|
||
|
||
### 18. 分阶段落地
|
||
|
||
#### 阶段 0A:被动探测
|
||
|
||
- 只记录消息和来源。
|
||
- 不修改参数、旧窗口过程调用、返回值和业务分发。
|
||
- 不为探测新增额外 EasyX `peekmessage`。
|
||
- 输出版本化、隐私受控的 JSONL 日志。
|
||
|
||
#### 阶段 0C:宽字符绘制与 IME 坐标探测
|
||
|
||
- 验证 `TextOutW` / `DrawTextW` / `GetTextExtentPoint32W` 在主画面和 EasyX 工作图像上的行为。
|
||
- 记录 `SetWorkingImage` 切换前后的真实目标 HDC。
|
||
- 用固定非敏感测试串覆盖 ASCII、中文、代理对和组合字符。
|
||
- 分离记录绘制结果、测量结果、像素证据和人工视觉结论。
|
||
- 验证 DPI awareness、`GetDpiForWindow`、客户区/屏幕坐标和 TextBox caret anchor。
|
||
- 覆盖 100%、125%、150% 和跨屏不同 DPI。
|
||
- 采集 `COMPOSITIONFORM` / `CANDIDATEFORM` 定位证据。
|
||
- 详细规格见 [`Module-20261001-0003-阶段0C宽字符绘制与IME坐标探测规格.md`](./Module-20261001-0003-阶段0C宽字符绘制与IME坐标探测规格.md)。
|
||
- 当前状态:0C-1 自动门槛通过;0C-2 当前 96/144 DPI 自动门槛通过;微软拼音与搜狗三个 anchor 已取得真实视觉证据,搜狗提交后 stale-position 回归通过。当前可用环境的 0C 基线已关闭,100%/125%/跨屏作为实机能力缺口继续记录;后续 0B 阶段 1 前置基线也已关闭。
|
||
|
||
#### 阶段 0B:隔离消费实验
|
||
|
||
使用测试专用规则;详细规格见 [`Module-20261001-0004-阶段0B原生键盘消息隔离消费实验规格.md`](./Module-20261001-0004-阶段0B原生键盘消息隔离消费实验规格.md):
|
||
|
||
```cpp
|
||
struct ProbeConsumeRule
|
||
{
|
||
UINT message;
|
||
std::optional<UINT> virtualKey;
|
||
std::optional<std::uint8_t> scanCode;
|
||
ImeStateCondition imeCondition; // Any / IdleOnly / ComposingOnly
|
||
ProbeAction action; // ObserveAndForward / ObserveAndConsume
|
||
};
|
||
```
|
||
|
||
- 每次只启用一条或一小组明确规则。
|
||
- 日志携带实验 ID 和规则 ID。
|
||
- 同步窗口协议禁止进入规则表。
|
||
- 正式构建默认没有任何消费规则。
|
||
- 动作枚举严格保持 `ObserveAndForward / ObserveAndConsume` 两值;“转发但抑制下游业务路由”属于阶段 1,不是 0B 的第三种 WndProc 动作。
|
||
- 实验结论使用 `SafeToConsume / MustForward / Inconclusive`,与动作枚举分离。
|
||
- 自动 A/B 必须单独回答:WndProc 消费目标 `WM_KEYDOWN` 后,宿主 `peekmessage(EX_KEY)` 是否仍返回对应虚拟键。
|
||
- 配置错误、规则模糊、IME 状态不可靠、出现记录丢失或运行环境与 0C baseline 不一致时一律 fail-open,实际动作退回观察并转发。
|
||
- 当前 F24 实测:`WM_KEYDOWN/UP` 的 Forward 对照均产生一次对应 `EX_KEY`,Consume 后对应 `EX_KEY` 均消失;错误 baseline 请求 Consume 时正确退回 Forward。该结论只证明 F24 的两观察面关系,不自动外推为生产消费白名单。
|
||
- 自动首键由 F8 改为 F24:当前中文 IME 环境下 F8 的 `SendInput` KeyDown 未到达目标 WndProc,截断层级尚未确认,不能形成有效 A/B 对照。
|
||
- 0B 自动证据现已强制要求严格序列、零丢失、真实 fixture PID 退出、`WM_CLOSE` 与 `session_end` 完整;宿主 flush 和 WndProc 关闭收口使用独立文件写锁串行。
|
||
- Composition 人工规则按真实原生观察匹配 `WM_KEYDOWN + VK_PROCESSKEY(229) + scanCode 1`,操作者动作仍为 Escape;规则不尝试在 WndProc 中恢复逻辑键。
|
||
- 0B 人工夹具使用固定客户区 anchor,并记录 `ImmGetContext` 与两个 setter 的分项结果;位置链未成功时请求 Consume 必须 fail-open。
|
||
- 0B 日志缓冲采用双固定缓冲,安全点只在状态锁内交换 buffer;文件序列化在锁外完成,避免把探测器自身锁竞争误报成输入链丢失。
|
||
- 0B 固定 IME 定位采用三阶段拆锁协议,定位结果写独立 `ime_position` 记录并以 `parentSequence` 关联父 enter;setter 同步重入的消息允许正常采集,但不得递归发起定位。
|
||
- 修复后微软拼音 Forward 人工运行保持响应,Escape 原生规则唯一命中并转发,定位结果 18/18 成功且无孤立父序号;截图确认组合串/候选 UI 位于固定锚点附近。该结论只关闭诊断死锁和 Forward 对照,不代替 Consume 与搜狗矩阵。
|
||
- 微软拼音 Consume 重跑中 Escape 原生规则唯一命中并返回 0,旧窗口过程未被调用,对应 EasyX `EX_KEY=absent`;定位 7/7 成功、零丢失且没有产生提交结果。与 Forward 的 `EX_KEY=present_once` 对照表明当前受控键在 WndProc 消费后不会再进入 EasyX 键面,但生产白名单仍需搜狗矩阵和最终 0B 评审。
|
||
- 搜狗候选态实测没有向目标 HWND 发送 START/COMPOSITION/END 生命周期;Escape KeyDown 仍为 `VK_PROCESSKEY + scanCode 1`,但 tracked IME state 保持 Idle。最终 Consume 请求按 `ime_condition_not_met` 运行时 fail-open 为 Forward,EasyX 键可见一次,结论为 `MustForward` 而不是真正 Consume。
|
||
- 阶段 1 采用默认 capture-and-forward,不建立生产消费白名单;所有 `VK_PROCESSKEY` 和已确认组合态键只供输入法/文本规范化,不进入控件业务路由。微软拼音单点 Consume 成功只作为“WndProc return 0 会阻断 EasyX 键面”的机制证据。
|
||
|
||
#### 阶段 1:InputCollector
|
||
|
||
- 输入事件结构和 NativeInputQueue。
|
||
- 按已确认双线程模型实现 SPSC 语义的跨线程发布;正确性版本先用短临界区互斥队列。
|
||
- `sequence` 使用 atomic 分配,队列同步独立提供 happens-before。
|
||
- KeyEvent 完整解析。
|
||
- Unicode/ANSI 解码。
|
||
- IME 状态机。
|
||
- EasyX `EX_KEY` 排空但不业务路由。
|
||
|
||
#### 阶段 2:FocusManager
|
||
|
||
- ControlHandle。
|
||
- 焦点申请与回退。
|
||
- Window/Dialog FocusScope。
|
||
- PointerDown 申请焦点。
|
||
- mutation 生命周期。
|
||
|
||
#### 阶段 3:KeyboardRouter
|
||
|
||
- Tab 导航。
|
||
- 句柄路由路径。
|
||
- 冒泡和框架默认动作。
|
||
- Button 和 Dialog 键盘行为。
|
||
|
||
#### 阶段 4:复合控件
|
||
|
||
- Table 翻页 Button。
|
||
- TabControl roving tab stop。
|
||
- 活动页面过滤和页签/页面路由隔离。
|
||
|
||
#### 阶段 5:TextBox
|
||
|
||
- UTF-16 编辑模型。
|
||
- 选区、光标、剪贴板。
|
||
- Ctrl 快捷键。
|
||
- 宽字符测量和自绘。
|
||
|
||
#### 阶段 6:IME 与 DPI 收口
|
||
|
||
- `GCS_RESULTSTR` 和回声去重。
|
||
- 系统组合窗口、候选框定位。
|
||
- 微软拼音、搜狗、DPI 和跨屏测试。
|
||
|
||
### 19. 已锁定决策
|
||
|
||
1. 输入采集先于焦点系统。
|
||
2. v1 键盘默认采集并转发,不预设消费。
|
||
3. NativeInputQueue 是唯一键盘业务源。
|
||
4. EasyX `EX_KEY` 只排空和观察,不进入业务路由。
|
||
5. v1 使用系统 IME UI,不自绘组合串。
|
||
6. IME 正式提交优先使用 `GCS_RESULTSTR`。
|
||
7. TextBox 内部使用 UTF-16。
|
||
8. 绘制优先使用宽字符 GDI,不默认降级 CP936。
|
||
9. 长期焦点、队列和遍历结果只使用 ControlHandle。
|
||
10. 路由结果区分停止冒泡和阻止框架默认动作。
|
||
11. TabControl 页签事件绝不传入活动页面。
|
||
12. IME 组合期间暂停框架快捷键、焦点导航和 Dialog 默认动作。
|
||
13. 模态循环采用最内层单 drainer。
|
||
14. TextBox v1 至少保证代理对安全,完整字素簇以后增强。
|
||
15. `pendingLead` 在输入法/代码页切换前清除。
|
||
16. `compositionId` 在 START 时分配,缺失 START 时惰性分配新 ID。
|
||
17. Space KeyUp 与 Enter 首次 KeyDown 的触发差异属于有意交互语义。
|
||
18. EasyX WndProc 是生产线程,宿主 `runEventLoop/peekmessage` 是消费线程;NativeInputQueue 必须跨线程同步。
|
||
19. 当前 EasyX HWND 为 ANSI,DBCS 解码和 `WM_IME_CHAR/WM_CHAR` 回声路径属于阶段 1 必经路径。
|
||
20. 阶段 1 先使用互斥 SPSC 队列;只有定义好变长文本所有权与满队列策略后才允许换成 lock-free ring。
|
||
21. 当前微软拼音和搜狗均以 `GCS_RESULTSTR` 为正式提交、`WM_IME_CHAR` 为匹配回声;阶段 1 不得把回声再次入队。
|
||
22. `WM_INPUTLANGCHANGEREQUEST` 只表示请求,不能提前更新代码页;只有 `WM_INPUTLANGCHANGE`,或挂接/重新获焦时确认 HKL 快照确已变化,才执行“清 pending lead → 更新代码页”。
|
||
23. IME 内部中英文模式或 TSF 输入配置文件切换不保证产生 `WM_INPUTLANGCHANGE`,不得把该消息当作所有输入模式变化的统一通知。
|
||
24. 当前环境只有 `zh-Hans-CN`,无法实测跨语言 HKL;阶段 1 在窗口挂接和重新获焦时必须用 HWND 属主线程的 `GetKeyboardLayout` 快照补偿潜在消息缺口。
|
||
25. 当前 EasyX 宽字符 GDI 绘制链可用;TextBox 自绘优先保留 UTF-16 并使用 W API,不启用 CP936 显示降级。
|
||
26. EasyX 批次必须从主画面进入和退出;工作图像只在批次内部切换,并通过显式合成上屏。
|
||
27. 在已验证的 DPI-unaware 96 与 Per-Monitor-V2 144 环境中,EasyX 绘制坐标可直接作为 HWND 客户区坐标;不得额外乘 DPI 比例。
|
||
28. IME 位置必须在焦点/caret/anchor 变化时立即预设,并在组合生命周期消息到达时再次校准;不得假定候选 UI 出现前一定会收到 `WM_IME_STARTCOMPOSITION/WM_IME_COMPOSITION`。
|
||
29. 原始 `VK_PROCESSKEY` 一律视为 IME 所有:WndProc 层采集并转发,阶段 1 不向 Button、Dialog、快捷键和焦点导航路由;不得依赖 `imeComposingAtCapture` 单独判断搜狗候选态。
|
||
30. 0B 没有建立跨 IME 生产消费白名单;阶段 1 从默认转发基线开始,后续任何窄消费规则都必须有独立逐键证据。
|
||
|
||
## 实现与影响
|
||
|
||
- 关键实现点:
|
||
- 阶段 0A 探测器已经进入框架;0C 独立视觉探测工程已实现;FocusManager、生产 NativeInputQueue 和 TextBox 改造尚未开始。
|
||
- 独立 EasyX 探测已经推翻 v0.4 的单线程假设,v0.5 据此补入跨线程队列约束;v0.6 根据微软拼音/搜狗真实日志锁定结果串与回声关系,并补入输入布局 request/change 分层;v0.7 根据专用短测区分 TSF 配置文件切换与跨语言 HKL,并增加重新获焦快照约束;v0.8 根据 0C 自动证据锁定宽字符绘制、批次进入顺序与当前 DPI 坐标规则;v0.9 根据搜狗真实失败与修复复验锁定 IME 位置双时机规则;v0.13 根据 0B 微软拼音 Consume 与搜狗运行时 fail-open 证据锁定默认转发、`VK_PROCESSKEY` 路由抑制和无生产消费白名单基线。
|
||
- 涉及文件 / 类 / 函数:
|
||
- 预计涉及 `Window`、`Control`、`Canvas`、`Button`、`TextBox`、`Table`、`TabControl`、`Dialog`。
|
||
- 实际文件和接口必须在阶段 0 与实现前只读检查后确定。
|
||
- 兼容性影响:
|
||
- 设计目标是保留现有鼠标行为和公共文本 API 的兼容入口。
|
||
- 输入链路和 TextBox 内部模型将发生架构级变化,必须分阶段接入。
|
||
- 性能影响:
|
||
- 输入事件会增加规范化和句柄解析开销,但不允许在 WndProc 中执行阻塞 I/O。
|
||
- TextBox 宽字符测量、选区和水平滚动需要缓存策略,具体数据待实现阶段评估。
|
||
- 风险点:
|
||
- EasyX 双线程、ANSI 窗口和当前微软拼音/搜狗回声顺序已确认,但其他 IME、窗口 A/W 属性和系统版本仍可能产生不同顺序。
|
||
- 模态重入和控件删除若破坏单 drainer/句柄不变量,可能产生难复现的顺序或生命周期问题。
|
||
- 该文档是设计基线,不应被误读为已实现或已验证能力。
|
||
|
||
## 测试与验证
|
||
|
||
- 测试范围:
|
||
- 设计评审覆盖输入采集、同步消息、Unicode、IME、焦点、复合控件、按键归属、TextBox 和模态重入。
|
||
- 独立线程归属探测、0A 构建、固定消息 smoke、微软拼音/搜狗真实输入链路测试、当前可用环境的 0C 基线,以及 0B 的 F24 KeyDown/KeyUp 自动 A/B、baseline fail-open、微软拼音 Forward/Consume 和搜狗运行时 fail-open 门槛均已完成;生产输入队列和焦点系统尚未开始。
|
||
- 验证步骤:
|
||
|
||
1. 先按关联规格实现阶段 0A 被动探测并取得消息链路证据。
|
||
2. 按阶段 0C 规格完成宽字符 GDI 与 IME 坐标探测,取得绘制与坐标证据。
|
||
3. 使用阶段 0B 的逐消息规则验证消费策略,不修改生产默认路径。
|
||
4. 阶段 1 至阶段 6 分别执行逻辑测试、仪器化运行测试和人工输入法/DPI 回归。
|
||
|
||
- 验证结果:
|
||
- v0.4 设计经过多轮评审,已作为后续探测和实现的基线。
|
||
- 2026-10-03:独立线程探针确认 EasyX WndProc/宿主循环双线程,当前 EasyX HWND 为 ANSI、中文代码页为 CP936。
|
||
- 2026-10-04:微软拼音和搜狗真实结果均为 `GCS_RESULTSTR -> WM_IME_CHAR matched -> WM_CHAR expired`;日志隐私检查通过,丢失均按 `wndproc_lock_contention` 显式记账。
|
||
- 2026-10-04:两次布局/模式切换未形成 `WM_INPUTLANGCHANGE`,并据此发现旧探测器未观察 `WM_INPUTLANGCHANGEREQUEST`;v0.6 已把请求与完成消息拆开。
|
||
- 2026-10-04:request 补观测的探测开启构建与 1 轮 UI 回归通过,8 份日志均可解析、sequence 严格递增且无文字原值泄露;共 1 条 `wndproc_lock_contention` 被显式记账。
|
||
- 2026-10-04:专用 Win+Space 短测产生 1243 条可解析记录,request/change 仍为 0;日志观察到 Win 键、焦点往返及 `VK_PROCESSKEY -> IME composition`,系统语言列表仅有 `zh-Hans-CN`,故本次结论限定为同 HKL 的 TSF 配置文件切换,跨语言 HKL 分支仍缺少实机样本。
|
||
- 2026-10-04:最终源码的 Debug x64 单元测试 15/15 通过。
|
||
- 2026-10-05:提交前重新执行独立线程探针、探测开启 UI 回归和探测关闭单元测试;再次确认 EasyX 双线程与 ANSI/CP936,8 份自动日志均严格递增、零文字原值泄露、零丢失,VSTest 15/15 通过。
|
||
- 2026-10-05:0C-1 在主画面/工作图像、批次内外的 40 个结构化绘制用例全部通过;30 张证据图带 SHA-256,W API、表面隔离、GDI 状态恢复、Flush/End 和后续 EasyX 绘制兼容性均有自动证据。
|
||
- 2026-10-05:0C-2 在 DPI-unaware 96 和独立 Per-Monitor-V2 144 两种视图中均未观察到 EasyX/GDI/HWND 坐标缩放差异;当前组合基线 ID 与完整哈希记录在 0003 规格。
|
||
- 2026-10-05:0C-3 三 anchor 探针与优雅退出/失败门槛烟测通过,但真实微软拼音/搜狗定位尚未执行,不计入阶段门槛通过。
|
||
- 2026-10-05:微软拼音三个 anchor 自动 IMM 调用和人工位置观察通过;第二轮虽然脚本参数为 `Sogou`,操作者确认实际未切换输入法,因此没有把它误记为搜狗证据。
|
||
- 2026-10-05 23:51:搜狗首次真实运行按预期门槛失败,F1/F2 未发生 IMM 定位调用,F3 为 2/2;截图确认候选框先位于窗口外,之后沿用 F3 旧位置。
|
||
- 2026-10-06:v0.9 将 IME 位置刷新改为“焦点/caret/anchor 变化立即预设 + IME 生命周期再次校准”;探测事件新增触发来源与分项计数,并完成三平台构建及真实搜狗回归。
|
||
- 2026-10-06:搜狗修复后运行自动与人工状态均为 `pass`;三个 anchor 均按请求定位,F3 提交后回到 F1 的截图与初始 F1 裁剪字节一致。当前可用环境 0C-3 关闭,随后进入 0B。
|
||
- 2026-10-06:0B F24 `WM_KEYDOWN/UP` 自动 A/B 均通过;Forward 对应 `EX_KEY=present_once`,Consume 对应 `EX_KEY=absent`。错误 baseline 的请求 Consume 正确 fail-open 为 Forward。真实 IME 人工矩阵未完成,因此阶段 1 仍不启动。
|
||
- 2026-10-06:0B Composition 首轮人工运行无效:夹具未设置 IME 位置,Escape 规则错误匹配 `VK_ESCAPE` 而实际 KeyDown 为 `VK_PROCESSKEY + scanCode 1`,且出现 3 条锁竞争丢失。已实现固定 IMM32 锚点、原生三元组规则与双缓冲采集修正;微软拼音/搜狗 A/B 需重新执行。
|
||
- 2026-10-07:微软拼音 Forward/Consume 有效复验均通过;Forward 对应 EasyX 键出现一次,Consume 后 EasyX 键消失,位置链分别为 18/18 与 7/7,均零丢失、无孤立定位记录。
|
||
- 2026-10-07:搜狗候选态没有向目标 HWND 暴露 START/COMPOSITION/END 生命周期;最终 Consume 请求以 `ime_condition_not_met` 正确 fail-open 为 Forward,规则零命中、零消费、EasyX 键出现一次,结构 verdict 为 `MustForward`。
|
||
- 2026-10-07:最终 Debug x64 单元测试 15/15、UI fixture 1 轮、F24 KeyDown A/B 新运行通过;既有 KeyUp 与 baseline mismatch 证据经当前分析器回放保持有效。阶段 1 以默认转发且无消费白名单为起点。
|
||
- 已知限制 / 遗留问题:
|
||
- §5.3 当前明确不建立生产消费白名单;若后续需要窄消费规则,仍必须补对应消息、虚拟键、IME 和环境的独立 0B 证据。
|
||
- §7.3 的微软拼音/搜狗回声状态机已有 0A 证据,其他 IME 仍需保持可诊断回退。
|
||
- 宽字符 GDI 与当前微软拼音/搜狗定位门槛已通过;100%/125% 与跨屏 DPI 仍由阶段 0C 规格作为实机能力缺口承接。
|
||
|
||
## 落地信息
|
||
|
||
- 关联功能变更 ID:无
|
||
- 关联 BUG / Fix:`BUG-20261006-0001` / `Fix-BUG-20261006-0001`;`BUG-20261006-0002` / `Fix-BUG-20261006-0002`
|
||
- Commit:`c218c25`(阶段 0A 诊断实现与线程归属复现);`c0eca1d`(探测暴露的原生窗口关闭修复);`43cf4ac`(阶段 0C 视觉与 IME 坐标探测)
|
||
- PR:无
|
||
- 发布版本:未发布
|
||
- 相关文档:
|
||
- [`Module-20261001-0002-阶段0A输入链路被动探测规格.md`](./Module-20261001-0002-阶段0A输入链路被动探测规格.md)
|
||
- [`Module-20261001-0003-阶段0C宽字符绘制与IME坐标探测规格.md`](./Module-20261001-0003-阶段0C宽字符绘制与IME坐标探测规格.md)
|
||
- [TranslateMessage](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-translatemessage)
|
||
- [WM_UNICHAR](https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-unichar)
|
||
- [WM_GETDLGCODE](https://learn.microsoft.com/en-us/windows/win32/dlgbox/wm-getdlgcode)
|
||
- [WM_IME_SETCONTEXT](https://learn.microsoft.com/en-us/windows/win32/intl/wm-ime-setcontext)
|
||
- [WM_IME_COMPOSITION](https://learn.microsoft.com/en-us/windows/win32/intl/wm-ime-composition)
|
||
- [ImmGetCompositionStringW](https://learn.microsoft.com/en-us/windows/win32/api/imm/nf-imm-immgetcompositionstringw)
|
||
- [ImmGetVirtualKey](https://learn.microsoft.com/en-us/windows/win32/api/immdev/nf-immdev-immgetvirtualkey)
|
||
- [ImmNotifyIME](https://learn.microsoft.com/en-us/windows/win32/api/immdev/nf-immdev-immnotifyime)
|
||
- [COMPOSITIONFORM](https://learn.microsoft.com/en-us/windows/win32/api/imm/ns-imm-compositionform)
|
||
- [CANDIDATEFORM](https://learn.microsoft.com/en-us/windows/win32/api/imm/ns-imm-candidateform)
|