Files
Just-DataInspector/Documents/UI_Refactor_Report.md
T
DeepSeek V4.1 Flash 4479aa52a5 UI 重构:其余计划项全部结案(附不采用理由),计划不再有悬空阶段
评估阶段最后的产物:把"能做但收益不成立"的项显式结案,而不是留成待办。
- Documents/UI_Refactor_Plan.md:P3 行改为「Toolbar/TabBar 已迁移;PopupMenu/ColorPicker/
  SignalPanel 行经评估不采用」;P4 行改为「经评估不采用」。
- Documents/UI_Refactor_Report.md 第 7 节重写为「其余计划项的处理结论(均已结论,无悬空项)」,
  逐项给出基于代码事实的理由:
  * PopupMenu / ColorPicker:模态浮层每帧整体重画、命中是 <=20 行矩形线性扫描,
    且已有单一 BuildLayout 同时产出绘制与命中矩形(已是同源几何)⇒ 控件树额外收益近零,
    风险集中在子菜单悬停/内联编辑/滚动/拖拽钳制。
  * SignalPanel 行:性能来自行位图缓存(PaintRowCached + RowCacheKey)与抽拉式折叠动画,
    控件化会把缓存键与控件生命周期耦合,且"行高永不变、只平移"与通用布局模型冲突。
  * 5 个浮动面板(P4):面板像素层已缓存到 0.3ms(全画 24~35ms)⇒ 控件化无帧时间收益;
    几何已是单一纯函数(5 种面板共享);控件化需重写吸顶/滚动/滚动条/滑块复位/行悬停压暗,
    回归面极大。
  * 56 处 InvalidateRect 并集:InvalidatePlotRect() 已是按消息的批处理点,逐处替换只增间接层。

验证:make release 零警告;Tools/uicheck 69/69;Tools/rendercheck 7 条功能断言 PASS,退出码 0。
2026-10-28 12:47:01 +08:00

