回退 5b52eef 并修复「屏幕不刷新」根因:拖动滚动条卡顿 / 悬停高亮卡住 / 信号拖拽无效果

根因(代码级确认)
- Render() 把界面画进后备缓冲区后用 GetDC + BitBlt 回送窗口,5b52eef 把回送写成
  `if (fullFrame) { BitBlt(整窗) } else if (panelOnly) { BitBlt(本帧重绘区域并集) }`,没有兜底 else。
- 「鼠标只在绘图区内移动」的帧(WM_MOUSEMOVE 的 plotOnlyMove 路径只 InvalidatePlotRect)恰好
  fullFrame == false && panelOnly == false ⇒ 后备缓冲区被正确重绘,窗口 DC 一个像素都不回送。
- 这一个根因同时造成用户报告的三个症状:悬停光晕卡住(移开不消失)、拖动滚动条不跟手/非常卡顿、
  拖拽信号换网格看不到任何结果。

修法
- git reset --hard HEAD~1 撤销 5b52eef(保留 backup-bad-commit 标签供 A/B)。
- 只挑回该提交里与更新区域无关的功能改动(用户明确要求过、本身正确):
  * SignalPanel::ExpandGroupForSignal():联动滚动前立刻展开折起的分组(不播展开动画);
  * FitAll() / FitSinglePointCluster() 不再复位 Tab().LaneScroll()(适应视图不影响滚动条);
  * 右下角「回到最上方」浮动按钮 + 网格滚动到可视区的平滑动画。
- Application.cpp 只保留 3 处最小改动:Render 里把 panelAnimating 计入整帧脏、WM_TIMER 推进
  AdvanceBackToTopAnimation() 且保持整窗失效、定时器存活判定带 BackToTopAnimationActive()。
  Render 的绘制与 BitBlt 路径逐字节等同于 82b5ebe。
- 彻底删除 m_paintPlot / m_panelDirty / panelOnly / InvalidatePanelRect / 并集回送。

验证(全部离屏执行,不移动真实光标、不抢焦点、不截屏)
- .preflight/diag(新增离屏探针,19/19):窗口移到屏幕外 + WS_EX_TOOLWINDOW + SW_SHOWNOACTIVATE,
  保证有真实更新区域与 WM_PAINT 但不影响用户。断言滚动条拖动(滑块与绘图区都在后备缓冲区被重绘)、
  悬停光晕出现与消退(像素完全回到基线)、在曲线上按下进入 MOVE_SIGNAL 且拖到另一网格后归属正确。
- .preflight/check 137/137。
- .preflight/diag/verify_blit.ps1(新增源码级护栏,10/10):断言 Render() 的回送块必须有无条件 else、
  不得出现 else if、m_paintPlot/m_panelDirty/panelOnly 必须不存在、失效只允许整窗或仅绘图区。
  A/B:换回 backup-bad-commit ⇒ 4 项如实失败;换回修复版 ⇒ 10/10。
- make release 零错误零警告。

文档:Documents/agent_mem.md 新增 §9.41、§11.2 第 108–116 条(含 §9.40 与旧 108–114 条的作废标注)
与变更日志一行。

教训(写进记忆):① 离屏探针无法验证「窗口回送 / 合成」(屏幕外 DWM 不合成、窗口 DC 恒为空白)⇒
回送类约束必须有源码级护栏;② 离屏窗口 + TrackMouseEvent 会立刻收到 WM_MOUSELEAVE,探针须丢弃该消息;
③ 网格回收会让下标左移,断言要用锚信号。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-16 17:33:08 +08:00
1 parent 82b5ebe4f9
commit f845616938
10 files changed
+460 -8

No files matched your search

