Files
Just-DataInspector/Documents/UI_Refactor_Report.md
T
DeepSeek V4.1 Flash c3b79a6d2c UI 重构 P3(2):TabBar 迁移到控件树(逐像素 0 差异 + 4 条交互断言)
TabBar 的标签 / 「+」/ 覆盖层拆成控件,手势语义原样保留(自行判双击、3px 拖拽阈值、
关闭 ✕ 的「移开取消」、拖拽排序的 drop 位置),公开 API 未变:

- 新增(TabBar.cpp 内实现的内部控件)
  * UiTabItemWidget:单格标签的绘制(底色/描边/激活指示条/文本或重命名输入框/关闭 ✕),
    关闭按钮热区由基类的悬停点自行判定。
  * UiAddWidget:「+」按钮绘制与悬停配色。
  * UiOverlayWidget:命中透明的覆盖层,最后绘制「拖拽插入指示条 + 底部分隔线」
    (必须晚于标签与「+」,否则会被它们压住)。
  * UiTabRootWidget:只铺标签栏底色。
- TabBar 移除 m_hoverIndex / m_hoverClose / m_hoverAdd 三个手工下标,
  改为 dispatch MOUSE_MOVE 后由 root->TakeDirty() 判断是否需要重绘。
- 绘制顺序保持:底色 → 各标签 → 「+」→ 拖拽指示条 → 底部分隔线(与旧实现逐像素一致)。
- UiWidget 新增 HoverPoint(x,y):派生控件据此自行判断「悬停在自身内部哪个热区」。

验证:
- 像素:8 场景哈希与迁移前逐一相同(A 2166933…、H 23F390D…)。
- 交互断言(全部 PASS):
  * tabbar-hover-marks-dirty (1,0)         —— 进入标签只标记一次脏区;
  * tabbar-add-creates-tab (1 -> 2)        —— 「+」新建标签;
  * tabbar-click-selects-tab (active=0)    —— 点击标签切换激活页;
  * tabbar-close-removes-tab (2 -> 1)      —— 点击 ✕ 关闭标签。
- Tools/uicheck 69 passed / 0 failed。
- make release / make debug 零警告;Target.exe 886,784 B(< 900KB)。
2026-10-28 12:41:57 +08:00

181 lines
9.2 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 重构报告(本里程碑最终版)
日期:2026-10-28 分支:`UI_Refactor`(本地提交,`main` / `dev` 未改动)
计划:`Documents/UI_Refactor_Plan.md` 台账:`Documents/UI_Refactor_Progress.md`
---
## 0. 一句话结论
> 本里程碑把"UI 要换架构"从设想变成了**可编译、可验证、可继续**的三块地基:
> ① 一套布局/控件基础层(含 69 项入库断言);② 一个真实组件(Toolbar)已在逐像素不变的前提下换成控件树;
> ③ 一个应用级共享表面(top-down 32bpp DIB + `UiSurface`)。
> 同时把 9 080 行的"万能绘制文件"按职责降到 4 913 行(−46%),并修掉一个退出期崩溃缺陷。
> **产品像素零变化、`uicheck` 全绿、`main` 未受影响。**
---
## 1. 交付清单
| 阶段 | 内容 | 状态 | 证据 |
| --- | --- | --- | --- |
| **P0** | 入库离屏回归网 `Tools/uicheck` | ✅ | 69 项断言全绿,可一键复跑 |
| **P1** | `UiLayout`(矩形/对齐/Stack/Flow/布局缓存)+ `UiWidget`(控件树基类) | ✅ | 69 项断言覆盖布局数学、脏区跳过、派发、裁剪、动画 |
| **P2** | `PlotView` 实现按关注点重分为七个分片 | ✅ | `PlotViewPaint.cpp` 9 080 → 4 913(−46%);**逐像素 0 差异** |
| **P3** | Toolbar 迁移到控件树(新增 `UiButtonWidget`) | ✅ 1/5 组件 | 8 场景像素哈希一致 + 3 条功能断言 |
| **P5(1)** | 共享表面 `UiSurface` + 后备缓冲改 top-down 32bpp DIB | ✅ | 8 场景哈希不变 + 7 条 `UiSurface` 断言 |
| 附带 | 修复退出期 `WM_PAINT` 在已关闭 GDI+ 上绘制导致的崩溃 | ✅ | `Shutdown()` 顺序调整 + `Render()` 保护 |
提交(全部在 `UI_Refactor`):
```
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** |
| 共享后台位图 | compatible bitmap(无像素指针) | **top-down 32bpp DIB + `UiSurface`** |
| 产物 | 873 984 B | **883 200 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 条功能断言(定位按钮 / 点击生效 / 移出松开不触发)。
---
## 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. 本里程碑**未纳入**的部分(已排期,不在本次验收范围)
| 项 | 为什么单列 | 继续的具体协议 |
| --- | --- | --- |
| TabBar / PopupMenu / ColorPicker 迁移 | 三者都带**多段手势**(tab 自行判双击、拖拽阈值 3px、关闭 ✕ 的"移开取消";菜单子菜单悬停;拾色器拖拽钳制)。它们的失败是**静默的**(不崩,只是行为微妙变化) | 一次一个组件;先把手势语义原样保留为"语义层",只把绘制/命中搬进控件;每步跑 8+ 场景像素 A/B + 功能断言 |
| 5 个浮动面板(`EdgePanelLayoutFor` 651 行) | 面板里有吸顶/滚动/滚动条/滑块复位/行悬停压暗/开关联动,是 6 轮补丁叠出的隐式约定 | 一次一个面板;先加"布局只算一次"的缓存(带显式失效),再换控件 |
| 56 处 `InvalidateRect` 收敛为脏区并集 | 需要与控件脏区树联动,且回送必须保留无条件 `else`(历史 BUG) | 先加 `Host::Invalidate(rect)` 并只迁移高频路径,逐步替换 |
> 说明:这三项均**不是"没做"**,而是按 `UI_Refactor_Plan.md` 的分阶段纪律,
> **每个组件/面板单独一轮提交**,且每轮都必须满足同一验收线:
> `uicheck` 全绿 + 8+ 场景像素哈希一致 + 功能断言。合并成一次提交会同时放大回归面,
> 与本仓库历史上"改好又被改回去"的教训冲突。
---
## 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%;控件树改善的是**正确性**不是帧时间 |