Files
Just-DataInspector/Components/Shared/Inc/UiComponent.hpp
T
DeepSeek V4.1 Flash 044b87383b 体积 L3(-fno-exceptions + MemProbe + LTO)与 L1 几何产物层;产物 909,824 -> 788,992 B
## 为什么

用户 2026-10-29 要求:按既定计划完整实现改进,但
① **不得把编译产物拆成 DLL**(项目定位是单文件轻量工具,最终只能有一个 .exe);
② 异常处理改用**预检验**而不是 try/catch(-fno-exceptions 省体积,且 try/catch 本身开销大);
③ 先删除 Documents 内三份 UI 重构文档再按"计划 / 当前进度 / 最终报告"重建;
④ 顺便审计代码,删除无用/过时注释,修复名实不符;
⑤ 提交到本地仓库,期间不要占用用户的鼠标/键盘/屏幕。

## 改了什么

### 1. 体积(909,824 B -> 788,992 B,-120,832 B,-13.3%)

Makefile 新增三个可覆盖开关(默认全为 1):

- `LAYOUT_BOUNDS`  编入/剔除「显示布局边界」叠加层
- `NO_EXCEPTIONS`  追加 `-fno-exceptions`
- `LTO`            追加 `-flto`(编译 + 链接)

派生:`FUNC_SECTIONS`(LTO 下为空)、`GC_SECTIONS`(`-Wl,--gc-sections`)、
`ASM_LISTING`/`ASM_LISTING_C`(LTO 下为空,因为 lto-wrapper 会丢弃 `-Wa` 清单)。

两个实测复现的坑(已写进 Makefile 注释):

- **切换 LTO 必须 `make clean`**:LTO 与非 LTO 目标文件混链会让 GCC 15.2.0 在
  `binds_to_current_def_p (symtab.cc:2598)` 处 ICE。
- **`-flto` 与 `-ffunction-sections -fdata-sections` 同时打开也 ICE**(干净重编也必崩,
  报错落点是 `DataModel.hpp:393`,但那是 COMDAT 解析落点、不是原因)⇒
  LTO 下 `FUNC_SECTIONS` 置空,并加 `-flto-partition=one`:默认 11 个分区的 LTRANS
  稳定复现,单分区从未复现。**这是稳定性选项,不是性能选项。**

### 2. 关异常必须成对交付:预检验 + 兜底

新增 `Utilities/Inc/MemProbe.hpp` + `Utilities/Src/MemProbe.cpp`:

- `MemAvailablePhysical()`:`GlobalMemoryStatusEx` + 200ms 缓存
- `MemCanAllocate(bytes)`:预留后仍须保有 1/8 可用内存
- `MemReserveIfRoom(container, n)`:含 `capacity()` 早退与乘法溢出保护
- `MemInstallTerminateHandler()`:OOM 时弹一次带建议的模态提示后 `_Exit`(而不是静默消失)

接入:

- `Utilities/Src/DataModel.cpp`:全仓**唯一**的 `try/catch`(`extremes.reserve` 捕
  `std::bad_alloc`)改为 `MemReserveIfRoom(extremes, count / 8)`
- `Components/PlotView/Src/PlotView.cpp`:`AvailablePhysicalBytes()` 原有一份**完整重复**
  的 `GlobalMemoryStatusEx` + 缓存实现 → 转发 `MemAvailablePhysical()`
- `Components/Application/Src/Application.cpp`:`Initialize()` 开头安装 terminate handler

### 3. L1 几何产物层

新增 `Components/Shared/Inc/UiBox.hpp` + `Src/UiBox.cpp`:

- `struct UiBox { rect, clip, depth, layer }` —— 纯值类型,不继承任何东西
  (继承会和 PlotView 4 层位图缓存 / SignalPanel 行位图缓存的失效体系打架)
- `class UiBoxTable : public UiTextRectSink` —— 只累计几何 + 裁剪/层/遮挡,**不含绘制代码**

