回退 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:
1 parent
82b5ebe4f9
commit
f845616938
10 files changed
+460
-8
No files matched your search
@@ -399,8 +399,17 @@ namespace jdi
|
||||
// 工具栏/标签栏/面板各自带悬停态,内容只在鼠标位于其中或发生交互时改变。
|
||||
// 绘图区内的拖拽(如双光标左右移动)期间这些区域完全静止,跳过其绘制,
|
||||
// 每帧省下约 6-8ms;弹出菜单 / 拾色器 / 加载遮罩会覆盖到装饰区域,必须整帧重绘。
|
||||
//
|
||||
// ⚠️⚠️ **面板动画(滚动到目标信号 / 分组折起、展开)必须由动画状态本身驱动重绘**:
|
||||
// 这两个动画每帧都在改变面板像素,而它们只由 `WM_TIMER` 驱动 —— 而 `WM_TIMER`
|
||||
// **不会**置 `m_chromeDirty`(它刻意在排除列表里),于是旧实现完全依赖
|
||||
// `WM_PAINT` 里“更新区域不在绘图区内 ⇒ 置脏”这个启发式:一旦某帧的更新区域恰好
|
||||
// 不含面板(例如与鼠标消息或 `kMessagePlotDerivedReady` 的失效区域交错),
|
||||
// 那一帧就**完全不重画面板** ⇒ 用户报告“过渡动画有时直接没了”。
|
||||
// ✓ 把动画状态直接纳入重绘条件(**只增加重绘条件,不改变任何绘制/回送路径**)。
|
||||
const bool panelAnimating = m_panel.ScrollAnimationActive() || m_panel.GroupAnimationActive();
|
||||
const bool overlaysVisible = m_menu.IsOpen() || m_colorPicker.IsOpen();
|
||||
const bool chromeDirty = m_chromeDirty || m_loading || overlaysVisible;
|
||||
const bool chromeDirty = m_chromeDirty || m_loading || overlaysVisible || panelAnimating;
|
||||
m_chromeDirty = false;
|
||||
// 提示浮层淡出由定时器驱动,其绘制范围局限在绘图区,但为稳妥起见整帧回送。
|
||||
const bool fullFrame = chromeDirty || m_plot.HintNeedsAnimation();
|
||||
@@ -472,10 +481,10 @@ namespace jdi
|
||||
m_hintTimerActive = false;
|
||||
}
|
||||
|
||||
// 信号列表与绘图区的动画(目标信号滚动 / 分组折起展开 / 网格滚动到可视区):
|
||||
// 仅在动画进行中保持定时重绘。
|
||||
// 信号列表与绘图区的动画(目标信号滚动 / 分组折起展开 / 网格滚动到可视区 /
|
||||
//「回到最上方」按钮的淡入淡出):仅在动画进行中保持定时重绘。
|
||||
if (m_panel.ScrollAnimationActive() || m_panel.GroupAnimationActive() ||
|
||||
m_plot.LaneScrollAnimationActive())
|
||||
m_plot.LaneScrollAnimationActive() || m_plot.BackToTopAnimationActive())
|
||||
{
|
||||
if (!m_scrollTimerActive)
|
||||
{
|
||||
@@ -1403,13 +1412,18 @@ namespace jdi
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
return 0;
|
||||
}
|
||||
// 信号列表与绘图区动画(「滚动到目标信号」/「分组折起、展开」/「网格滚动到可视区」):
|
||||
// 推进一步并重绘(Render 会在动画结束后停表)。
|
||||
// 信号列表与绘图区动画(「滚动到目标信号」/「分组折起、展开」/「网格滚动到可视区」/
|
||||
//「回到最上方」按钮的淡入淡出):推进一步并整窗重绘(Render 会在动画结束后停表)。
|
||||
// ⚠️ 这里**保持整窗失效**(不要改成“只失效动画所在的区域”):面板与绘图区的
|
||||
// 回送范围由 `Render` 按 `fullFrame` 决定,任何“精确失效”都可能让屏幕上留下
|
||||
// 未刷新的区域(历史上就因此让滚动条/时间轴不跟手);而整窗失效下动画帧
|
||||
// 也只有 ~6-8ms(124 网格),完全在 16ms 预算内。
|
||||
if (wParam == kScrollTimerId)
|
||||
{
|
||||
m_panel.AdvanceScrollAnimation();
|
||||
m_panel.AdvanceGroupAnimations();
|
||||
m_plot.AdvanceLaneScrollAnimation();
|
||||
m_plot.AdvanceBackToTopAnimation();
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -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; /**< 拖拽所作用的网格下标。 */
|
||||
|
||||
@@ -273,6 +273,99 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::ScrollLanesToTop()
|
||||
{
|
||||
// 已经在最上方:什么都不做(按钮此时也不该可见)。
|
||||
if (Tab().LaneScroll() <= 0.5f)
|
||||
{
|
||||
Tab().LaneScroll() = 0.0f;
|
||||
m_laneScrollAnimActive = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 复用与「点选信号滚入可视区」完全相同的平滑过渡(ease-out cubic)。
|
||||
const float distance = Tab().LaneScroll();
|
||||
const float durationMs = std::max(
|
||||
kLaneScrollAnimMinMs, std::min(kLaneScrollAnimMaxMs, distance * kLaneScrollAnimMsPerPixel));
|
||||
m_laneScrollAnimFrom = Tab().LaneScroll();
|
||||
m_laneScrollAnimTo = 0.0f;
|
||||
m_laneScrollAnimStartMs = GetTickCount64();
|
||||
m_laneScrollAnimDurationMs = static_cast<std::uint32_t>(durationMs + 0.5f);
|
||||
m_laneScrollAnimActive = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::UpdateBackToTopTarget()
|
||||
{
|
||||
// 目标可见性 = 网格需要滚动条(说明网格多到需要滚动)且已经向下滚动了一段距离。
|
||||
const bool want = m_scrollBarVisible &&
|
||||
(Tab().LaneScroll() > kBackToTopShowOffset * m_scale);
|
||||
if (want == m_backToTopFadeIn && !m_backToTopAnimating)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (want == m_backToTopFadeIn)
|
||||
{
|
||||
return false; // 方向未变且正在过渡:让它继续
|
||||
}
|
||||
m_backToTopFadeIn = want;
|
||||
m_backToTopAnimating = true;
|
||||
m_backToTopAnimStartMs = GetTickCount64();
|
||||
if (!want)
|
||||
{
|
||||
// 淡出开始时取消悬停/按下态(按钮即将消失)。
|
||||
m_backToTopHover = false;
|
||||
m_backToTopPressed = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::AdvanceBackToTopAnimation()
|
||||
{
|
||||
if (!m_backToTopAnimating)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::uint64_t now = GetTickCount64();
|
||||
const std::uint64_t elapsed =
|
||||
(now >= m_backToTopAnimStartMs) ? (now - m_backToTopAnimStartMs) : 0;
|
||||
float progress = (kBackToTopFadeMs > 0.0f)
|
||||
? static_cast<float>(elapsed) / kBackToTopFadeMs
|
||||
: 1.0f;
|
||||
if (progress > 1.0f)
|
||||
{
|
||||
progress = 1.0f;
|
||||
}
|
||||
// ease-out cubic:与其它过渡一致的手感(起步快、末尾慢)。
|
||||
const float eased = 1.0f - std::pow(1.0f - progress, 3.0f);
|
||||
const float from = m_backToTopFadeIn ? 0.0f : 1.0f;
|
||||
const float to = m_backToTopFadeIn ? 1.0f : 0.0f;
|
||||
m_backToTopAlpha = from + (to - from) * eased;
|
||||
|
||||
if (progress >= 1.0f)
|
||||
{
|
||||
m_backToTopAlpha = to;
|
||||
m_backToTopAnimating = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
gd::RectF PlotView::BackToTopRect() const
|
||||
{
|
||||
if (!m_scrollBarVisible)
|
||||
{
|
||||
return gd::RectF();
|
||||
}
|
||||
const float size = kBackToTopSize * m_scale;
|
||||
const float margin = kBackToTopMargin * m_scale;
|
||||
// ⚠️ 贴着**绘图区**右下角(`m_plotRect` 已经让出了右侧滚动条的位置,
|
||||
// 因此按钮不会压到滚动条上)。
|
||||
return gd::RectF(m_plotRect.X + m_plotRect.Width - margin - size,
|
||||
m_plotRect.Y + m_plotRect.Height - margin - size, size, size);
|
||||
}
|
||||
|
||||
bool PlotView::LaneSpansMultipleGroups(int laneIndex) const
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
|
||||
@@ -163,7 +163,10 @@ namespace jdi
|
||||
{
|
||||
Tab().ViewInitialized() = true;
|
||||
}
|
||||
Tab().LaneScroll() = 0.0f;
|
||||
// ⚠️ **不再复位网格纵向滚动(`Tab().LaneScroll()`)**:用户 2026-10-16 明确要求
|
||||
// “适应视图不要影响滚动条” —— 加载了大量网格并向下翻动后点「适应视图」,
|
||||
// 旧实现会把视界瞬移回最上方(用户报告)。适应视图只负责时间轴/幅值轴。
|
||||
m_laneScrollAnimActive = false;
|
||||
// 只放下进行中的选区与拖拽,**不改变「选区放大」等缩放模式的启用状态**
|
||||
// (用户明确要求:点适应视图后仍保持已启用的缩放模式)。
|
||||
m_rubberRect = gd::RectF();
|
||||
@@ -359,7 +362,8 @@ namespace jdi
|
||||
static_cast<double>(kTimeTickTarget) * kSinglePointClusterSecondsPerCell;
|
||||
SetTimeView(sharedTime - span * 0.5, sharedTime + span * 0.5);
|
||||
Tab().ViewInitialized() = true;
|
||||
Tab().LaneScroll() = 0.0f;
|
||||
// ⚠️ 同 `FitAll()`:**不复位网格纵向滚动**(用户 2026-10-16 要求)。
|
||||
m_laneScrollAnimActive = false;
|
||||
// ⚠️ **不要**在这里复位 `ZoomMode()`:用户明确要求"点适应视图后仍保持已启用的
|
||||
// 缩放模式"(`FitAll` 的常规路径同样只更新提示、不动缩放模式)。
|
||||
m_rubberRect = gd::RectF();
|
||||
|
||||
@@ -847,6 +847,18 @@ namespace jdi
|
||||
return LoadCursorW(nullptr, IDC_SIZENS);
|
||||
}
|
||||
|
||||
// 右下角「回到最上方」浮动按钮:与其它可点击控件一致,用手型光标作为提示。
|
||||
// ⚠️ 必须在信号悬停 / 图例 / 面板判定**之前**(它绘制在最上层)。
|
||||
if (m_backToTopAlpha > 0.01f)
|
||||
{
|
||||
const gd::RectF backToTop = BackToTopRect();
|
||||
if (backToTop.Width > 0.0f &&
|
||||
UiPointInRect(backToTop, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
}
|
||||
}
|
||||
|
||||
// 实验性功能面板与横向光标优先于信号悬停提示判定(面板会盖住波形)。
|
||||
// 面板**只有标题栏**可以拖拽(显示抓握光标);✕ 与面板主体一律保持原始箭头。
|
||||
if (m_tab != nullptr)
|
||||
@@ -982,6 +994,19 @@ namespace jdi
|
||||
|
||||
const float fx = static_cast<float>(x);
|
||||
|
||||
// 右下角「回到最上方」按钮的悬停高亮(仅未拖拽时;按住其它东西时不该抢)。
|
||||
if (m_dragMode == PlotDragMode::NONE)
|
||||
{
|
||||
const gd::RectF backToTop = BackToTopRect();
|
||||
const bool hot = m_backToTopAlpha > 0.01f && backToTop.Width > 0.0f &&
|
||||
UiPointInRect(backToTop, fx, static_cast<float>(y));
|
||||
if (hot != m_backToTopHover)
|
||||
{
|
||||
m_backToTopHover = hot;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
switch (m_dragMode)
|
||||
{
|
||||
case PlotDragMode::PAN:
|
||||
@@ -1479,6 +1504,18 @@ namespace jdi
|
||||
m_pendingPanelClose = false;
|
||||
m_pendingPanelCloseLane = -1;
|
||||
|
||||
// 右下角「回到最上方」浮动按钮:按下只登记,**抬起**时仍命中才真正生效
|
||||
//(与其它 ✕ / 按钮的「可取消」交互一致),且**不**进入任何拖拽模式。
|
||||
if (m_backToTopAlpha > 0.01f)
|
||||
{
|
||||
const gd::RectF backToTop = BackToTopRect();
|
||||
if (backToTop.Width > 0.0f && UiPointInRect(backToTop, fx, fy))
|
||||
{
|
||||
m_backToTopPressed = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_scrollBarVisible &&
|
||||
UiPointInRect(m_scrollBarRect, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
@@ -1968,6 +2005,19 @@ namespace jdi
|
||||
{
|
||||
bool changed = (m_dragMode != PlotDragMode::NONE);
|
||||
|
||||
// 右下角「回到最上方」按钮:按下后仍在按钮上抬起 ⇒ 平滑滚回最上方。
|
||||
if (m_backToTopPressed)
|
||||
{
|
||||
m_backToTopPressed = false;
|
||||
const gd::RectF backToTop = BackToTopRect();
|
||||
if (backToTop.Width > 0.0f &&
|
||||
UiPointInRect(backToTop, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
ScrollLanesToTop();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 实验性功能面板的 ✕:只有"按下后仍在同一个 ✕ 上抬起"才真正关闭该功能。
|
||||
if (m_pendingPanelClose)
|
||||
{
|
||||
|
||||
@@ -73,6 +73,20 @@ namespace jdi
|
||||
inline const float kLaneScrollAnimMinMs = 130.0f; /**< 最短时长(短距离)。 */
|
||||
inline const float kLaneScrollAnimMaxMs = 300.0f; /**< 最长时长(长距离)。 */
|
||||
inline const float kLaneScrollAnimMsPerPixel = 0.35f; /**< 每像素距离增加的时长。 */
|
||||
/**
|
||||
* @brief 右下角「回到最上方」浮动按钮的几何与过渡参数。
|
||||
*
|
||||
* 用户 2026-10-16 要求:参考长文档网页的常见做法(向上箭头浮动按钮),
|
||||
* 仅在向下滚动时出现、回到上方时消失,出现/消失都要有过渡动画。
|
||||
*/
|
||||
inline const float kBackToTopSize = 36.0f; /**< 按钮边长(逻辑像素,圆形直径)。 */
|
||||
inline const float kBackToTopMargin = 12.0f; /**< 与绘图区右下角的边距。 */
|
||||
inline const float kBackToTopFadeMs = 170.0f; /**< 淡入/淡出时长(毫秒)。 */
|
||||
/** 向下滚动超过该距离(逻辑像素)才显示按钮(避免轻微滚动就弹出)。 */
|
||||
inline const float kBackToTopShowOffset = 24.0f;
|
||||
/** 向上箭头相对按钮尺寸的比例(箭头顶端到中心、半宽)。 */
|
||||
inline const float kBackToTopArrowDepth = 0.24f;
|
||||
inline const float kBackToTopArrowHalfWidth = 0.20f;
|
||||
inline const float kEnvelopeMinPixelSpan = 0.5f; /**< 包络纵向跨度小于该像素值时合并为单个顶点。 */
|
||||
inline const float kCursorHandleReserve = 9.0f; /**< 光标顶部三角把手占用的高度(数据点贴边时需避让)。 */
|
||||
inline const float kMarkerEdgeInset = 7.0f; /**< 数据点越界贴边时距网格可见边界的间距。 */
|
||||
|
||||
@@ -662,6 +662,10 @@ namespace jdi
|
||||
|
||||
BuildLaneVisuals();
|
||||
|
||||
// 「回到最上方」浮动按钮:目标可见性由当前滚动位置决定(过渡由宿主定时器推进)。
|
||||
// 在绘制前刷新一次,保证本帧就画出正确的过渡形态。
|
||||
UpdateBackToTopTarget();
|
||||
|
||||
// 「边沿计数」的扫描预算**按帧重置**(见 `m_edgeScanFrame` 的说明):
|
||||
// 旧实现按 `GetTickCount64()` 分桶,帧率一高(`PostMessage` 自持重绘会把它拉高)
|
||||
// 就会在同一系统 tick 内连画多帧而**完全不推进**统计 —— 用户报告
|
||||
@@ -735,6 +739,8 @@ namespace jdi
|
||||
PaintEditField(g);
|
||||
}
|
||||
PaintScrollBar(g);
|
||||
// 右下角「回到最上方」浮动按钮:在滚动条之上绘制(随 `m_backToTopAlpha` 淡入淡出)。
|
||||
PaintBackToTop(g);
|
||||
|
||||
// 绘图区外框(比网格内部分界线更重,形成明确整体边界)。
|
||||
// 用 GDI 画 4 条线:GDI+ 的 DrawRectangle 带抗锯齿时单帧要花掉 1ms 以上。
|
||||
@@ -6380,5 +6386,51 @@ namespace jdi
|
||||
UiFillRoundRect(g, thumb, kScrollBarThickness * m_scale * 0.5f, m_theme->borderStrong);
|
||||
}
|
||||
|
||||
void PlotView::PaintBackToTop(gd::Graphics& g)
|
||||
{
|
||||
if (m_backToTopAlpha <= 0.01f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const gd::RectF rect = BackToTopRect();
|
||||
if (rect.Width <= 1.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 过渡:整个按钮(底色/描边/箭头/投影)按 `m_backToTopAlpha` 淡入淡出。
|
||||
const float alpha = std::min(1.0f, std::max(0.0f, m_backToTopAlpha));
|
||||
const auto faded = [alpha](std::uint32_t color) -> std::uint32_t
|
||||
{
|
||||
const std::uint32_t a =
|
||||
static_cast<std::uint32_t>(static_cast<float>((color >> 24) & 0xFFu) * alpha);
|
||||
return (color & 0x00FFFFFFu) | ((a & 0xFFu) << 24);
|
||||
};
|
||||
|
||||
const float radius = rect.Width * 0.5f; // 圆形按钮(与图例托底/GX 徽标同一套配色)
|
||||
// 投影:两层递减透明度的圆角矩形(与弹出菜单/浮动面板一致,不引入系统阴影 API)。
|
||||
UiFillRoundRect(g, gd::RectF(rect.X + 1.0f * m_scale, rect.Y + 2.0f * m_scale, rect.Width,
|
||||
rect.Height),
|
||||
radius, faded(0x1A000000u));
|
||||
UiFillRoundRect(g, gd::RectF(rect.X, rect.Y + 1.0f * m_scale, rect.Width, rect.Height), radius,
|
||||
faded(0x14000000u));
|
||||
|
||||
const bool hot = m_backToTopHover || m_backToTopPressed;
|
||||
UiFillRoundRect(g, rect, radius, faded(hot ? m_theme->accentSoft : m_theme->cardBg));
|
||||
UiDrawRoundRect(g, rect, radius, faded(hot ? m_theme->accent : m_theme->borderStrong),
|
||||
hot ? 1.6f : 1.0f);
|
||||
|
||||
// 向上箭头:竖杆 + 两条斜线组成的箭头(自绘,不依赖字体)。
|
||||
const float cx = rect.X + rect.Width * 0.5f;
|
||||
const float cy = rect.Y + rect.Height * 0.5f;
|
||||
const float depth = rect.Width * kBackToTopArrowDepth;
|
||||
const float halfWidth = rect.Width * kBackToTopArrowHalfWidth;
|
||||
const std::uint32_t ink = faded(hot ? m_theme->accent : m_theme->textStrong);
|
||||
const float stroke = std::max(1.6f, 2.0f * m_scale);
|
||||
UiDrawLine(g, cx, cy + depth, cx, cy - depth, ink, stroke);
|
||||
UiDrawLine(g, cx, cy - depth, cx - halfWidth, cy - depth * 0.15f, ink, stroke);
|
||||
UiDrawLine(g, cx, cy - depth, cx + halfWidth, cy - depth * 0.15f, ink, stroke);
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
@@ -343,6 +343,18 @@ namespace jdi
|
||||
*/
|
||||
void ScrollToSignalAnimated(const SignalRef& ref);
|
||||
|
||||
/**
|
||||
* @brief 若目标信号所在分组处于**当前模式的折起状态**,自动把它展开。
|
||||
*
|
||||
* 折起的分组不会列出信号行 ⇒ 行集合里找不到目标信号,任何“滚动到该信号”都会落空
|
||||
*(用户 2026-10-16 报告:点网格图例联动时,被折起的组不会自动展开、列表不动)。
|
||||
* 展开是**立刻生效**的(不播放展开动画),因为随后的滚动目标依赖稳定的行位置。
|
||||
*
|
||||
* @param [IN] ref 目标信号。
|
||||
* @return 发生了展开返回 true。
|
||||
*/
|
||||
bool ExpandGroupForSignal(const SignalRef& ref);
|
||||
|
||||
/**
|
||||
* @brief 推进一步滚动动画(由宿主定时器驱动)。
|
||||
* @return 动画仍在进行返回 true(宿主应保持定时重绘)。
|
||||
|
||||
@@ -325,6 +325,14 @@ namespace jdi
|
||||
void SignalPanel::ScrollToSignalAnimated(const SignalRef& ref)
|
||||
{
|
||||
EnsureRows();
|
||||
if (!ref.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
// ⚠️ **目标信号所在分组若处于折起状态,行集合里根本没有它** —— 无论怎么滚都看不到它。
|
||||
// 用户 2026-10-16 报告:点击网格内的图例触发联动时,被折起的组不会自动展开 ⇒ 联动失效。
|
||||
// 因此先自动展开该分组(立刻生效,不做展开动画:滚动目标需要稳定的行位置)。
|
||||
ExpandGroupForSignal(ref);
|
||||
float desired = m_scrollOffset;
|
||||
if (!SignalScrollTarget(ref, desired))
|
||||
{
|
||||
@@ -334,6 +342,25 @@ namespace jdi
|
||||
StartScrollAnimationTo(desired);
|
||||
}
|
||||
|
||||
bool SignalPanel::ExpandGroupForSignal(const SignalRef& ref)
|
||||
{
|
||||
if (ref.groupIndex < 0 || !IsGroupCollapsed(ref.groupIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
SetGroupCollapsed(ref.groupIndex, false);
|
||||
// 该分组若正在播放折起动画,一并作废(目标行的位置会跳回展开态,动画没有意义了)。
|
||||
const int anim = FindGroupAnimation(ref.groupIndex);
|
||||
if (anim >= 0)
|
||||
{
|
||||
m_groupAnims.erase(m_groupAnims.begin() + anim);
|
||||
}
|
||||
// 行集合必须重建(展开后信号行才会出现)——滚动目标依赖它。
|
||||
m_rowsDirty = true;
|
||||
EnsureRows();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SignalPanel::AdvanceScrollAnimation()
|
||||
{
|
||||
if (!m_scrollAnimActive)
|
||||
|
||||
@@ -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`(离屏计时 / 逐像素对比)。
|
||||
|
||||
Reference in new issue
Block a user