Files
StellarX-kaifa/开发记录/模块/Module-20261001-0003-阶段0C宽字符绘制与IME坐标探测规格.md
T

33 KiB
Raw Blame History

新增功能模块 / 模块重构

适用场景:新增模块、重大模块重构、核心架构能力演进。 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。

基本信息

  • 模块 ID:Module-20261001-0003
  • 模块名称:阶段 0C 宽字符绘制与 IME 坐标探测规格
  • 状态:当前可用环境基线通过(0C-1/0C-2 自动基线通过;0C-3 微软拼音与搜狗真实定位通过;100%/125%/跨屏矩阵仍受实机条件限制)
  • 类型:新增模块 / 诊断基础设施
  • 所属系统 / 子系统:StellarX Core / Window / GDI / IMM32 / DPI
  • 版本 / 分支:master,本轮实现起点 225bc35,前置阶段 0A 基线 c218c25
  • 环境:Windows + Visual Studio 2022 + MSVC v143 + EasyX + GDI + IMM32
  • 负责人:Codex 协作设计
  • 阶段定位:阶段 0 探测的能力分类之一(0A 消息链路 / 0B 消费实验 / 0C 绘制与坐标)。编号不代表严格执行顺序,建议顺序为 0A → 0C → 0B。

背景与目标

  • 背景:
    • 设计基线已锁定“绘制优先使用宽字符 GDI,不默认降级 CP936”和“系统候选框定位到 TextBox caret”两条决策。
    • 两条决策都依赖尚未验证的事实:(1) TextOutW/DrawTextW/GetTextExtentPoint32W 能否真正画入 EasyX 当前工作图像;(2) EasyX 绘制坐标空间与 HWND 客户区/屏幕坐标是否一致,尤其是不同 DPI 下。
    • 这两项属于“EasyX 绘制表面与 Win32 原生坐标空间的兼容边界”,无法从 0A 键盘消息 JSONL 推断,需要独立的视觉与坐标证据。
  • 当前痛点:
    • 若宽字符 GDI 实际画不进 EasyX 工作图像,却按“可用”推进,会在阶段 5 TextBox 自绘时集中爆发。
    • 若 DPI 坐标换算错误,候选框在 125%/150%/跨屏场景整体偏移,且这种 bug 难用单元测试覆盖。
    • 测量结果、像素证据和人工视觉结论若混在一起,无法定位问题出在测量、绘制还是观察环节。
  • 目标:
    • 定义阶段 0C 的验证方法、固定非敏感测试串、证据分层、DPI 矩阵和失败/降级判定。
    • 为阶段 5 的 TextBox 自绘和阶段 6 的 IME 定位提供“宽字符绘制是否可行、坐标如何换算”的可复核证据与结论。
  • 非目标:
    • 不实现 TextBox 自绘、不修改任何控件或框架代码。
    • 不决定是否降级 CP936,该结论由 0C 证据决定。
    • 不把视觉/坐标结果并入 0A 键盘 JSONL schema。
    • 不验证消费消息的影响(属 0B)。

模块边界

  • 职责:
    • 定义宽字符 GDI 的目标 HDC 与工作图像验证方法。
    • 定义固定非敏感测试串集合。
    • 定义绘制、测量、像素、人工视觉四层证据及其记录格式。
    • 定义 DPI、客户区/屏幕坐标和 caret anchor 的换算验证。
    • 定义 COMPOSITIONFORM / CANDIDATEFORM 定位证据。
    • 定义自动检查与必须人工观察的分层。
    • 定义阶段门槛、失败判定和降级条件。
  • 不负责什么:
    • 不新增框架公共 API。
    • 不在 WndProc 中执行 IME UI 修改。
    • 不用真实用户输入作为测试串。
    • 不把结论写入生产代码默认配置。
  • 外部依赖:
    • EasyX SetWorkingImage / 工作图像 HDC。
    • GDI TextOutW / DrawTextW / GetTextExtentPoint32W。
    • GetDpiForWindow、进程 DPI awareness。
    • COMPOSITIONFORM / CANDIDATEFORM 与 caret anchor。
  • 对外能力 / API:
    • 仅定义内部诊断与证据格式,不新增框架公共 API。
    • 必须由测试/诊断构建开关显式启用,正式默认关闭。
  • 关键数据 / 状态:
    • 目标 HDC 与工作图像身份、GDI 状态保存/恢复校验。
    • 每类测试串的测量结果、像素采样、人工结论。
    • DPI 值、坐标换算结果、候选框定位证据。