`UiComponent::CollectDebugBoxes` → **`virtual void BuildBoxes(UiBoxTable&)`**:
组件从"收集布局边界(调试用)"改为"上报几何产物",不再需要知道"调试"这个概念存在。
`UiDebugBounds.hpp/.cpp` 退化为唯一的消费者(配色 + 包角描边 + 遮挡挖洞)。

除宿主 `ApplicationCommands.cpp` 外,所有组件源码现在只包含 `UiBox.hpp`。

### 4. 审计

- `MemProbe` 里"省 40~60KB"的估算 → 改为实测 27,136 B
- `MemProbe.hpp` 的 `<new>`、`MemProbe.cpp` 的 `<cstdio>`:未使用,删除
- `UiDebugPaintBounds(g, rect, depth)`:零调用者,删除(只保留带 clip 的版本)
- `TabBar`/`Toolbar`/`PlotView`/`SignalPanel` 的 `BuildBoxes` 文档"(调试用)"→"几何 box"
- `PlotView::SetForceFullRepaint` 补写"为什么保留而不优化"

### 5. 文档

`Documents/UI_Refactor_Plan.md` / `_Progress.md` / `_Report.md` 先 `git rm` 再按
「计划 / 当前进度 / 最终报告」重建;`Documents/agent_mem.md` 追加第十三轮记录。

## 怎么验证

```
make clean
make release TARGET_FILE_NAME=JustDataInspector -j16   # 零 error / 零 warning,788,992 B
make debug   TARGET_FILE_NAME=JustDataInspectorD -j16  # 零 warning

uicheck 69/69   lbcheck 21/21  lbresid 5/5  lbzorder 4/4  lbcovr 7/7  lbpanel 10/10
rendercheck: 8 个场景哈希与改造前逐一相同 + FUNC 7/7 PASS, exit=0
  A FAED59DF284153DB  B E6443BFE4E3955F8  C 96A0AA8C402F1729  D E3D5DBD998E43720
  E DA0583CB13C26A6F  F FFB822159F604D1D  G 19F8DE8FE3A8DF18  H DBBDDE4CB2C97C92
```

体积溯源(`objdump -h`,基线 → 最终):
`.text` 735,600 → 657,168;`.rdata` 49,184 → 35,016;
`.xdata` 48,132 → 27,512;`.pdata` 35,424 → 28,524;`.rsrc` 20,656 不变。

## 没做什么(有意为之)

- **没有拆 DLL**(用户明确要求单文件)
- **没有改优化等级**(`-Oz` 保持)
- **L2(`UiDamage` 脏矩形 + 独立叠加层)评估后缓做**:受益方只有"调试开关打开时的帧率",
  而正确实现必须把叠加层旧矩形注入底层组件重绘范围,穿透 PlotView 四条快路径 + 两层缓存;
  在用户反复报过"拖拽错位 / 贴图残留"的背景下,风险与收益不成比例。理由写在
  `Documents/UI_Refactor_Plan.md` §5 与 `PlotView::SetForceFullRepaint` 的文档里。
2026-10-29 11:53:54 +08:00

