回退 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

+53
View File
@@ -953,6 +953,27 @@ namespace jdi
/** @brief 网格滚动动画是否正在进行。 */
bool LaneScrollAnimationActive() const { return m_laneScrollAnimActive; }
/**
* @brief 把网格滚动**平滑地**回到最上方(供右下角浮动按钮调用)。
* @return 开始了动画(或已在上方)返回 true。
*/
bool ScrollLanesToTop();
/**
* @brief 右下角「回到最上方」浮动按钮是否可见(含淡入淡出过程中的可见性)。
* @return 可见返回 true。
*/
bool BackToTopVisible() const { return m_backToTopAlpha > 0.01f; }
/**
* @brief 推进一步「回到最上方」按钮的淡入/淡出过渡(由宿主定时器驱动)。
* @return 仍在过渡中返回 true(宿主应保持定时重绘)。
*/
bool AdvanceBackToTopAnimation();
/** @brief 「回到最上方」按钮是否正在淡入/淡出。 */
bool BackToTopAnimationActive() const { return m_backToTopAnimating; }
/**
* @brief 取消网格滚动动画(用户自己滚动网格时调用,避免动画与手动滚动抢滚动位置)。
*/
@@ -1994,6 +2015,25 @@ namespace jdi
*/
void PaintScrollBar(gd::Graphics& g);
/**
* @brief 计算右下角「回到最上方」浮动按钮的矩形(客户区坐标)。
* @return 按钮矩形;当前不该显示时返回空矩形(`m_backToTopAlpha` 为 0 时仍返回几何,
* 便于命中测试与过渡动画)。
*/
gd::RectF BackToTopRect() const;
/**
* @brief 绘制右下角「回到最上方」浮动按钮(含按 `m_backToTopAlpha` 的淡入淡出)。
* @param [IN,OUT] g 绘图上下文。
*/
void PaintBackToTop(gd::Graphics& g);
/**
* @brief 依据当前滚动位置刷新「回到最上方」按钮的目标可见性(需要时启动过渡)。
* @return 视觉状态发生变化返回 true。
*/
bool UpdateBackToTopTarget();
/**
* @brief 绘制网格标题(信号图例)。
* @param [IN,OUT] g 绘图上下文。
@@ -2048,6 +2088,19 @@ namespace jdi
std::uint64_t m_laneScrollAnimStartMs = 0; /**< 动画起始时刻(`GetTickCount64()`)。 */
std::uint32_t m_laneScrollAnimDurationMs = 0; /**< 动画总时长(毫秒)。 */
/**
* @brief 右下角「回到最上方」浮动按钮(只在网格需要滚动条、且已向下滚动时出现)。
*
* 用户 2026-10-16 要求:参考长文档网页的常见做法,用一个向上箭头的浮动按钮;
* 仅在向下滚动时出现、回到上方时消失,并且**出现/消失都要有过渡动画**(不能突变)。
*/
float m_backToTopAlpha = 0.0f; /**< 当前不透明度(0 = 完全隐藏)。 */
bool m_backToTopFadeIn = false; /**< 当前过渡方向(true = 正在淡入)。 */
bool m_backToTopAnimating = false; /**< 是否正在淡入/淡出。 */
bool m_backToTopHover = false; /**< 鼠标是否悬停在按钮上。 */
bool m_backToTopPressed = false; /**< 是否正在按钮上按住左键。 */
std::uint64_t m_backToTopAnimStartMs = 0; /**< 本段过渡的起始时刻。 */
PlotDragMode m_dragMode = PlotDragMode::NONE; /**< 当前拖拽模式。 */
PlotRubberAxis m_rubberAxis = PlotRubberAxis::UNDECIDED; /**< 选区放大拖拽的轴向。 */
int m_dragLane = -1; /**< 拖拽所作用的网格下标。 */