修复「信号列表滚动把组名/信号名画到筛选框上」+「边沿统计面板在不移动鼠标时显示冻结」

一、列表滚动越界绘制(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:
DeepSeek V4.1 Flash committed 2026-10-15 12:13:40 +08:00
1 parent 6eedbe3c5d
commit 3bca3b1201
6 files changed
+304 -62

No files matched your search

+19
View File
@@ -152,6 +152,25 @@ namespace jdi
void UiText(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect, std::uint32_t argb,
TextAlign align = TextAlign::LEFT);
/**
* @brief 在**显式裁剪矩形**内绘制单行文本(除裁剪范围外与 `UiText` 完全一致)。
*
* `UiText` 走 `DrawTextW`,而 **GDI 文本不遵循 GDI+ 的 `SetClip`**:滚动类列表在滚出
* 视口的行上仍会把文字画到相邻区域(例如把组名/信号名画到上方筛选框上)。
* 本函数在同一个 HDC 上做 `SaveDC` + `IntersectClipRect` 并在结束后完整还原,
* 因此对齐、垂直居中与省略号规则都与 `UiText` 一模一样,只是多了 GDI 级裁剪。
*
* @param [IN,OUT] g 绘图上下文。
* @param [IN] text 文本内容。
* @param [IN] font 字体句柄。
* @param [IN] rect 文本布局矩形。
* @param [IN] argb 文本颜色。
* @param [IN] align 水平对齐方式。
* @param [IN] clipRect 裁剪矩形(超出部分不绘制)。
*/
void UiTextClipped(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect,
std::uint32_t argb, TextAlign align, const gd::RectF& clipRect);
/**
* @brief 测量单行文本的像素宽度(无需 GDI+ 上下文,可在布局阶段调用)。
* @param [IN] font 字体句柄。
+93 -41
View File
@@ -355,51 +355,103 @@ namespace jdi
g.DrawLine(&pen, x1, y1, x2, y2);
}
namespace
{
/**
* @brief `UiText` 与 `UiTextClipped` 的公共实现。
*
* ⚠️ `clip == nullptr` 时**不加任何 GDI 裁剪**(与历史行为完全一致);
* 提供裁剪时在同一个 HDC 上 `SaveDC`/`IntersectClipRect` 并完整还原 ——
* 这是唯一能让 `DrawTextW` 受限于指定范围的办法(GDI+ 的 `SetClip` 对 GDI 文本无效)。
*
* @param [IN,OUT] g 绘图上下文。
* @param [IN] text 文本内容。
* @param [IN] font 字体句柄。
* @param [IN] rect 文本布局矩形。
* @param [IN] argb 文本颜色。
* @param [IN] align 水平对齐方式。
* @param [IN] clip 裁剪矩形(nullptr = 不裁剪)。
*/
void DrawUiText(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect,
std::uint32_t argb, TextAlign align, const gd::RectF* clip)
{
if (text.empty() || font == nullptr || rect.Width <= 0.0f || rect.Height <= 0.0f)
{
return;
}
if (clip != nullptr && (clip->Width <= 0.0f || clip->Height <= 0.0f))
{
return; // 裁剪区为空:任何文字都落在界外
}
HDC hdc = g.GetHDC();
if (hdc == nullptr)
{
return;
}
// GDI 文本不受 GDI+ 裁剪区约束:需要限制范围时显式裁剪,结束前完整还原 DC 状态。
int saved = 0;
if (clip != nullptr)
{
saved = SaveDC(hdc);
if (saved != 0)
{
IntersectClipRect(hdc, static_cast<int>(std::floor(clip->X)),
static_cast<int>(std::floor(clip->Y)),
static_cast<int>(std::ceil(clip->X + clip->Width)),
static_cast<int>(std::ceil(clip->Y + clip->Height)));
}
}
HGDIOBJ oldFont = SelectObject(hdc, font);
const int oldMode = SetBkMode(hdc, TRANSPARENT);
const COLORREF oldColor = SetTextColor(hdc, ToColorRef(argb));
RECT rc;
rc.left = static_cast<LONG>(std::floor(rect.X));
rc.top = static_cast<LONG>(std::floor(rect.Y));
rc.right = static_cast<LONG>(std::ceil(rect.X + rect.Width));
rc.bottom = static_cast<LONG>(std::ceil(rect.Y + rect.Height));
UINT flags = DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX | DT_END_ELLIPSIS;
switch (align)
{
case TextAlign::CENTER:
flags |= DT_CENTER;
break;
case TextAlign::RIGHT:
flags |= DT_RIGHT;
break;
case TextAlign::LEFT:
default:
flags |= DT_LEFT;
break;
}
DrawTextW(hdc, text.c_str(), static_cast<int>(text.size()), &rc, flags);
SetTextColor(hdc, oldColor);
SetBkMode(hdc, oldMode);
SelectObject(hdc, oldFont);
if (saved != 0)
{
RestoreDC(hdc, saved);
}
g.ReleaseHDC(hdc);
}
} // namespace
void UiText(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect, std::uint32_t argb,
TextAlign align)
{
if (text.empty() || font == nullptr || rect.Width <= 0.0f || rect.Height <= 0.0f)
{
return;
}
DrawUiText(g, text, font, rect, argb, align, nullptr);
}
HDC hdc = g.GetHDC();
if (hdc == nullptr)
{
return;
}
HGDIOBJ oldFont = SelectObject(hdc, font);
const int oldMode = SetBkMode(hdc, TRANSPARENT);
const COLORREF oldColor = SetTextColor(hdc, ToColorRef(argb));
RECT rc;
rc.left = static_cast<LONG>(std::floor(rect.X));
rc.top = static_cast<LONG>(std::floor(rect.Y));
rc.right = static_cast<LONG>(std::ceil(rect.X + rect.Width));
rc.bottom = static_cast<LONG>(std::ceil(rect.Y + rect.Height));
UINT flags = DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX | DT_END_ELLIPSIS;
switch (align)
{
case TextAlign::CENTER:
flags |= DT_CENTER;
break;
case TextAlign::RIGHT:
flags |= DT_RIGHT;
break;
case TextAlign::LEFT:
default:
flags |= DT_LEFT;
break;
}
DrawTextW(hdc, text.c_str(), static_cast<int>(text.size()), &rc, flags);
SetTextColor(hdc, oldColor);
SetBkMode(hdc, oldMode);
SelectObject(hdc, oldFont);
g.ReleaseHDC(hdc);
void UiTextClipped(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect,
std::uint32_t argb, TextAlign align, const gd::RectF& clipRect)
{
DrawUiText(g, text, font, rect, argb, align, &clipRect);
}
float UiMeasureTextWidth(HFONT font, const std::wstring& text)