Files
Just-DataInspector/Documents/UI_Refactor_Progress.md
T
DeepSeek V4.1 Flash 0ec8055e95 重构(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 同步变更日志与「第七轮」硬约束。
2026-10-28 14:35:32 +08:00

345 lines
21 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 重构进度(UI_Refactor 分支)
> 本文件是**跨会话/跨 Agent 的交接台账**。每次提交前更新;每一行都要能对应到一个可复跑的验证结果。
> 计划见 `Documents/UI_Refactor_Plan.md`;硬约束见 `Documents/agent_mem.md` §11.2。
## 分支状态
| 分支 | 基线 | 用途 |
| --- | --- | --- |
| `main` | `df715a0` | 可发布,**不受本次重构影响** |
| `dev` | `4e51b41` | 日常开发基线;本分支上已 **删除过时报告** `Documents/refactor_report_2026-10-16.md`(−508 行) |
| `UI_Refactor` | 从 `dev` 切出 | 本次 UI 重构(全部提交只留在本地) |
**分支操作取证(可直接复跑)**
```powershell
git show 4e51b41 --stat --oneline # dev: Documents/refactor_report_2026-10-16.md | 508 ----------
Test-Path Documents\refactor_report_2026-10-16.md # False(已不在工作区/分支里)
git log --oneline --all -- Documents/refactor_report_2026-10-16.md # 末条即为 4e51b41 的删除提交
git log --oneline main -1 # df715a0(main 未被任何重构提交触碰)
```
## 阶段看板
> **本计划已全部结案**:可达成项已落地(P0/P1/P2/P3-Toolbar+TabBar/P5-共享表面),
> 其余项经评估**不采用**并写明理由(见 `UI_Refactor_Report.md` §7)—— 计划里没有悬空阶段。
| 阶段 | 内容 | 结论 |
| --- | --- | --- |
| **P0** | 离屏回归网 `Tools/uicheck` | ✅ 已落地(69 项断言) |
| **P1** | `UiLayout` + `UiWidget` 基础层 | ✅ 已落地 |
| **P2** | `PlotViewPaint.cpp` 零行为分片 | ✅ 已落地(9 080→4 913,逐像素 0 差异) |
| **P3** | chrome 迁移 | ✅ Toolbar / TabBar 已迁移;PopupMenu / ColorPicker / SignalPanel 行**经评估不采用** |
| **P4** | 5 个浮动面板控件化 | **经评估不采用**(理由见 Report §7) |
| **P5** | 共享表面 `UiSurface` + top-down DIB | ✅ 已落地;脏区并集**经评估不采用** |
---
## 提交记录
### 1. `P0 + P1`:UI 基础层与离屏回归网
**新增文件**
| 文件 | 行数 | 职责 |
| --- | --- | --- |
| `Components/Shared/Inc/UiLayout.hpp` | ~250 | 布局原语 + `UiLayoutCache` |
| `Components/Shared/Src/UiLayout.cpp` | ~300 | 矩形原语 / 对齐 / Stack / Flow / 缓存 |
| `Components/Shared/Inc/UiWidget.hpp` | ~330 | 控件基类接口与设计说明 |
| `Components/Shared/Src/UiWidget.cpp` | ~560 | 树 / Measure·Arrange / Paint(脏区+裁剪)/ 派发 / 仲裁 / 动画 |
| `Tools/uicheck/main.cpp` | ~570 | 离屏回归网(62 项断言) |
| `Tools/uicheck/build.ps1` | ~70 | 链接 `Build/<Config>/*.o`(排除 `Main.o`) |
**已实现并验证的行为**
- 布局:`UiAlignInRect`(含 `STRETCH`、超尺寸夹取、偏移后置)、`UiRectSnapInward`(设备像素内侧对齐)、
`UiStackRects`(固定 / `kUiSizeAuto` 均分 / **绝不产生负尺寸**)、`UiFlowRects`(换行 + `overflow` 下标)。
- 缓存:`UiLayoutCache` 用 `std::deque` 保证**引用稳定**;容量淘汰按"最久未命中";`Invalidate` 为**定向**失效。
- 布局 pass:`Measure` 命中缓存不重复调用 `OnMeasure`;`InvalidateMeasure` **只**沿祖先链失效(兄弟不重算)。
- 绘制:`dirty` 为空 = 全画;与子树矩形无交集 ⇒ **整棵子树跳过**;有效裁剪 = 祖先裁剪切 ∩ 自身矩形 ∩ 脏区;
不可见节点跳过。
- 派发:**只沿命中链**(最深的命中节点 → 祖先冒泡),**绝不分发给同层兄弟**;`capture` 期间事件直投捕获者;
`modal` 只向模态子树派发;`hit-transparent` 让事件穿透;`QUERY_CURSOR` 由最上层认领者返回;
键盘/字符沿**焦点链**派发(无焦点交给根,保证宿主级快捷键可用)。
- 动画:`AdvanceAnimation` 逐节点下发同一时间戳并汇总"是否仍有动画"。
- 悬停:进入 / 离开各只触发一次,跨子节点迁移时正确配对。
**过程中修掉的两个自身缺陷**
1. `UiWidget` 缺 `AdvanceAnimationSelf` / `AnimationActiveSelf` 两个 protected 虚函数(`.cpp` 已引用、头文件漏声明)。
2. 最初的派发实现是"遍历同层兄弟、未消费就继续" ⇒ **一个点击可能同时被视觉上位于下方的控件处理**;
已改为"只沿命中链派发 + 祖先兜底"。回归网里对应的断言从"绝对次数"改为**增量断言**
(绝对次数会被前面用例计数污染,本轮踩过一次)。
**验证**
```
make release TARGET_FILE_NAME=TargetCheck # 零警告
powershell -File Tools/uicheck/build.ps1 # 链接 Build/Release/*.o(排除 Main.o)
Tools/uicheck/uicheck.exe # 62 passed, 0 failed
```
**未做(有意)**
- 未改动任何现有组件(Toolbar / TabBar / SignalPanel / PlotView 仍走旧路径),因此**本提交对产品像素零影响**。
- 未把 `UiWidget` 接入 `Application`,避免在 P2 之前引入行为差异。
---
### 2. `P2`:`PlotViewPaint.cpp` 按关注点分片(零行为变更)
**为什么**:该文件 9 080 行里混住四类内容 —— 真绘制 ≈3.5k / 光栅化缓存 ≈2.5k / 缓存键 ≈1.1k /
布局与命中几何 ≈1.3k。按"函数名前缀"分片是上一轮膨胀的直接原因,本轮按**关注点**再分一次。
**做法**:**机械搬移成员函数定义**(不改一行逻辑)。`PlotViewInternal.hpp` 里的 `pvdetail` 全部 `inline`
⇒ 跨分片安全,无需改任何 include、无需改 `Makefile`(按 basename 自动收集 `.cpp`)。
| 文件 | 搬入内容 | 行数变化 |
| --- | --- | --- |
| `PlotViewRaster.cpp`(新) | 光晕离屏层(`PaintSignalHalo*` / `UploadHaloLayer` / `ReleaseHaloLayer`)<br>场景缓存(`Ensure/Capture/Blit/Invalidate/ReleaseSceneCache`)<br>装饰层(`Ensure/Release/Render/BlitDecorCache` / `ResolveAlphaBlend`)<br>宽基底图层与预算(`EnsureLaneLayer` … `BlitWideBuffer`、并行重建、滚动补条、预热) | 1 876 |
| `PlotViewCacheKey.cpp`(新) | `MakeSceneCacheKey` / `SceneCacheMatches` / `SceneHaloRef`<br>`SceneContentHash` / `SceneGeometryHash` / `SceneDecorHash`<br>`CollectSceneStyles` / `FindStyleOnlyLanes` / `CollectSceneDerived` / `RepaintSceneCachePatch` | 474 |
| `PlotViewPaint.cpp` | 只剩绘制(`Paint*` / `Render*` / 输入框绘制) | 9 080 → **6 252**(−31%) |
**验证(关键)**
1. `make release TARGET_FILE_NAME=TargetCheck` **零警告**;`Target.exe` 873 984 → **875 008 B**(+1 KB,
属分片后各 TU 的对齐差异;仍远低于 900 KB 阈值)。
2. **逐像素 A/B**:临时写了一个渲染哈希探针(仓库外),对三个场景渲染后备缓冲区并取 FNV 哈希:
* A 默认全览;B 深放大 + 自由标记(含固定点)+ 均值滤波 + 弱峰调整 + 时间光标;C 再平移一帧(触发宽基底滚动/补条)。
* **分片前**:`A 2166933039626F66 / B C444ECFEB18FD73D / C 7984446D8E7A4418`
* **分片后**:**完全相同**(三个场景逐一一致)。
* 该探针未入库(一次性验证用),结论记录在此以便复现。
3. `Tools/uicheck` 62/62 全绿。
4. 顶层注释从"四个分片"更新为"六个分片(按关注点)",并写明"`Paint/PaintLane` 仍只允许定义在 `PlotViewPaint.cpp`"。
**⚠️ 复现 A/B 的坑**:机械搬移后用旧的 `.bak` 回退时,`Build/<Config>/` 里会残留
`PlotViewRaster.o` / `PlotViewCacheKey.o`,链接探针(`Build/Release/*.o`)会因重复符号失败 ——
**必须先删掉这两个陈旧 `.o`** 再链接。
---
### 3. `P2 续`:把"布局与命中几何"从绘制分片里拆出(`PlotViewPanels.cpp`)
**为什么**:`EdgePanelLayoutFor`(651 行,19 个调用点)、`EdgePanelWidth`、`LaneBadgeRect`、
`FreeMarkerValue/Delta/Contents/LabelRects`、`ProbeFreeMarker` 都是**几何**,却被塞在绘制分片里 ——
改一次布局要动 9 千行的绘制文件,回归面被放大。
**做法**:同样机械搬移(5 个不连续块),`PlotViewPaint.cpp` 从 6 645 → **4 913** 行。
**最终七个分片**(按关注点)
| 文件 | 行数 | 职责 |
| --- | --- | --- |
| `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 | 内容指纹与缓存键 |
**累计效果**:`PlotViewPaint.cpp` **9 080 → 4 913(−46%)**,最大文件从 9.1k 降到 4.9k。
**验证**
1. `make release TARGET_FILE_NAME=TargetCheck` 零警告;`Target.exe` **877 568 B**(< 900 KB)。
2. **逐像素 A/B**:同三场景哈希与迁移前**完全一致**
(`A 2166933039626F66 / B C444ECFEB18FD73D / C 7984446D8E7A4418`)。
3. `Tools/uicheck` 62/62。
4. 顶层注释更新为"七个分片(按关注点)",并标明 `Paint/PaintLane` 只允许定义在 `PlotViewPaint.cpp`。
---
### 4. `P3`:Toolbar 迁移到控件树(**逐像素 0 差异**)
**新增**:`Components/Shared/{Inc,Src}/UiButtonWidget.*` —— 通用"文字按钮"控件,视觉规则与旧 Toolbar
按钮逐像素一致(主按钮强调色实底 / 普通按钮 1px 描边 / 可切换选中态 / 禁用态配色);
交互由自己处理:按下即 `CaptureMouse()` 进入按下态,**在自身范围内**松开才触发 `onClick`,
指针离开时 `CancelPress()`。
**`Toolbar` 内部重写(公开 API 一字未改)**:
| 旧实现 | 新实现 |
| --- | --- |
| `std::vector<Button>` 里既存配置又存矩形/悬停/按下 | `Button` 只存**静态配置 + 控件指针**;几何与外观状态归控件 |
| `m_hoverIndex` / `m_pressedIndex` 手工维护 | 由 `UiWidget` 的**悬停链**与 capture 维护 |
| `Paint` 自己遍历按钮绘制 | 根控件(`ToolbarRootWidget`:底色 + 底部分隔线)+ 按钮子节点,`root->Paint(ctx)` |
| `OnMouseMove/Down/Up/Leave` 各自扫按钮矩形 | 统一翻译成 `UiEvent` → `root->Dispatch(event)`;动作经 `onClick` 回调登记、抬起后执行 |
| `Button::hover` 死字段 | 删除(`Hovered()` 由基类提供) |
**新增框架约定(重要)**:`UiWidget::Paint` 的裁剪由"严格等于自身矩形"改为
**"自身矩形外扩 `kUiClipBleed`(1px)"**。原因:边框/描边以矩形边界为中心绘制,1px 笔宽有一半在矩形外,
严格裁剪会把外半侧切掉 ⇒ 控件看起来变细(与旧绘制代码不再逐像素一致)。
`uicheck` 对应断言改为"裁剪 ⊇ 自身矩形且不超出外扩范围"。
**验证**
1. `make release` / `make debug` **零警告**;`Target.exe` **881 664 B**(< 900 KB)。
2. **逐像素 A/B(8 个场景)**:迁移前后哈希逐一相同
`A 2166933… / B C444ECF… / C 7984446… / D BF85587… / E 1C663E4… / F 0535292… / G 7A58172… / H 23F390D…`
(A 全览;B 深放大+自由标记+均值滤波+弱峰调整+光标;C 平移;D 悬停按钮;E 按下未松开;
F 松开;G 悬停主按钮;H 移出工具栏)。
3. **功能验证**(证明点击真的翻译成动作,而不是"看起来没坏"):
* `FUNC locate-cursor-button PASS (1291,73)` —— 用控件自身矩形定位按钮;
* `FUNC click-cursor-changes-mode PASS (1 -> 2)` —— 点击「光标」按钮后光标模式 单→双;
* `FUNC release-outside-does-not-fire PASS (2 -> 2)` —— 按下后移出再松开,不触发动作。
4. `Tools/uicheck` **62/62**。
---
### 5. `P5(1)`:共享表面 `UiSurface` + 后备缓冲改 top-down 32bpp DIB(**逐像素 0 差异**)
**新增** `Components/Shared/{Inc,Src}/UiSurface.*`:`{ dc, rect, stride, bits, version, scale }`。
`SubView(localRect)` 把子矩形换算到宿主坐标、**夹取**在本表面内、按 `stride` 重算 `bits` 偏移、传递 `version`。
> ⚠️ 文件头注释里明确写了取舍:**画控件只需要 `dc + rect`**;`bits` 只在 CPU 侧合成
> (memcpy / alpha 混合 / 重采样)时才有意义。用 `bits` 直写像素来"画"控件会破坏 ClearType、
> 引入 stride / 翻转 / 写序三类错,是净亏损。
**`Application`**:
* 后备缓冲由 `CreateCompatibleBitmap` 改为 **top-down 32bpp `CreateDIBSection`**
(`biHeight` 取负 ⇒ `bits` 从第一行开始、y 向下递增);
* 新增 `m_backBits` / `m_backStride` / `m_backVersion`,重建位图时 `++m_backVersion`;
* 新增 `BackBufferSurface()` 对外暴露整块表面。
**顺带修掉一个真实缺陷(退出期访问违例)**:
| 问题 | 修法 |
| --- | --- |
| `Shutdown()` 里先 `GdiplusShutdown` 再 `DestroyWindow`:`DestroyWindow` 期间派发的 `WM_PAINT` 会进 `Render()`,在**已关闭**的 GDI+ 上构造 `Graphics` ⇒ 崩溃 | 顺序改为 **先销毁窗口 / 注销类,最后 shutdown GDI+** |
| 任何晚到的绘制都可能踩到这条路径 | `Render()` 开头加保护:`!m_gdiplusReady || m_hwnd == nullptr` 直接返回 |
> 另:临时探针此前的退出崩溃其实来自"探针自己又调了一次 `GdiplusShutdown`"(`Application::Shutdown`
> 内部已 shutdown)——与本轮修复无关,但顺序调整与 `Render` 保护保留为防御性正确做法。
**验证**
1. 8 场景像素哈希与 DIB 改造前**逐一相同**;3 条工具栏功能断言仍 PASS。
2. `Tools/uicheck` **69/69**(新增 7 条 `UiSurface` 断言:bits 按 stride 偏移、矩形夹取、
完全在外返回无效表面、`version`/`scale` 传递)。
3. `make release` / `make debug` 零警告;`Target.exe` **883 200 B**(< 900 KB)。
---
### 6. `P3(2)`:TabBar 迁移到控件树(**逐像素 0 差异**)
| 旧实现 | 新实现 |
| --- | --- |
| `m_hoverIndex` / `m_hoverClose` / `m_hoverAdd` | 删除;悬停由框架悬停链维护,脏区用 `root->TakeDirty()` |
| `Paint` 一次画全条 | 根控件(底色)+ 标签控件 + 「+」控件 + **命中透明覆盖层**(拖拽条 + 分隔线,必须最后画) |
| 关闭 ✕ 热区手算 | 标签控件用 `HoverPoint()` 自行判定 |
**手势语义一字未改**(自行判双击、3px 拖拽阈值、`DropIndexAt`、✕ 的“移开取消”)。
验证:8 场景哈希一致;4 条交互断言全 PASS
(hover 只脏一次 / 「+」新建 / 点击切换 active=0 / ✕ 关闭 2→1);`uicheck` 69/69;产物 886,784 B。
---
## 后续可选工作(**计划项均已结案**,以下为将来若要推进时的入口)
本计划的每个阶段都已有结论(落地 / 不采用),**当前没有悬空待办**。若将来要重新开启,
按下列入口与判据推进;判据固定为:`uicheck` 全绿 + **8+ 场景像素哈希一致** + **功能断言**。
1. 若要将 `PopupMenu` / `ColorPicker` 控件化(目前评估为不采用,见 Report §7):
沿用 TabBar 的套路——手势语义原样保留,只把"绘制 + 命中矩形"搬进控件;
先用 `Tools/rendercheck` 记录菜单/拾色器的场景哈希,再逐控件替换。
2. 若要将 5 个浮动面板控件化(目前评估为不采用):先做"布局只算一次"的缓存
(必须用**覆盖全部输入的键**,否则就是历史上"漏混字段 → 贴旧像素"那类静默 BUG)。
3. 若要引入脏区并集(目前评估为不采用):必须在控件树全面接入之后再做,
并保留"回送必须有无条件 `else`"(历史:某分支缺兜底 ⇒ 一个像素都没回送)。
---
## 第七轮(2026-10-28):统一组件协议 · 去手写链路 · 大文件按职责拆分 · 审查 · 工具剥离
### 进度总览
| # | 任务 | 状态 | 验证 |
| --- | --- | --- | --- |
| 1 | 新增 `UiComponent` 顶层协议,四组件 `override` | ✅ | 编译零警告 |
| 2 | `Application` 组件表 `m_components` + `BuildComponentTable` | ✅ | — |
| 3 | `ApplyDpi` / `Relayout` / `WM_MOUSELEAVE` / `WM_SETCURSOR` 四条链改表驱动 | ✅ | `uicheck` 69/69 |
| 4 | `UiStackRects` 首次用于生产代码(内容区三列) | ✅ | 像素哈希不变 |
| 5 | `SignalPanelInteraction.cpp` 2329 → 4 片 | ✅ | 8 哈希不变 |
| 6 | `PopupMenu.cpp` 1901 → 3 片 + `PopupMenuInternal.hpp` | ✅ | 8 哈希不变 |
| 7 | 代码审查(注释/名实不符/残留引用) | ✅ | 5 处修正 |
| 8 | `Tools/` 从提交树剥离 + `.gitignore` | ✅ | `git ls-files Tools` 为空 |
| 9 | 三份文档更新 | ✅ | — |
### 1. `UiComponent`:为什么要它、为什么不用 `UiWidget`
新建 `Components/Shared/Inc/UiComponent.hpp`,纯接口、无数据成员:
```
SetScale(float) / Layout(const gd::RectF&) / Area() / Paint(gd::Graphics&)
HitTest(int,int) / OnMouseLeave() / IsEditing() / QueryCursor(int,int)
```
`Toolbar` / `TabBar` / `SignalPanel` / `PlotView` 本来就各自拥有这些**同名同签名**的成员,
所以改造只需 `: public UiComponent` + 给已有定义加 `override` + 补一个 `Area()`
(`PlotView` 另需一个非 const 的 `QueryCursor` 转发,因为它自己的 `QueryCursor` 是 const、
而树上的命中查询可能惰性建立行缓存)。
**结论:这一步不是负优化** —— 生成代码几乎不变(全是静态派发,无虚表之外的额外开销),
却让宿主从"认识每一个组件"变成"认识一份表"。体积实测 `TargetCheck.exe` 891 392 B,
仍低于 900 KB 阈值;`Target.exe` 886 784 B 与改造前一致。
### 2. 宿主侧:手写链 → 组件表(4 处)
| 消息 | 改造前 | 改造后 |
| --- | --- | --- |
| `ApplyDpi` | 逐个 `SetScale`(6 行,新增组件必须记得加) | `for (entry : m_components) entry.component->SetScale(scale)` |
| `Relayout` | 手算 panel/splitter/plot 三组 x 坐标 | `UiStackRects(内容区, HORIZONTAL, {panelW, splitW, AUTO}, 0, rects)` |
| `WM_MOUSELEAVE` | 逐个 `OnMouseLeave` 并分别累积 | 循环累积 `chromeChanged` / `plotChanged` |
| `WM_SETCURSOR` | `if (绘图区) … else if (面板) …` | 从表尾(最上层)向前找首个 `HitTest` 命中者,取其 `QueryCursor` |
保留显式的三条链(**有意,不是漏改**):左键按下 / 松开 / 鼠标移动。
它们各自挂着一串"组件专属后置动作"(提交重命名、构建派生信号、面板联动滚动、
更新拖拽光标、判断 `plotOnlyMove` 决定局部还是整帧失效)。
把这些塞进表里要引入 lambda 或回调字段,可读性反而下降 —— 属于用户明确禁止的"过度抽象"。
### 3. 拆分对照
| 原文件 | 拆后 | 内容职责 |
| --- | --- | --- |
| `SignalPanelInteraction.cpp` 2 329 | **831** | 命中测试、鼠标/键盘派发、光标形状 |
| + `SignalPanelEdit.cpp` | 616 | 筛选框、组名/信号名、数值输入、颜色与透明度 |
| + `SignalPanelScroll.cpp` | 205 | 折起动画、联动滚动、滚动到目标信号 |
| + `SignalPanelMenu.cpp` | 527 | 系数/偏移、镜像、右键菜单与命令回执 |
| `PopupMenu.cpp` 1 901 | **706** | `OnEdit*` 交互分支 |
| + `PopupMenuLayout.cpp` | 753 | 尺寸测量、项矩形排版、滚动可见性 |
| + `PopupMenuPaint.cpp` | 397 | 背景/阴影/复用 `UiControls` 的项绘制 |
| + `Inc/PopupMenuInternal.hpp` | 103 | `pmdetail` 常量 + `inline PaintResetIcon` |
拆分方式:**整函数搬移**(不重写、不改签名),共享的尺寸常量与辅助函数收敛到
`*Internal.hpp`(`constexpr` / `inline`,跨 TU 安全)。
这样每个 TU 的行数落在 200–900 的可读区间,而逐像素输出保持 0 差异(哈希佐证)。
### 4. 代码审查结果
| 文件 | 问题 | 处理 |
| --- | --- | --- |
| `SignalPanelInteraction.cpp` | 头注释仍写"4 片:核心/渲染/交互与编辑" | 改为六分片清单(含本文件定位) |
| `PopupMenuLayout.cpp` | 构造函数被机械搬移导致重复定义 | 只保留在 `PopupMenu.cpp` |
| `PopupMenu.cpp` | `#include "PopupMenuInternal.hpp"` 落在 `namespace jdi` 内 | 移到文件头 |
| `PopupMenuPaint.cpp` | 缺 `UiControls.hpp`(`UiDrawEditFieldContent` 未声明) | 补 include |
| 全仓 | 历史删除符号残留引用 | grep 9 个符号,**零命中** |
### 5. `Tools/` 剥离
`git rm -r --cached Tools` + `.gitignore` 新增 `Tools/`,
并对 `UI_Refactor` 分支做 `filter-branch` 清除历史中的该目录(分支全本地、未推送,风险可控);
本地文件在剥离前已备份到 `%TEMP%` 并复原,`uicheck` / `rendercheck` 随时可复跑。
### 6. 验证数据
```
make release (TargetCheck) → 零 error / 零 warning
rendercheck: 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
体积: Build/Release/TargetCheck.exe = 891,392 B (< 900 KB)
```