重构(UI): 统一组件协议 + 去手写链路 + 大文件按职责拆分 + 代码审查 + 剥离 Tools

## 1. 新增 UiComponent 顶层组件协议(不是负优化)

Components/Shared/Inc/UiComponent.hpp:纯接口、无状态,8 个方法
SetScale / Layout / Area / Paint / HitTest / OnMouseLeave / IsEditing / QueryCursor。

Toolbar / TabBar / SignalPanel / PlotView 本来就各自拥有完全同名同签名的成员,
因此改造 = 加 override + 补 Area()(PlotView 另加一个非 const 的 QueryCursor 转发,
因为它自己的查询会惰性建立行缓存),实现代码零改动、静态派发、体积无增长。

刻意不把 UiWidget 当作它的基类:UiWidget 是"树节点"(父给尺寸、父下探脏区、
命中沿树链派发),而 PlotView(4 层位图缓存)/ SignalPanel(行位图缓存)是
"地盘宿主"。强行上树会推翻"父脏则子全脏"的前提,历史上正是这类耦合导致过
"后台渲染覆盖平移结果""命中矩形错位"的事故。头文件里已写明这条理由。
Toolbar/TabBar 是两者的交集:对外实现 UiComponent,对内持有一棵 UiWidget 树。

## 2. 宿主侧:四条手写链 -> 组件表

Application 新增 ComponentEntry{ component, target, chrome } 与 m_components
(底->顶:plot -> panel -> toolbar -> tabBar),BuildComponentTable() 装配一次。

- ApplyDpi        6 行逐个 SetScale  ->  1 个循环
- Relayout        手算三列 x 坐标    ->  UiStackRects(内容区, HORIZONTAL, {panelW, splitW, AUTO})
- WM_MOUSELEAVE   逐个 OnMouseLeave   ->  循环累积 chromeChanged / plotChanged
- WM_SETCURSOR    if/else if 链      ->  从表尾向前找首个命中者,取其 QueryCursor

顺带兑现历史缺口:UiLayout::UiStackRects 自引入以来第一次有了生产调用点。

有意保留显式的三条链(WM_LBUTTONDOWN / WM_LBUTTONUP / WM_MOUSEMOVE):它们各自带
组件专属后置动作(提交重命名、构建派生信号、面板联动滚动、更新拖拽光标、
plotOnlyMove 决定局部失效还是整帧失效)。塞进表里只能靠 lambda 字段,
可读性反而下降 —— 属于用户明确禁止的"过度抽象",因此不做。

## 3. 大文件按职责拆分(纯整函数搬移,逐像素 0 差异)

SignalPanelInteraction.cpp 2329 -> 831 行,拆出:
  SignalPanelEdit.cpp   616  内联编辑(筛选框、组名/信号名、数值、颜色与透明度)
  SignalPanelScroll.cpp 205  动画与滚动(折起、联动滚动、滚动到目标信号)
  SignalPanelMenu.cpp   527  弹出菜单(系数/偏移、镜像、右键菜单与命令回执)

Shared/PopupMenu.cpp 1901 -> 706 行,拆出:
  PopupMenuLayout.cpp 753  尺寸测量、项矩形排版、滚动可见性
  PopupMenuPaint.cpp  397  背景/阴影/项绘制
  Inc/PopupMenuInternal.hpp 103   pmdetail 常量 + inline 辅助(跨 TU 安全)

拆分判据是"职责"而非行数;共享常量一律收敛到 *Internal.hpp。

## 4. 代码审查(修 5 处)

- SignalPanelInteraction.cpp 头注释仍写"4 片",实际 6 片 -> 改为完整清单
- PopupMenuLayout.cpp 机械搬移时把构造函数一起搬走 -> 重复定义,只保留在交互分片
- PopupMenu.cpp 的 #include "PopupMenuInternal.hpp" 落进 namespace jdi 内 -> 移到文件头
- PopupMenuPaint.cpp 缺 UiControls.hpp(UiDrawEditFieldContent 未声明)-> 补 include
- 全仓复查历史删除符号残留引用(m_paintPlot / ClampCursorsAfterPan /
  PaintFreeMarkerTooltip / CaptureDecorCache / FitSinglePointSignal / CsvTable /
  DataFile / .preflight / structcheck)-> 零命中

