修复「信号列表滚动把组名/信号名画到筛选框上」+「边沿统计面板在不移动鼠标时显示冻结」
一、列表滚动越界绘制(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
@@ -11823,6 +11823,62 @@ namespace jdi
|
|||||||
mix(static_cast<std::uint64_t>(peakItem != nullptr ? peakItem->iterations + 2 : 0));
|
mix(static_cast<std::uint64_t>(peakItem != nullptr ? peakItem->iterations + 2 : 0));
|
||||||
mix(static_cast<std::uint64_t>(peakItem != nullptr ? peakItem->maxIterations + 2 : 0));
|
mix(static_cast<std::uint64_t>(peakItem != nullptr ? peakItem->maxIterations + 2 : 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ⚠️⚠️ **边沿统计的数字本身就是面板内容,必须进缓存键**(用户报告的缺陷根因):
|
||||||
|
// 面板像素图层按本键缓存,而界面上的「上升沿 / 下降沿」是**分帧推进**出来的中间结果 ——
|
||||||
|
// 若这些数字不进键,则「统计推进 → 数字变化」不会让键变化 ⇒ 面板一直贴旧像素
|
||||||
|
// ⇒ 用户不动鼠标时统计永远停在“统计中…”或某个中间值,**只有移动鼠标**(改变别的键)
|
||||||
|
// 才会显示出早已算完的正确结果。
|
||||||
|
// ⇒ 把该面板当前要显示的 (上升沿, 下降沿, 是否扫完) 一并 mix 进去:推进会让键变化 ⇒
|
||||||
|
// 面板重绘 ⇒ 显示随进度更新,直到扫完(扫完后数字不再变,重新命中缓存)。
|
||||||
|
if (kind == EdgePanelKind::COUNT)
|
||||||
|
{
|
||||||
|
for (const SignalRef& ref : lane.signals)
|
||||||
|
{
|
||||||
|
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||||
|
if (resolved.data == nullptr || resolved.style == nullptr || resolved.axis == nullptr ||
|
||||||
|
!resolved.visible)
|
||||||
|
{
|
||||||
|
continue; // 与绘制时的可见性判定保持一致
|
||||||
|
}
|
||||||
|
std::size_t rising = 0;
|
||||||
|
std::size_t falling = 0;
|
||||||
|
bool complete = true;
|
||||||
|
EdgeCountsFor(*resolved.axis, *resolved.data, *resolved.style, lane.edgeValue,
|
||||||
|
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling, complete);
|
||||||
|
mix(static_cast<std::uint64_t>(rising));
|
||||||
|
mix(static_cast<std::uint64_t>(falling));
|
||||||
|
mix(complete ? 1ull : 0ull);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (kind == EdgePanelKind::CENTER)
|
||||||
|
{
|
||||||
|
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
||||||
|
{
|
||||||
|
if (rowIndex >= layout.rowStats.size() || layout.rowStats[rowIndex].Width <= 0.0f)
|
||||||
|
{
|
||||||
|
continue; // 与绘制时的条件一致:只有这一排才显示统计数字
|
||||||
|
}
|
||||||
|
const SignalRef& ref = layout.rowRefs[rowIndex];
|
||||||
|
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||||
|
const Lane::EdgeCenterItem* item = lane.FindEdgeCenterItem(ref);
|
||||||
|
if (resolved.data == nullptr || resolved.style == nullptr || resolved.axis == nullptr ||
|
||||||
|
item == nullptr)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::size_t rising = 0;
|
||||||
|
std::size_t falling = 0;
|
||||||
|
bool complete = true;
|
||||||
|
EdgeCenterCountsFor(*resolved.axis, *resolved.data, *resolved.style, item->points,
|
||||||
|
item->iterations, EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(),
|
||||||
|
rising, falling, complete);
|
||||||
|
mix(static_cast<std::uint64_t>(rising));
|
||||||
|
mix(static_cast<std::uint64_t>(falling));
|
||||||
|
mix(complete ? 1ull : 0ull);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 弱峰调整的滑块行矩形(每信号两行)。
|
// 弱峰调整的滑块行矩形(每信号两行)。
|
||||||
for (const gd::RectF& rect : layout.rowLevelTracks)
|
for (const gd::RectF& rect : layout.rowLevelTracks)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -439,6 +439,35 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
void PaintSignalRow(gd::Graphics& g, int row);
|
void PaintSignalRow(gd::Graphics& g, int row);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 在列表区内绘制行内文本(**显式裁剪到列表矩形**)。
|
||||||
|
*
|
||||||
|
* ⚠️ `UiText` 走 GDI `DrawTextW`,**不遵循 GDI+ 的 `SetClip`**:列表需要滚动时,
|
||||||
|
* 被滚出视口的行(顶部部分可见的两三行)会把组名/信号名画到上方筛选框上。
|
||||||
|
* 因此行内文本一律经本函数绘制(见 `UiTextClipped`)。
|
||||||
|
*
|
||||||
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
|
* @param [IN] text 文本内容。
|
||||||
|
* @param [IN] font 字体句柄。
|
||||||
|
* @param [IN] rect 文本布局矩形。
|
||||||
|
* @param [IN] argb 文本颜色。
|
||||||
|
* @param [IN] align 水平对齐方式。
|
||||||
|
*/
|
||||||
|
void PaintRowText(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect,
|
||||||
|
std::uint32_t argb, TextAlign align);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 在列表区内填充行内矩形(**夹取到列表矩形**)。
|
||||||
|
*
|
||||||
|
* ⚠️ `UiFillRect` 对大面积的**不透明**矩形会改走 GDI `FillRect`(同样不受 GDI+ 裁剪约束),
|
||||||
|
* 高 DPI 下行的面积足以超过该阈值 ⇒ 行背景/选中底色必须先把矩形夹到列表矩形。
|
||||||
|
*
|
||||||
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
|
* @param [IN] rect 目标矩形。
|
||||||
|
* @param [IN] argb 填充颜色。
|
||||||
|
*/
|
||||||
|
void FillRowRect(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 绘制样式编辑区(固定四行:颜色 / 线宽 / 样式 / 透明度)。
|
* @brief 绘制样式编辑区(固定四行:颜色 / 线宽 / 样式 / 透明度)。
|
||||||
* @param [IN,OUT] g 绘图上下文。
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
|
|||||||
@@ -1017,6 +1017,27 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SignalPanel::PaintRowText(gd::Graphics& g, const std::wstring& text, HFONT font,
|
||||||
|
const gd::RectF& rect, std::uint32_t argb, TextAlign align)
|
||||||
|
{
|
||||||
|
// 行内文本必须显式裁剪到列表矩形:GDI 文本不遵循 GDI+ 的 `SetClip`,
|
||||||
|
// 否则列表滚动时顶部那些“部分可见”的行会把组名/信号名画到筛选框上。
|
||||||
|
UiTextClipped(g, text, font, rect, argb, align, m_listRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SignalPanel::FillRowRect(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb)
|
||||||
|
{
|
||||||
|
// 夹取而不是裁剪:纯色填充下两者等价,而夹取对 `UiFillRect` 的 GDI 快路径同样有效
|
||||||
|
//(大面积不透明矩形会改走 GDI `FillRect`,它同样不受 GDI+ 裁剪约束)。
|
||||||
|
gd::RectF clipped = rect;
|
||||||
|
clipped.Intersect(m_listRect);
|
||||||
|
if (clipped.Width <= 0.0f || clipped.Height <= 0.0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UiFillRect(g, clipped, argb);
|
||||||
|
}
|
||||||
|
|
||||||
void SignalPanel::PaintFileRow(gd::Graphics& g, int row)
|
void SignalPanel::PaintFileRow(gd::Graphics& g, int row)
|
||||||
{
|
{
|
||||||
if (m_dataSet == nullptr || m_tab == nullptr || row < 0 || row >= static_cast<int>(m_rows.size()))
|
if (m_dataSet == nullptr || m_tab == nullptr || row < 0 || row >= static_cast<int>(m_rows.size()))
|
||||||
@@ -1035,10 +1056,10 @@ namespace jdi
|
|||||||
const bool hovered = (m_hoverRow == row);
|
const bool hovered = (m_hoverRow == row);
|
||||||
if (hovered)
|
if (hovered)
|
||||||
{
|
{
|
||||||
UiFillRect(g, rect, m_theme->hover);
|
FillRowRect(g, rect, m_theme->hover);
|
||||||
}
|
}
|
||||||
UiFillRect(g, gd::RectF(rect.X, rect.Y, 2.0f * m_scale, rect.Height),
|
FillRowRect(g, gd::RectF(rect.X, rect.Y, 2.0f * m_scale, rect.Height),
|
||||||
WithAlpha(m_theme->borderStrong, 0x90));
|
WithAlpha(m_theme->borderStrong, 0x90));
|
||||||
|
|
||||||
const float baseX = rect.X + kPadding * m_scale;
|
const float baseX = rect.X + kPadding * m_scale;
|
||||||
const float centerY = rect.Y + rect.Height * 0.5f;
|
const float centerY = rect.Y + rect.Height * 0.5f;
|
||||||
@@ -1076,14 +1097,14 @@ namespace jdi
|
|||||||
std::max(20.0f, statsRight - statsWidth - 6.0f * m_scale - nameAreaLeft);
|
std::max(20.0f, statsRight - statsWidth - 6.0f * m_scale - nameAreaLeft);
|
||||||
|
|
||||||
// 序号以强调色突出,便于在多组之间快速定位。
|
// 序号以强调色突出,便于在多组之间快速定位。
|
||||||
UiText(g, groupLabel, m_fonts->uiBold, gd::RectF(nameLeft, rect.Y, groupWidth, rect.Height),
|
PaintRowText(g, groupLabel, m_fonts->uiBold, gd::RectF(nameLeft, rect.Y, groupWidth, rect.Height),
|
||||||
m_theme->accent, TextAlign::LEFT);
|
m_theme->accent, TextAlign::LEFT);
|
||||||
|
|
||||||
// 竖线分隔符:以绘制方式实现,与文字基线无关,排版更稳。
|
// 竖线分隔符:以绘制方式实现,与文字基线无关,排版更稳。
|
||||||
UiFillRect(g, gd::RectF(nameLeft + groupWidth + kGroupSeparatorGap * m_scale,
|
FillRowRect(g, gd::RectF(nameLeft + groupWidth + kGroupSeparatorGap * m_scale,
|
||||||
rect.Y + 7.0f * m_scale, kGroupSeparatorWidth * m_scale,
|
rect.Y + 7.0f * m_scale, kGroupSeparatorWidth * m_scale,
|
||||||
rect.Height - 14.0f * m_scale),
|
rect.Height - 14.0f * m_scale),
|
||||||
WithAlpha(m_theme->borderStrong, 0xB0));
|
WithAlpha(m_theme->borderStrong, 0xB0));
|
||||||
|
|
||||||
if (m_editField == StyleEditField::FILE_NAME && m_editRow == row)
|
if (m_editField == StyleEditField::FILE_NAME && m_editRow == row)
|
||||||
{
|
{
|
||||||
@@ -1091,13 +1112,13 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
UiText(g, UiEllipsize(group->Title(), m_fonts->uiBold, nameWidth), m_fonts->uiBold,
|
PaintRowText(g, UiEllipsize(group->Title(), m_fonts->uiBold, nameWidth), m_fonts->uiBold,
|
||||||
gd::RectF(nameAreaLeft, rect.Y, nameWidth, rect.Height), m_theme->textStrong,
|
gd::RectF(nameAreaLeft, rect.Y, nameWidth, rect.Height), m_theme->textStrong,
|
||||||
TextAlign::LEFT);
|
TextAlign::LEFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
UiText(g, stats, m_fonts->uiSmall, gd::RectF(statsRight - statsWidth, rect.Y, statsWidth, rect.Height),
|
PaintRowText(g, stats, m_fonts->uiSmall, gd::RectF(statsRight - statsWidth, rect.Y, statsWidth, rect.Height),
|
||||||
m_theme->textMuted, TextAlign::RIGHT);
|
m_theme->textMuted, TextAlign::RIGHT);
|
||||||
|
|
||||||
// 关闭按钮(从工作区移除该文件)。
|
// 关闭按钮(从工作区移除该文件)。
|
||||||
const bool closeHot = hovered && (m_hoverPart == RowPart::CLOSE);
|
const bool closeHot = hovered && (m_hoverPart == RowPart::CLOSE);
|
||||||
@@ -1130,12 +1151,12 @@ namespace jdi
|
|||||||
|
|
||||||
if (selected)
|
if (selected)
|
||||||
{
|
{
|
||||||
UiFillRect(g, rect, m_theme->selection);
|
FillRowRect(g, rect, m_theme->selection);
|
||||||
UiFillRect(g, gd::RectF(rect.X, rect.Y, 2.0f * m_scale, rect.Height), m_theme->accent);
|
FillRowRect(g, gd::RectF(rect.X, rect.Y, 2.0f * m_scale, rect.Height), m_theme->accent);
|
||||||
}
|
}
|
||||||
else if (hovered)
|
else if (hovered)
|
||||||
{
|
{
|
||||||
UiFillRect(g, rect, m_theme->hover);
|
FillRowRect(g, rect, m_theme->hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
const float baseX = rect.X + kPadding * m_scale + kIndent * m_scale;
|
const float baseX = rect.X + kPadding * m_scale + kIndent * m_scale;
|
||||||
@@ -1177,8 +1198,8 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
const float nameWidth = std::max(16.0f, nameRightLimit - nameLeft);
|
const float nameWidth = std::max(16.0f, nameRightLimit - nameLeft);
|
||||||
const std::uint32_t nameColor = visible ? m_theme->text : m_theme->textMuted;
|
const std::uint32_t nameColor = visible ? m_theme->text : m_theme->textMuted;
|
||||||
UiText(g, UiEllipsize(signal->Title(), m_fonts->ui, nameWidth), m_fonts->ui,
|
PaintRowText(g, UiEllipsize(signal->Title(), m_fonts->ui, nameWidth), m_fonts->ui,
|
||||||
gd::RectF(nameLeft, rect.Y, nameWidth, rect.Height), nameColor, TextAlign::LEFT);
|
gd::RectF(nameLeft, rect.Y, nameWidth, rect.Height), nameColor, TextAlign::LEFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (badgeVisible)
|
if (badgeVisible)
|
||||||
@@ -1188,8 +1209,8 @@ namespace jdi
|
|||||||
badgeHot ? m_theme->accentSoft : m_theme->cardBg);
|
badgeHot ? m_theme->accentSoft : m_theme->cardBg);
|
||||||
UiDrawRoundRect(g, badgeRect, kBadgeHeight * m_scale * 0.5f,
|
UiDrawRoundRect(g, badgeRect, kBadgeHeight * m_scale * 0.5f,
|
||||||
badgeHot ? m_theme->accent : m_theme->border, 1.0f);
|
badgeHot ? m_theme->accent : m_theme->border, 1.0f);
|
||||||
UiText(g, L"G" + std::to_wstring(laneIndex + 1), m_fonts->monoSmall, badgeRect,
|
PaintRowText(g, L"G" + std::to_wstring(laneIndex + 1), m_fonts->monoSmall, badgeRect,
|
||||||
badgeHot ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
|
badgeHot ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 「移除该信号」✕:位于信号条目最右端,尺寸/样式/右边距与分组行的关闭 ✕ 完全一致。
|
// 「移除该信号」✕:位于信号条目最右端,尺寸/样式/右边距与分组行的关闭 ✕ 完全一致。
|
||||||
|
|||||||
@@ -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. 变更日志(本文件维护记录)
|
## 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-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(**另一台设备**) | **「调整」数值符号规则 + 边沿统计按帧推进(含首帧公平分摊)+ 浮动面板排版与弱峰复位控件(详见 §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(**另一台设备**) | **数据层大重构(文件→分组 / 时间轴一等公民 / 加载器抽象)+ 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 的
|
⚠️ **`##TX` 文本块必须兼容两种写法**:规范的「U32 长度 + UTF-16/UTF-8」**与** CANape 的
|
||||||
「无长度前缀的 NUL 结尾裸文本」;只按规范解析会得到空通道名。
|
「无长度前缀的 NUL 结尾裸文本」;只按规范解析会得到空通道名。
|
||||||
压缩块(`##DZ`/`##HL`/`##SD`)、`##DL` 数据列表、字符串/MIME 通道**未实现**(跳过该组)。
|
压缩块(`##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 离屏验证方法学(本机偏好,强烈建议沿用)
|
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
void UiText(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect, std::uint32_t argb,
|
||||||
TextAlign align = TextAlign::LEFT);
|
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+ 上下文,可在布局阶段调用)。
|
* @brief 测量单行文本的像素宽度(无需 GDI+ 上下文,可在布局阶段调用)。
|
||||||
* @param [IN] font 字体句柄。
|
* @param [IN] font 字体句柄。
|
||||||
|
|||||||
+93
-41
@@ -355,51 +355,103 @@ namespace jdi
|
|||||||
g.DrawLine(&pen, x1, y1, x2, y2);
|
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,
|
void UiText(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect, std::uint32_t argb,
|
||||||
TextAlign align)
|
TextAlign align)
|
||||||
{
|
{
|
||||||
if (text.empty() || font == nullptr || rect.Width <= 0.0f || rect.Height <= 0.0f)
|
DrawUiText(g, text, font, rect, argb, align, nullptr);
|
||||||
{
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
HDC hdc = g.GetHDC();
|
void UiTextClipped(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect,
|
||||||
if (hdc == nullptr)
|
std::uint32_t argb, TextAlign align, const gd::RectF& clipRect)
|
||||||
{
|
{
|
||||||
return;
|
DrawUiText(g, text, font, rect, argb, align, &clipRect);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
float UiMeasureTextWidth(HFONT font, const std::wstring& text)
|
float UiMeasureTextWidth(HFONT font, const std::wstring& text)
|
||||||
|
|||||||
Reference in new issue
Block a user