修复悬停高亮变长方形色块/联动失效 + 信号列表动画卡顿真因(行位图缓存)
一、悬停高亮恢复"整条曲线发光"(上一轮裁成鼠标附近一段 ⇒ 满幅方波上就是个长方形色块) 光晕的语义是"整条曲线一起发光",对编码器这种满幅方波,一列包络本来就跨满网格高, 裁成 144px 宽 × 满高的窗口在屏幕上就是一个像选区框的色块(用户 2026-10-18 第二轮报告)。 修法(PlotViewPaint.cpp): - PaintSignalHaloFor 恢复**整条**几何(包络带 FillPath / 折线 DrawLines); - 新增 PaintSignalHaloCached:光晕几何只取决于「信号 + 视界 + 网格几何 + 样式」,**与鼠标无关** ⇒ 渲染一次进离屏位图,之后每帧只做一次 AlphaBlend(源位图预乘 alpha,用 256×256 查表); - 缓存键与 AcquirePolyline 同源,**不含网格屏幕位置**(滚动网格只按新位置贴同一张位图)。 210ms/帧(整条描边)→ 一次性 15ms 渲染 + 每帧 ~2ms 贴图。 halocheck 校验:悬停必命中(60/60 位置)、光晕贴合带宽(outsideBand=0)、不越出网格、 bbox 覆盖整条曲线(不是中段方块)、列表行悬停联动仍生效。 二、信号列表动画卡顿的三个真因(探针 panelstutter:129 个可见网格 + 反复"展开→切组复选框→收起") 1) 面板每帧重画 ~36 行 × ~0.35ms(十来个 GDI+ 原语)⇒ panel.Paint 24~35ms; 2) 动画帧里仍在预热网格图层:每帧重建 1 个网格图层,2500 万采样信号 10~30ms ⇒ 帧被拉长; 3) 勾选分组会把上百个**离屏**网格排进待重画队列,随后每帧补 1 个 ⇒ 数秒持续掉帧。 修法: 1) 行位图缓存(SignalPanel::PaintRowCached):按"行的可见内容"做键,整行渲染进离屏 DIB, 命中时一次 BitBlt。panel.Paint 24~35ms → 5~6ms。 ⚠️ 必须 DIB + 内存 DC(gd::Bitmap 上 GDI 会关闭 ClearType,整列文字退化); ⚠️ 行内几何在 RowRect 里平移(GDI 文本不认 GDI+ 世界变换,否则文字画到小位图之外); ⚠️ 渲染到行位图时不按列表矩形裁剪,裁剪放到贴图时的 DC(GDI 不认 GDI+ 裁剪, 必须自己 SaveDC + IntersectClipRect,折起动画时行不许画到组名上); ⚠️ 缓存上限 192 行(≈10MB)。静态画面逐像素一致(面板区域 54/275200 不同)。 2) 新增 PlotView::SetAnimationBusy:UI 动画期间不预热网格图层(队列保留,动画结束后继续)。 3) 待重画队列只保留可视 + 预取带内的网格;RollWideBuffer 不再登记离屏网格。 实测:展开 p50 35ms → 13ms、收起 33ms → 13ms、动画帧内图层重建 56 次 → 0 次。 三、试过并放弃(避免以后重走) - ExtTextOutW 替代 DrawTextW:快 2~4 倍但光栅化不同(面板 10% 像素差异)⇒ 观感变化,放弃; - 用 gd::Bitmap 做行位图:文字丢失 ClearType,放弃。 四、验证(全部离屏,不占用鼠标/键盘/屏幕) - 新增 .preflight/perf/panelstutter_main.cpp(8/8)、halocheck_main.cpp(16/16); - 回归 verify 49/49、panelanim 158/158、hoverdrag 20/20、gridanim 93/93、lanescroll 23/23、 datacheck 69/69、edgecheck 46/46、errcheck 19/19、panflash 19/19、stress 6/6; - make release / make debug 零警告(TARGET_FILE_NAME=TargetCheck/TargetCheckD)。 文档:Documents/agent_mem.md 追加本轮小节(含 4 条新规则:高亮语义不得为性能让步、 GDI 文本的三个隐形陷阱、面板卡顿先量"每行成本×可见行数"与"本帧是否顺带做绘图区重活"、 动画帧内绝不做可延迟的整帧级工作)。
This commit is contained in:
1 parent
0efaa8c0ca
commit
24f0bf849d
11 files changed
+853
-197
No files matched your search
@@ -806,6 +806,76 @@ namespace jdi
|
||||
*/
|
||||
void PaintSignalRow(gd::Graphics& g, int row);
|
||||
|
||||
/**
|
||||
* @brief 把一行(分组行 / 信号行)**绘制进离屏位图缓存**,再整块贴到列表上。
|
||||
*
|
||||
* ⚠️ 一行的绘制要调用十来个 GDI+ 原语(圆角托底 / 复选框 / 色块 / 两段文字…),
|
||||
* 实测 **0.35ms/行**;列表一屏能放 35~40 行 ⇒ 单帧 **12~15ms** 全花在重复绘制
|
||||
* **内容完全没变**的行上(分组折起/展开动画只是在整行平移)。
|
||||
* 因此按"行的可见内容"做键缓存位图:动画帧只需把命中的位图按新位置贴出来。
|
||||
*
|
||||
* ⚠️ 缓存期间行的绘制**不能**裁到列表矩形(`m_rowClipRender`):位图是整行内容,
|
||||
* 裁剪由贴图时的 GDI+ 裁剪区负责 —— 否则部分滚出上边界的行会永久丢掉顶部像素。
|
||||
*
|
||||
* @param [IN,OUT] g 绘图上下文(调用方已把裁剪区设为列表矩形)。
|
||||
* @param [IN] row 行下标。
|
||||
*/
|
||||
void PaintRowCached(gd::Graphics& g, int row);
|
||||
|
||||
/**
|
||||
* @brief 计算一行的缓存键(涵盖影响该行像素的全部状态)。
|
||||
* @param [IN] row 行下标。
|
||||
* @return 键值。
|
||||
*/
|
||||
std::uint64_t RowCacheKey(int row) const;
|
||||
|
||||
/** @brief 作废全部行位图缓存(行集合 / 字体 / 主题变化时调用)。 */
|
||||
void InvalidateRowCache();
|
||||
|
||||
/** @brief 一行的离屏位图缓存项。 */
|
||||
struct RowBitmapCache
|
||||
{
|
||||
/**
|
||||
* @brief 该行的像素(DIB 内存 DC,已铺面板底色)。
|
||||
*
|
||||
* ⚠️ 必须是 **DIB + 内存 DC**,不能用 `gd::Bitmap`:GDI 在 GDI+ 位图的临时 HDC 上
|
||||
* 会**关闭 ClearType**(退化为灰度抗锯齿),列表文字因此变细、观感与正文不一致
|
||||
*(实测:整列文字丢失亚像素渲染)。DIB 内存 DC 上 ClearType 与屏幕一致。
|
||||
*/
|
||||
HDC dc = nullptr;
|
||||
HBITMAP dib = nullptr;
|
||||
std::uint64_t key = 0; /**< 内容键(不一致即重建)。 */
|
||||
int width = 0; /**< 位图宽度(= 列表宽度)。 */
|
||||
int height = 0; /**< 位图高度(= 行高)。 */
|
||||
};
|
||||
|
||||
std::vector<RowBitmapCache> m_rowBitmapCache; /**< 与 `m_rows` 同序的行位图缓存。 */
|
||||
|
||||
/**
|
||||
* @brief 行位图缓存的**数量上限**(超过即整体作废重建)。
|
||||
*
|
||||
* 一行位图 ≈ 列表宽 × 行高 × 4B(430×30×4 ≈ 51KB)。用户滚动浏览上千行时,
|
||||
* 缓存会按"画过的行下标"逐渐长大,必须有上限(这里 192 行 ≈ 10MB)。
|
||||
*/
|
||||
static constexpr std::size_t kRowBitmapCacheLimit = 192;
|
||||
std::size_t m_rowBitmapLiveCount = 0; /**< 当前持有的行位图数量。 */
|
||||
|
||||
/** @brief 行绘制正被渲染进离屏位图(此时禁止按列表矩形裁剪)。 */
|
||||
bool m_rowClipRender = false;
|
||||
|
||||
/**
|
||||
* @brief 行位图缓存渲染期间的**几何平移量**(把整行搬到"位图坐标系")。
|
||||
*
|
||||
* ⚠️ 不能用 `gd::Graphics::TranslateTransform`:行内**文本走 GDI `DrawTextW`**,
|
||||
* 而 GDI 文本**不遵循 GDI+ 的世界变换**(`Graphics::GetHDC` 拿到的 DC 只有裁剪区),
|
||||
* 于是文字会被画到绝对坐标处、落到小位图之外(实测量产现象:行里只剩复选框/色块,
|
||||
* 文字全部消失)。因此在**几何层**平移:`RowRect` 是行内一切矩形的唯一来源,
|
||||
* 它统一减去该原点即可(`SignalBadgeRect` / ✕ / 名称框等全部自动跟随)。
|
||||
*/
|
||||
bool m_rowRenderActive = false;
|
||||
float m_rowRenderOriginX = 0.0f; /**< 位图左上角(屏幕 x)。 */
|
||||
float m_rowRenderOriginY = 0.0f; /**< 位图左上角(屏幕 y)。 */
|
||||
|
||||
/**
|
||||
* @brief 判断哪个分组行需要「悬浮」在列表顶部(返回行下标,无需悬浮时返回 -1)。
|
||||
*
|
||||
|
||||
@@ -374,6 +374,9 @@ namespace jdi
|
||||
{
|
||||
m_rows.clear();
|
||||
m_rowsDirty = false;
|
||||
// 行集合重建后行下标的含义可能变化(增删/折起展开)⇒ 行位图缓存整体作废。
|
||||
// 这同时把"不再显示的行"的位图内存还回去(缓存条目按行数增长)。
|
||||
InvalidateRowCache();
|
||||
|
||||
if (m_dataSet == nullptr || m_tab == nullptr)
|
||||
{
|
||||
@@ -500,10 +503,15 @@ namespace jdi
|
||||
// 「组名悬浮」期间把分组行钉在列表顶部绘制(见 `PaintStickyGroupRow`):
|
||||
// 本函数是行内所有派生的唯一几何来源,因此只需在这里覆盖顶部坐标,
|
||||
// 名称框 / ✕ / 徽标等位置会自动跟随。
|
||||
//
|
||||
// ⚠️ 行位图缓存渲染时整体平移到"位图坐标系"(见 `m_rowRenderActive` 的说明:
|
||||
// GDI 文本不认 GDI+ 变换,必须在几何层平移)。
|
||||
const float renderOffsetX = m_rowRenderActive ? -m_rowRenderOriginX : 0.0f;
|
||||
const float renderOffsetY = m_rowRenderActive ? -m_rowRenderOriginY : 0.0f;
|
||||
if (m_rowYOverrideActive)
|
||||
{
|
||||
return gd::RectF(m_listRect.X, m_rowYOverrideY, m_listRect.Width,
|
||||
m_rows[static_cast<std::size_t>(row)].height);
|
||||
return gd::RectF(m_listRect.X + renderOffsetX, m_rowYOverrideY + renderOffsetY,
|
||||
m_listRect.Width, m_rows[static_cast<std::size_t>(row)].height);
|
||||
}
|
||||
|
||||
float top = m_listRect.Y - m_scrollOffset;
|
||||
@@ -516,7 +524,8 @@ namespace jdi
|
||||
{
|
||||
top -= m_rowShift[static_cast<std::size_t>(row)];
|
||||
}
|
||||
return gd::RectF(m_listRect.X, top, m_listRect.Width, m_rows[static_cast<std::size_t>(row)].height);
|
||||
return gd::RectF(m_listRect.X + renderOffsetX, top + renderOffsetY, m_listRect.Width,
|
||||
m_rows[static_cast<std::size_t>(row)].height);
|
||||
}
|
||||
|
||||
int SignalPanel::GroupLastRowIndex(int row, int rowCount) const
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
// 分片按“职责”划分:核心(状态/布局/几何)、渲染、交互与编辑,见各文件名。
|
||||
#include "SignalPanelInternal.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
@@ -186,14 +188,10 @@ namespace jdi
|
||||
g.SetClip(m_rowClipRect, gd::CombineModeIntersect);
|
||||
}
|
||||
|
||||
if (m_rows[static_cast<std::size_t>(row)].kind == RowKind::GROUP)
|
||||
{
|
||||
PaintGroupRow(g, row);
|
||||
}
|
||||
else
|
||||
{
|
||||
PaintSignalRow(g, row);
|
||||
}
|
||||
// ⚠️ 整行走**位图缓存**:分组折起/展开动画只是在平移行,
|
||||
// 行的像素一个也没变;逐行重画十来个 GDI+ 原语要 0.35ms/行
|
||||
//(一屏 35~40 行 ⇒ 每帧 12~15ms),这是"只有文字的列表却滚动卡顿"的真因。
|
||||
PaintRowCached(g, row);
|
||||
g.Restore(rowSave);
|
||||
ClearRowClip();
|
||||
}
|
||||
@@ -227,6 +225,14 @@ namespace jdi
|
||||
// 否则列表滚动时顶部那些“部分可见”的行会把组名/信号名画到筛选框上。
|
||||
// 另外,分组折起动画期间行高会连续缩小 —— 此时必须裁到**本行矩形**内,
|
||||
// 否则垂直居中的文字会溢出到相邻行上。
|
||||
//
|
||||
// ⚠️ **行位图缓存渲染时反而不能裁剪**(`m_rowClipRender`):位图是整行内容,
|
||||
// 裁剪留到贴图时由 GDI+ 裁剪区完成(否则部分滚出上边界的行会永久丢掉顶部像素)。
|
||||
if (m_rowClipRender)
|
||||
{
|
||||
UiText(g, text, font, rect, argb, align);
|
||||
return;
|
||||
}
|
||||
UiTextClipped(g, text, font, rect, argb, align, m_rowClipActive ? m_rowClipRect : m_listRect);
|
||||
}
|
||||
|
||||
@@ -235,14 +241,251 @@ namespace jdi
|
||||
// 夹取而不是裁剪:纯色填充下两者等价,而夹取对 `UiFillRect` 的 GDI 快路径同样有效
|
||||
//(大面积不透明矩形会改走 GDI `FillRect`,它同样不受 GDI+ 裁剪约束)。
|
||||
gd::RectF clipped = rect;
|
||||
clipped.Intersect(m_rowClipActive ? m_rowClipRect : m_listRect);
|
||||
if (clipped.Width <= 0.0f || clipped.Height <= 0.0f)
|
||||
if (!m_rowClipRender)
|
||||
{
|
||||
return;
|
||||
clipped.Intersect(m_rowClipActive ? m_rowClipRect : m_listRect);
|
||||
if (clipped.Width <= 0.0f || clipped.Height <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
UiFillRect(g, clipped, argb);
|
||||
}
|
||||
|
||||
void SignalPanel::InvalidateRowCache()
|
||||
{
|
||||
for (RowBitmapCache& cache : m_rowBitmapCache)
|
||||
{
|
||||
if (cache.dc != nullptr)
|
||||
{
|
||||
DeleteDC(cache.dc);
|
||||
cache.dc = nullptr;
|
||||
}
|
||||
if (cache.dib != nullptr)
|
||||
{
|
||||
DeleteObject(cache.dib);
|
||||
cache.dib = nullptr;
|
||||
}
|
||||
}
|
||||
m_rowBitmapCache.clear();
|
||||
m_rowBitmapLiveCount = 0;
|
||||
}
|
||||
|
||||
std::uint64_t SignalPanel::RowCacheKey(int row) const
|
||||
{
|
||||
std::uint64_t hash = 1469598103934665603ull;
|
||||
const auto mix = [&hash](std::uint64_t value)
|
||||
{
|
||||
hash ^= value;
|
||||
hash *= 1099511628211ull;
|
||||
};
|
||||
const auto mixFloat = [&mix](float value)
|
||||
{
|
||||
std::uint32_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
mix(bits);
|
||||
};
|
||||
const auto mixText = [&mix](const std::wstring& text)
|
||||
{
|
||||
mix(text.size());
|
||||
for (const wchar_t character : text)
|
||||
{
|
||||
mix(static_cast<std::uint64_t>(character));
|
||||
}
|
||||
};
|
||||
|
||||
if (row < 0 || row >= static_cast<int>(m_rows.size()))
|
||||
{
|
||||
return hash;
|
||||
}
|
||||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||||
mix(static_cast<std::uint64_t>(item.kind));
|
||||
mix(static_cast<std::uint64_t>(item.groupIndex + 1));
|
||||
mix(static_cast<std::uint64_t>(item.signalIndex + 1));
|
||||
mixFloat(item.height);
|
||||
mixFloat(m_scale);
|
||||
mix(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(m_theme)));
|
||||
mix(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(m_fonts)));
|
||||
mix(static_cast<std::uint64_t>((m_hoverRow == row) ? (static_cast<int>(m_hoverPart) + 1) : 0));
|
||||
|
||||
if (item.kind == RowKind::GROUP)
|
||||
{
|
||||
const SignalGroup* group =
|
||||
(m_dataSet != nullptr) ? m_dataSet->GroupAt(item.groupIndex) : nullptr;
|
||||
mixText((group != nullptr) ? group->name : std::wstring());
|
||||
mixText(GroupStatsText(row));
|
||||
mix(IsGroupCollapsed(item.groupIndex) ? 1ull : 0ull);
|
||||
mix(static_cast<std::uint64_t>(item.displayCount + 1));
|
||||
if (m_tab != nullptr)
|
||||
{
|
||||
mix(m_tab->GroupAllVisible(item.groupIndex) ? 1ull : 0ull);
|
||||
mix(m_tab->GroupHasVisible(item.groupIndex) ? 2ull : 0ull);
|
||||
}
|
||||
const bool editing = (m_editField == StyleEditField::GROUP_TITLE && m_editRow == row);
|
||||
mix(editing ? 1ull : 0ull);
|
||||
if (editing)
|
||||
{
|
||||
mixText(m_edit.Text());
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
const SignalRef ref{item.groupIndex, item.signalIndex};
|
||||
const SignalData* signal = (m_dataSet != nullptr) ? m_dataSet->Resolve(ref) : nullptr;
|
||||
const SignalStyle* style = StyleOf(ref);
|
||||
mixText((signal != nullptr) ? signal->name : std::wstring());
|
||||
mix((SelectedRef() == ref) ? 1ull : 0ull);
|
||||
if (style != nullptr)
|
||||
{
|
||||
mix(static_cast<std::uint64_t>(style->color));
|
||||
mixFloat(style->opacity);
|
||||
}
|
||||
if (m_tab != nullptr)
|
||||
{
|
||||
mix(m_tab->IsVisible(ref) ? 1ull : 0ull);
|
||||
mix(static_cast<std::uint64_t>(m_tab->LaneOfSignal(ref) + 1));
|
||||
}
|
||||
const bool editing = (m_editField == StyleEditField::SIGNAL_NAME && m_editRow == row);
|
||||
mix(editing ? 1ull : 0ull);
|
||||
if (editing)
|
||||
{
|
||||
mixText(m_edit.Text());
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
void SignalPanel::PaintRowCached(gd::Graphics& g, int row)
|
||||
{
|
||||
if (m_dataSet == nullptr || m_tab == nullptr || m_theme == nullptr || row < 0 ||
|
||||
row >= static_cast<int>(m_rows.size()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const gd::RectF rect = RowRect(row);
|
||||
if (rect.Height <= 0.5f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const int originX = static_cast<int>(std::floor(m_listRect.X));
|
||||
const int originY = static_cast<int>(std::floor(rect.Y));
|
||||
const int width = static_cast<int>(std::ceil(m_listRect.Width));
|
||||
const int height = static_cast<int>(std::ceil(rect.Height));
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// ⚠️ 上限检查必须在**取引用之前**:`InvalidateRowCache()` 会清空容器,
|
||||
// 清理后原有引用(`RowBitmapCache&`)就悬空了。
|
||||
if (m_rowBitmapLiveCount >= kRowBitmapCacheLimit)
|
||||
{
|
||||
InvalidateRowCache();
|
||||
}
|
||||
if (m_rowBitmapCache.size() < m_rows.size())
|
||||
{
|
||||
m_rowBitmapCache.resize(m_rows.size());
|
||||
}
|
||||
RowBitmapCache& cache = m_rowBitmapCache[static_cast<std::size_t>(row)];
|
||||
const std::uint64_t key = RowCacheKey(row);
|
||||
const bool sameSize = (cache.dc != nullptr) && cache.width == width && cache.height == height;
|
||||
if (!sameSize || cache.key != key)
|
||||
{
|
||||
if (!sameSize)
|
||||
{
|
||||
if (cache.dc != nullptr)
|
||||
{
|
||||
DeleteDC(cache.dc);
|
||||
cache.dc = nullptr;
|
||||
if (m_rowBitmapLiveCount > 0)
|
||||
{
|
||||
--m_rowBitmapLiveCount;
|
||||
}
|
||||
}
|
||||
if (cache.dib != nullptr)
|
||||
{
|
||||
DeleteObject(cache.dib);
|
||||
cache.dib = nullptr;
|
||||
}
|
||||
// ⚠️ DIB + 内存 DC(而不是 `gd::Bitmap`):GDI 在 GDI+ 位图的临时 HDC 上
|
||||
// 会关闭 ClearType,导致列表文字丢失亚像素渲染、与正文观感不一致。
|
||||
BITMAPINFO info = {};
|
||||
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
info.bmiHeader.biWidth = width;
|
||||
info.bmiHeader.biHeight = -height; // 自上而下
|
||||
info.bmiHeader.biPlanes = 1;
|
||||
info.bmiHeader.biBitCount = 32;
|
||||
info.bmiHeader.biCompression = BI_RGB;
|
||||
void* bits = nullptr;
|
||||
HDC memoryDc = CreateCompatibleDC(nullptr);
|
||||
HBITMAP dib = (memoryDc != nullptr)
|
||||
? CreateDIBSection(memoryDc, &info, DIB_RGB_COLORS, &bits, nullptr, 0)
|
||||
: nullptr;
|
||||
if (memoryDc == nullptr || dib == nullptr)
|
||||
{
|
||||
if (dib != nullptr)
|
||||
{
|
||||
DeleteObject(dib);
|
||||
}
|
||||
if (memoryDc != nullptr)
|
||||
{
|
||||
DeleteDC(memoryDc);
|
||||
}
|
||||
return;
|
||||
}
|
||||
SelectObject(memoryDc, dib);
|
||||
cache.dc = memoryDc;
|
||||
cache.dib = dib;
|
||||
cache.width = width;
|
||||
cache.height = height;
|
||||
++m_rowBitmapLiveCount;
|
||||
}
|
||||
{
|
||||
gd::Graphics rowGraphics(cache.dc);
|
||||
rowGraphics.SetSmoothingMode(gd::SmoothingModeAntiAlias);
|
||||
rowGraphics.SetPixelOffsetMode(gd::PixelOffsetModeHalf);
|
||||
rowGraphics.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit);
|
||||
rowGraphics.Clear(ToGdiColor(m_theme->panelBg));
|
||||
// ⚠️ 用**几何平移**而不是 `TranslateTransform`(见 `m_rowRenderActive`)。
|
||||
m_rowRenderActive = true;
|
||||
m_rowRenderOriginX = static_cast<float>(originX);
|
||||
m_rowRenderOriginY = static_cast<float>(originY);
|
||||
m_rowClipRender = true;
|
||||
if (m_rows[static_cast<std::size_t>(row)].kind == RowKind::GROUP)
|
||||
{
|
||||
PaintGroupRow(rowGraphics, row);
|
||||
}
|
||||
else
|
||||
{
|
||||
PaintSignalRow(rowGraphics, row);
|
||||
}
|
||||
m_rowClipRender = false;
|
||||
m_rowRenderActive = false;
|
||||
}
|
||||
cache.key = key;
|
||||
}
|
||||
// ⚠️ 整块 1:1 搬运(不经过 GDI+ 采样/混合),字的亚像素边缘才能原样保留。
|
||||
// ⚠️ **必须自己给 DC 设裁剪**:`Graphics::GetHDC()` 交给我们的 DC 上,(a) GDI 绘制
|
||||
// 不受 GDI+ `SetClip` 的约束(这是本文件多处注释过的坑),(b) 折起/展开动画时行会
|
||||
// 滑进组名栏,必须裁到 `m_rowClipRect`。因此这里显式 `IntersectClipRect` 后再 `BitBlt`。
|
||||
const HDC targetDc = g.GetHDC();
|
||||
if (targetDc != nullptr)
|
||||
{
|
||||
const int saved = SaveDC(targetDc);
|
||||
if (saved != 0)
|
||||
{
|
||||
const gd::RectF clip = m_rowClipActive ? m_rowClipRect : m_listRect;
|
||||
IntersectClipRect(targetDc, 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)));
|
||||
}
|
||||
BitBlt(targetDc, originX, originY, width, height, cache.dc, 0, 0, SRCCOPY);
|
||||
if (saved != 0)
|
||||
{
|
||||
RestoreDC(targetDc, saved);
|
||||
}
|
||||
g.ReleaseHDC(targetDc);
|
||||
}
|
||||
}
|
||||
void SignalPanel::PaintGroupRow(gd::Graphics& g, int row)
|
||||
{
|
||||
if (m_dataSet == nullptr || m_tab == nullptr || row < 0 || row >= static_cast<int>(m_rows.size()))
|
||||
|
||||
Reference in new issue
Block a user