# 新增功能模块 / 模块重构 > 本记录描述通用输入系统阶段 3 的实际实现、生产换轨边界与验证证据。阶段 3 完成 KeyEvent 路由和 Button 接入,但不代表 TextBox 文本编辑、真实 Dialog FocusScope 或复合控件高级键盘行为已经完成。 ## 基本信息 - 模块 ID:Module-20261009-0007 - 模块名称:阶段 3 键盘定向路由、Button 接入与 EX_KEY 生产换轨 - 状态:已完成;实现、自动回归、微软拼音/搜狗及 KEY1/KEY5 人工门槛全部关闭 - 类型:新增模块 / 架构演进 / 兼容性迁移 - 所属系统 / 子系统:StellarX Core / Window / Native Input / Focus / Keyboard Routing / Button - 版本 / 分支:`master`,实现提交 `29df994` - 环境:Windows + Visual Studio 2022 + MSVC v143 + EasyX + MBCS 工程配置 - 负责人:Codex 协作实现 ## 背景与目标 - 阶段 1 已建立 WndProc 采集、规范化和跨线程 NativeInputQueue;阶段 2 已建立稳定身份焦点树和 FocusScope 机制。 - 阶段 3 要把 `KeyEvent` 从当前逻辑焦点叶子沿逻辑父链定向冒泡,并接入 Tab、Button Enter/Space、窗口激活与 IME 所有权门控。 - 最终切片必须一次性停止旧 `EX_KEY -> Control::handleEvent()` 生产交付,同时继续排空 EasyX 队列,避免新旧业务源并存或内部队列累积。 ## 实现边界 ### 已实现 - 公共 `RoutedKeyEvent`、`KeyRouteResult` 和 `Control::handleKeyEvent()` 默认空实现。 - 内部 `KeyboardRouter`:固定容量原始身份路径、逐节点重新解析、叶子到祖先冒泡、独立的停止冒泡与阻止框架默认动作结果位。 - 单事件 mutation transaction 与可嵌套 `managedDispatchActive` 边界;用户回调后不保留裸 `Control*`。 - 采集侧原子 `{ keyboardFocusActive, generation }` 快照;失活、跨激活代次、IME 所有和 Alt/System 输入在业务路由前被抑制。 - 失活和模态入口沿当前焦点路径取消 keyboard armed 等瞬时状态,但保留逻辑焦点。 - Tab / Shift+Tab 冒泡后默认动作:每次按键重新采集活动 FocusScope 的当前焦点树,支持循环、无焦点首/末项与 `preventFrameworkDefault`。 - Button 独立 `keyboardArmed`:Space 首次 KeyDown 按下、KeyUp 确认;Enter 首次 KeyDown 确认;自动重复不重复激活。 - Escape、焦点丢失、模式变化、隐藏、窗口失活和模态隔离取消 armed;按压色与阶段 2 焦点框叠加。 - 生产默认改为 Routed;Window 主循环和 Dialog 模态循环同时继续排空 `EX_KEY`,但不再交给 `handleEvent()`。 - `easyXKeyboardMessagesDrained` 与 `legacyEasyXKeyboardBusinessDeliveries` 诊断计数,用于证明排空持续且旧业务为零。 - `WM_GETDLGCODE` 窄同步分支:先取 EasyX 旧窗口过程结果,只对明确普通 Tab 查询合并 `DLGC_WANTTAB`。 - UI 运行器可指定 fixture、结果根目录和 Legacy/Routed 预期,并输出每场景结构化耗时 `run-summary.json`。 - 生产端到端 `keyboard-routing` 场景覆盖 Space 激活首按钮、Tab 换焦和 Enter 激活第二按钮。 - Window 背景与非模态 Canvas 空白左键清焦点并消费,避免焦点框残留和重叠兄弟穿透。 - Table 内部分页 Button 焦点变化提升为 Table 整体重绘;同父 Canvas 的下层持久 coverage 命中上层时,上层完整恢复优先于局部脏子树提交。 ### 明确未实现 - `TextInputEvent` / `CompositionEvent` 向 TextBox 的正式业务交付;当前仍只排空、观察和计数。 - TextBox UTF-16 编辑内核、自绘、caret、选区、剪贴板和 IME anchor。 - Table 方向键/翻页命令、TabControl roving-tab-stop 和方向键切页。 - 真实 Dialog FocusScope、模态 Dialog 子树键盘路由、默认按钮和 Escape 关闭;当前模态循环抑制底层 Window 业务路由。 - `DLGC_WANTALLKEYS`、`DLGC_WANTCHARS`、`DLGC_WANTARROWS`;没有相应功能前不提前声明。 - WndProc 生产消费白名单;阶段 0B 没有给出跨 IME 安全白名单,因此原生按键仍按默认策略采集并转发。 ## 关键契约 1. NativeInputQueue 是生产键盘业务源;EasyX `EX_KEY` 仅排空和观察。 2. Routed 与 LegacyCompatibility 互斥。Legacy 仅供测试在事件循环锁定前选择,不能运行中切换,也不是公开兼容开关。 3. 每次只从队列弹出一条事件,释放队列锁后才进入焦点、控件和用户回调;不得预取整批事件。 4. 路由开始时构造原始身份路径,之后逐节点重新解析。早期回调删除的后续节点跳过;回调改变焦点只影响下一事件。 5. `stopBubble` 与 `preventFrameworkDefault` 独立;二者都不能回溯改变已经完成的 WndProc 返回值。 6. 原始 `VK_PROCESSKEY` 是单键 IME 所有权威;`imeComposingAtCapture` 是独立生命周期轴。两者任一命中时,阶段 3 不执行控件、Tab 或默认动作。 7. 激活门控同时比较事件采集快照和当前原子 generation;窗口重新激活不能让旧周期积压键重新生效。 8. Button Space 在 KeyUp 确认,Enter 在首次 KeyDown 确认;命令阻止框架默认动作但继续允许祖先观察。 9. WndProc `WM_GETDLGCODE` 只在 Routed 普通 Tab 查询时增加 `DLGC_WANTTAB`;System/Alt+Tab 不认领。 10. `EX_MOUSE` 永久保留在 `Control::handleEvent()`;本次迁移只切断 `EX_KEY` 业务交付。 ## 六个实现切片 ### 切片 1:公共契约与互斥门控 - 新增 routed key 公共类型、Control 默认入口和内部 Legacy/Routed 模式。 - Window 与 Dialog 共用旧业务交付判定,测试 Routed 模式从第一天起就先关闭自己的旧 EX_KEY 业务路径,避免双投递。 ### 切片 2:身份路由与 mutation - 固定容量 256 的 `FocusTargetId` 路径;循环、超深或构造期失效时在调用控件前 fail closed。 - Control、Canvas、Table、TabControl 提供递归身份分发;借用指针不逃逸当前调用。 ### 切片 3:激活、IME 与模态门控 - NativeInputEvent 携带采集时激活快照。 - `VK_PROCESSKEY`、标准组合生命周期内按键、Alt/System、失活、跨代和模态期底层输入不进入业务回调。 - 失活与模态入口调用焦点路径暂停协议,保留逻辑焦点。 ### 切片 4:Tab 默认动作 - 无 Ctrl 的 Tab KeyDown 在冒泡后执行;Shift 反向、边界循环、无焦点进入首/末项。 - 每键重新获取焦点树;Table 分页 Button 在首次 draw 前的空结果不缓存。 ### 切片 5:Button 键盘状态机 - `keyboardArmed` 与鼠标/Toggle 的 `click` 分离,只在绘制层合并按压视觉。 - Space/Enter、自动重复、Escape 和全部取消边界均由单测覆盖。 ### 切片 6:生产原子换轨 - `Window::routedKeyboardPipeline` 生产默认改为 true;主循环和模态循环同时切断旧 EX_KEY 业务交付。 - 新增窄 `WM_GETDLGCODE` 分支、排空/旧业务计数、生产端到端 UI 场景、迁移说明和换轨前后 A/B。 - `TextInputEvent` 与 `CompositionEvent` 不提前交给控件,避免阶段 3 越界实现 TextBox。 ## 实现文件 - `include/StellarX/KeyboardRouting.h` - `include/StellarX/InputEvents.h` - `include/StellarX/Control.h` / `src/StellarX/Control.cpp` - `include/StellarX/FocusManager.h` / `src/StellarX/FocusManager.cpp` - `include/StellarX/Button.h` / `src/StellarX/Button.cpp` - `include/StellarX/Window.h` / `src/StellarX/Window.cpp` - `src/StellarX/KeyboardRouter.cpp` - `src/StellarX/Dialog.cpp` - `src/StellarX/SxNativeInput.cpp` - `src/StellarX/internal/SxKeyboardRouterInternal.h` - `src/StellarX/internal/SxEventLoopInternal.h` - `src/StellarX/internal/SxNativeInputInternal.h` - `Canvas`、`Table`、`TabControl` 对应递归分发实现 - `tests/StellarXTests.cpp` - `tests/UiFixture.cpp` - `scripts/run-ui-tests.ps1` ## 自动验证 ### 逻辑与构建矩阵 - Debug x64:67/67。 - x64 ASan:67/67,无 sanitizer 报错。 - Release x64:67/67。 - Debug Win32:67/67。 - KEY1–KEY6:全部编译/链接成功。 - 覆盖路由顺序、结果位独立性、删除/改焦点重入、激活代次、IME/Alt/System/模态抑制、Tab 当前树、Button 状态机、Routed 默认/Legacy 锁定和窄 `DLGC_WANTTAB`。 ### 换轨前后 UI A/B - 换轨前 fixture:`TestResults/Stage3CutoverAB/baseline/StellarX.UiFixture.exe`。 - 基线 SHA-256:`3F6267B6D5EE85A7A97244C3DAE21DB44CEFBD185F56A38516E5E2F6F6234974`。 - 共同八场景执行 5 对 5:Legacy 40/40、Routed 40/40。 - 结构化证据: - `TestResults/Stage3CutoverAB/legacy/20261009-152906-951-18728` - `TestResults/Stage3CutoverAB/legacy-expanded/20261009-153941-779-34800` - `TestResults/Stage3CutoverAB/routed-formal/20261009-153745-045-33364` - `TestResults/Stage3CutoverAB/routed-expanded/20261009-154044-849-12588` - Routed 首轮 ownership 为 `1330 ms`,其余四次 `531–601 ms`,与 Legacy `517–709 ms` 重叠;其它场景平均差为 `-19 ms` 至 `+43 ms`。未观察到稳定耗时劣化,但仍承认交互桌面存在残余偶发波动。 ### Routed 正式 UI - 连续两轮九个业务场景共 18/18;普通/模态原生关闭检查每轮通过。 - `keyboard-routing` 2/2:首 Button Space 激活一次,Tab 聚焦第二 Button,Enter 激活一次。 - `input-shadow` 两轮一致: - Routed 4000/4000,Down/Up 各 2000; - EasyX `EX_KEY` 排空 4000; - 旧 EX_KEY 业务交付 0; - pushed/popped 4000/4000、depth 0、maximumDepth 40; - sequence 严格、零 fault、零规范化异常、内存增长 0。 - 最初受限桌面运行因 `MoveMouseToScreen` 返回 Access denied,未进入产品断言;授权交互桌面后全部重跑,失败样本不计作产品回归。 - PowerShell 7 宿主同时注入 `PATH` / `Path` 时,旧 `test.ps1` 会让 MSBuild 在创建 `CL.exe` 前以 `MSB6001` 失败。运行器已改为只在检测到重复键时通过 `cmd` 子进程删除大写 `PATH`、保留同值 `Path`;不修改调用者环境。修复后 PowerShell 7 与 Windows PowerShell 5.1 的 Debug x64 均为 66/66。 ### 人工反馈修复自动复验 - 新增 `CanvasBackgroundPointerDownClearsFocusAndConsumes` 后,四配置单测均为 67/67,KEY1–KEY6 全部编译/链接成功。 - 扩展 `focus` UI 连续 3/3 通过,证据位于 `TestResults/ui/20261009-220625-941-15096`;覆盖 Table 分页焦点框即时出现/消失、Window/Canvas 空白清焦点和 KEY5 同批上下层重绘。 - 同一 fixture 的完整 UI 1/1、九个业务场景 9/9 通过,证据位于 `TestResults/ui/20261009-220940-941-31488`;Tooltip、Dialog、input-shadow 与 keyboard-routing 均通过。 - 移除 UiFixture 中用于定位问题的临时 Button JSON 诊断字段后,用户从项目终端重新执行最终源码完整 UI。证据位于 `TestResults/ui/20261009-223506-755-18932`:fixture SHA-256 为 `F439C0636CD6B979A47C4594BC5AB3728F6342F49602FE43078435011A877793`,与 `run-summary.json` 一致;九个业务场景 9/9 通过,普通/模态原生关闭均通过。 - 自动化过程中曾分别出现一次沙箱 `SendInput Access denied`、一次 Button 点击未命中和一次 ownership 等待超时;失败证据保留。随后定向 3/3 与完整 UI 通过只说明未形成稳定产品回归,不把这些桌面输入波动写成从未发生。 ## 兼容性与迁移 - `Control::handleEvent()` 仍保留,但生产循环不再向其交付 `EX_KEY`;第三方源码控件必须把键盘逻辑迁到 `handleKeyEvent()`。 - `EX_MOUSE` 和既有鼠标命中/分发保持不变。 - 新增虚接口再次改变 `Control` vtable;已编译的第三方派生控件必须使用当前头文件和 Core 库重新编译。 - 默认 `handleKeyEvent()` 为空,因此未迁移的第三方控件不会崩溃,但其旧键盘能力不会继续生效。 - 详细迁移示例见 [`../MIGRATION.md`](../MIGRATION.md)。 ## 人工门槛与后续 - 微软拼音与搜狗已分别完成真实候选输入复验:候选出现/取消和 Space 交互正常,未报告卡顿、重复提交或输入法按键误触发 Button。 - Button Space/Enter 行为已由人工确认符合锁定语义:Space 按下显示按压态、松开执行;Enter 首次按下立即执行且不显示持续按压态。 - KEY1/KEY5 人工反馈暴露三项问题:Table 分页焦点框延迟、Window/Canvas 空白点击不清焦点、同父 Canvas 下层重绘覆盖 D-2 上层区域;对应实现和自动回归已补齐。 - 模态 Dialog Tab 不可用和非模态作用域顺序属于阶段 4 真实 Dialog FocusScope 范围,本轮不做临时补丁。 - 用户已定向复测 KEY1 与 KEY5,暂未发现问题:Table 分页焦点框即时出现/消失,Window/Canvas 空白点击清焦点,KEY5 D-1 → D-2 未再发生覆盖。 - 阶段 3 验收至此关闭;后续可单独讨论阶段 4 的 Table、TabControl 和真实 Dialog FocusScope 接入,但本记录不提前宣称这些能力已经实现。 ## 落地信息 - 关联功能变更 ID:无 - 关联 BUG / Fix:`BUG-20261009-0001` / `Fix-BUG-20261009-0001`;`BUG-20261009-0002` / `Fix-BUG-20261009-0002` - Commit:`29df994`(阶段 3 键盘路由、生产换轨与焦点重绘收口) - PR:无 - 发布版本:未发布 - 相关文档: - [`Module-20261001-0001-通用输入焦点与键盘路由系统设计.md`](./Module-20261001-0001-通用输入焦点与键盘路由系统设计.md) - [`Module-20261007-0005-阶段1输入采集与规范化实现.md`](./Module-20261007-0005-阶段1输入采集与规范化实现.md) - [`Module-20261008-0006-阶段2通用焦点管理器实现.md`](./Module-20261008-0006-阶段2通用焦点管理器实现.md) - [`../MIGRATION.md`](../MIGRATION.md) - [`../TESTING.md`](../TESTING.md)