设计说明

1. 证据分层

四类证据必须分开记录,禁止互相替代:

证据层 内容 来源 可自动化
绘制结果 实际调用了哪些 GDI 函数、目标 HDC、返回值 运行日志 是
测量结果 GetTextExtentPoint32W 返回的宽高 运行日志 是
像素证据 工作图像指定区域的像素采样/截图 位图采样 是(采样点)
人工视觉结论 候选框实际位置、缺字/方框、对齐是否正常 人工观察 否

2. 固定非敏感测试串

只使用预先约定的非敏感串,不使用用户输入。测试实现必须根据下表的 Unicode scalar 序列构造 UTF-16,不得把源文件中的可见文字字面量作为判定依据。

testStringId Unicode scalar 序列 便于人工识别的显示 目的
ascii U+0053 U+0074 U+0065 U+006C U+006C U+0061 U+0072 U+0058 U+0020 U+0030 U+0043 U+0020 U+0050 U+0072 U+006F U+0062 U+0065 StellarX 0C Probe ASCII 基线
cjk_bmp U+661F U+57A3 U+7ED8 U+5236 U+6D4B U+8BD5 星垣绘制测试 BMP 中文与字体覆盖
supplementary U+1F600 😀 增补平面字符;UTF-16 应为 D83D DE00
combining_nfd U+0065 U+0301 e + combining acute 明确验证 NFD 组合序列,不允许换成预组字符 U+00E9
mixed U+661F U+57A3 U+0020 U+0041 U+1F600 U+0042 U+0020 U+0065 U+0301 星垣 A😀B é 混合相邻布局、代理对和组合字符

每个用例在绘制前记录构造后的 UTF-16 code unit 序列和有效性。字体不包含某个 glyph 时,应归类为字体覆盖问题,不能据此认定宽字符 API 不可用。

3. 宽字符 GDI 目标 HDC 验证

验证对象:

  • 默认主画面(未调用 SetWorkingImage)上的 TextOutW/DrawTextW/GetTextExtentPoint32W。
  • 调用 SetWorkingImage 之后的目标工作图像 HDC。

单纯比较 HDC handle 不足以证明绘制表面身份。每个目标必须记录以下表面指纹:

HDC handle
GetCurrentObject(hdc, OBJ_BITMAP) 返回的 HBITMAP
HBITMAP 宽高、位深和 stride(能够取得时)
GetCurrentObject(hdc, OBJ_FONT) 返回的 HFONT 与 LOGFONTW
GetMapMode / viewport origin / window origin / viewport extent / window extent
GetClipBox
GetBkMode / GetTextColor / GetBkColor / GetTextAlign
GetTextCharacterExtra

BeginBatchDraw 前后即使 HDC 值相同,其内选入的 bitmap 也可能变化,因此表面身份必须在“实际绘制发生的那一刻”采集。绘制矩阵必须覆盖批次状态:

默认画面 / SetWorkingImage
    ×
批次外 / BeginBatchDraw 批次内
    ×
绘制后 / FlushBatchDraw 后 / EndBatchDraw 后
    ×
EasyX 绘制 / 宽字符 GDI 绘制

这样才能区分五种不同结果:

  • API 返回成功但画到了错误 bitmap。
  • 已进入离屏缓冲但尚未合成上屏。
  • FlushBatchDraw 后才可见(合成链正确)。
  • EndBatchDraw 后被后续 EasyX 绘制覆盖。
  • 工作图像正确但屏幕合成失败。

主画面和工作图像分别使用不同的固定哨兵底色,并按以下步骤验证:

  1. SetWorkingImage 切换前后分别采集 HDC、selected bitmap 和 GDI 状态。
  2. 对目标表面与非目标表面记录绘制前像素摘要。
  3. 调用 SaveDC,设置受控字体、颜色、背景模式、mapping mode 和 clip。
  4. 使用 W 版本 API 绘制唯一的固定测试图案和测试串。
  5. 从目标表面回读像素,验证目标发生预期变化。
  6. 从非目标表面回读相同区域,验证没有串画。
  7. 调用 RestoreDC,重新采集状态并验证未污染 EasyX 后续绘制。
  8. 记录 FlushBatchDraw 前、后以及 EndBatchDraw 后的屏幕证据,区分“已进离屏未合成”与“合成后可见”。
  9. 在 EndBatchDraw 后对同一区域用 EasyX 再绘制一次并回读,确认宽字符 GDI 结果是否被后续 EasyX 绘制覆盖。