+133
View File
@@ -2447,6 +2447,90 @@ Components/Shared/Src/UiControls.cpp
---
### 9.41 回退 `5b52eef` 并修复「屏幕不刷新」根因(拖动滚动条卡顿 / 悬停高亮卡住 / 信号拖拽无效果)(本轮,本机)
> ⚠️⚠️ **本轮撤销提交 `5b52eef`(§9.40 随之作废,作废范围见下与 §11.2 的编号注记)。**
> 用户报告:以加载 `TestData/MF4-Record.MF4` 并显示全部信号为例 —— ① 拖动网格右侧滑动条非常卡顿、
> 几乎拖不动;② 悬停到某条信号触发高亮后,鼠标移开(不落到另一条曲线上)高亮**仍然卡住**;
> ③ 几乎无法正常拖拽信号到其它网格;④ 预期的流畅动画帧没有实现。用户要求「撤销上次的提交并着手定位并修复」。
#### 一、根因:`Render()` 的回送被写成「两个条件都不满足 ⇒ 一帧都不回送」(代码级确认)
`Application::Render()` 把整个界面画进后备缓冲区 `m_backDc`,最后 `GetDC` + `BitBlt` 回送窗口。
`5b52eef` 把它改成了:
```cpp
if (fullFrame) { BitBlt(windowDc, 0, 0, width, height, m_backDc, 0, 0, SRCCOPY); }
else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
// ← 再也没有兜底 else
```
而 `fullFrame = chromeDirty || overlaysVisible || m_plot.HintNeedsAnimation()`,
`panelOnly = !chromeDirty && (m_panelDirty || panelAnimating)`。
**「鼠标在绘图区内移动」这一类帧恰好满足 `fullFrame == false && panelOnly == false`**
(`WM_MOUSEMOVE` 的 `plotOnlyMove` 路径只 `InvalidatePlotRect`,既不动 `m_chromeDirty` 也不动 `m_panelDirty`)
⇒ **后备缓冲区被正确重绘,窗口 DC 一个像素都没更新。**
这**一个根因**同时解释了用户报告的三条症状:
* 悬停光晕已画进后备缓冲区却不回送 ⇒ 屏幕停在旧像素 ⇒ **“高亮卡住、移开不消失”**;
* 拖动滚动条时滑块与波形都不回送 ⇒ 只有偶尔整窗失效(定时器 / 装饰脏)那一帧才刷新
⇒ **“非常卡顿 / 拖不动”**;
* 拖拽信号换网格后不回送 ⇒ 用户看不到任何结果 ⇒ **“几乎无法拖拽”**。
(§9.40 想省下的“面板动画帧重画绘图区的 ~5ms”,代价就是这条正确性,得不偿失。)
#### 二、修法:整体撤销该机制,回到「整窗失效 + 无条件兜底回送」
- `git reset --hard HEAD~1` 撤销 `5b52eef`(先打标签 `backup-bad-commit` 供 A/B)。
- **只挑回该提交里“与更新区域无关”的**功能改动(它们本身是对的,用户也明确要求过):
- `SignalPanel::ExpandGroupForSignal()`(联动滚动前**立刻展开**折起的分组,不播展开动画);
- `FitAll()` / `FitSinglePointCluster()` **不再复位** `Tab().LaneScroll()`(“适应视图不影响滚动条”);
- 右下角「回到最上方」浮动按钮(`BackToTopRect` / 170ms 淡入淡出 / `ScrollLanesToTop` / `PaintBackToTop`)
与「网格滚动到可视区」的平滑动画(`ScrollLaneIntoView` 只启动、`AdvanceLaneScrollAnimation` 推进、
手动滚动时 `CancelLaneScrollAnimation()`)。
- 宿主 `Application.cpp` 只保留 **3 处最小改动**:`Render` 里把 `panelAnimating` 计入整帧脏;
`WM_TIMER` 推进 `AdvanceBackToTopAnimation()` 并**保持整窗失效**;定时器存活判定带上
`BackToTopAnimationActive()`。**`Render` 的绘制与 BitBlt 路径逐字节等同于 `82b5ebe`。**
- **彻底删除** `m_paintPlot` / `m_panelDirty` / `panelOnly` / `InvalidatePanelRect` / 并集回送。
#### 三、验证(全部离屏,不占用用户的鼠标 / 键盘 / 屏幕)
- **`.preflight/diag`(本轮新增,19/19 通过)**:把窗口 `MoveWindow` 到屏幕外(`-30000,-30000`)
+ `WS_EX_TOOLWINDOW` + `SW_SHOWNOACTIVATE` ⇒ **窗口真实可见、有真实更新区域与 `WM_PAINT`,
但完全不出现在用户屏幕上、不抢焦点、不进任务栏**。四项断言:
**[A]** 后备缓冲区可读;**[B]** 拖动网格滚动条 ⇒ 滚动量变化 + **滑块与绘图区都在后备缓冲区里被重绘**;
**[C]** 悬停曲线 ⇒ 进入悬停态且绘图区像素变化(光晕可见),移开 ⇒ 悬停态清除且**像素完全回到基线**;
**[D]** 在曲线上按下进入 `MOVE_SIGNAL`(**不是** `LANE_REORDER`)、拖到另一网格松手后该信号归属正确。
- **`.preflight/check` 137/137**(原 [J]「面板动画帧跳过绘图区」改写为「面板动画帧**必须**重绘、
且不把绘图区画坏」,并新增“只改绘图区的帧仍必须在后备缓冲区里更新”)。
- **`.preflight/diag/verify_blit.ps1`(本轮新增,源码级护栏,10/10 通过)**:断言 `Render()` 的回送块里
**必须有无条件 `else`**、**不得出现 `else if`**、`m_paintPlot`/`m_panelDirty`/`panelOnly` **必须不存在**、
失效方式只允许“整窗”或“仅绘图区”。
- **A/B**:把 `Application.cpp` 换回 `backup-bad-commit` ⇒ 护栏 **4 项如实失败**;换回修复版 ⇒ 10/10。
- `make release` **零错误零警告**。
#### 四、⚠️ 方法论教训(已写进 §11.2 第 108–116 条)
1. **离屏探针读不到“窗口回送”**:窗口在屏幕外时 DWM 不会合成,`GetDC` 读回的窗口 DC 恒为空白
⇒ “画进了后备缓冲区但没回送”的缺陷在离屏探针里**永远假绿**(本轮就是这样假绿的)。
凡是**回送 / 合成**相关的约束,必须有**源码级护栏**兜底。
2. **离屏窗口 + `TrackMouseEvent` 的陷阱**:产品在 `WM_MOUSEMOVE` 里注册 `TME_LEAVE`,
而离屏窗口上真实鼠标并不在它上面 ⇒ 系统**立刻**兑现一条 `WM_MOUSELEAVE`,刚设置的悬停态
马上被清掉、探针永远观察不到悬停效果。探针的消息泵必须**丢弃 `WM_MOUSELEAVE`**(仅限探针,不改产品)。
3. **对“网格会被回收 / 下标会左移”的断言必须换成锚信号**:`MoveSignalToLane` 后源网格若被移空会被回收,
其后所有网格下标**左移**(本轮实测 124 → 123)⇒ 「移动后下标 == 目标下标」会假失败。
4. **失败时先怀疑探针自己的取点与判据**:本轮先后误判“悬停不生效”“信号没换网格”,
实际都是探针取点踩到了「GX 重排徽标 / 图例 / 时间光标线」这些**优先级更高的热区**,
或用了错误的比较基准(拿滚动后的 rect 当比较区域)。
5. `Copy-Item` **保留时间戳** ⇒ 用备份覆盖源文件后必须 `(Get-Item f).LastWriteTime = Get-Date`
再 `make`,否则测的还是旧目标文件(本轮 A/B 又踩了一次,与 §9.40 同)。
6. 不要对**源码文件**做 PowerShell 文本往返(`Get-Content`/`Set-Content`):
PS 5.1 按 ANSI/GBK 解码 UTF-8,会把 C++ 源里的中文注释**整份写坏**并加上 BOM。
只能改用文件编辑工具改源文件(本轮就这样毁过一次 `.preflight/diag/main.cpp`)。
---
## 10. 变更日志(本文件维护记录)
| 日期 | 设备/模型 | 摘要 |
@@ -2503,6 +2587,8 @@ Components/Shared/Src/UiControls.cpp
---
| 2026-10-16 | DeepSeek-V4.1-Flash(**本机**) | **回退 5b52eef 并修复“屏幕不刷新”根因(拖动滚动条卡顿 / 悬停高亮卡住 / 信号拖拽无效果,详见 §9.41;§9.40 随之作废)**。根因(代码级):`Render()` 的回送被写成 `if (fullFrame) … else if (panelOnly) …` 而**没有兜底 else**,而“鼠标只在绘图区内移动”的帧恰好两个条件都不满足 ⇒ 后备缓冲区画对了、窗口 DC 一个像素都不回送(悬停光晕 / 滑块 / 拖拽结果全部停在旧像素)。修法:`git reset --hard` 撤销该提交(留 `backup-bad-commit` 标签),只挑回与更新区域无关的功能改动(联动前展开折起组 / 适应视图不动滚动条 / 「回到最上方」按钮 / 网格滚动平滑动画),彻底删除 `m_paintPlot`/`m_panelDirty`/`panelOnly`/并集回送;宿主只保留 3 处最小改动且 `Render` 的绘制与 BitBlt 与 `82b5ebe` 逐字节相同。验证:`.preflight/diag` **19/19**(新增离屏可见窗口探针:滚动条拖动 / 悬停光晕出现与消退 / 信号拖拽换网格)、`.preflight/check` **137/137**、源码护栏 `.preflight/diag/verify_blit.ps1` **10/10**(A/B:换回坏版本 ⇒ 4 项如实失败)、`make release` 零警告。教训:① 离屏探针**测不到窗口回送**(屏幕外 DWM 不合成、窗口 DC 恒空白)⇒ 回送类约束必须有源码护栏;② 离屏窗口 + `TrackMouseEvent` 会**立刻**收到 `WM_MOUSELEAVE`,探针须丢弃该消息;③ 网格回收会让下标左移,断言要用锚信号。 |
## 11. 跨设备协作须知(起源设备视角)
> **本文件是唯一权威记忆**。所有设备 / Agent 共用它,**不要再新建 `agent_mem_origin.md` 等旁支记忆文件**。
@@ -3020,6 +3106,53 @@ Components/Shared/Src/UiControls.cpp
对“瞬移”同样成立、毫无判别力。要断言:触发后立即仍几乎未动、中间帧单调递进、
收尾精确到位(本轮 A/B 就是靠这三条排除了“直接赋值”的实现)。
> ⚠️⚠️ **编号注记(2026-10-16)**:提交 `5b52eef`(已于 **§9.41** 整体回退)曾以 **108–114** 号
> 写入下列规则,**它们全部作废**:108「`panelOnly = !chromeDirty && (m_panelDirty || panelAnimating)`」、
> 109「动画帧必须**精确失效**、禁止整窗失效(`InvalidatePanelRect`/`InvalidatePlotRect`)」、
> 110「`WM_PAINT` 辨认“更新区域完全在面板内”(`m_panelDirty`)+ `m_paintPlot` 跳过整个 `PlotView::Paint`
> + 回送范围改为“本帧重绘区域并集”」、111「联动滚动前先展开折起分组」、112「适应视图不得改变 `LaneScroll`」、
> 113「右下角回到最上方浮动按钮」、114「`Copy-Item` 保留时间戳」。
> **其中 111 / 112 / 113 / 114 的内容本身是对的,已并入下面的 111–113 条继续生效**;
> **108 的一半(“动画由动画状态本身驱动重绘”)正确、实现改为“计入整帧脏 + 整窗失效”;
> 109 / 110 属于被证伪的方案 —— 禁止再用。**
108. ⚠️⚠️ **`Render()` 的窗口回送必须存在“无条件兜底分支”**:
`if (fullFrame) { BitBlt(整窗) } else { BitBlt(绘图区) }` —— **绝不能**写成
`if (fullFrame) … else if (某条件) …`。否则存在“两个条件都不满足”的帧组合
(典型:`WM_MOUSEMOVE` 的 `plotOnlyMove` 路径只失效绘图区,既不动 `m_chromeDirty` 也不动面板脏),
这些帧会**一个像素都不回送** ⇒ 后备缓冲区正确、屏幕却停在旧画面
(用户表现:悬停高亮卡住、滚动条滑块跟不动、拖拽信号看不到结果)。
**回送区域永远“宁多勿少”**;省回送带宽的收益远小于一次屏幕不刷新的代价。
109. ⚠️⚠️ **禁止“按窗口更新区域决定本帧画什么 / 回送什么”的优化**(`m_paintPlot` / `m_panelDirty` /
`panelOnly` / 并集回送这一整套都已删除)。它把正确性绑在 `GetUpdateRect` 与各失效点“恰好配套”上,
一旦出现新的失效路径(如本轮“只失效绘图区”的鼠标移动)就会静默出错;
而且**它无法被离屏探针验证**(见第 113 条)。
110. ⚠️ **面板动画必须由“动画状态本身”驱动重绘**(正确洞见保留):`WM_TIMER` 不置 `m_chromeDirty`,
所以 `Render` 要把 `m_panel.ScrollAnimationActive() || m_panel.GroupAnimationActive()` 计入整帧脏,
否则某些帧根本不重画面板(用户:“过渡动画有时直接没了”)。
**实现方式改为整窗失效**(不是“只失效面板矩形”)。
111. ⚠️ **联动滚动前必须先展开折起的分组**:折起的分组不列出信号行 ⇒ 行集合里找不到目标,
`SignalScrollTarget` 直接返回、列表不动。
✓ `SignalPanel::ExpandGroupForSignal()`(**立刻生效**、不播展开动画,因为滚动目标需要稳定行位置),
由 `ScrollToSignalAnimated()` 在算滚动目标前调用。
112. ⚠️ **「适应视图」不得改变网格纵向滚动**:`FitAll()` 与 `FitSinglePointCluster()` 里那两次
`Tab().LaneScroll() = 0` 已删除(用户明确要求“适应视图不要影响滚动条”),只取消滚动动画。
右下角「回到最上方」浮动按钮(仅 `m_scrollBarVisible` 且已下滚 `> kBackToTopShowOffset`(24)
时出现;170ms ease-out cubic 淡入淡出;按下只登记 `m_backToTopPressed`、**不进入拖拽**;
抬起仍在同一按钮上才 `ScrollLanesToTop()`)与「网格滚动到可视区」的平滑动画继续生效。
113. ⚠️⚠️ **离屏探针无法验证“窗口回送 / 合成”**:窗口在屏幕外时 DWM 不合成,`GetDC` 读回的窗口 DC
恒为空白 ⇒ “画进了后备缓冲区但没回送窗口”的缺陷在离屏探针里**永远假绿**(本轮就是这样)。
**回送 / 合成相关的约束必须有源码级护栏**(本机:`.preflight/diag/verify_blit.ps1`,断言回送块
存在无条件 `else`、无 `else if`、且 `m_paintPlot`/`m_panelDirty`/`panelOnly` 不存在)。
114. ⚠️ **离屏探针的消息泵必须丢弃 `WM_MOUSELEAVE`**:产品在 `WM_MOUSEMOVE` 里注册
`TrackMouseEvent(TME_LEAVE)`,而离屏窗口上真实鼠标不在它上面 ⇒ 系统**立刻**兑现一条
`WM_MOUSELEAVE`,刚设置的悬停态马上被清掉(表现为“探针永远测不到悬停”)。仅影响探针。
115. ⚠️ **涉及“网格会被回收 / 下标会左移”的断言必须用锚信号**:`MoveSignalToLane` 把源网格移空后
该网格被回收,其后所有网格下标**左移**(实测 124 → 123)⇒ 「移动后下标 == 目标下标」会假失败;
改为「移动后与**目标网格里的某个锚信号**同网格」。
116. ⚠️ **对“看起来坏了”的现象,先怀疑探针自己的取点与判据**:本轮先后误判“悬停不生效”
“信号没换网格”,实际都是探针取点踩到了「GX 重排徽标 / 图例 / 时间光标线」这些
**优先级更高的热区**,或用了错误的比较基准(拿滚动后的 rect 当比较区域)。
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)
- **不要占用用户的鼠标 / 键盘 / 屏幕**:优先 `.preflight/uitest`(离屏 harness:`SW_HIDE` 窗口 + 直接注入消息 + `BitBlt` 从 `m_backDc` 截图)或 `.preflight/perf`(离屏计时 / 逐像素对比)。