Files
Just-DataInspector/Documents/UI_Refactor_Report.md
T
DeepSeek V4.1 Flash 9a9802e589 UI 重构:报告更新为 P0-P3(含 Toolbar 迁移),并补记框架裁剪约定变更
- Documents/UI_Refactor_Report.md:标题改为 P0/P1/P2/P3-Toolbar;新增第 6 节
  「P3:Toolbar 迁移到控件树」(新旧实现对照表、UiButtonWidget、kUiClipBleed 约定、
  8 场景像素哈希 + 3 条功能断言);关键数字更新(产物 881,664 B、已迁移 1 个组件);
  重排后续章节编号(7 未做项 / 8 下一步 / 9 结论)。
- Documents/UI_Refactor_Progress.md:阶段看板标记 P3 为「Toolbar 已完成,其余待做」,
  补第 4 条提交记录与新的「下一步」。

验证:make release / make debug 零警告;Tools/uicheck 62/62;Target.exe 881,664 B。
2026-10-28 12:29:59 +08:00

12 KiB
Raw Blame History

UI 重构报告(P0 / P1 / P2 / P3-Toolbar)

日期:2026-10-28 分支:UI_Refactor(从 dev 切出;dev 又由 main@df715a0 切出) 所有提交只在本地仓库,main / dev 未受影响。计划见 UI_Refactor_Plan.md,台账见 UI_Refactor_Progress.md。


1. 本次交付了什么

阶段 交付物 状态
P0 入库离屏回归网 Tools/uicheck(62 项断言,build.ps1 + main.cpp) ✅
P1 UI 基础层 Components/Shared/{Inc,Src}/UiLayout.* + UiWidget.*(约 1 440 行含注释) ✅
P2 PlotView 实现按关注点重分为七个分片;PlotViewPaint.cpp 9 080 → 4 913 行(−46%) ✅
P3 Toolbar 已迁移到控件树(新增 UiButtonWidget,公开 API 未变、逐像素 0 差异) ✅(1/5 组件)

五个提交:

84a349f  UI 重构 P3:Toolbar 迁移到控件树(新增 UiButtonWidget,逐像素 0 差异 + 功能断言)
eb11e63  UI 重构:补充进度台账、阶段报告与共享记忆同步
ec3f93f  UI 重构 P2 续:把布局与命中几何从绘制分片拆出(PlotViewPanels.cpp,逐像素 0 差异)
b532046  UI 重构 P2:PlotViewPaint.cpp 按关注点分片(机械搬移,逐像素 0 差异)
8bdf48c  UI 重构 P0+P1:新增布局/控件基础层与离屏回归网(对产品像素零影响)

2. 关键数字

指标 重构前 重构后
PlotViewPaint.cpp 9 080 行 4 913 行(−46%)
最大源文件 9 080(PlotViewPaint.cpp) 4 913(PlotViewPaint.cpp)
PlotView 实现文件数 4 7(职责单一)
自动化 UI 探针 0 62 项断言(入库、可复跑)
已迁移到控件树的组件 0 Toolbar(1 个,含 8 场景像素 A/B + 3 条功能断言)
产物 873 984 B 881 664 B(+7.7 KB,< 900 KB 阈值)
产品像素差异 — 0(分片三场景 + 迁移八场景哈希逐一相同)

3. 新增的 UI 基础层(P1)

3.1 UiLayout:布局原语 + 布局缓存

能力 说明
矩形原语 UiRectIntersect / UiRectUnion / UiRectDeflate / UiRectHasArea
设备像素内侧对齐 UiRectSnapInward(上界 floor、下界 ceil)—— 离屏位图贴图源矩形必须用它,否则补丁会多算一行盖到相邻网格(历史 1px 接缝 BUG)
对齐 UiAlignInRect(BEGIN/CENTER/END/STRETCH、超尺寸夹取、偏移后置)
Stack UiStackExtent / UiStackRects(固定尺寸 + kUiSizeAuto 均分剩余、绝不产生负尺寸)
Flow UiFlowRects(换行 + overflow 下标,供图例 +N 使用)
Abs UiAbsRect(绘图区叠加物的主力布局)
缓存 UiLayoutCache:std::deque 保证引用稳定、定向失效、按"最久未命中"淘汰

设计取舍:不实现 Grid(绘图区不是表格,用不上);Card 只是皮肤不是布局; 真正需要的是 Abs(主力)/ Anchor / Stack / Flow / Measure-driven。

3.2 UiWidget:控件树基类