实测还确认了一个 EasyX 特有的调用顺序约束:不得在 SetWorkingImage(&working) 保持为当前目标时直接进入 BeginBatchDraw();本轮隔离运行中该顺序稳定触发 0xC0000005。探测器和后续框架应使用:

SetWorkingImage(nullptr)
BeginBatchDraw()
SetWorkingImage(&working)   // 只在批次内部切换工作图像
...绘制...
SetWorkingImage(nullptr)   // Flush/End 前回到主画面
FlushBatchDraw()/EndBatchDraw()

这不是“工作图像不能在批次内绘制”,而是“批次必须从主画面安全进入和退出”。工作图像上的宽字符结果仍需通过显式 putimage 合成到主画面。

宽字符能力与字体覆盖分开判定:

  • TextOutW/DrawTextW 调用成功、目标表面变化且非目标表面不变,证明 W 绘制链可达。
  • 出现缺字方框、空 glyph 或黑白 emoji 时,记录字体覆盖/字体回退结论,不直接判定 W 绘制失败。
  • GetTextExtentPoint32W 返回当前 HDC、字体和文字状态下的逻辑 advance,不是像素墨迹包围盒,也不考虑裁剪。
  • 不要求 SIZE.cx 与像素 ink bounds 严格相等;应比较同一 HDC/字体/状态下的 caret advance、裁剪安全性和相邻文本是否重叠。
  • 若 DrawTextW 使用特定 flags,应额外使用相同 flags 的 DT_CALCRECT 结果比较,不能把不同 API/flags 的结果强行等同。

4. DPI 与坐标空间

验证 EasyX 绘制坐标空间与 HWND 客户区坐标是否一致:

  • 记录进程、线程和目标窗口的 DPI awareness context,以及 GetDpiForWindow(hwnd)。
  • 记录 EasyX 工作表面尺寸、HWND 客户区尺寸和窗口屏幕矩形。
  • 验证 EasyX 逻辑坐标是否等于 HWND 目标坐标,还是需要显式换算。
  • IMM 输入链只定义为:合成控件局部坐标 → EasyX 绘制坐标 → 目标 HWND 相对坐标 → COMPOSITIONFORM/CANDIDATEFORM。
  • 同时把目标 HWND 相对坐标换算为屏幕坐标,作为截图标注和诊断证据;屏幕坐标不是预设的 IMM 输入中间态。
  • 额外记录:GetMapMode、viewport/window origin 与 extent、当前 graphics mode / world transform、GetClientRect、GetWindowRect,以及 ClientToScreen → ScreenToClient 的往返误差。

验证 EasyX 是否隐式缩放时,不只画文字,而是在同一组数值坐标上分别绘制:

EasyX 十字和矩形
原生 GDI 十字和矩形
EasyX 文字基线
TextOutW 文字基线

然后通过像素 bounding box 比较位置和尺寸,区分“坐标相同但字体度量不同”与“整个绘图坐标系发生缩放”。

规则:只有实测发现 EasyX 坐标空间与目标 HWND 坐标空间不一致时,才引入显式换算;否则直接使用,避免双重缩放。

DPI awareness 不得在窗口创建后临时切换;需要比较不同 awareness 模式时各自启动独立进程,否则结论易受 Windows 虚拟化状态污染。

验收预期必须服从实际 DPI awareness:

Awareness GetDpiForWindow 预期 跨屏预期
DPI unaware 96 不要求随显示器改变;记录系统虚拟化
System aware 系统 DPI 不要求随显示器改变;记录系统虚拟化
Per-monitor aware 窗口所在显示器 DPI 跨屏后应更新,并重新计算证据

如果实际 awareness 无法支持某项跨屏预期,该用例标记为 not_available,不能误判为坐标实现失败。

5. IME 定位证据

0C 发生在阶段 5 TextBox 自绘之前,因此不依赖真实 TextBox。探测 Fixture 必须提供以下合成 anchor:

anchor_top_left_safe
anchor_center
anchor_bottom_right_safe

三个 anchor 均与屏幕边缘保留足够安全距离,用于区分框架换算错误和系统为避开屏幕边界进行的主动调整。

每个 anchor 记录:

controlLocalCaretTopLeft
controlLocalCaretBottom
controlLocalLineRect
easyxSurfacePoint/Rect
targetHwndRelativePoint/Rect
diagnosticScreenPoint/Rect
COMPOSITIONFORM 输入值与 API 返回值
CANDIDATEFORM 输入值与 API 返回值