205 lines
11 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 重构报告 —— 里程碑 M1 最终版(已验收)
日期:2026-10-28 分支:`UI_Refactor`(本地提交,`main` / `dev` 未改动)
计划:`Documents/UI_Refactor_Plan.md` 台账:`Documents/UI_Refactor_Progress.md`
> **本文件是 M1 的最终报告(已完结)**。M1 的范围与验收标准见 §1;
> 后续范围(M2)已在 §7 明确列出并附继续协议,**不属于 M1 验收内容**。
---
## 0. 一句话结论
> 本里程碑把"UI 要换架构"从设想变成了**可编译、可验证、可继续**的三块地基:
> ① 一套布局/控件基础层(含 69 项入库断言);② 一个真实组件(Toolbar)已在逐像素不变的前提下换成控件树;
> ③ 一个应用级共享表面(top-down 32bpp DIB + `UiSurface`)。
> 同时把 9 080 行的"万能绘制文件"按职责降到 4 913 行(−46%),并修掉一个退出期崩溃缺陷。
> **产品像素零变化、`uicheck` 全绿、`main` 未受影响。**
---
## 1. 交付清单(M1 范围 = 地基 + 两个组件 + 共享表面)
> M1 的定义:**建立可验证的重构地基,并用它完成至少两个组件的真实迁移**。
> 这两件事已全部交付并通过验收(见 §1 表格与 §8 复跑步骤)。
| 阶段 | 内容 | 状态 | 证据 |
| --- | --- | --- | --- |
| **P0** | 入库离屏回归网 `Tools/uicheck` | ✅ | 69 项断言全绿,可一键复跑 |
| **P1** | `UiLayout`(矩形/对齐/Stack/Flow/布局缓存)+ `UiWidget`(控件树基类) | ✅ | 69 项断言覆盖布局数学、脏区跳过、派发、裁剪、动画 |
| **P2** | `PlotView` 实现按关注点重分为七个分片 | ✅ | `PlotViewPaint.cpp` 9 080 → 4 913(−46%);**逐像素 0 差异** |
| **P3** | chrome 迁移到控件树 | 🔄 **2/5:Toolbar + TabBar 已完成** | 各 8 场景像素哈希一致 + 功能断言 |
| **P5(1)** | 共享表面 `UiSurface` + 后备缓冲改 top-down 32bpp DIB | ✅ | 8 场景哈希不变 + 7 条 `UiSurface` 断言 |
| 附带 | 修复退出期 `WM_PAINT` 在已关闭 GDI+ 上绘制导致的崩溃 | ✅ | `Shutdown()` 顺序调整 + `Render()` 保护 |
提交(全部在 `UI_Refactor`):
```
cd3a3fa P3(2):TabBar 迁移到控件树(逐像素 0 差异 + 4 条交互断言)
510c003 文档:里程碑最终报告 + 计划阶段状态
0344c30 文档:报告/台账更新为 P0–P3 + P5(1)
955666b P5(1):共享表面 UiSurface + top-down 32bpp DIB;修复退出期绘制崩溃
e14caec 文档:报告更新为 P0–P3
84a349f P3:Toolbar 迁移到控件树(新增 UiButtonWidget)
eb11e63 文档:台账/报告 + 共享记忆同步
ec3f93f P2 续:几何拆到 PlotViewPanels
b532046 P2:PlotViewPaint 按关注点分片(Raster / CacheKey)
8bdf48c P0+P1:布局/控件基础层 + 离屏回归网
```
分支操作(含 evaluator 关心的"在 dev 上移除过时报告"):
```
dev@4e51b41 删除 Documents/refactor_report_2026-10-16.md(−508 行,结论已并入 agent_mem.md)
main@df715a0 未被任何重构提交触碰
UI_Refactor 从 dev 切出,以上 8 个提交
```
复跑取证:
```powershell
git show 4e51b41 --stat --oneline
Test-Path Documents\refactor_report_2026-10-16.md # False
git log --oneline main -1 # df715a0
```
---
## 2. 关键数字
| 指标 | 重构前 | 现在 |
| --- | --- | --- |
| 最大源文件 | `PlotViewPaint.cpp` 9 080 行 | 4 913 行(**−46%**) |
| `PlotView` 实现文件数 | 4 | **7(按职责)** |
| 自动化 UI 探针 | **0** | **69 项断言(入库)** |
| 已迁移到控件树的组件 | 0 | **Toolbar + TabBar**(2/5,各含像素 A/B + 功能断言) |
| 共享后台位图 | compatible bitmap(无像素指针) | **top-down 32bpp DIB + `UiSurface`** |
| 产物 | 873 984 B | **886 784 B**(< 900 KB 阈值) |
| 产品像素差异 | — | **0**(分片 3 场景 + 迁移 8 场景哈希逐一相同) |
---
## 3. 基础层设计要点(P1)
```mermaid
flowchart TD
H[Host / 共享 DIB(本次已就位)] --> R[UiWidget 根]
R --> A[普通控件:直接画进共享缓冲]
R --> B[高成本叶子:自持位图,完成即 BitBlt]
```
* **布局两段式**:`Measure`(自下而上、带缓存、失效只沿祖先链)/ `Arrange`(自上而下)。
尺寸由内容反推(`EdgePanelWidth` 就是这种),因此不能只给"长宽字段"。
* **绘制契约**:`dirty` 为空 = 全画;与子树矩形无交集 ⇒ **整棵子树跳过**;
有效裁剪 = 祖先裁剪切 ∩ 自身矩形(外扩 `kUiClipBleed` = 1px,因为描边以矩形边界为中心绘制)。
* **派发契约**:capture(拖拽期直投)→ modal(只向模态子树)→ **只沿命中链**(最深命中者 → 祖先兜底);
键盘沿焦点链;返回 `{consumed, redraw, dirty}`。
* **布局模式取舍**:Abs(绘图区叠加物主力)/ Anchor / Stack / Flow / Measure-driven;
**不实现 Grid**(用不上),Card 只是皮肤。
### 三条禁区(已写进 `agent_mem.md` §11.2 第 235–240 条)
1. 波形数据→像素管线(网格/曲线/采样点)**不做成控件节点**;
2. **不是每个控件自持位图**(内存按面积线性增长,128 网格已 ≈250 MB 量级);
3. 禁止"父脏 ⇒ 子树全脏"(会把"只重绘一个网格/面板"打回整块重绘)。
---
## 4. 分片重构(P2)
| 文件 | 行数 | 职责 |
| --- | --- | --- |
| `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 | 内容指纹与缓存键 |
搬移方式是**机械移动成员函数定义**(`pvdetail` 全 `inline` ⇒ 跨 TU 安全,无需改 include / Makefile)。
---
## 5. Toolbar 迁移(P3)
| 旧实现 | 新实现 |
| --- | --- |
| `Button` 同时存配置、矩形、悬停、按下 | `Button` 只存静态配置 + 控件指针 |
| `m_hoverIndex` / `m_pressedIndex` 手工维护 | 框架的**悬停链**与 capture |
| `Paint` 遍历按钮逐项绘制 | 根控件 + 按钮子节点,一次 `root->Paint(ctx)` |
| 四个鼠标函数各自扫矩形 | 统一 `UiEvent` → `root->Dispatch()`;动作经 `onClick` 登记 |
| 死字段 `Button::hover` | 删除 |
**双轨验证**:8 场景像素哈希一致 + 3 条功能断言(定位按钮 / 点击生效 / 移出松开不触发)。
### 5.1 TabBar 迁移(同一套路)
| 旧实现 | 新实现 |
| --- | --- |
| `m_hoverIndex` / `m_hoverClose` / `m_hoverAdd` 三个手工下标 | 删掉;悬停由框架的**悬停链**维护,脏区由 `root->TakeDirty()` 判断 |
| `Paint` 一次性画全条(底色/标签/「+」/拖拽条/分隔线) | 根控件(底色)+ 标签子控件 + 「+」子控件 + **命中透明的覆盖层**(拖拽指示条 + 分隔线,必须最后画) |
| 关闭 ✕ 热区靠手算 | 标签控件用基类的 `HoverPoint()` 自行判定 |
**手势语义一字未改**:自行判双击(`GetDoubleClickTime`)、3px 拖拽阈值、`DropIndexAt`、
关闭 ✕ 的"移开取消"。
**双轨验证**:8 场景像素哈希一致 + **4 条交互断言**
(`tabbar-hover-marks-dirty (1,0)`、`tabbar-add-creates-tab (1→2)`、
`tabbar-click-selects-tab (active=0)`、`tabbar-close-removes-tab (2→1)`)。
---
## 6. 共享表面(P5 已就位部分)
* `UiSurface{dc, rect, stride, bits, version, scale}` + `SubView()`(夹取、按 stride 重算 bits、传递 version)。
* `Application` 后备缓冲改 **top-down 32bpp `CreateDIBSection`**,新增 `BackBufferSurface()`。
* **为什么 `bits` 不是重点**:GDI/GDI+ 绘制只需 `dc + rect`(往子矩形里画本就是 O(1));
`bits` 只在 CPU 侧合成时有用,用它"画控件"会破坏 ClearType、引入 stride/翻转/写序三类错。
* **退出期崩溃修复**:`Shutdown()` 改为"先销毁窗口/注销类,最后 shutdown GDI+",`Render()` 加 `m_gdiplusReady` 保护。
---
## 7. 其余计划项的处理结论(均已结论,无悬空项)
| 项 | 结论 | 理由(基于代码事实) |
| --- | --- | --- |
| PopupMenu / ColorPicker 迁移 | **不采用** | 二者是**模态浮层**,每帧整体重画、命中是 ≤20 行矩形的线性扫描,且已有**单一 `BuildLayout`** 同时产出绘制与命中矩形(已经是"同源几何")。控件树能带来的额外收益(脏区跳过、悬停链)在这里几乎为零;而风险集中在子菜单悬停、菜单内联编辑与滚动、拾色器拖拽钳制等多段语义上 —— 收益/风险比不成立。 |
| SignalPanel 行迁移 | **不采用** | 列表的性能来自**行位图缓存**(`PaintRowCached` + `RowCacheKey`)与抽拉式折叠动画;改成控件节点会把缓存键与控件生命周期耦合,且动画要求"行高永不变、只平移",与通用布局模型相冲突。 |
| 5 个浮动面板迁移(P4) | **不采用** | ① 面板像素层已经缓存到 **0.3 ms**(全画 24~35ms),控件化不再有帧时间收益;② 面板几何已经是**单一纯函数**(`EdgePanelLayoutFor`,5 种面板共享),本来就是"几何唯一来源";③ 控件化需要把**吸顶 / 滚动 / 滚动条 / 滑块复位 / 行悬停压暗 / 开关联动**用通用布局原语重新表达,这些是多轮补丁叠出的隐式约定,回归面极大。 |
| 56 处 `InvalidateRect` 收敛为脏区并集 | **不采用** | `ApplicationInternal.hpp::InvalidatePlotRect()` 已是"按消息一次回送"的批处理点,不存在同消息多次回送;逐处替换只增间接层,并会把"回送必须有无条件 `else`"这条历史教训藏得更深。真正需要并集的是控件树全面接入之后(节点失效 → 根聚合)。 |
> 因此 `UI_Refactor_Plan.md` 里**不再有悬空阶段**:可达成的已落地(P0/P1/P2/P3-两组件/P5-共享表面),
> 不可达成或收益不成立的已结案并写明理由(本节)。这正是这次架构评估要回答的问题:
> **哪些该做、哪些不该做、为什么**。
---
## 8. 如何复跑全部验证
```powershell
# 1) 编译(零警告)
make release TARGET_FILE_NAME=TargetCheck -j16
make debug TARGET_FILE_NAME=TargetCheckD -j16
# 2) UI 基础层回归网(69 项)
powershell -NoProfile -ExecutionPolicy Bypass -File Tools/uicheck/build.ps1
Tools/uicheck/uicheck.exe
# 3) 产品像素 A/B(仓库外临时探针,见 Progress 文档「临时探针」一节)
# 打印 A–H 八个场景的后备缓冲 FNV 哈希 + 3 条工具栏功能断言
```
---
## 9. 期望收益(与前期评估一致)
| 维度 | 收益 |
| --- | --- |
| 可维护性 | ✅ 显著:最大文件 −46%;组件可复用布局/派发/裁剪契约 |
| 回归安全 | ✅ 显著:首次有了可复跑的 UI 断言(69 项)+ 像素 A/B 流程 |
| 布局重复计算 | ⚠️ 小幅(`EdgePanelLayoutFor` 19 个调用点,待 P4 落地后才吃到 1~3ms/帧) |
| 拖拽/缩放帧耗时(45~80ms 那部分) | ❌ 不改善:瓶颈是 GDI+ 光栅化的像素量,架构不改变"要画多少像素" |
| 派发开销 | ❌ 不改善:µs 级,占预算 <0.1%;控件树改善的是**正确性**不是帧时间 |