完成 StellarX 核心重构、缺陷修复与测试收口

This commit is contained in:
Codex
2026-09-30 01:23:18 +08:00
parent 9155a86a8a
commit 568ffc40eb
85 changed files with 5011 additions and 645 deletions
@@ -0,0 +1,64 @@
# Fix-BUG-20251209-0001
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20251209-0001
- 关联 BUG ID: BUG-20251209-0001
- 修复目标: TabControl 由不可见变为可见时,仅绘制激活页;无激活页时不绘制任何页,避免页面重叠和脏帧。
- 状态:已完成
- 负责人: 未记录
- 分支 / 版本: master / v2.3.2
## 根因分析
- 根因: `TabControl::setIsVisible(true)` 对所有页签 / 页面统一传播 true 并置脏,导致恢复可见后本应不可绘制的非激活页也进入绘制流程。
- 触发条件: 执行 `setIsVisible(false)` 后再执行 `setIsVisible(true)`。
- 为什么之前没发现:[可选] 缺少“隐藏 -> 显示”回归用例,尤其缺少“多个页面但只允许激活页绘制”的断言。
- 关键证据:[可选] PDF 历史记录明确将疑似位置定位到 `TabControl::setIsVisible()` 与 `show` 状态传播。
## 修复方案
- 修复思路: TabControl 可见性向页面传播时按激活状态过滤;显示时只允许激活页可见,隐藏时所有页都不可见。
- 关键改动:
- `TabControl.cpp::setIsVisible()` 在参数为 true 时,根据激活页判定对子页面传递 true / false。
- 参数为 false 时,对所有页面传递 false。
- 绘制入口可选择增加“仅绘制激活页”的防御性约束。
- 涉及文件 / 类 / 函数:
- `TabControl.cpp`
- `TabControl::setIsVisible()`
- 影响的 API / 行为:[可选] 对外 API 无变化;行为层面修正错误绘制。
- 关键约束 / 不变量:[可选] 任意时刻可参与绘制的页面集合最多只有一个激活页。
- 回滚点 / 开关:[可选] 回退 TabControl 页面可见性传播逻辑。
## 影响评估
- 影响范围: TabControl 隐藏 / 显示、页面激活状态、页面绘制链路。
- 兼容性影响:无(纠错式兼容)
- 行为变化:有(非激活页不再随 TabControl 显示而被绘制)
- 性能影响:无负面影响,减少非激活页绘制后预期更优或持平。
- 回归风险:
- 如果激活判定逻辑异常导致“无激活页”,恢复显示后会表现为空白页;该行为符合修复目标。
- 如果外部代码错误依赖非激活页仍可见,可能观察到行为差异。
## 验证结果
- 验证步骤:
1. 创建含不少于 2 个页的 TabControl,并设置激活页为 0。
2. 调用 `setIsVisible(false)`,再通过事件调用 `setIsVisible(true)`。
3. 确认仅激活页显示,非激活页不绘制、不重叠。
4. 切换激活页后重复隐藏 / 显示,确认仍只绘制当前激活页。
5. 验证无激活页场景,确认不绘制任何页。
- 验证结果: PDF 历史记录标记已修复。
- 回归检查:[可选] 嵌套容器可见性、裁剪、鼠标事件命中、布局刷新、脏标记 / 快照重绘路径。
- 验证证据:[可选]
## 落地信息
- Commit: 0c1cf2938f0d6c5684eeb318f17a8443e0c8cf5a
- PR:[可选] git
- 发布版本: v2.3.2
- 备注: 来源原始 Fix 记录未包含 `-XXXX` 后缀,入库时按 BUG ID 补充为 `Fix-BUG-20251209-0001`。
@@ -0,0 +1,69 @@
# Fix-BUG-20251220-0001
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20251220-0001
- 关联 BUG ID: BUG-20251220-0001
- 修复目标: 正确支持绘制前设置默认激活页,并在首次绘制时激活对应页面;运行时调用则立即切换,且不崩溃。
- 状态:已完成
- 负责人: 未记录
- 分支 / 版本: master / v2.3.2
## 根因分析
- 根因: `setActiveIndex()` 在设置默认激活时直接调用页签按钮点击回调,但绘制前页签按钮部分内部指针尚未初始化,导致空指针访问。
- 触发条件: 绘制前调用 `TabControl::setActiveIndex()`。
- 为什么之前没发现:[可选] 测试缺失,未覆盖绘制前设置默认激活页的生命周期场景。
- 关键证据:[可选] 最小复现仅需创建 TabControl 并在绘制前调用 `setActiveIndex(0)`。
## 修复方案
- 修复思路: 首次绘制前调用 `setActiveIndex()` 时只记录默认激活索引,不立即调用页签按钮点击回调;在 `TabControl::draw()` 完成基础绘制后再应用默认激活。
- 关键改动:
- `TabControl::setActiveIndex()` 在首次绘制前仅记录默认激活页索引。
- `TabControl::draw()` 绘制完成后检查是否为首次绘制、是否设置默认激活页、索引是否合法。
- 新增成员 `TabControl::IsFirstDraw`,用于判断是否首次绘制。
- 新增成员 `TabControl::defaultActivation`,用于记录默认激活索引;未设置时为 `-1`。
- 涉及文件 / 类 / 函数:
- `TabControl.h`
- `TabControl.cpp`
- `TabControl::setActiveIndex()`
- `TabControl::draw()`
- 影响的 API / 行为:[可选] 对外接口不变;内部延迟首次激活流程。
- 关键约束 / 不变量:[可选]
- 首次绘制前的默认激活只记录,不触发未初始化对象的点击回调。
- 首次绘制后调用 `setActiveIndex()` 应立即切换激活页。
- 回滚点 / 开关:[可选] 回退默认激活延迟应用逻辑。
## 影响评估
- 影响范围: TabControl 默认激活、首次绘制、运行时程序化切页。
- 兼容性影响:无(对外接口不变)
- 行为变化:有(绘制前设置默认激活页改为首次绘制后应用)
- 性能影响:无明显影响。
- 回归风险:
- 默认激活索引越界时的行为需要与既有预期保持一致。
- 开关模式下页签按钮选中态需要与激活页同步。
## 验证结果
- 验证步骤:
1. 创建页签数为 2 的 TabControl。
2. 分别在绘制前调用 `setActiveIndex(0)`、`setActiveIndex(1)`、越界索引。
3. 确认索引 0 / 1 在首次绘制时激活对应页面,按钮处于选中状态。
4. 运行过程中通过 Button 回调调用 `setActiveIndex()`,确认立即切换激活页。
5. 验证未调用 `setActiveIndex()` 时默认不激活任何页面。
- 验证结果: PDF 历史记录标记已修复。
- 回归检查:[可选] 页签点击切换、程序化切换、默认不激活场景、开关模式选中态、页签数量变化与越界索引行为。
- 验证证据:[可选]
## 落地信息
- Commit: 0c1cf2938f0d6c5684eeb318f17a8443e0c8cf5a
- PR:[可选] git 推送
- 发布版本: v2.3.2
- 备注: 来源原始 Fix 记录未包含 `-XXXX` 后缀,入库时按 BUG ID 补充为 `Fix-BUG-20251220-0001`。
@@ -0,0 +1,65 @@
# Fix-BUG-20260109-0001
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260109-0001
- 关联 BUG ID: BUG-20260109-0001
- 修复目标: 修复 TextBox 在 Window 创建 / 绘制前调用 `setText()` 设置默认文本时因即时绘制导致的崩溃。
- 状态:已完成
- 负责人: 未记录
- 分支 / 版本: master / v3.0.0
## 根因分析
- 根因: `TextBox::setText()` 将“状态更新”和“立即绘制”耦合在一起。窗口尚未 `initgraph()`、EasyX 图形上下文未就绪时,`draw()` 内部进入 `Control::saveBackground()`,调用 `SetWorkingImage()` / `getimage()` 等依赖有效图形环境的逻辑,最终崩溃。
- 触发条件: 创建 TextBox 后,在 Window 初始化 / 首次绘制前调用 `TextBox::setText()`。
- 为什么之前没发现:[可选] 常规示例多为 Window 完成初始化后再设置文本,缺少控件生命周期前置调用的最小回归用例。
- 关键证据:[可选] 调用链为 `TextBox::setText()` -> `TextBox::draw()` -> `Control::saveBackground()` -> EasyX 图形上下文相关 API。
## 修复方案
- 修复思路: `TextBox::setText()` 改为仅执行文本赋值和脏标记更新,不再在 setter 内直接调用 `draw()`;绘制统一交给 Window / 容器重绘流程。
- 关键改动:
- 移除 `TextBox::setText(std::string text)` 中的直接 `draw()` 调用。
- 保留长度裁剪、文本赋值、`dirty=true`。
- 将可视更新延后到后续统一重绘周期。
- 涉及文件 / 类 / 函数:
- `TextBox.cpp`
- `TextBox::setText()`
- 影响的 API / 行为:[可选] 对外 API 不变;`setText()` 不再保证即时绘制。
- 关键约束 / 不变量:[可选]
- setter 不得直接调用 `draw()` 或任何依赖图形上下文的 API。
- `saveBackground()` / `getimage()` / `putimage()` / `SetWorkingImage()` 只能在 Window 图形上下文就绪并进入统一绘制流程后执行。
- 回滚点 / 开关:[可选] 回退 setter 内即时绘制逻辑。
## 影响评估
- 影响范围: TextBox 默认文本设置、程序化文本更新、绘制时序。
- 兼容性影响:有(如果外部代码依赖 `setText()` 的即时绘制副作用,需要调整为触发一次重绘或等待绘制周期)
- 行为变化:有(`setText()` 语义收敛为更新状态并标脏)
- 性能影响:正向或无影响,减少 setter 内重复绘制与抓屏开销。
- 回归风险:
- 程序化调用 `setText()` 后,如果应用没有触发任何重绘路径,界面更新可能延迟。
- 依赖即时刷新语义的旧代码可能出现短暂视觉滞后。
## 验证结果
- 验证步骤:
1. 构造最小复现:在 Window 创建 / `draw()` 前创建 TextBox 并调用 `setText("default")`,确认不再崩溃。
2. Window 初始化后,通过事件输入或直接调用 `setText()` 更新文本,确认后续一次重绘时文本正确显示。
3. 覆盖 INPUT / READONLY / PASSWORD 三种模式,确认长度裁剪与显示逻辑不受影响。
4. 覆盖窗口 resize,确认背景快照机制仍按既有逻辑工作,无残影或错位。
- 验证结果: PDF 历史记录标记已修复。
- 回归检查:[可选] TextBox 三模式输入与显示、背景快照保存 / 恢复 / 丢弃、Window 首次绘制、对话框打开 / 关闭强制重绘、resize 收口重绘。
- 验证证据:[可选]
## 落地信息
- Commit: 5cb59b36525cde93de83313551296c6458694110
- PR:[可选] git
- 发布版本: v3.0.0
- 备注: 来源原始 Fix 记录未包含 `-XXXX` 后缀,入库时按 BUG ID 补充为 `Fix-BUG-20260109-0001`。
@@ -0,0 +1,69 @@
# Fix-BUG-20260109-0002
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260109-0002
- 关联 BUG ID: BUG-20260109-0002
- 修复目标: 修复 TabControl 页签切换与 Table 重置数据后出现的新旧内容像素叠加,确保快照机制不被污染。
- 状态:已完成
- 负责人: 未记录
- 分支 / 版本: master / v3.0.0
## 根因分析
- 根因: `Control::setIsVisible(true)` 立即向上 `requestRepaint()`,导致页签切换 / 关闭过程中,父容器可能处于 `hasSnap=0`、`dirty=1` 或刚 `discardBackground()` 的快照未就绪状态。此时 `Canvas::requestRepaint(partial)` 直接对子控件 Table 调用 `draw()`,随后 `saveBackground()` 又把已经包含旧表格像素的屏幕区域截入快照,形成背景快照污染。
- 触发条件: 切换到按钮1页签 -> 重置表格数据 -> 切换 / 关闭其他页签 -> 再切回按钮1;或重置数据当次容器快照未就绪时触发局部重绘。
- 为什么之前没发现:[可选] 历史版本 `setIsVisible(true)` 不会立即 `requestRepaint()`;常规交互未必同时满足页签切换 / 表格重置 / Null 填充 / 透明文字背景的组合条件。
- 关键证据:[可选] 日志中出现 Canvas 丢弃快照后,Table `show=1` 立即触发局部重绘,随后保存 Table 背景快照。
## 修复方案
- 修复思路: 回退 `show=true` 立即请求重绘的行为,并为 Canvas / TabControl 的局部重绘增加快照就绪护栏;当容器快照未就绪或 dirty 未清理时,禁止 partial fast-path,降级为 full draw。
- 关键改动:
- `Control::setIsVisible()` 中 `show=true` 时不再立即 `requestRepaint()`,改为仅置脏;`show=false` 保持擦除逻辑。
- `Canvas::requestRepaint()` 增加护栏,`dirty` / 无快照 / 无缓存图时将 partial 降级为 full draw。
- `TabControl::requestRepaint()` 同步增加局部重绘护栏。
- 可选修正 `Table::setData(vector<string>)` 列补齐 off-by-one,降低潜在越界或异常噪声。
- 涉及文件 / 类 / 函数:
- `Control.cpp`
- `Canvas.cpp`
- `TabControl.cpp`
- `Table::setData()`(可选)
- 影响的 API / 行为:[可选] 对外 API 不变;显示时序与局部重绘降级策略发生内部调整。
- 关键约束 / 不变量:[可选]
- 局部重绘前,父容器必须持有有效快照且不处于 dirty 未清理状态。
- `setIsVisible(true)` 不得在快照未就绪阶段触发子控件抢跑绘制。
- 回滚点 / 开关:[可选] 回退 visible 置脏与 partial 降级护栏。
## 影响评估
- 影响范围: TabControl 页签切换、Canvas 局部重绘、Table 数据重置、背景快照捕获。
- 兼容性影响:无 API 变化;若外部依赖 `show=true` 即刻刷新,需要确保后续绘制收口会触发重绘。
- 行为变化:有(快照未就绪时局部重绘降级为全量重绘)
- 性能影响:在快照未就绪阶段单次重绘成本增加;稳定交互阶段仍保留局部重绘路径。
- 回归风险:
- 降级条件过宽可能导致频繁全量重绘。
- 显示状态更新后,如果外部没有触发任何重绘,可能存在视觉延迟。
- 程序化页签激活流程可能受重绘时序影响,需要回归。
## 验证结果
- 验证步骤:
1. 按稳定复现路径:按钮1 -> 重置表格数据 -> 切换 / 关闭其他页签 -> 切回按钮1,重复 50 次以上。
2. 在按钮1停留时连续快速点击重置 100 次,并穿插页签切换、窗口最小化 / 恢复、拖拽 / resize。
3. 反复对 Table / Canvas 调用 `setIsVisible(false / true)`,确认隐藏擦除正确、显示后无残影。
4. 分别在 TableFillMode 为 Null 与非 Null 模式下验证。
- 验证结果: PDF 历史记录标记已修复。
- 回归检查:[可选] TabControl 切换 / 关闭 / 重复打开、Canvas 添加 / 移除控件与 resize、Table clearData / setData / 翻页、全局窗口 resize / 最小化恢复。
- 验证证据:[可选] PDF 记录中关联 `QQ202619-144430.mp4`。
## 落地信息
- Commit: 5cb59b36525cde93de83313551296c6458694110
- PR:[可选] git 推送
- 发布版本: v3.0.0
- 备注: 来源原始 Fix 记录未包含 `-XXXX` 后缀,入库时按 BUG ID 补充为 `Fix-BUG-20260109-0002`。
@@ -0,0 +1,58 @@
# Fix-BUG-20260122-0001
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260122-0001
- 关联 BUG ID: BUG-20260122-0001
- 修复目标: 将 `Window::runEventLoop()` 中普通控件事件分发顺序由正序遍历改为逆序遍历,以保持 Z 序直觉。
- 状态:已完成
- 负责人: 未记录
- 分支 / 版本: master / v3.0.1
## 根因分析
- 根因: 之前版本修改 `Window::runEventLoop()` 时,将原本的逆序事件分发错误改成了正序遍历。
- 触发条件: 普通控件重叠时,事件落在多个控件的命中区域。
- 为什么之前没发现:[可选] 测试缺失,提交时未进行详细代码审查。
- 关键证据:[可选] PDF 历史记录说明该问题属于代码审查发现的遍历顺序错误。
## 修复方案
- 修复思路: 对 `controls` 使用逆序遍历,让绘制顺序更靠上的控件优先处理事件。
- 关键改动:
- 将 `for (auto& c : controls)` 调整为 `for (auto it = controls.rbegin(); it != controls.rend(); ++it)`。
- 涉及文件 / 类 / 函数:
- `Window.cpp`
- `Window::runEventLoop()`
- 影响的 API / 行为:[可选] 无对外 API 变化;重叠控件事件命中顺序修正。
- 关键约束 / 不变量:[可选] 普通控件事件分发应保持逆序遍历,避免下层控件抢事件。
- 回滚点 / 开关:[可选] 回退普通控件事件遍历顺序。
## 影响评估
- 影响范围: 普通控件点击、悬停、重叠控件事件命中。
- 兼容性影响:无(内部逻辑修改)
- 行为变化:有(重叠控件由上层优先处理事件)
- 性能影响:无明显影响。
- 回归风险: 控件重叠时的事件分发行为变化,需要确认符合绘制 Z 序。
## 验证结果
- 验证步骤:
1. 创建重叠普通控件。
2. 在重叠区域触发点击与悬停。
3. 确认上层控件优先收到事件。
- 验证结果: PDF 历史记录标记已修复;原记录未提供详细验证证据。
- 回归检查:[可选] 普通控件事件分发、重叠按钮点击、悬停状态变化。
- 验证证据:[可选] 无
## 落地信息
- Commit: b765e54eff28ebcd0bd99a81dec80b3ac0769337
- PR:[可选] git 推送
- 发布版本: v3.0.1
- 备注: PDF 原文合入分支写作 `mster`,本次按仓库语境整理为 `master`。
@@ -0,0 +1,59 @@
# Fix-BUG-20260203-0001
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260203-0001
- 关联 BUG ID: BUG-20260203-0001
- 修复目标: 避免对话框打开时错误合成并下发 `WM_MOUSEMOVE`,只在对话框关闭后刷新可见控件 hover 状态。
- 状态:已完成
- 负责人: 未记录
- 分支 / 版本: master / v3.0.1
## 根因分析
- 根因: 合成 `WM_MOUSEMOVE` 的代码位于 `if (needredraw || dialogClose)` 范围内,但执行合成下发时没有再判断 `dialogClose` 是否为真。
- 触发条件: 打开对话框触发重绘判断。
- 为什么之前没发现:[可选] 早期未引入日志系统,隐藏的事件下发问题不易被发现。
- 关键证据:[可选] 对话框打开日志中出现“对话框关闭,合成WM_MOUSEMOVE已下发”。
## 修复方案
- 修复思路: 合成 `WM_MOUSEMOVE` 前显式判断 `dialogClose`,只有对话框关闭路径才执行合成并下发。
- 关键改动:
- 在 `Window::runEventLoop()` 中,对合成 `WM_MOUSEMOVE` 下发逻辑增加 `dialogClose == true` 条件。
- 涉及文件 / 类 / 函数:
- `Window.cpp`
- `Window::runEventLoop()`
- 影响的 API / 行为:[可选] 无对外 API 变化。
- 关键约束 / 不变量:[可选] `dialogClose == true` 才能合成并下发 `WM_MOUSEMOVE`。
- 回滚点 / 开关:[可选] 回退 `dialogClose` 条件判断。
## 影响评估
- 影响范围: 对话框打开 / 关闭后的 hover 状态刷新、普通控件事件下发。
- 兼容性影响:无
- 行为变化:有(对话框打开阶段不再下发合成 `WM_MOUSEMOVE`)
- 性能影响:持平或更优,减少不必要事件下发。
- 回归风险: 暂无;需确认对话框关闭后 hover 状态仍能正确刷新。
## 验证结果
- 验证步骤:
1. 创建包含对话框的最小 Demo。
2. 启动日志系统。
3. 触发对话框弹出并观察日志。
4. 关闭对话框并确认只在关闭阶段出现合成 `WM_MOUSEMOVE` 下发。
- 验证结果: PDF 历史记录标记已修复。
- 回归检查:[可选] 非模态对话框事件处理、关闭后 hover 状态刷新、普通控件悬停状态。
- 验证证据:[可选] PDF 记录包含 2026-02-10 运行日志。
## 落地信息
- Commit: b765e54eff28ebcd0bd99a81dec80b3ac0769337
- PR:[可选] git 推送
- 发布版本: v3.0.1
- 备注: 来源原始 Fix 记录未包含 `-XXXX` 后缀,入库时按 BUG ID 补充为 `Fix-BUG-20260203-0001`。
@@ -0,0 +1,63 @@
# Fix-BUG-20260206-0001
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260206-0001
- 关联 BUG ID: BUG-20260206-0001
- 修复目标: 只在对话框打开或关闭时触发一次必要全量重绘,避免对话框保持打开期间持续全量重绘。
- 状态:已完成
- 负责人: 未记录
- 分支 / 版本: master / v3.0.1
## 根因分析
- 根因: 对话框打开状态判断基于“是否有对话框可见”,导致只要有对话框保持打开,`needredraw` 就持续为 true。
- 触发条件: 触发对话框打开并保持可见。
- 为什么之前没发现:[可选] 之前没有引入日志系统,该问题属于性能类隐藏问题,不容易观察。
- 关键证据:[可选] 日志中连续出现“对话框打开/关闭,触发全量重绘”。
## 修复方案
- 修复思路: Window 新增 `dialogOpen` 状态,只有对话框刚打开时才触发打开相关重绘;完成重绘后立即复位。
- 关键改动:
- `Window` 新增变量 `dialogOpen`。
- 对话框弹出时设置 `dialogOpen=true`。
- `needredraw` 判断逻辑改为只在 `dialogOpen` 或关闭事件发生时触发。
- 重绘后立即将 `dialogOpen` 复位为 false。
- 涉及文件 / 类 / 函数:
- `Window.cpp`
- `Dialog.cpp`
- `Window::runEventLoop()`
- 影响的 API / 行为:[可选] 无对外 API 变化。
- 关键约束 / 不变量:[可选] `dialogOpen == true` 才触发对话框打开相关重绘判断;重绘后必须复位。
- 回滚点 / 开关:[可选] 回退 `dialogOpen` 状态门控。
## 影响评估
- 影响范围: 对话框打开 / 关闭后的全量重绘、主事件循环性能。
- 兼容性影响:无
- 行为变化:有(对话框保持打开时不再持续触发全量重绘)
- 性能影响:持平或更优,减少重复全量绘制。
- 回归风险: 未知;需确认对话框打开时仍能置顶并正确清理关闭残留。
## 验证结果
- 验证步骤:
1. 触发对话框弹出。
2. 观察日志,确认打开时只触发一次必要全量重绘。
3. 保持对话框打开,确认不再持续刷全量重绘日志。
4. 关闭对话框,确认关闭路径仍能重绘清理残留。
- 验证结果: PDF 历史记录标记已修复。
- 回归检查:[可选] 对话框打开 / 关闭、非模态对话框、普通控件重绘、背景快照刷新。
- 验证证据:[可选] PDF 记录包含 2026-02-15 修复后运行日志。
## 落地信息
- Commit: b765e54eff28ebcd0bd99a81dec80b3ac0769337
- PR:[可选] git 推送
- 发布版本: v3.0.1
- 备注: 来源原始 Fix 记录未包含 `-XXXX` 后缀,入库时按 BUG ID 补充为 `Fix-BUG-20260206-0001`。
@@ -0,0 +1,58 @@
# Fix-BUG-20260217-0001
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260217-0001
- 关联 BUG ID: BUG-20260217-0001
- 修复目标: `TabControl::getActiveIndex()` 在页签列表为空或无页签激活时返回 `-1`,有页签激活时返回对应索引。
- 状态:已完成
- 负责人: 未记录
- 分支 / 版本: master / v3.0.1
## 根因分析
- 根因: `getActiveIndex()` 未处理“页签列表不为空但所有页签都未激活”的情况。
- 触发条件: 调用 `TabControl::getActiveIndex()` 时,页签列表非空且没有任何激活页签。
- 为什么之前没发现:[可选] 测试缺失,未覆盖无激活页但列表非空的返回值场景。
- 关键证据:[可选] 代码分析确认旧逻辑会返回最后一个页签索引。
## 修复方案
- 修复思路: 增加无激活页兜底逻辑;页签列表为空或遍历后未找到激活页时统一返回 `-1`。
- 关键改动:
- 修改 `TabControl::getActiveIndex()`,在无激活页时返回 `-1`。
- 涉及文件 / 类 / 函数:
- `TabControl.cpp`
- `TabControl::getActiveIndex()`
- 影响的 API / 行为:[可选] 修正返回值语义;调用方可可靠用 `-1` 判断无激活页。
- 关键约束 / 不变量:[可选] 无激活页必须返回 `-1`,不能返回任意有效索引。
- 回滚点 / 开关:[可选] 回退无激活页兜底返回逻辑。
## 影响评估
- 影响范围: TabControl 激活页查询、依赖 `getActiveIndex()` 的业务判断。
- 兼容性影响:无(修复 BUG)
- 行为变化:有(无激活页从错误有效索引变为 `-1`)
- 性能影响:无明显影响。
- 回归风险: 低;需确认有激活页和空列表场景返回值正确。
## 验证结果
- 验证步骤:
1. 调用 `TabControl::getActiveIndex()`,验证页签列表为空时返回 `-1`。
2. 验证页签列表不为空且所有页签未激活时返回 `-1`。
3. 验证存在激活页时返回对应索引。
- 验证结果: PDF 历史记录标记已修复。
- 回归检查:[可选] TabControl 默认激活、页签切换、无激活页状态。
- 验证证据:[可选] 无
## 落地信息
- Commit: b765e54eff28ebcd0bd99a81dec80b3ac0769337
- PR:[可选] git
- 发布版本: v3.0.1
- 备注: 来源原始 Fix 记录未包含 `-XXXX` 后缀,入库时按 BUG ID 补充为 `Fix-BUG-20260217-0001`。
@@ -62,7 +62,7 @@
- 验证方式:
1. 在 `>1000` 旧保护分支中先加 `DEBUG` 日志,确认 `KEY == 1 / 2` 命中。
2. 替换为非法尺寸保护后,编译 [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)。
2. 替换为非法尺寸保护后,编译 [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Window.cpp)。
3. 按 `KEY == 1 ~ 4` 做逻辑推演。
- 理论结果:
- `KEY == 1 / 2`:现在应打印“大跨度但继续执行收口”日志,并继续完成 `尺寸调整已完成`。
@@ -24,7 +24,7 @@
- 之前更关注 Tooltip 的显示、Hover 切换和对话框遮挡链路
- “逻辑状态已隐藏,但屏幕区域无人回贴”的问题只有在当前托管重绘路径下才更稳定暴露
- 关键证据:
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp) 中 `hideTooltip()` 在托管分发阶段只作废快照,不回贴 Tooltip 自身区域
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Button.cpp) 中 `hideTooltip()` 在托管分发阶段只作废快照,不回贴 Tooltip 自身区域
## 修复方案
@@ -36,7 +36,7 @@
- 只要 `tipVisible == true`,统一调用 `tipLabel.hide()`
- 保留 `tipHoverTick` 重置逻辑
- 涉及文件 / 类 / 函数:
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp)
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Button.cpp)
- `Button::hideTooltip()`
- 影响的 API / 行为:
- 无公开 API 变化
@@ -64,7 +64,7 @@
- 验证步骤:
1. 修改 [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp) 中 `Button::hideTooltip()` 的 Tooltip 隐藏分支
1. 修改 [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Button.cpp) 中 `Button::hideTooltip()` 的 Tooltip 隐藏分支
2. 编译 `Button.cpp`
3. 编译 `z-testDome.cpp` 并指定 `KEY=4`,确认 Tooltip 相关 demo 入口可正常编译
@@ -34,9 +34,9 @@
- `TabControl`:按自己的真实绘制顺序补画页签按钮与页面
- overlay 补画前统一 `invalidateBackgroundSnapshot()`,避免旧快照反贴旧背景
- 涉及文件 / 类 / 函数:
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Window.cpp)
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Canvas.cpp)
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/TabControl.cpp)
- 影响的 API / 行为:[可选]
- 无对外 API 变化
- 局部重绘行为更严格遵守视觉合成顺序
@@ -62,8 +62,8 @@
- 验证步骤:
1. 编译 [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)、[`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)、[`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
2. 编译 [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp) `KEY=5`
1. 编译 [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Window.cpp)、[`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/Canvas.cpp)、[`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/src/StellarX/TabControl.cpp)
2. 编译 [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/app/z-testDome.cpp) `KEY=5`
3. 回归 `Table` 翻页、相交 `Canvas`、`TabControl` 页签/页面遮挡场景
- 验证结果:
@@ -0,0 +1,66 @@
# Fix-BUG-20260929-0001
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260929-0001
- 关联 BUG ID: BUG-20260929-0001
- 修复目标: 避免 Table 运行期重置后的分页按钮旧快照覆盖新页码
- 状态:已完成
- 负责人: Codex
- 分支 / 版本: 当前工作区
## 根因分析
- 根因: `Table::draw()` 先通过 `drawPageNum()` 绘制新页码,随后 `drawButton()` 才重新定位上一页和下一页按钮。按钮的 `setX()/setY()` 会回贴旧位置背景快照;当旧按钮区域与新页码位置相交时,新页码被局部擦除。
- 触发条件: 运行期替换表头或数据后,表格宽度和分页控件位置发生变化。
- 为什么之前没发现: 既有检查覆盖普通翻页重绘,但没有覆盖“表格结构尺寸变化 + 三个分页控件同帧搬移”的首帧顺序。
- 关键证据: 后续 hover 只触发重绘、不再改变分页控件位置,此时页码立即恢复完整,说明页码内容和设脏逻辑本身有效。
## 修复方案
- 修复思路: 将分页内部控件的布局刷新前移到 Table 新一帧绘制之前。先更新页码文本,再统一移动页码 Label 和两个分页按钮并清理旧快照,最后绘制表体、页码和按钮。
- 关键改动:
- 集中生成当前页码文本,避免布局阶段和绘制阶段使用不同字符串。
- 当分页布局失效时,在 Table 回贴并重绘自身之前完成页码 Label 的尺寸、位置和文本更新。
- 在同一预布局阶段完成上一页、下一页按钮的位置更新,确保旧快照不会覆盖本帧已经绘制的分页元素。
- 保留最终“表体 -> 页码 -> 分页按钮”的绘制顺序。
- 涉及文件 / 类 / 函数:
- `src/StellarX/Table.cpp`
- `Table::draw()`
- `Table::drawPageNum()`
- 影响的 API / 行为: 无公共 API 变化;仅修正运行期重置后的首帧绘制结果。
- 关键约束 / 不变量: Table 内部控件因布局变化产生的旧快照回贴必须发生在父 Table 绘制新内容之前。
- 回滚点 / 开关: 回退 `Table::draw()` 中的分页预布局块及统一页码文本生成函数。
## 影响评估
- 影响范围: Table 数据、表头、每页行数变化后的分页区重绘。
- 兼容性影响:无
- 行为变化:有(重置后的首帧直接显示完整页码)
- 性能影响:可忽略,仅在分页布局失效时提前执行原有位置刷新。
- 回归风险:
- 透明 Table 背景下的分页区快照恢复顺序变化。
- 页码位数变化造成 Label 宽度变化时,需要确认左右按钮仍与页码保持正确间距。
- 隐藏分页按钮后再恢复显示时,需要确认按钮仍能初始化并正确定位。
## 验证结果
- 验证步骤:
1. 执行 `scripts/test.ps1 -Suite NonUI`。
2. KEY1 页签 1 点击页内按钮后,不移动鼠标立即检查完整页码。
3. 连续重置三次,并执行上一页、下一页切换。
4. hover 页签 2、页签 3 并切换后返回,确认分页区无残影。
- 验证结果: 2026-09-29 执行 `scripts/test.ps1 -Suite NonUI`,Debug x64、Debug x64 ASan、Release x64、Debug Win32 均为 15/15 通过;ASan 未报告内存错误;KEY1 至 KEY6 均编译、链接通过。用户于 2026-09-30 完成 KEY1 至 KEY6 手工回归,暂未发现明显问题。
- 回归检查: KEY1 表格重置与翻页;KEY5 Table 分页、页码和上层浮层。
- 验证证据:[可选]
## 落地信息
- Commit: 未提交
- PR:[可选]
- 发布版本:[可选]
- 备注: UI 像素结果已由用户按 `TESTING.md` 完成人工验收。