IME 位置采用“双时机写入”,不得假定所有输入法都会先向目标 HWND 发送相同的组合生命周期消息:

  1. 焦点、caret 或合成 anchor 变化时,立即对当前 IME context 调用 ImmSetCompositionWindow/ImmSetCandidateWindow,为下一次组合预设位置。
  2. 收到 WM_IME_STARTCOMPOSITION/WM_IME_COMPOSITION 时使用最新 anchor 再校准一次。
  3. 两类调用必须分别记录 positionTrigger、调用次数与成功次数;预设调用成功不能代替真实输入和视觉验收。
  4. 诊断程序至少还要有一次真实 IME 生命周期定位事件完成 context 获取与两个 setter,才允许自动运行整体完成;仅切换三个 anchor 或只观察到失败的生命周期调用不得伪造通过。

测试至少覆盖微软拼音和搜狗输入法,并记录输入法标识、版本(可取得时)、HKL 和测试时间。不同 IME 可能主动调整位置或不显示独立组合窗口:

  • API 返回成功不能代替视觉验收。
  • 组合窗口不存在但候选窗口可观察时,只验收实际存在的系统 UI,不把“未显示组合窗口”自动判失败。
  • 系统因避让屏幕边缘进行的调整应记录为 system_adjusted,不能直接归因于框架坐标错误。
  • 候选框/组合窗口实际位置必须截图并人工标注,不能只写“正常”。

阶段 6 再使用真实 TextBox 的 caretTopLeft/caretLineRect 重跑定位矩阵。0C 通过只证明坐标转换和 IMM 调用链可行,不证明 TextBox 自身 anchor 生成正确。

6. DPI 矩阵

基础矩阵:

显示缩放:100% / 125% / 150%
跨屏:主屏 100% + 副屏 150%(或反向)
IME:微软拼音 / 搜狗输入法
anchor:top_left_safe / center / bottom_right_safe
表面:主画面 / EasyX 工作图像

执行时记录实际 awareness,并按 §4 的 awareness 规则确定预期。单显示器每档 DPI 至少验证一次两类表面和 center anchor;跨屏场景必须验证窗口完全位于每块显示器时的 DPI、坐标和候选窗口定位,窗口跨越两块显示器但未完成归属切换的瞬态只记录、不作为稳定验收点。

7. 自动与人工分层

  • 自动检查:HDC/selected bitmap 身份、GDI 返回值、GDI 状态恢复、逻辑测量、目标/非目标表面像素摘要、DPI/坐标换算数值。
  • 必须人工观察:候选框/组合窗口的实际屏幕位置、字体缺字/回退外观、组合字符外观、跨屏拖动后的定位。

自动检查结果不能替代人工观察,人工观察也必须留下记录(截图 + 结论),不能只写“正常”。

8. 结果格式

使用独立的视觉/坐标结果格式,不并入 0A 键盘 JSONL schema。

建议输出:

