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

208 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`:控件树基类
```mermaid
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 写得快"转换成"你敢合多少"。