flowchart TD
    H[UiHost(P5)<br/>共享 DIB · 脏区并集 · 一次回送 · 动画 tick]
    H --> R[UiWidget 根]
    R --> A[普通控件:直接画进共享缓冲]
    R --> B[高成本叶子:自持位图,完成即 BitBlt]
    A --> S[SurfaceView{dc, rect, stride, bits, version}]
    B --> S

已实现的契约:

契约 内容
两段式布局 Measure(自下而上、带缓存、失效只沿祖先链)/ Arrange(自上而下、回调 OnArrange 摆子节点)
脏区下探绘制 dirty 为空 = 全画;与子树矩形无交集 ⇒ 整棵子树 return;有效裁剪 = 祖先裁剪切 ∩ 自身 ∩ 脏区
裁剪契约 进入时收紧 GDI+ 裁剪并暴露 ctx.clip;GDI 绘制不受其约束 ⇒ 派生类必须把 ctx.clip 传给 UiTextClipped 一类辅助函数
事件派发 capture(拖拽期直投)→ modal(只向模态子树)→ 只沿命中链(最深命中者 → 祖先兜底);键盘沿焦点链;{consumed, redraw, dirty}
交互仲裁 CaptureMouse / ReleaseMouse(仅持有者可释放)/ SetModal / SetHitTransparent / RequestFocus
光标链 QUERY_CURSOR 由最上层认领者返回,未认领交给宿主
动画 AdvanceAnimation(nowMs) 逐节点下发同一时间戳并汇总"是否仍有动画"
缓存键 ContentKey()(派生类提供)+ BaseContentKey()(矩形/可见性),父层只聚合

三条禁区(写进 agent_mem.md §11.2 第 235–240 条)

  1. 波形数据→像素管线(网格/曲线/采样点)不做成控件节点;
  2. 不是每个控件自持位图(内存按面积线性增长:128 网格已 ≈250 MB 量级);
  3. 禁止"父脏 ⇒ 子树全脏"(会把"只重绘一个网格/面板"打回整块重绘)。

4. 分片重构(P2)

4.1 七个分片

文件 行数 职责
PlotViewPaint.cpp 4 913 绘制(Paint* / Render* / 输入框绘制)
PlotViewInteraction.cpp 3 430 命中、鼠标/键盘、光标形状、面板层序
PlotViewFeatures.cpp 3 266 光标读数、提示、缩放、自适应、派生曲线、边沿统计
PlotViewRaster.cpp 1 876 光晕层 / 场景缓存 / 装饰层 / 宽基底图层与预算
PlotViewPanels.cpp 1 359 面板布局、图例徽标几何、自由标记取值与探测
PlotView.cpp 889 视图核心(构造、布局、坐标映射、网格结构)
PlotViewCacheKey.cpp 474 内容指纹与缓存键

4.2 为什么能"零风险"搬移

  • PlotViewInternal.hpp 里 pvdetail 的工具函数全部 inline ⇒ 跨 TU 安全的;
  • 所有搬移的都是成员函数定义,声明仍在 PlotView.hpp;
  • 因此不需要改任何 include,也不需要改 Makefile(按 basename 自动收集 .cpp)。

4.3 验证方式(重点)

不是"能编译就算过",而是逐像素 0 差异:

场景 内容
A 默认全览(4 条信号)
B 深放大 0.90–1.10 + 自由标记(含固定点、透明度 0.8)+ 均值滤波(点数 2 / 迭代 1)+ 弱峰调整(60% / 2 次)+ 时间光标
C 在 B 基础上再平移一帧(触发宽基底图层滚动/补条路径)

渲染后备缓冲区后取 FNV 哈希:

迁移前 A 2166933039626F66  B C444ECFEB18FD73D  C 7984446D8E7A4418
迁移后 A 2166933039626F66  B C444ECFEB18FD73D  C 7984446D8E7A4418   ← 三场景完全一致

哈希与 Tools/uicheck 都在每次搬移后重跑;make release / make debug 零警告。


5. 过程中修掉的自身缺陷

# 缺陷 修法
1 UiWidget 头文件漏声明 AdvanceAnimationSelf / AnimationActiveSelf(.cpp 已引用) 补 protected 虚函数
2 最初的派发是"遍历同层兄弟、未消费就继续" ⇒ 一次点击可能被视觉上位于下方的控件也处理 改为"只沿命中链派发 + 祖先兜底"
3 探针用绝对次数断言事件派发次数(被前面用例的计数污染) 全部改成增量断言
4 分片后回退做 A/B 时,Build/<Config>/ 残留 PlotViewRaster.o / PlotViewCacheKey.o,链接探针因重复符号失败 记录为复现步骤的硬要求:先删陈旧 .o

