Files
Just-DataInspector/Documents/UI_Refactor_Progress.md
T
DeepSeek V4.1 Flash ab19a6e72e feat(UI): 显示布局边界(Android 风格)+ 工具栏设置菜单(齿轮)+ 三处可见收益优化
## 1. 新功能:齿轮按钮 -> 应用设置菜单 -> 显示布局边界

- ToolbarAction::SETTINGS 位于「导入文件」左侧;工具栏布局改为
  「左侧固定簇(kLeftClusterCount = 2:齿轮 + 导入文件)+ 右侧按钮组」。
- 齿轮是矢量化绘制(8 齿 + 外环 + 轴孔,全部按 half 的比例推导),
  不用字体字符「⚙」——该字符在不同回退字体里大小与基线不一致,会忽大忽小、垂直不居中。
  UiButtonWidget 新增 UiButtonIcon(NONE / GEAR)与 SetIcon。
- MenuScope::APP_SETTINGS(新增)+ 设置菜单:HEADER「调试」+ 勾选切换项「显示布局边界」
  (keepOpen:点击后菜单保持打开,勾选态由 UpdateItemLabel 回写)。
  锚点取 Toolbar::ButtonRect(SETTINGS) 的控件实测矩形,宿主不再自己推算按钮几何。

## 2. 布局边界:基类给协议,组件自己上报

- UiComponent 新增 virtual PaintDebugBounds(g, scale),默认只画 Area()。
- Toolbar / TabBar 覆盖:跳过与区域重合的根控件,遍历 m_root->Children()(层级 1);
  TabBar 另上报每个 ✕(层级 2)。
- SignalPanel 覆盖:表头 / 筛选框 / 列表 / 样式区(1)+ 滚动条、样式行、当前可见列表行(2)。
- PlotView 覆盖:每个 LaneVisual::rect(1);不上报随数据变化的瞬态几何(曲线/胶囊/提示)。
- UiWidget 新增 PaintDebugBounds(g, scale, depth) 递归整棵控件树 —— 已上树的控件零成本获得边界。
- 宿主 PaintLayoutBounds 补客户区外框、分隔条、打开的菜单/拾色器。

视觉与实现约束(Android「Show layout bounds」风):
- 只描边、不填充(几十个嵌套矩形叠加填充会把内容糊住);
- 层级配色 青蓝(2px) / 品红 / 琥珀 / 绿(1px);
- 线宽按设备像素固定(不乘 scale),临时 PixelOffsetModeNone + 关抗锯齿 + 整像素对齐,
  保证 1px 就是 1px;
- ⚠️ 只在 Render 最后一步叠加,绝不进入任何位图缓存(PlotView 4 层缓存 / SignalPanel 行位图缓存),
  否则关掉开关后线条会残留;
- ⚠️ fullFrame 并入 m_showLayoutBounds:开启时必须整帧回送,否则「只回送绘图区」的快路径
  会让装饰区边界停在旧帧上。

## 3. 可见收益优化(行为不变)

- Toolbar::Layout:控件树改为按需重建(m_treeDirty),只在规格/字体/主题/缩放/动态文本
  变化时重建。拖动面板分隔条时 Relayout 每帧调用一次 -> 不再每帧 10 次堆分配 + 8 次测量缓存失效。
- Toolbar::SetActionLabel:只写 m_labelOverrides(唯一权威)+ 置树脏,消除「覆盖表与控件文本
  双写」可能不同步的问题。
- UiWidget::DispatchRedraw(event):新增;收敛 Toolbar / TabBar 里重复的
  Dispatch(); return UiRectHasArea(TakeDirty());(TakeDirty 会清零,漏一次就把下一次真实变化
  误判成「无变化」)。

## 4. 验证

- make release / make debug:零 error 零 warning;TargetCheck.exe 897,024 B(< 900 KB)。
- 新增离屏探针 .preflight/lbcheck:21 / 21 PASS
  (齿轮在导入文件左侧/同高/更窄;开启后青蓝 24,930 px、品红 19,734 px;关闭后逐像素还原;
   菜单 scope、锚定贴合按钮、宽度未被撑大、条目初始未勾选、点击翻转开关、菜单保持打开)。
- Tools/rendercheck:8 个场景的「工具栏带以下区域」哈希与改造前**逐一相同**
  (证明改动像素被完全限制在工具栏带内);整帧哈希因新增按钮整体变化,属预期。
  FUNC 断言 7 / 7 PASS。
- Tools/uicheck:69 passed / 0 failed。