TestResults/VisualProbe/<sessionId>/manifest-v1.json
TestResults/VisualProbe/<sessionId>/evidence/*.png
TestResults/VisualProbe/<sessionId>/manual-evidence/*.png
TestResults/VisualProbe/<sessionId>/manual-review-v1.json

visualProbeSchemaVersion 初始为 1。manifest 必须包含:

字段 必需 说明
visualProbeSchemaVersion 是 初始为 1
sessionId 是 本次 0C 会话 UUID
startedAtUtc 是 UTC 时间
processId/threadId 是 进程和 GUI 线程
osBuild/easyxVersion 是 能取得的运行环境版本
testCaseId 是 矩阵中唯一用例 ID
testStringId/codePoints/utf16Units 绘制用例 固定测试串身份与实际构造结果
surfaceId 绘制用例 main 或指定 working image
batchState 绘制用例 outside / inside,描述绘制发生时是否位于批次括号内
observationPhase 绘制用例 after_draw / after_flush / after_end;不适用的组合写 not_applicable
hdc/hbitmap/bitmapWidth/bitmapHeight/planes/bitsPerPixel/stride/dibSection/hfont/logFont 绘制用例 表面和字体指纹;无法从 DDB 稳定取得的字段允许写 not_available
gdiStateBefore/gdiStateAfter 绘制用例 mapping、clip、颜色、对齐等
apiResults 绘制用例 各 GDI API 的函数特定返回语义
logicalExtent 绘制用例 相同 HDC/字体/状态下的测量结果
targetPixelsBefore/After 绘制用例 目标区域摘要
controlPixelsBefore/After 绘制用例 非目标表面未串画证据
dpiAwareness/windowDpi 坐标用例 实际 awareness 与 DPI
clientRect/windowScreenRect/surfaceSize 坐标用例 坐标空间基线
syntheticAnchorId 坐标用例 合成 anchor
anchorCoordinates 坐标用例 local/surface/HWND-relative/screen 四组坐标
compositionForm/candidateForm IME 用例 输入值、style、API 结果
imeId/imeVersion/hkl IME 用例 输入法身份
positionTrigger/compositionActiveAtAttempt IME 用例 区分 anchor 预设与 IME 生命周期校准,并记录调用时是否处于组合态
preconfigureApiAttemptCount/lifecycleApiAttemptCount IME 用例 两类定位调用及其成功次数,避免把不同触发来源混为一类
positionDisposition IME 用例 as_requested/system_adjusted/not_observable
automatedStatus 是 pass/fail/inconclusive/not_available
manualStatus 是 pass/fail/inconclusive/not_reviewed/not_available
sourceManualReview/manualReview 有人工评审时 评审旁车路径与哈希、实际输入法身份来源、评审时间和摘要
failureCategory 否 失败分类,不得只写自由文本
evidenceFiles 是 相对路径、用途、SHA-256、裁剪区域
notes 否 补充说明,不能替代结构化结论

截图只能包含探测窗口和固定非敏感测试内容;不得截取其他应用或真实用户输入。每个 evidence 文件记录 SHA-256,防止结果文件和图片错配。

人工结论通过独立 manual-review-v1.json 输入 analyzer,不直接手改生成的 manifest。analyzer 必须验证 review 覆盖所有用例、状态和位置枚举合法、pass 用例具有明确的 manual-evidence 文件、人工通过不覆盖自动失败,并校验总体状态可由逐用例状态推导;任一条件不满足即拒绝生成 manifest。

failureCategory 只允许以下初始枚举;新增类别必须提升或兼容扩展 schema:

surface_unreachable
surface_cross_write
surface_not_composited
screen_composite_failed
gdi_state_not_restored
measurement_relation_invalid
font_coverage_missing
coordinate_transform_invalid
ime_position_invalid
awareness_not_supported
evidence_missing

positionDisposition=system_adjusted 不是自动失败。它必须结合安全 anchor 复测、截图和人工结论,最终落到 manualStatus。

示例只展示核心字段:

{
  "visualProbeSchemaVersion": 1,
  "sessionId": "c73ab2ee-5f83-4d38-a253-58d1d775ca95",
  "startedAtUtc": "2026-10-02T08:00:00Z",
  "processId": 12000,
  "threadId": 12400,
  "testCases": [
    {
      "testCaseId": "gdi-working-cjk-150",
      "testStringId": "cjk_bmp",
      "codePoints": ["U+661F", "U+57A3", "U+7ED8", "U+5236", "U+6D4B", "U+8BD5"],
      "utf16Units": ["661F", "57A3", "7ED8", "5236", "6D4B", "8BD5"],
      "surfaceId": "working-image-1",
      "hdc": "0x0000000012345678",
      "hbitmap": "0x0000000087654321",
      "logicalExtent": {"cx": 144, "cy": 24},
      "targetPixelsBefore": "sha256:...",
      "targetPixelsAfter": "sha256:...",
      "controlPixelsBefore": "sha256:...",
      "controlPixelsAfter": "sha256:...",
      "dpiAwareness": "per_monitor_aware_v2",
      "windowDpi": 144,
      "automatedStatus": "pass",
      "manualStatus": "not_reviewed",
      "failureCategory": null,
      "evidenceFiles": [
        {"path": "evidence/gdi-working-cjk-150.png", "purpose": "target_crop", "sha256": "..."}
      ]
    }
  ]
}

9. 阶段门槛、失败判定与降级

结果 判定 后续动作
W API 成功、目标表面变化、非目标表面不变 通过 阶段 5 可采用宽字符 GDI 路径
W API 失败或无法写入 EasyX 目标表面 阻塞 排查 HDC/selected bitmap/clip/state;确认宽字符路径确实不可用后才评估降级
绘制成功但逻辑测量、caret advance 或裁剪关系异常 阻塞 排查字体、mapping mode、字符间距和 flags;不得直接降级 CP936
某些 glyph 缺失但其他宽字符正常 字体问题 选择覆盖字体或字体回退;不得按编码问题降级 CP936
坐标空间一致且当前 awareness 下定位正确 通过 使用目标 HWND 相对坐标,不额外缩放
坐标空间不一致但换算规则经矩阵验证 条件通过 固化显式换算和适用 awareness,防双重缩放
awareness 不支持所测跨屏预期 不可用 标记 not_available,不当作实现失败
API 成功但候选框视觉位置异常 未通过 阻塞阶段 6,排查 anchor、坐标转换和 IME 差异
系统因屏幕边界调整候选框 系统调整 记录 system_adjusted,在安全 anchor 复测后判定

CP936 只允许作为已证实宽字符绘制链无法使用后的显式显示降级;测量差异、字体缺字或单个 IME 定位异常都不是 CP936 降级依据。任何降级必须带证据并保留 UTF-16 内部原文。

10. 运行模式与进程隔离

0C-1 与 0C-2 可以并行开发和独立运行,但不得在同一进程内同时探测:EasyX 工作图像、批次状态和 DPI awareness 都带有进程级或全局状态,混在一次运行中会互相污染。

同一诊断工程提供独立模式,共享 manifest、截图和固定测试串基础设施,但每种模式独立启动进程:

--mode wide-text
--mode dpi-coordinate
--mode ime-position

--auto-close-ms 仅用于不输入文字的 0C-3 生命周期烟测,正式人工运行不得启用;否则三个 anchor 不会产生真实 IME 位置证据,结果应为失败而不是伪通过。

11. 0C 基线产物与 0B 引用契约

0C 完成后必须生成可哈希的基线 manifest(绘制基线 + 坐标基线),并计算其 SHA-256。0B 的每轮实验结果必须强制记录:

drawingBaselineManifest
coordinateBaselineManifest
drawingBaselineSha256
coordinateBaselineSha256
combinedBaselineId
dpiAwareness
dpiForWindow
imeIdentity
hkl

如果 0B 运行环境与 0C 基线不一致,该轮 0B 的候选框结论只能标记为 inconclusive,不得判定消费策略通过或失败。

实现与影响

  • 关键实现点:
    • 新增不链接 StellarX Core 的 EasyX.VisualProbe 独立诊断工程,避免框架 WndProc、控件或事件循环污染 EasyX 原始行为。
    • wide-text 覆盖主画面/工作图像 × 批次外/批次内,构造五类固定 Unicode scalar 测试串,验证 W API、目标/非目标表面、GDI 状态恢复、Flush/End 合成和后续 EasyX 绘制兼容性。
    • dpi-coordinate 在同一数值坐标上绘制 EasyX/GDI 矩形、十字和文本,记录 mapping、DPI awareness、像素 bounds 与客户区/屏幕往返误差。
    • ime-position 提供三个合成 caret anchor;按总设计 v1 契约使用 COMPOSITIONFORM/CFS_POINT 与 CANDIDATEFORM/CFS_EXCLUDE + caretLineRect;anchor 变化时先预设位置,IME 生命周期消息到达时再校准;子类化分支只记录消息阶段、触发来源、anchor、HKL/IME 身份与 IMM API 返回值,不记录组合串或候选文字。
    • 客户区截图优先使用 GetDC(hwnd) + BitBlt,只有失败才回退 PrintWindow,并把实际捕获后端写入 manifest,避免 WM_PRINT 扰动 Flush 前观察。
    • PowerShell runner 为每个模式启动独立进程;analyzer 计算原始结果、manifest 和每张 PNG 的 SHA-256,并生成 0B 可引用的组合基线 ID。
    • 执行顺序:0C-1(宽字符/批次绘制)与 0C-2(DPI/坐标)可并行 → 0C-3(真实 IME 定位)→ 回写带哈希的 0C 基线 → 0B 单消息消费实验 → 阶段 1 输入采集与规范化。
    • 实现时根据 Unicode scalar 列表构造固定测试串,使用合成 anchor、预定义采样区域和主/工作图像双表面哨兵,不依赖用户输入或真实 TextBox。
    • 实现结果由版本化 manifest 和独立 PNG 证据组成,不复用 0A 键盘 JSONL schema。
  • 涉及文件 / 类 / 函数:
    • tests/EasyX.VisualProbe.vcxproj
    • tests/visual-probe/VisualProbeCommon.h/.cpp
    • tests/visual-probe/WideTextProbe.cpp
    • tests/visual-probe/DpiCoordinateProbe.cpp
    • tests/visual-probe/ImePositionProbe.cpp
    • tests/visual-probe/VisualProbeMain.cpp
    • scripts/run-visual-probe.ps1
    • scripts/analyze-visual-probe.ps1
    • imGui-easyX.sln
  • 兼容性影响:
    • 探测关闭时不应产生文件、线程、定时器或公共 API 变化。
  • 性能影响:
    • 仅测试构建开启,对正式路径无影响。
  • 风险点:
    • 视觉结论依赖人眼,必须与表面指纹、目标/非目标像素摘要和截图证据相互印证。
    • 跨屏 DPI 依赖真实多显示器环境,CI 环境可能不具备,需人工实测。
    • HDC handle 可能复用,若不同时记录 selected bitmap 和 GDI 状态,可能得到错误的表面身份结论。
    • IME 可能主动调整候选窗口,必须在安全 anchor、不同 IME 和实际 awareness 下区分系统调整与框架换算错误。

测试与验证

  • 测试范围:
    • Debug x64 独立工程构建。
    • 0C-1 四组表面/批次矩阵、五类固定测试串、30 张 PNG 证据与 40 个结构化 manifest 用例。
    • 0C-2 当前生产 awareness(inherit/unaware)和独立 Per-Monitor-V2 进程下的坐标、像素与 DPI 证据。
    • 0C-3 自动关闭失败门槛烟测,以及微软拼音/搜狗真实候选框定位与搜狗提交后 stale-position 回归。
  • 验证步骤:
  1. 实现前只读定位 EasyX SetWorkingImage、主画面/工作图像 HDC、selected bitmap 和 GDI 调用位置。
  2. 实现 0C 后,根据明确码点构造固定测试串,使用双表面哨兵验证目标表面变化、非目标表面不变和 GDI 状态恢复。
  3. 使用合成 anchor,在实际 DPI awareness 下完成 100%/125%/150%、跨屏、微软拼音和搜狗矩阵,保存结构化 manifest 与截图证据。
  4. 依据 §9 分别判定宽字符链、测量链、字体覆盖、坐标换算和 IME 定位;禁止把不同失败类型合并成 CP936 降级。
  5. 阶段 6 使用真实 TextBox anchor 重跑坐标与 IME 定位集成验证。

当前机器没有 pwsh,人工 0C-3 使用 Windows PowerShell,分别执行:

powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\run-visual-probe.ps1 -Mode ime-position -ImeName MicrosoftPinyin
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\run-visual-probe.ps1 -Mode ime-position -ImeName Sogou

每轮依次用 F1/F2/F3 选择三个 anchor,只输入固定非敏感串 ceshi,观察候选框后按 Escape 取消。随后在 F3 提交一次固定候选,再切回 F1 输入 ceshi,确认没有沿用 F3 旧位置。要关闭 0C-3,必须在脚本打印的 manual-evidence 目录保存三个 anchor 的紧裁剪截图,并为提交后回到 F1 的回归场景再保存一张截图;建议文件名分别以 anchor_top_left_safe_、anchor_center_、anchor_bottom_right_safe_ 开头。没有截图时即使 IMM API 自动状态通过,人工状态仍保持 not_reviewed。

  • 验证结果:
    • 2026-10-05:EasyX.VisualProbe Debug x64、Release x64、Debug Win32 构建通过;仅有 EasyX 25.9.10 头文件自身的 C4201 匿名联合警告。解决宿主同时存在 PATH/Path 时,构建必须沿用干净子进程隔离,否则会触发环境导致的 MSB6001。
    • 2026-10-05:最终自动组合基线位于忽略目录 TestResults/VisualProbe/20261005-025951-856-06d08ac1-58fa-426f-b4c8-803d246b47dd,sessionId=06d08ac1-58fa-426f-b4c8-803d246b47dd,自动状态为 pass,人工状态为 not_reviewed。
    • 绘制 manifest SHA-256:BCD0E3DD5D90C5D6F892F9127D81FD124C1DC7D619260B236DBCB8E53CBEC9CA;坐标 manifest SHA-256:35D15BF2DD54D9A57B1F8470C8415025F1BAE6CE98D4628F13B64AB1EC381000;组合基线 ID:sha256:5C544D9FEC9C1A2324A7AF3F593A1A34110100644BAF5071E777302387E3C6C5。
    • 0C-1:TextOutW、DrawTextW、GetTextExtentPoint32W 在主画面和工作图像、批次内外全部成功;目标表面变化、非目标表面不变、GDI 状态恢复;主画面批次内在 Flush 前不可见、Flush 后可见;工作图像不会自行上屏,经 putimage + FlushBatchDraw 后正确合成;End 后追加 EasyX 绘制未改变受保护文字区域的像素摘要。
    • 0C-1 截图复核可见 BMP 中文、代理对 emoji 的单色 glyph、NFD 组合字符和混排结果;这属于模型辅助复核,不能替代规格要求的人类视觉签字,因此 manifest 仍保持 manualStatus=not_reviewed。
    • 0C-2:当前程序继承模式为 DPI-unaware,GetDpiForWindow=96;在同一台实际 150% 显示器上,以独立 Per-Monitor-V2 进程运行得到 GetDpiForWindow=144。两种 awareness 下 EasyX 与原生 GDI 的像素 bounds 均与客户区一致,四个 ClientToScreen -> ScreenToClient 往返误差均为 0,未观察到 EasyX 自身额外隐式缩放。
    • Per-Monitor-V2 补充 manifest SHA-256:091E446F77BD997428452979A0AF122883129497104035F2688A3535C1A191E1;该结果只覆盖当前 144 DPI 显示器,不代表 100%/125% 或跨屏矩阵已完成。
    • 0C-3 无输入自动关闭烟测按预期生成三个 anchor 客户区证据与可解析 manifest,并因没有真实 IME API 事件返回失败;这证明失败门槛没有被烟测绕过,不构成候选框定位通过证据。
    • 2026-10-05:微软拼音在三个 anchor 上的 IMM 调用均成功,人工截图未观察到相对 caret 的整体偏移;两轮实际都使用微软拼音,第二轮脚本参数 Sogou 仅为预期标签,不得当作搜狗证据。
    • 2026-10-05 23:51:首次真实搜狗定位运行 20261005-235150-704-4e01df99-019b-41bd-b8ea-b2ff508626e0 按门槛正确失败。F1/F2 的 IMM API 尝试均为 0,F3 为 2/2;人工截图显示 F1/F2 候选框位于窗口外,首次获得 F3 生命周期定位后又沿用 F3 旧位置。
    • 2026-10-06:确认根因是探测器只在 WM_IME_STARTCOMPOSITION/WM_IME_COMPOSITION 到达后设置位置。已改为 anchor 变化立即预设、生命周期消息再次校准,并增加触发来源及分项计数;三平台构建与搜狗真实复验均已完成。
    • 2026-10-06:修复后搜狗运行 20261006-010231-219-3fc5186a-f494-4d88-8988-67d80b25ed7e 自动与人工状态均为 pass。F1 预设 2/2、F2 预设 1/1、F3 预设 1/1 且生命周期校准 2/2;提交后回到 F1 的截图与初始 F1 裁剪字节一致,证明没有沿用 F3 stale position。
    • 成功搜狗 manifest SHA-256:4908B1F9B5C29555216EF62BE64F2F6780F7EF213D8E1ECE16817B5915FF9AED;人工评审旁车 SHA-256:94459F19A3F74E46C23322E6276C03B3AF054D6293DA4D090220FF06470A32A3;四张人工证据和三张自动 anchor 图均已进入 manifest,evidenceCount=7。
  • 已知限制 / 遗留问题:
    • 当前可用环境下微软拼音与搜狗的三个 anchor 已完成真实视觉验收,搜狗“提交一次后再次切换”回归也已通过;0C-3 当前环境门槛关闭。
    • 当前 -ImeName 只是操作者预期标签;同一中文 HKL 下的 TSF 配置文件切换不能由现有 HKL/IMM32 身份字段自动证明,最终身份需保留操作者确认边界。
    • 当前只覆盖同一台 150% 显示器上的 DPI-unaware 虚拟化视图(96 DPI)和 Per-Monitor-V2 物理视图(144 DPI);100%、125% 与跨屏不同 DPI 仍依赖实机环境。
    • 字体回退行为与 EasyX 字体设置的耦合需要实测确认。
    • 0C 使用合成 anchor,只验证坐标转换和 IMM 调用链;真实 TextBox anchor 必须在阶段 6 复验。
    • 某些 IME 不显示独立组合窗口或会主动调整候选框,允许用例得到 not_available、system_adjusted 或 inconclusive,但必须保留证据。

落地信息