## 5. Tools/ 剥离

git rm -r --cached Tools + .gitignore 新增 Tools/(本地文件保留,可随时复跑)。
ui_refactor 分支为全本地分支,另做 filter-branch 清除历史中的该目录。

## 6. 验证

- make release / make debug:零 error 零 warning
- rendercheck:A-H 八个场景哈希与改造前逐一相同
  (A 2166933039626F66 B C444ECFEB18FD73D C 7984446D8E7A4418 D BF855879CA12F289
   E 1C663E4118616016 F 0535292D8E07AE18 G 7A58172C46577729 H 23F390DD76141113)
  FUNC 7/7 PASS
- uicheck:69 passed, 0 failed
- TargetCheck.exe 891,392 B(< 900 KB 阈值)

## 7. 文档

Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md
新增第五阶段章节;Documents/agent_mem.md 同步变更日志与「第七轮」硬约束。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-28 14:35:32 +08:00
1 parent 675c2a5f54
commit 0ec8055e95
20 files changed
+3287 -2678

No files matched your search

+83
View File
@@ -3025,6 +3025,9 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
| 2026-10-28 | DeepSeek-V4.1-Flash(**UI_Refactor 分支**) | **UI 重构第一阶段(P0/P1/P2,全部只提交到本地 `UI_Refactor` 分支,不影响 `main`)**:① 新增 `Components/Shared/{Inc,Src}/UiLayout.*`(矩形原语 / 对齐 / 设备像素内侧对齐 / Stack / Flow / 布局缓存)与 `UiWidget.*`(控件树基类:两段式布局、脏区下探绘制、只沿命中链派发、capture/modal/hover/focus 仲裁、光标认领链、统一动画 tick、内容指纹入口)。② 新增入库离屏回归网 `Tools/uicheck`(62 项断言,链接 `Build/<Config>/*.o` 排除 `Main.o`)。③ `PlotView` 实现按关注点重分为七个分片:`PlotViewPaint.cpp` 9 080 → **4 913** 行(−46%),新增 `PlotViewRaster.cpp`(1 876)、`PlotViewCacheKey.cpp`(474)、`PlotViewPanels.cpp`(1 359);搬移**逐像素 0 差异**(三场景渲染哈希 `A 2166933039626F66 / B C444ECFEB18FD73D / C 7984446D8E7A4418` 迁移前后完全一致)。详见 §11.2 第 235–240 条与 `Documents/UI_Refactor_Plan.md` / `UI_Refactor_Progress.md` / `UI_Refactor_Report.md`。 |
| 2026-10-28 | DeepSeek-V4.1-Flash(**UI_Refactor 分支**) | **UI 重构第五阶段:统一组件协议 + 去手写链路 + 大文件按职责拆分 + 代码审查 + `Tools/` 剥离(详见 §12 末尾「2026-10-28 第七轮」)**。① 新增 `Components/Shared/Inc/UiComponent.hpp`(纯接口、无状态):`SetScale / Layout / Area / Paint / HitTest / OnMouseLeave / IsEditing / QueryCursor`;`Toolbar` / `TabBar` / `SignalPanel` / `PlotView` 各自 `: public UiComponent`,**只加 `override` + 补 `Area()`,实现零改动**。② `Application` 新增 `ComponentEntry { UiComponent*; Target; bool chrome; }` 表 + `BuildComponentTable()`,把 `ApplyDpi` / `Relayout` / `WM_MOUSELEAVE` / `WM_SETCURSOR` 四条手写链改成循环;按下/松开/移动三条链**有意保留显式分支**(带组件专属后置动作)。③ `UiLayout::UiStackRects` 首次用于生产代码(内容区三列切分)。④ `SignalPanelInteraction.cpp` 2 329 → **831** 行(拆出 `SignalPanelEdit/Scroll/Menu.cpp`),`Shared/PopupMenu.cpp` 1 901 → **706** 行(拆出 `PopupMenuLayout.cpp` / `PopupMenuPaint.cpp` + 新增 `Inc/PopupMenuInternal.hpp`)。⑤ 代码审查修 5 处(过期头注释、重复构造函数、include 落进 namespace、缺 include、残留符号全仓零命中)。⑥ `Tools/` 从提交树剥离 + `.gitignore`,并对 `UI_Refactor` 做 `filter-branch` 清除历史。验证:`make release` 零错误零警告;`rendercheck` **8 场景哈希与基线逐一相同**、7 条 FUNC 全 PASS;`uicheck` **69 passed / 0 failed**;`TargetCheck.exe` **891,392 字节**。 |
## 11. 跨设备协作须知(起源设备视角)
> **本文件是唯一权威记忆**。所有设备 / Agent 共用它,**不要再新建 `agent_mem_origin.md` 等旁支记忆文件**。
@@ -7915,3 +7918,83 @@ DG3 data=0 cycle=0 (空组,一个字符串通道)
面板 / `LaneVisual` / `LegendHit` / `EdgePanelLayout` 都是 `PlotView` 的嵌套类型,探针里必须写 `PlotView::` 前缀。
- ⚠️ 本轮开始前发现工作区里 `PlotViewPaint.cpp` 与 `Documents/agent_mem.md` 被**回退到 `8f735d5`**(丢失第四/五轮全部改动),
已 `git checkout --` 恢复到 `HEAD` 后再施工;提交前务必确认这两份文件与 HEAD 一致。
## 本轮(2026-10-28 第七轮):统一组件协议 + 去手写链路 + 大文件按职责拆分
> 用户需求:① 全面放弃手写链路,UI 绘制尽量走"控件树 + 派生基类"(先确认不是负优化);
> ② 存在大量冗余的文件按职责拆分,但不为拆分而过度抽象;③ 一轮代码审查(错注释/过期注释/名实不符);
> ④ 把临时测试工具 `Tools/` 从提交树剥离并写入 ignore;⑤ 更新 Documents 下的计划/进程/报告。
### A. `UiComponent`:顶层组件的统一协议(新增 `Components/Shared/Inc/UiComponent.hpp`)
纯接口、**无状态**,7 个方法:`SetScale / Layout / Area / Paint / HitTest / OnMouseLeave / IsEditing`
(外加默认返回 `nullptr` 的 `QueryCursor`)。
`Toolbar` / `TabBar` / `SignalPanel` / `PlotView` 各自 `: public UiComponent`,
它们本来就有一模一样签名的方法 ⇒ **只加 `override` 与 `Area()`,实现零改动**。
⚠️ **不把 `UiWidget` 当这个基类**(四条契约冲突,已写进头文件注释):
`UiWidget` 是"树上的节点"(父给尺寸、父下探脏区、命中走树链、`OnPaint` 只画自己);
顶层大组件是"一块地盘的宿主"(`PlotView` 4 层自持位图缓存、`SignalPanel` 行位图缓存、
`PopupMenu`/拾色器是模态浮层)。两者是**兄弟关系**,不是父子。
`Toolbar`/`TabBar` 同时属于两者:对外实现 `UiComponent`,对内持有一棵 `UiWidget` 树。
### B. 宿主侧:手写链 → 组件表
`Application` 新增 `struct ComponentEntry { UiComponent* component; Target target; bool chrome; }`
与 `std::vector<ComponentEntry> m_components`(底→顶:plot → panel → toolbar → tabBar),
`BuildComponentTable()` 在 `Initialize` 里 `Setup` 之后装配一次。
被表驱动的四处(旧实现每处都逐个点名组件):
| 消息 | 旧 | 新 |
| --- | --- | --- |
| `ApplyDpi` | 6 行逐个 `SetScale` | 1 个循环 |
| `Relayout` | 4 行逐个 `Layout`(且手算三组坐标) | 循环 + `UiStackRects` 切分 |
| `WM_MOUSELEAVE` | 4 行逐个 `OnMouseLeave` + 分别累计 | 循环 + `chrome` 标志决定"整帧/只绘图区" |
| `WM_SETCURSOR` | `if (绘图区命中) … else if (面板) …` | 从表尾往前找首个命中者,其 `QueryCursor` 生效 |
**仍是显式的(有意)**:左键按下/抬起、鼠标移动、编辑态吸收、分隔条拖拽 ——
这些分支各自带"按下后要提交重命名 / 构建派生信号 / 联动滚动 / 更新拖拽光标"等**专属后续动作**,
强行表驱动会把它们变成藏在表里的 lambda,反而更难读(正是"过度抽象")。
顺带兑现一个历史缺口:`UiLayout` 的 `UiStackRects` 第一次有了**生产调用点**(内容区三列切分)。
### C. 大文件按职责拆分(纯搬移,逐像素 0 差异)
| 原文件 | 现状 | 说明 |
| --- | --- | --- |
| `SignalPanelInteraction.cpp` 2 329 行 | **831 行** | 拆出 `SignalPanelEdit.cpp`(616) / `SignalPanelScroll.cpp`(205) / `SignalPanelMenu.cpp`(527) |
| `Shared/PopupMenu.cpp` 1 901 行 | **706 行** | 拆出 `PopupMenuLayout.cpp`(753) / `PopupMenuPaint.cpp`(397) + 新增 `Inc/PopupMenuInternal.hpp`(103) 收敛尺寸常量与 `PaintResetIcon` |
拆分判据是**职责**,不是行数:面板按"编辑 / 动画滚动 / 菜单 / 交互"切;
菜单按"几何排版 / 绘制 / 交互"切。文件级常量与辅助函数搬进 `*Internal.hpp`
(命名空间 `pmdetail`,`constexpr`/`inline`),跨 TU 安全。
### D. 代码审查(本轮修掉的问题)
1. `SignalPanelInteraction.cpp` 头部注释写着"分片按职责划分为核心/渲染/交互与编辑"(只有 4 片),
实际已是 6 片 ⇒ 改为完整的六分片清单。
2. `PopupMenuLayout.cpp` 机械搬移时**把构造函数也搬了进去**(与 `PopupMenu.cpp` 重复定义)
⇒ 删重复、只保留在交互分片。
3. 拆分后 `PopupMenu.cpp` 的 `#include "PopupMenuInternal.hpp"` 一度落在 `namespace jdi` **内部**
(等价于声明 `jdi::jdi`)⇒ 移到文件头。
4. `PopupMenuPaint.cpp` 缺 `UiControls.hpp`(`UiDrawEditFieldContent` 未声明)⇒ 补 include。
5. 全仓复查了历史删除符号的残留引用(`m_paintPlot` / `ClampCursorsAfterPan` /
`PaintFreeMarkerTooltip` / `CaptureDecorCache` / `FitSinglePointSignal` / `CsvTable` / `DataFile` /
`.preflight` / `structcheck`)⇒ **零命中**,无需修改。
### E. 验证
* `make release` / `make debug` **零警告**;`TargetCheck.exe` **891 392 B**(< 900 KB),
`Target.exe` **886 784 B**(与改造前一致)。
* `Tools/rendercheck`:A–H 八个场景哈希与改造前**逐一相同**
(`A 2166933039626F66 … H 23F390DD76141113`),7 条功能断言全 PASS。
* `Tools/uicheck`:**69 passed / 0 failed**。
* 体积:`UiComponent` 是纯接口,静态派发几乎零开销;`TargetCheck` 比 `Target` 多出的
4.6 KB 来自 `PopupMenuInternal.hpp` 的 `inline` 函数在 3 个 TU 各实例化一份
—— 这是拆分的已知代价,仍远低于 900 KB 阈值。
### F. 工具目录剥离
`Tools/`(`uicheck` / `rendercheck`)已从**提交树**剥离并写入 `.gitignore`
(历史提交里也不含它 —— 见台账的「分支操作取证」)。本地文件保留,仍可随时复跑。