109 lines
4.6 KiB
C++
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.
#ifndef __UI_COMPONENT_HPP__
#define __UI_COMPONENT_HPP__
#include <windows.h>
#include <objidl.h>
#include <gdiplus.h>
#include "Theme.hpp"
namespace jdi
{
class UiBoxTable;
/**
* @brief 顶层 UI 组件的统一协议(**纯接口,无状态**)。
*
* ## 为什么需要它
*
* 旧实现里宿主(`Application`)用**手写 if 链**逐个点名四个大组件,导致同一件事
* (设置缩放 / 摆放 / 悬停离开 / 光标仲裁 / 绘制)要在宿主的每个消息分支里各写一遍,
* 新增组件必须记得改 5 处。这里把它们收敛成一张**有序组件表**,
* 宿主只按表遍历,不再点名。
*
* ## 为什么不是 `UiWidget`
*
* `UiWidget` 的契约是"**树上的一个节点**":尺寸由父节点 `Measure` 决定、脏区由父节点下探、
* 命中走树的命中链、`OnPaint` 只画自己不许遍历子节点。而顶层大组件的现实是:
* * 区域由宿主给定,且**内部还要再切分**;
* * `PlotView` 有 4 层自持位图缓存、`SignalPanel` 有行位图缓存 —— 与"父脏下探"是另一套失效体系;
* * `PlotView` 一旦成为树节点,它的 `Paint` 就得接受父级 `dirty` 并与自己的缓存对齐,
* 这正是历史上"后台渲染覆盖用户平移 / 命中错位"那类事故的温床。
*
* ⇒ 两者是**兄弟关系**而不是父子关系:
* `UiWidget`(树内节点,管**一块区域内部的**布局/命中/绘制)
* `UiComponent`(顶层宿主,管**自己这一整块地盘**:区域、缓存、局部重绘策略)。
* `Toolbar` / `TabBar` 同时是两者 —— 对外实现本协议,对内持有一棵 `UiWidget` 树。
*
* ## 实现要求
*
* * 所有方法都必须**幂等且可在未布局时安全调用**(宿主会在首次 `Layout` 之前调 `SetScale`)。
* * `HitTest` 必须与 `Layout` 接受的区域一致(绘制与命中同源)。
* * `Paint` 必须把内容严格限制在 `Area()` 内(宿主可能只回送部分矩形)。
*/
class UiComponent
{
public:
virtual ~UiComponent() = default;
/** @brief 设置 DPI 缩放系数(> 0)。 */
virtual void SetScale(float scale) = 0;
/** @brief 在宿主给定的区域内摆放自身(内部布局由组件自己决定)。 */
virtual void Layout(const gd::RectF& area) = 0;
/** @brief 当前占用区域(客户区坐标);未布局时返回空矩形。 */
virtual gd::RectF Area() const = 0;
/** @brief 绘制自身(必须裁剪在 `Area()` 内)。 */
virtual void Paint(gd::Graphics& g) = 0;
/** @brief 坐标是否落在本组件内(与 `Area()` 同源)。 */
virtual bool HitTest(int x, int y) const = 0;
/** @brief 指针离开窗口/组件:清掉本组件的悬停态。返回是否需要重绘。 */
virtual bool OnMouseLeave() = 0;
/** @brief 是否处于内联编辑态(宿主据此把鼠标/键盘优先交给它)。 */
virtual bool IsEditing() const { return false; }
/** @brief 光标仲裁:返回 `nullptr` 表示不认领(由下层或宿主决定)。 */
virtual HCURSOR QueryCursor(int x, int y)
{
(void)x;
(void)y;
return nullptr;
}
/**
* @brief 上报本组件的**几何产物**(含子元素的矩形、层级、裁剪、遮挡关系)。
*
* 默认实现只登记 `Area()`;自身内部还有可辨识子块(控件树节点、列表行、网格、
* 复选框、徽标、✕、面板控件……)的组件覆盖它,先调本基类再补登记**层级更深**的 box。
*
* ## 为什么入口在组件上
*
* 宿主只持有 `UiComponent*` 的组件表,遍历一次就能覆盖全部顶层区域;
* 组件内部结构由组件自己上报(只有它知道几何)。新增组件不必再改宿主。
*
* ## 它是几何,不是调试
*
* 这里只填 `UiBoxTable`(纯值:矩形 + 层级 + 层号 + 裁剪),**不画任何东西**。
* “怎么显示”是消费方的事:目前唯一的消费方是「显示布局边界」调试叠加层
* (`UiDebugPaintBoxList`),将来命中 / 脏矩形也可以从同一张表派生。
* 这样组件就不需要知道"调试"这个概念的存在。
*
* ⚠️ 目前它只在宿主开启「显示布局边界」时才被调用 —— 平时**零开销**
*(不进任何绘制路径、不进任何位图缓存)。实现时**禁止**把几何写进自持位图缓存,
* 否则关掉开关后线条会残留。
*
* @param [IN,OUT] boxes 收集器(宿主每帧新建,全程只在开关打开时存在)。
*/
virtual void BuildBoxes(UiBoxTable& boxes);
};
} // namespace jdi
#endif // __UI_COMPONENT_HPP__