## 5. 文档

Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md 追加第六阶段;
Documents/agent_mem.md 同步变更日志与「2026-10-28 第八轮」硬约束与验证坑。
2026-10-28 15:46:45 +08:00

404 lines
24 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)
```
---
## 第八轮(2026-10-28):显示布局边界 + 三处可见收益优化
### 进度总览
| # | 任务 | 状态 | 验证 |
| --- | --- | --- | --- |
| 1 | `Toolbar` 控件树按需重建(`m_treeDirty`) | ✅ | 8 场景区域哈希不变 |
| 2 | `Toolbar::SetActionLabel` 单一真值来源 | ✅ | 光标按钮文本仍随模式变化 |
| 3 | `UiWidget::DispatchRedraw` 收敛重复判断 | ✅ | `uicheck` 69/69 |
| 4 | `UiButtonWidget` 支持矢量图标(GEAR) | ✅ | 放大 4 倍核对形状 |
| 5 | `ToolbarAction::SETTINGS` + 左侧固定簇布局 | ✅ | 4 条几何断言 |
| 6 | `MenuScope::APP_SETTINGS` + 设置菜单 | ✅ | 5 条菜单断言 |
| 7 | `UiComponent::PaintDebugBounds` 协议 + 四组件上报 | ✅ | 无开关时零像素影响 |
| 8 | `UiWidget::PaintDebugBounds` 递归控件树 | ✅ | 品红描边 19 734 px |
| 9 | `Application::PaintLayoutBounds` + 整帧回送 | ✅ | 关闭后逐像素还原 |
| 10 | 文档更新 | ✅ | — |
### 关键文件
**新增**
* `Components/Shared/Inc/UiDebugBounds.hpp` / `Src/UiDebugBounds.cpp` —— 调试边界描边原语
(层级配色、只描边不填充、整像素锐利对齐)。
* `Components/Shared/Src/UiComponent.cpp` —— `PaintDebugBounds` 默认实现(只画 `Area()`)。
**修改**
* `Shared/Inc/UiComponent.hpp`、`Shared/Inc|Src/UiWidget.hpp/.cpp`、`Shared/Inc|Src/UiButtonWidget.hpp/.cpp`
* `Toolbar/Inc|Src/*`、`TabBar/Inc|Src/*`、`SignalPanel/Inc/SignalPanel.hpp` + `Src/SignalPanel.cpp`
* `PlotView/Inc/PlotView.hpp` + `Src/PlotViewPaint.cpp`
* `Shared/Inc/PopupMenu.hpp`(`MenuScope::APP_SETTINGS`、`Rect()`)、`Shared/Inc/ColorPickerPopup.hpp`(`Rect()`)
* `Application/Inc/Application.hpp`、`Inc/ApplicationInternal.hpp`、
`Src/Application.cpp`(Render 末尾叠加 + `fullFrame`)、`Src/ApplicationCommands.cpp`(菜单/派发/边界绘制)
### 验证数据
```
make release (TargetCheck) -> 零 error / 零 warning, 897,024 B
.preflight/lbcheck -> 21 passed, 0 failed
INFO area-pixels=24930 node-pixels=19734
INFO gear=(15.0,55.0 50.0x37.5) menu=(15.0,97.0 185.0x73.8)
rendercheck 工具栏以下区域哈希(改造前 == 改造后):
AB AABF558F499583EA BB 1B8B2D389D41B988 CB CAC17AA78C515079 DB 154617738CD9C422
EB DA484B02EEEC778A FB B3CE740F60CF3688 GB 4DD14C0C2EC4CE91 HB D84400B723089F43
rendercheck FUNC 7/7 PASS
uicheck 69 passed, 0 failed
```
> 说明:整帧哈希 A–H 因为工具栏左侧多了一个齿轮按钮而整体变化(预期),
> 因此给探针加了「工具栏带以下」的区域哈希用于**证明改动范围被完全限制在工具栏内**。
### 验证坑(本轮新增)
* PS 5.1 读中文 `.ps1` 会乱码 ⇒ 探针/脚本里只写 ASCII,中文注释另存 UTF-8 无 BOM 数据文件。
* 想拿"改造前"的区域基线必须 `git stash push -u`(**含未跟踪文件**),否则新增的
`UiDebugBounds.cpp` / `UiComponent.cpp` 仍会被 Makefile 的 `Src/*.cpp` 通配扫到而编译失败。
* 「工具栏带以下」的起点必须用**运行时** `m_toolbarRect.Y + Height`;写死 70 会切进工具栏,
齿轮正好落在被统计的区间里,导致本应相同的哈希全都不同。