Files
Just-DataInspector/Components/Shared/Inc/UiBox.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

148 lines
5.8 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_BOX_HPP__
#define __UI_BOX_HPP__
#include <windows.h>
#include <objidl.h>
#include <gdiplus.h>
#include <cstddef>
#include <vector>
#include "PaintUtil.hpp"
#include "Theme.hpp"
namespace jdi
{
/**
* @brief 一个几何 box(**纯值类型**,不含行为、不含生命周期)。
*
* ## 它是什么
*
* 描述"某块界面元素占据哪个矩形、属于第几层、在第几个绘制层、被裁在哪个矩形里"。
* 这就是界面在某一帧的**几何产物**:绘制、命中、调试描边理论上都应该从它派生,
* 而不是各自再算一遍。
*
* ## 为什么不是控件节点
*
* 刻意不继承控件基类:`PlotView` 有 4 层自持位图缓存、`SignalPanel` 有行位图缓存,
* 它们要求"像素由组件自己负责"。把几何塞进继承体系会和缓存失效体系打架,
* 而 box 只是一个可复制的矩形 + 几个整数标签。
*
* ## 为什么 `clip` 与 `rect` 分开存
*
* 组件内部的元素可能落在地盘之外(典型:滚出可视区的网格,Y 是负数)。
* 消费方必须能区分"这块几何真实位置在哪"和"它最多能显示到哪",
* 因此裁剪矩形跟着 box 走,而不是靠全局状态。
*/
struct UiBox
{
gd::RectF rect; /**< 矩形(客户区坐标)。 */
gd::RectF clip; /**< 裁剪矩形(= 所属组件的区域;空 = 不裁剪)。 */
int depth = 0; /**< 层级:0 = 顶层组件区域,≥1 = 内部控件。深 = 更靠近叶子。 */
int layer = 0; /**< 绘制层:越大越靠上,由 `UiBoxTable::PushLayer` 分配。 */
};
/**
* @brief 几何 box 的收集器(值语义容器,**不含任何绘制代码**)。
*
* ## 职责边界
*
* 这里只负责"**累积几何 + 维护裁剪/层/遮挡这些几何事实**"。
* 怎么把 box 变成屏幕上的线,是 `UiDebugBounds` 的事(那是调试专用的消费者)。
* 分开的原因:几何是**稳定事实**(命中、遮挡、将来的脏矩形都要用),
* 而"用彩色包角描边"只服务于一个开关,两者不该长在同一个类里。
*
* ## 两个排序键缺一不可
*
* * `layer`:保证**后收集的内容压住先收集的**。没有它,"信号面板的行边界"
* 会盖住"右键弹出的二级菜单"(菜单是后收集的浮层)。
* * `depth`:同一层内先画父(浅层)再画子(深层),子元素的包角才不被父覆盖。
*
* ## 开销约束
*
* 只在宿主开启「显示布局边界」时才被构造和填充 —— 关闭时**零开销**
* (不进绘制路径、不进任何位图缓存)。若将来要用它做常驻命中表,
* 必须显式改为"按需重建",不能每帧无限期累积。
*/
class UiBoxTable : public UiTextRectSink
{
public:
/** @brief 清空(复用同一实例时调用),同时把绘制层复位为 0。 */
void Clear();
/**
* @brief 进入下一个绘制层。
*
* 宿主在每个组件收集完之后调用一次;组件内部若有"浮层压住本体"的情形
* (例如菜单的二级子菜单)也应在收集子层之前调用。
*/
void PushLayer() { ++m_layer; }
/**
* @brief 显式设置当前绘制层(绘制期间收集文本几何时用)。
* @param [IN] layer 层号(越大越靠上)。
*/
void SetLayer(int layer) { m_layer = layer; }
/** @brief 回报一段文本的墨迹矩形(由 `PaintUtil` 在绘制文本时调用)。 */
void OnTextRect(const gd::RectF& rect) override;
/**
* @brief 设置后续 box 的裁剪矩形(= 当前组件的区域)。
*
* ⚠️ **必须像裁剪绘制一样裁剪几何**:组件内部元素可能落在地盘之外
* (典型:滚出可视区的网格 `lane.rect` 的 Y 会是负数),不裁剪就会登记到相邻组件
* (工具栏 / 标签栏)的地盘上 —— 这正是用户报告"工具栏上出现大量残留"的直接原因。
*
* @param [IN] clip 裁剪矩形;传空矩形表示不裁剪。
*/
void SetClip(const gd::RectF& clip) { m_clip = clip; }
/** @brief 追加一个 box(空矩形 / 与裁剪矩形无交集的 box 自动忽略)。 */
void Add(const gd::RectF& rect, int depth);
/** @brief 追加一组同层级的 box(空矩形逐个忽略)。 */
void AddAll(const std::vector<gd::RectF>& rects, int depth);
/**
* @brief 登记一个**遮挡矩形**(浮层本体,如弹出菜单 / 拾色器)。
*
* ⚠️ 必须做这件事:box 的消费方是**在所有内容之后**工作的,仅靠 `layer` 排序
* 只能决定"两个 box 重叠时谁压谁",并不能阻止"下层组件的边框画在不透明浮层上面"。
* 登记为遮挡后,**绘制层比它低**的 box 在该区域内会被裁掉。
*
* @param [IN] rect 遮挡矩形(客户区坐标;空矩形忽略)。
*/
void AddOccluder(const gd::RectF& rect);
/** @brief 是否为空。 */
bool Empty() const { return m_boxes.empty(); }
/** @brief box 数量。 */
std::size_t Count() const { return m_boxes.size(); }
/** @brief 只读访问(**未排序**,消费方需要顺序时自己按 `(layer, depth)` 排序)。 */
const std::vector<UiBox>& Items() const { return m_boxes; }
/** @brief 一个遮挡矩形(含它所在的绘制层)。 */
struct Occluder
{
gd::RectF rect; /**< 遮挡矩形。 */
int layer = 0; /**< 所在绘制层(只遮挡更低的层)。 */
};
/** @brief 遮挡矩形集合(只读)。 */
const std::vector<Occluder>& Occluders() const { return m_occluders; }
private:
std::vector<UiBox> m_boxes;
std::vector<Occluder> m_occluders;
int m_layer = 0; /**< 当前绘制层。 */
gd::RectF m_clip; /**< 当前裁剪矩形(空 = 不裁剪)。 */
};
} // namespace jdi
#endif // __UI_BOX_HPP__