另外顺手清理:agent_mem.md 侧新增 §11.2 第 235–240 条 + §10 变更日志行; documents/refactor_report_2026-10-16.md(过时报告)在 dev 分支删除,结论已并入 agent_mem.md。


6. P3:Toolbar 迁移到控件树(真实组件落地)

这是第一个真正换成控件树的现有组件(不是新增代码,而是替换旧实现),公开 API 一字未改。

旧实现 新实现
Toolbar::Button 同时存配置、矩形、悬停、按下 Button 只存静态配置 + 控件指针;几何与外观状态归控件
m_hoverIndex / m_pressedIndex 手工维护 由 UiWidget 的悬停链与 capture 维护
Paint 自己遍历按钮逐项绘制 根控件(底色 + 底部分隔线)+ 按钮子节点,一次 root->Paint(ctx)
四个鼠标函数各自扫按钮矩形 统一翻译 UiEvent → root->Dispatch();动作经 onClick 登记、抬起后执行
死字段 Button::hover(从未赋值) 删除,改用基类 Hovered()

新增 UiButtonWidget(Components/Shared):主按钮/普通按钮/可切换/禁用四套配色与旧实现逐像素一致; 按下即 CaptureMouse(),"在自身范围内松开才算点击",指针离开时 CancelPress()。

框架约定调整:UiWidget::Paint 的裁剪由"严格等于自身矩形"改为**"自身矩形外扩 kUiClipBleed(1px)"** —— 边框以矩形边界为中心绘制,严格裁剪会切掉外半侧、控件变细(与旧代码不再像素一致)。

验证(两类,缺一不可)

  1. 像素:8 个场景(全览 / 深放大+自由标记+滤波+弱峰+光标 / 平移 / 悬停按钮 / 按下 / 松开 / 悬停主按钮 / 移出工具栏)哈希迁移前后逐一相同。
  2. 功能(证明"点击真的翻译成动作",而不是"看起来没坏"):
    • locate-cursor-button PASS (1291,73) —— 用控件自身矩形定位「光标」按钮;
    • click-cursor-changes-mode PASS (1 -> 2) —— 点击后光标模式 单→双;
    • release-outside-does-not-fire PASS (2 -> 2) —— 按下后移出再松开,不触发动作。

7. 明确未做的事(避免误解)

未做 原因
迁移 TabBar / PopupMenu / ColorPicker / SignalPanel 行 其余 chrome 组件:需逐个迁移 + 逐组件 A/B(Toolbar 已示范套路)
UiHost + 共享 32bpp DIB + SurfaceView 属 P5;需同时把 56 处 InvalidateRect 收敛成脏区并集,必须与迁移同批验证
引入多线程光栅 本项目实测 1.03x(GDI+ 内部串行),且有"后台改写视界覆盖用户平移"的历史事故
每控件独立位图 内存按面积线性增长(128 网格 ≈250 MB)
删除 1 295 行硬核"为什么"注释 它们是防回归的核心资产,行数不是问题

8. 下一步建议(按收益/风险排序)

  1. P3 继:TabBar(标签 + 「+」+ 重命名内联输入框)→ 再 PopupMenu / ColorPickerPopup 条目行。 判据同 Toolbar:8+ 场景像素哈希一致 + 功能断言(点击/悬停/取消、重命名提交与取消)。
  2. P4:浮动面板迁移:EdgePanelLayoutFor(651 行)→ 真控件(Stack + Abs + Measure-driven), 同时消掉面板里 10 多个平行数组(rowRefs/rowChecks/rowPoints/rowOrder/rowLevelTracks/...)。 ⚠️ 每个面板单独一轮:吸顶/滚动/滚动条/滑块复位/行悬停压暗/开关联动是 6 轮补丁叠出的隐式约定。
  3. P5:共享 DIB + 脏区并集(CreateCompatibleBitmap → top-down 32bpp CreateDIBSection, SurfaceView 带 version 作废机制)。
  4. 每阶段一个可独立回滚的提交;main / dev 只接受"uicheck 全绿 + 像素 A/B 通过"的提交。

9. 一句话结论

本阶段首次把一个现有组件(Toolbar)真的换成了控件树,并且同时以像素哈希一致与功能断言证明 行为未变;同时新增了布局/控件基础层、入库回归网,并把 9 080 行的"万能绘制文件"降到 4 913 行(−46%)。 后续每个组件迁移都能复用同一套路(uicheck + 像素 A/B + 功能断言), 从而把"AI 写得快"转换成"你敢合多少"。