修复「信号列表滚动把组名/信号名画到筛选框上」+「边沿统计面板在不移动鼠标时显示冻结」
一、列表滚动越界绘制(SignalPanel + PaintUtil) 根因:UiText 走 GDI DrawTextW,而 GDI 文本不遵循 GDI+ 的 Graphics::SetClip。 列表虽然用 g.SetClip(m_listRect) 裁剪,但被滚出视口的「部分可见」行(分组行、信号名) 仍会把文字画到列表上方的筛选框区域(用户报告 MF4 多信号场景);UiFillRect 对大面积的 不透明矩形会改走 GDI FillRect,同样越界。 修法: * PaintUtil 新增 UiTextClipped(g, text, font, rect, argb, align, clipRect) ——在同一个 HDC 上 SaveDC + IntersectClipRect + RestoreDC;UiText 重构为转发 clip = nullptr,行为与旧实现完全一致。 * SignalPanel 新增 PaintRowText()(裁剪到 m_listRect)与 FillRowRect()(夹取到 m_listRect),分组行与信号行的全部文本、行底色/选中底色都改走它们。 二、两个边沿计数浮动面板「鼠标不动就不刷新」(PlotView::PanelSlotKey) 根因:浮动面板使用像素图层缓存(m_panelDc + m_panelSlots + PanelSlotKey),而界面上显示的 「上升沿 / 下降沿」是分帧推进出来的中间结果,PanelSlotKey 里没有这些数字 ⇒ 统计推进不会 让键变化 ⇒ 面板一直贴旧像素 ⇒ 显示停在「统计中…」或某个中间值,只有移动鼠标(改变面板 层序 / 悬停 / 鼠标坐标等其它键)才会重绘并显示早已算完的正确结果;扫描推进本身并没有停。 修法:PanelSlotKey 内按面板实际显示的条目回调 EdgeCountsFor(COUNT 面板)/ EdgeCenterCountsFor(CENTER 面板),把 (rising, falling, complete) 一并 mix 进键。 三、验证(离屏 .preflight/check:SW_HIDE 窗口、不注入真实鼠标键盘、进程/线程 BELOW_NORMAL) 39/39 通过(连跑两次稳定): * [A] 40 信号同一网格、滚轮下滚:列表上边界以上区域逐像素 0 变化,列表内部仍有变化 (证明是裁剪而不是整块不画); * [B] 3 信号 × 300 万采样、两种统计同时开启:面板像素图层缓存键在扫描期间持续变化, 且不注入任何鼠标消息即可 complete; * [C] 真实 TestData/MF4-Record.MF4(124 信号 / 3 条时间轴):同样 0 像素变化。 A/B:git stash 回退本轮修复后重跑 ⇒ [A] 1571 像素被污染、[B] 两个面板的缓存键均不推进, 如实失败。 make release 零警告(用户正在运行 Target.exe,链接用临时 -o 名字验证通过)。 四、规则同步 Documents/agent_mem.md 新增 §9.28、§11.2 第 53/54 条硬约束与变更日志。
This commit is contained in:
1 parent
6eedbe3c5d
commit
3bca3b1201
6 files changed
+304
-62
No files matched your search
@@ -1453,6 +1453,55 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
|
||||
|
||||
---
|
||||
|
||||
### 9.28 信号列表滚动越界绘制 + 边沿统计面板「显示冻结」(本轮,起源设备)
|
||||
|
||||
> 用户报告两项:① MF4 文件信号过多、列表需要滚动时,**组名/信号名被画到上方「筛选信号」框上**;
|
||||
> ② 改完某个信号的中心轴滤波参数并确认后,**全程不移动鼠标**时两个边沿计数浮动面板的统计数字
|
||||
> 一直不刷新,移动鼠标才显示正确的最终结果。两项都已定位根因并修复;
|
||||
> 离屏验证 `[A]/[B]/[C]` **39/39 通过**,并做了 A/B(回退修复后两项均如实失败)。
|
||||
|
||||
#### 一、列表滚动把文字画到筛选框上(`SignalPanel` + `PaintUtil`)
|
||||
|
||||
- **根因**:`UiText` 走 GDI `DrawTextW`,而 **GDI 文本不遵循 GDI+ 的 `Graphics::SetClip`**。
|
||||
列表虽然用 `g.SetClip(m_listRect)` 裁剪,但被滚出视口的「部分可见」行(尤其是分组行与信号名)
|
||||
仍会把文字画到列表上方的筛选框区域。`UiFillRect` 对大面积**不透明**矩形走 GDI `FillRect`,同理越界。
|
||||
- **修法**:`PaintUtil` 新增 **`UiTextClipped(g, text, font, rect, argb, align, clipRect)`**
|
||||
(在同一个 HDC 上 `SaveDC` + `IntersectClipRect` 并完整还原;`UiText` 重构为转发 `clip = nullptr`,
|
||||
行为与旧实现逐位一致);`SignalPanel` 新增 `PaintRowText()`(裁剪到 `m_listRect`)与
|
||||
`FillRowRect()`(夹取到 `m_listRect`),**分组行与信号行的全部文本、行底色/选中底色都改走它们**。
|
||||
- ⚠️ **新硬约束**:滚动/裁剪类容器内的 GDI 文本必须**显式**裁剪,不能依赖 `g.SetClip`。
|
||||
|
||||
#### 二、边沿统计面板在「鼠标不动」时显示冻结(`PlotView::PanelSlotKey`)
|
||||
|
||||
- **根因**:浮动面板有**像素图层缓存**(`m_panelDc` + `m_panelSlots` + `PanelSlotKey`)。
|
||||
界面上显示的「上升沿 / 下降沿」是**分帧推进**出来的中间结果,而 `PanelSlotKey` 里**没有**这些数字
|
||||
⇒ 「统计推进 → 数字变化」不会让键变化 ⇒ 面板一直贴旧像素 ⇒ 显示停在「统计中…」或某个中间值;
|
||||
**只有移动鼠标**(改变面板层序 / 悬停 / 鼠标坐标等其它键)才会重绘并显示出早已算完的正确结果。
|
||||
注意:**扫描推进本身没有停**(`EdgePanelWidth` 每帧都会被调,从而调 `EdgeScanFor` 并自持请求下一帧),
|
||||
停住的只有**显示**。
|
||||
- **修法**:`PanelSlotKey` 内按面板实际显示的条目回调 `EdgeCountsFor`(COUNT 面板)/
|
||||
`EdgeCenterCountsFor`(CENTER 面板),把 `(rising, falling, complete)` 一并 mix 进键
|
||||
⇒ 推进即让键变化 ⇒ 面板重绘 ⇒ 显示随进度更新;扫完后数字稳定,重新命中缓存(不再每帧重画)。
|
||||
|
||||
#### 三、验证(离屏 `.preflight/check`,`SW_HIDE` + 不注入真实输入 + 进程/线程 `BELOW_NORMAL`)
|
||||
|
||||
- **[A]** 40 信号同一网格 + 分组展开 ⇒ 列表可滚动;滚轮下滚后断言
|
||||
「列表上边界**以上**区域逐像素 **0 变化**」(修复前 **1571 像素被污染**),并断言
|
||||
「列表**内部**像素仍有变化」(证明是“裁剪”而不是“整块不画”)。
|
||||
- **[B]** 3 信号 × 300 万采样同一网格、同时开启两种边沿计数:断言面板**像素图层缓存键在扫描期间
|
||||
持续变化** + 面板像素确实刷新(修复前键**不变化** = 显示冻结),并在**不注入任何鼠标消息**的
|
||||
前提下最终 `complete`(统计值与参考实现一致)。
|
||||
⚠️ 构造要点:信号名要**足够长**(使面板宽度由固定文字决定)且穿越次数少,否则「数字位数变化」
|
||||
本身就会改变面板矩形 ⇒ 缓存键也跟着变 ⇒ 该用例在**修复前也会“通过”**(失去判别力)。
|
||||
⚠️ 校验最终值要**直接读累积状态**(`m_edgeScans`),不要用不同阈值去调 `EdgeCountsFor` —— 它会
|
||||
把状态**就地重置**,反而丢弃已扫完的结果。
|
||||
- **[C]** 真实 `TestData/MF4-Record.MF4`(**124 信号 / 3 条时间轴**):同样的滚动断言,0 像素变化
|
||||
(直接复现用户报告的文件场景)。
|
||||
- **A/B 验证**:把本轮修复 `git stash` 掉重跑 ⇒ `[A]`(1571 px)与 `[B]`(两个面板缓存键均不推进)
|
||||
**如实失败**;恢复后 **39/39 通过**(连跑两次稳定);`make release` 零警告。
|
||||
|
||||
---
|
||||
|
||||
## 10. 变更日志(本文件维护记录)
|
||||
|
||||
| 日期 | 设备/模型 | 摘要 |
|
||||
@@ -1493,6 +1542,7 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
|
||||
| 2026-10-14 | DeepSeek-V4.1-Flash(**起源设备**) | **边沿计数改用「绘制值」+ 派生信号烘入时间偏移 + 图例托底统一为 GX 徽标款 + 调整二级面板向上展开(详见 §9.21)**。① **边沿计数恒为 0**:`EdgeCountsFor`/`EdgeCenterCountsFor` 只比原始采样值,而阈值是屏幕上的值 → 「系数 50」的信号统计恒为 0;修法:判定值改为 `ApplyDrawTransform(style, values[SourceIndexFor(...)])`(中心轴两侧同套变换;水平镜像时曲线窗口也要先映射下标);缓存新增 **`styleKey`**(否则改完「调整」仍命中旧计数)。② **区间边界点被误计**:`EdgeIndexRange` 左端“多取一个采样点”的外扩**已删除**,收紧为“**两个端点都在区间内**才计入”(光标恰落在越过阈值的点 B 上时 B 不再被算作上升沿)。③ **`offsetTime` 也烘进派生采样值**(**推翻 §9.19 的“唯一例外”**):按 `time[i]-offsetTime` 取最近采样点重采样(单调时间轴用只前进的游标,O(n);越界置 NaN),新信号样式**全默认**;非单调时间轴退回到把 `offsetTime` 带在样式上。④ **图例托底 = GX 徽标同款**(同高 / 圆角 = 高度一半 / 同配色 / 水平内边距 = 胶囊半径),文本改用新增字体 **`uiTiny`(10 逻辑 px)**;`kLegendPlateRadius` 已删。⑤ **`PopupMenu::Open(..., expandUp=true)`**(锤点为**左下角**、向上展开,`m_buildExpandUp` 供 `RebuildLayout` 复用)+ 「系数/偏移」「镜像」面板锤在按钮左上角再上移 `kAdjustMenuGap`。验证:`.preflight/uitest` 新增 `[AR]`(五组断言全部与**独立参考实现**逐位比对,含 A/B:“按原始采样值算会得到 7/6 个边沿”);**并同步修正 harness 里两处旧规则断言**(`[AP]` 派生信号继承 → 默认样式 + 烘值;`[AN]` 图例带无托底 → 装饰矩形逐块且暴露列可见底层内容);合计 **722/722 通过**,`make release` 零警告。 |
|
||||
| 2026-10-15 | DeepSeek-V4.1-Flash(**另一台设备**) | **「调整」数值符号规则 + 边沿统计按帧推进(含首帧公平分摊)+ 浮动面板排版与弱峰复位控件(详见 §9.26)**。① **符号**:Y 轴偏移 0 不再显示 `+0.00`;系数非 0 正值带 `+`(编辑态初始文本仍无符号);`Δt:` 后按正负带符号(`Δt:+5s 60ms` / `Δt:-5s 60ms` / `Δt:0s`)。② **边沿统计卡住**根因:预算按 `GetTickCount64()` 分桶(系统粒度 15.6ms)而 `PostMessage` 自持重绘把帧率拉高 ⇒ 同一 tick 内多帧完全不推进 → 改为 **`Paint` 开头按帧重置**;并给**每个状态独立每帧配额**(`share = max(6, m_edgeScans.size())`,避免同帧新建状态被第一个独占整额)→ 实测首帧 5 个状态全部推进、**9 帧内全部扫完且不注入任何鼠标消息**、与参考实现逐位一致。③ **面板排版**:信号名宽度上限 `kEdgePanelNameMaxWidth=150`(300 字符名字只多 44px,旧实现多 5100px);统计信息**两列等宽**(`2×max(列宽)+间距`)⇒ 多信号“上升沿/下降沿”列起点固定、垂直对齐;未完成时**两个数字后都带 `…`**(旧实现只跟在“下降沿”后)。④ **弱峰调整**:每行滑块右侧新增**复位控件**(复用还原图标,悬停高亮 + `IDC_HAND` + 提示;点击恢复幅度 0 / 迭代 1 且上限回 10);上限输入框提示改为“输入上限(当前N)”;滑块标签去掉多余的 `%` / `N`。验证:新增离屏探针 `panelcheck` **46/46 通过**(全部走真实菜单/点击路径),既有 `verify` 47/47(另 2 条为本机未同步的旧规则断言),`make release` 零警告。 |
|
||||
| 2026-10-15 | DeepSeek-V4.1-Flash(**另一台设备**) | **数据层大重构(文件→分组 / 时间轴一等公民 / 加载器抽象)+ CSV 多时间轴 + MF4 支持(详见 §9.27 与 `Documents/refactor_report_2026-10-15.md`)**。① **解耦**:`DataFile`→**`SignalGroup`**(删除 `filePath`/`fileName`/单一 `time`/`timeColumnName`/`timeMonotonic`/`minTime`/`maxTime`),新增 **`TimeAxis`**(1..N 条/组)与 `SignalData::axisIndex`,`SignalData` 删除冗余的 `rawHeader` 与**全仓无人使用的 `unit`**;`SignalRef::fileIndex`→`groupIndex`;`DataSet`/`TabConfig` 全套改名并新增 `Axis(ref)`;采样/统计 API(`SampleSignalAt`/`FindNearestSample`/`VisibleIndexRange`/`EdgeIndexRange`/`EdgeCountsFor`/`EdgeCenterCountsFor`/`EdgeScanFor`/`MeanFilterValueAt`/`WeakPeakValueAt`/`SampleSpacingPixels`)**首参由 `const DataFile&` 改为 `const TimeAxis&`**;`ResolvedSignal` 携带 `{group, axis}`。② **加载器抽象**:新增 `DataTable.hpp`(`DataTable`/`LoadProgress`/`TableBlock`/`LoadedSource`)、`DataLoader.*`(唯一入口 `LoadSignalSource` 按扩展名分发)、`CsvLoader.*`、`Mf4Loader.*`,删除 `CsvTable.*`;唯一装配点 `BuildSignalGroup` 与格式无关(新增格式只写加载器)。③ **CSV 多时间轴**:每个 `time` 列开启新块、各块按自己的时间列裁剪 ⇒ 支持各轴不等长(实测 3 条轴 7/4/7),**不再把其它 `time` 列当成信号**;时间轴判据收紧。④ **MF4 支持**:加载后新建分组、组名=文件名,全文件信号归入该组;每个通道组(CG)一条时间轴并自动去重(实测 27 CG → 3 轴 / 124 信号);实现 MDF v4 块解析(DG/CG/CN/CC/DT)、位级整数与 32/64 位浮点解码、线性换算 `phys = cc_val[0] + cc_val[1]*raw`、无效位处理;**`##TX` 兼容规范的「长度+UTF-16」与 CANape 的「裸 NUL 结尾文本」**(否则通道名全空)。⑤ 顺带修复:`Application::Initialize` 在窗口类已注册时不再弹模态框(旧实现导致同进程二次构造永久阻塞),`Shutdown` 注销窗口类;UI 文案统一到「分组」,对话框支持 `*.mf4/*.mdf`。验证:新增 `datacheck` **69/69**、`edgecheck` **46/46**(替代被编码破坏的 `panelcheck`)、既有 `verify` **47/47**(另 2 条本机旧规则断言)、`make release` 零警告。 |
|
||||
| 2026-10-15 | DeepSeek-V4.1-Flash(**起源设备**) | **信号列表滚动越界绘制 + 边沿统计面板「显示冻结」(详见 §9.28)**。① **列表把组名/信号名画到筛选框上**(MF4 多信号场景):根因是 `UiText` 走 GDI `DrawTextW`,**GDI 文本不遵循 GDI+ 的 `SetClip`** —— 被滚出视口的「部分可见」行会把文字画到列表上方的筛选框上(`UiFillRect` 的大面积不透明矩形走 GDI `FillRect` 同理)。修法:`PaintUtil` 新增 **`UiTextClipped`**(同一 HDC `SaveDC` + `IntersectClipRect` + `RestoreDC`;`UiText` 转发 `clip=nullptr`,行为不变),`SignalPanel` 新增 `PaintRowText`(裁到 `m_listRect`)/ `FillRowRect`(夹到 `m_listRect`),分组行与信号行的全部文本与行底色均改走它们。② **边沿统计面板在不移动鼠标时显示冻结**:面板像素图层缓存键(`PanelSlotKey`)**不含**面板上显示的统计数字,而统计是**分帧推进**的中间结果 ⇒ 推进不会让键变化 ⇒ 面板一直贴旧像素 ⇒ 显示停在「统计中…」/中间值,**只有移动鼠标**(改变面板层序/悬停/鼠标坐标等其它键)才重绘并显示早已算完的正确结果(扫描推进本身没停)。修法:`PanelSlotKey` 内按面板实际显示的条目回调 `EdgeCountsFor`/`EdgeCenterCountsFor` 并把 `(rising, falling, complete)` mix 进键。验证:新增离屏 `.preflight/check`(`SW_HIDE` + 不注入真实输入 + `BELOW_NORMAL`)**39/39 通过**(连跑两次稳定):`[A]` 40 信号滚轮下滚后「列表上方区域逐像素 0 变化」且「列表内部仍有变化」(修复前 **1571 像素被污染**)、`[B]` 3 信号 × 300 万采样下两种统计面板的**缓存键持续变化**且**无鼠标输入即完成**(修复前键不变化)、`[C]` 真实 `MF4-Record.MF4`(**124 信号 / 3 条时间轴**)同样 0 像素变化;**并做了 A/B**(`git stash` 回退修复后 `[A]/[B]` 如实失败);`make release` 零警告。 |
|
||||
|
||||
---
|
||||
|
||||
@@ -1680,6 +1730,21 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
|
||||
⚠️ **`##TX` 文本块必须兼容两种写法**:规范的「U32 长度 + UTF-16/UTF-8」**与** CANape 的
|
||||
「无长度前缀的 NUL 结尾裸文本」;只按规范解析会得到空通道名。
|
||||
压缩块(`##DZ`/`##HL`/`##SD`)、`##DL` 数据列表、字符串/MIME 通道**未实现**(跳过该组)。
|
||||
53. ⚠️ **滚动 / 裁剪容器内的 GDI 文本必须显式裁剪,不能依赖 GDI+ 的 `SetClip`**:
|
||||
`UiText` 走 `DrawTextW`,**GDI 文本不受 `Graphics::SetClip` 约束** —— 信号列表滚动时,
|
||||
被滚出视口的「部分可见」行会把组名/信号名画到上方的筛选框上(用户报告 MF4 多信号场景)。
|
||||
✓ 用新增的 `PaintUtil::UiTextClipped(g, text, font, rect, argb, align, clipRect)`
|
||||
(同一个 HDC 上 `SaveDC` + `IntersectClipRect` + `RestoreDC`,其余行为与 `UiText` 完全一致);
|
||||
行内**纯色填充**还要把矩形**夹到**容器矩形(`UiFillRect` 对大面积不透明矩形走 GDI `FillRect`,同样越界)。
|
||||
54. ⚠️ **浮动面板像素图层的缓存键必须包含「面板上显示的动态内容」**:
|
||||
边沿统计(上升沿/下降沿)是**分帧推进**的中间结果;`PanelSlotKey` 不含这些数字时,
|
||||
「推进 → 数字变化」不会让键变化 ⇒ 面板一直贴旧像素 ⇒ 用户在**不移动鼠标**时看到统计「永不刷新」
|
||||
(移动鼠标改变其它键后才显示正确值;注意扫描推进本身并未停止)。✓ `PanelSlotKey` 内按面板
|
||||
**实际显示的条目**调用 `EdgeCountsFor` / `EdgeCenterCountsFor`,把 `(rising, falling, complete)`
|
||||
mix 进键。
|
||||
⚠️ 推论(写显示类测试时必须注意):**任何“内容随时间变化”的面板都要把该内容计入键**,
|
||||
否则既会显示陈旧像素,也会掩盖“自持帧推进”的真实行为;反之,**为使显示类用例真正有判别力**,
|
||||
测试数据要避开“其它变量恰好也在改变缓存键”的情形(如数字位数变化改变面板宽度)。
|
||||
|
||||
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)
|
||||
|
||||
|
||||
Reference in new issue
Block a user