- Documents/UI_Refactor_Progress.md 补齐 P2 续(PlotViewPanels.cpp)的完成状态、七个分片行数、 逐像素 A/B 哈希与"回退到旧文件做 A/B 时必须先删陈旧 .o"的复现要求。 - 新增 Documents/UI_Refactor_Report.md:第一阶段(P0/P1/P2)交付物、关键数字、 基础层契约、验证方式、过程中修掉的 4 个自身缺陷、明确未做项与下一步排序。 - Documents/agent_mem.md:§11.2 追加第 235-240 条(基础层位置与边界、只沿命中链派发、 Paint 三条契约、布局缓存纪律、PlotView 七分片硬约束、uicheck 回归网), 并在 §10 变更日志追加本阶段一行。 验证:make release / make debug 零警告;Tools/uicheck 62 passed / 0 failed; Target.exe 877,568 B(< 900KB)。
9.0 KiB
9.0 KiB
UI 重构报告(第一阶段:P0 / P1 / P2)
日期: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%) |
✅ |
三个提交:
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 项断言(入库、可复跑) |
| 产物 | 873 984 B | 877 568 B(+3.6 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 条)
- 波形数据→像素管线(网格/曲线/采样点)不做成控件节点;
- 不是每个控件自持位图(内存按面积线性增长:128 网格已 ≈250 MB 量级);
- 禁止"父脏 ⇒ 子树全脏"(会把"只重绘一个网格/面板"打回整块重绘)。
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. 明确未做的事(避免误解)
| 未做 | 原因 |
|---|---|
迁移 Toolbar / TabBar / SignalPanel / PlotView 到 UiWidget |
属 P3/P4;需逐组件迁移、每个组件单独 A/B,本轮只打地基 |
UiHost + 共享 32bpp DIB + SurfaceView |
属 P5;需同时把 56 处 InvalidateRect 收敛成脏区并集,必须与迁移同批验证 |
| 引入多线程光栅 | 本项目实测 1.03x(GDI+ 内部串行),且有"后台改写视界覆盖用户平移"的历史事故 |
| 每控件独立位图 | 内存按面积线性增长(128 网格 ≈250 MB) |
| 删除 1 295 行硬核"为什么"注释 | 它们是防回归的核心资产,行数不是问题 |
7. 下一步建议(按收益/风险排序)
- P3:Toolbar 迁移(最小、最易验证)→ 用
uicheck+ 新旧像素对比收口。 - P4:浮动面板迁移:
EdgePanelLayoutFor(651 行)→ 真控件(Stack + Abs + Measure-driven), 同时消掉面板里 10 多个平行数组(rowRefs/rowChecks/rowPoints/rowOrder/rowLevelTracks/...)。 ⚠️ 每个面板单独一轮:吸顶/滚动/滚动条/滑块复位/行悬停压暗/开关联动是 6 轮补丁叠出的隐式约定。 - P5:共享 DIB + 脏区并集(
CreateCompatibleBitmap→ top-down 32bppCreateDIBSection,SurfaceView带version作废机制)。 - 每阶段一个可独立回滚的提交;
main/dev只接受"uicheck全绿 + 像素 A/B 通过"的提交。
8. 一句话结论
本阶段没有改动任何产品行为:新增了一套可复用、可测试的布局/控件基础层, 并把 9 080 行的"万能绘制文件"按关注点降到 4 913 行(−46%), 两者都用逐像素 0 差异与 62 项离屏断言证明。后续每个组件迁移都可以复用这套回归网, 从而把"AI 写得快"转换成"你敢合多少"。