refactor(panels): 面板静态特征收敛为唯一登记表 + 标记面板绘制分段(§17.6 第 1/2 项)

- 新增 pvdetail::PanelKindTraits + PanelTraitsOf(唯一 switch):标题 / 排版类别 /
  滚动字段 / 面板状态 / 功能开关同表登记;新增面板只改「枚举 + 一行」,
  -Wswitch 仍在唯一登记点生效。PanelTitle 改为「前缀 + rangeSuffix 标志」,
  不再依赖 switch(标题降级为数据,不再需要函数指针)。
- PaintPanelMarkerGroups(约 340 行)拆成 6 个具名函数,主函数只保留绘制顺序
  (普通分组栏 -> 条目 -> 表头 -> 吸顶栏 -> 滚动条 -> 透明度行);纯搬移、0 字节变化。
- 顺手修正该函数体遗留的多余缩进。

验证:release/debug/tools 零 warning;UnitTests 260/19;OffscreenChecks -all 7/133;
panels -mode compare 7 面板像素哈希逐项不变;rowgeom compare 965+7 / 1268+7 键 0 差异。
产物 781,824 -> 782,336 B(+512,全部来自登记表)。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-04 17:22:06 +08:00
1 parent a9f1563896
commit 0c26da07d2
8 files changed
+761 -453

No files matched your search

+35 -1
View File
@@ -1201,7 +1201,8 @@ namespace jdi
* @brief 设置某种面板的**内容滚动偏移**(客户区像素,0 = 顶部)—— 唯一实现。
*
* ⚠️ 下面七个 `SetLane*Scroll` 都只是本函数的一行转发(保留是为了不改调用点与原语义)。
* 新增面板时**只需**在 `LanePanelScrollOf` / `PanelStyleOf` / `PanelTitle` 三处登记,
* 新增面板时**只需**在 `pvdetail::PanelTraitsOf`(`PlotViewInternal.hpp`)加一行:
* 标题 / 排版类别 / 滚动字段 / 面板状态 / 功能开关都在那一行里,
* 不要再复制一份"取字段 → 夹取 → 比较 → 写回"的实现。
*
* @param [IN] laneIndex 网格下标。
@@ -2574,6 +2575,39 @@ namespace jdi
*/
const std::vector<FreeMarkerContentEntry>& FreeMarkerContents(int laneIndex) const;
// ---- 「自由标记」面板内容绘制的**分段入口**(第 41 轮)-----------------------
// `PaintPanelMarkerGroups` 只负责**顺序**(普通分组栏 → 条目 → 表头 → 吸顶分组栏 →
// 滚动条 → 透明度滑块),各段的细节在这里。提取是纯搬移,
// 判据 = `panels -mode compare`(像素哈希)+ `rowgeom -mode compare`(逐行几何)。
// ⚠️ 顺序本身是硬约束:表头与吸顶分组栏必须画在条目**之后**,否则向上滚动的条目
// 会盖住它们(这是原实现里唯一真正需要"读整段代码才看得见"的约束)。
/** @brief 把「自由标记」六列的实测文本宽度写回 `stored`(= `Lane::freeMarkerColumnWidths`)。 */
void MeasureMarkerColumnWidths(const Lane& tabLane,
const std::vector<FreeMarkerContentEntry>& contents,
std::array<float, 6>& stored);
/** @brief 「自由标记」列分隔竖线(数据行与表头共用同一套列边界)。 */
void PaintMarkerColumnSeparators(gd::Graphics& g, const EdgePanelLayout& layout, float y,
float height);
/** @brief 「自由标记」一个信号分组栏(三角 + 名称 + `N 标记点` + ✕)。 */
void PaintMarkerGroupBar(gd::Graphics& g, const EdgePanelLayout& layout, const Lane& tabLane,
const EdgePanelLayout::MarkerGroup& group, std::size_t laneIndex);
/** @brief 「自由标记」一个固定点条目(六列 + 分隔线 + 行尾 ✕)。 */
void PaintMarkerRow(gd::Graphics& g, const EdgePanelLayout& layout, const Lane& tabLane,
const EdgePanelLayout::MarkerRow& markerRow,
const FreeMarkerContentEntry& entry, std::size_t laneIndex);
/** @brief 「自由标记」一个展开分组的表头行(不透明底衬 + 六列标题)。 */
void PaintMarkerTableHeader(gd::Graphics& g, const EdgePanelLayout& layout,
const EdgePanelLayout::MarkerGroup& group);
/** @brief 「自由标记」底部「标记透明度」滑块行。 */
void PaintMarkerOpacityRow(gd::Graphics& g, const EdgePanelLayout& layout, const Lane& tabLane,
std::size_t laneIndex);
/** 按网格下标的「自由标记」内容缓存(`mutable`:只在绘制时按需重建)。 */
mutable std::vector<FreeMarkerContentCache> m_freeMarkerContent;
+98 -108
View File
@@ -1096,99 +1096,21 @@ namespace jdi
// ⚠️ 这些 `switch` **不得写 `default:`** —— 让 `-Wswitch` 在新增
// `EdgePanelKind` 而漏登记时直接报警(编译期保护,0 运行时代价)。
/** @brief 取某种面板的 UI 状态(展开/折起/位置)——可写版本。 */
inline LanePanelState& LanePanelStateOf(Lane& lane, EdgePanelKind kind)
{
switch (kind)
{
case EdgePanelKind::COUNT:
return lane.edgePanel;
case EdgePanelKind::CENTER:
return lane.edgeCenterPanel;
case EdgePanelKind::MEAN:
return lane.meanFilterPanel;
case EdgePanelKind::PEAK:
return lane.weakPeakPanel;
case EdgePanelKind::MARK:
return lane.freeMarkerPanel;
case EdgePanelKind::RESAMPLE:
return lane.resamplePanel;
case EdgePanelKind::CLIP:
return lane.clipPanel;
}
// 枚举值越界(只可能来自非法强制转换):返回一个安全的占位对象。
return lane.edgePanel;
}
/** @brief 取某种面板的 UI 状态——只读版本。 */
inline const LanePanelState& LanePanelStateOf(const Lane& lane, EdgePanelKind kind)
{
return LanePanelStateOf(const_cast<Lane&>(lane), kind);
}
/** @brief 取某种面板的**内容滚动偏移**(客户区像素)——可写版本。 */
inline float& LanePanelScrollOf(Lane& lane, EdgePanelKind kind)
{
switch (kind)
{
case EdgePanelKind::COUNT:
return lane.edgeCountScroll;
case EdgePanelKind::CENTER:
return lane.edgeCenterScroll;
case EdgePanelKind::MEAN:
return lane.meanFilterScroll;
case EdgePanelKind::PEAK:
return lane.weakPeakScroll;
case EdgePanelKind::MARK:
return lane.freeMarkerScroll;
case EdgePanelKind::RESAMPLE:
return lane.resampleScroll;
case EdgePanelKind::CLIP:
return lane.clipScroll;
}
return lane.meanFilterScroll;
}
/** @brief 取某种面板的**内容滚动偏移**——只读版本。 */
inline const float& LanePanelScrollOf(const Lane& lane, EdgePanelKind kind)
{
return LanePanelScrollOf(const_cast<Lane&>(lane), kind);
}
/** @brief 该面板对应的功能是否已启用(右键菜单里的开关)。 */
inline bool LanePanelEnabled(const Lane& lane, EdgePanelKind kind)
{
switch (kind)
{
case EdgePanelKind::COUNT:
return lane.edgeCount;
case EdgePanelKind::CENTER:
return lane.edgeCenter;
case EdgePanelKind::MEAN:
return lane.meanFilter;
case EdgePanelKind::PEAK:
return lane.weakPeak;
case EdgePanelKind::MARK:
return lane.freeMarker;
case EdgePanelKind::RESAMPLE:
return lane.resample;
case EdgePanelKind::CLIP:
return lane.clip;
}
return false;
}
// ---- 面板的「排版类别」与「滚动几何」的唯一来源 --------------------------------
// ---- 面板的「排版类别」与「静态特征」的唯一来源 --------------------------------
//
// ⚠️⚠️ 为什么要有这一层:面板的**分类布尔量**过去在三个地方各写一份
// (`EdgePanelWidth` / `EdgePanelLayoutFor` / `PaintEdgeCursor`),
// 每份都是 6 个 `kind == XXX` + 3 条组合判断。新增一种面板时若只改其中两处,
// 就会出现"宽度按 A 类算、绘制按 B 类画"的错位 —— 这正是历史上
// 「元素超出浮动面板」「内容区多出一段空白」「滚动条画在表头上」这类缺陷的共同形态。
// 现在只在这里分派一次,三个消费者都读同一份结果。
// 现在只在这里登记一次,所有消费者都读同一份结果。
//
// ⚠️ 同样**不写 `default:`**:新增 `EdgePanelKind` 而未登记时,
// ⚠️ `PanelTraitsOf` 的 `switch` **不写 `default:`**:新增 `EdgePanelKind` 而未登记时,
// `-Wswitch`(属 `-Wall`)会直接编译报警。
//
// ⚠️ 标题里只有「边沿计数」「中心轴边沿计数」需要追加运行时后缀(`EdgeRangeSuffix()`
// 取决于双光标是否启用),因此表里存的是**标题前缀 + 一个标志**,
// 而不是 `std::wstring`(无法 constexpr)或函数指针(会把"数据"重新变成"代码")。
/**
* @brief 一种浮动面板的排版类别(互斥 + 若干可选部件)。
@@ -1225,40 +1147,108 @@ namespace jdi
bool footerButtons() const { return buildButton || twoButtons; }
};
/** @brief 取某种面板的排版类别(唯一分类来源)。 */
inline PanelStyle PanelStyleOf(EdgePanelKind kind)
/** @brief `PanelStyle` 的三个具名构造函数(聚合初始化顺序易错,这里给出语义化入口)。 */
inline constexpr PanelStyle ListPanelStyle(bool buildButton, bool twoButtons) noexcept
{
return PanelStyle{true, false, false, buildButton, twoButtons, false};
}
inline constexpr PanelStyle CountTablePanelStyle() noexcept
{
return PanelStyle{false, true, false, false, false, false};
}
inline constexpr PanelStyle MarkerPanelStyle() noexcept
{
return PanelStyle{false, false, true, false, false, true};
}
/**
* @brief 一种浮动面板的全部**静态特征**(排版类别 / 标题 / 三个字段偏移)。
*
* 新增一种面板时**只允许**改三处:`EdgePanelKind` 加一个枚举值、
* 本表加一行、`PanelTitle` 不需要动(标题就在这一行里)。
* 其余消费者(宽度 / 布局 / 绘制 / 滚动 / 命中 / 调试盒 / 缓存键)全部自动跟上。
*/
struct PanelKindTraits
{
PanelStyle style; /**< 排版类别与底部控件。 */
const wchar_t* title = L""; /**< 标题前缀(`rangeSuffix` 为真时再追加区间后缀)。 */
bool rangeSuffix = false; /**< 标题是否追加 `EdgeRangeSuffix()`(仅两种边沿计数)。 */
float Lane::*scroll = nullptr; /**< 内容滚动偏移。 */
LanePanelState Lane::*panel = nullptr; /**< 面板 UI 状态(展开/折起/位置)。 */
bool Lane::*enabled = nullptr; /**< 功能开关(右键菜单里的勾选项)。 */
};
/** @brief 取某种面板的全部静态特征(**唯一登记点**)。 */
inline constexpr PanelKindTraits PanelTraitsOf(EdgePanelKind kind)
{
PanelStyle style;
switch (kind)
{
case EdgePanelKind::COUNT:
style.countTable = true;
break;
return PanelKindTraits{CountTablePanelStyle(), L"边沿计数", true,
&Lane::edgeCountScroll, &Lane::edgePanel, &Lane::edgeCount};
case EdgePanelKind::CENTER:
style.listRows = true;
break;
return PanelKindTraits{ListPanelStyle(false, false), L"中心轴边沿计数", true,
&Lane::edgeCenterScroll, &Lane::edgeCenterPanel,
&Lane::edgeCenter};
case EdgePanelKind::MEAN:
style.listRows = true;
style.buildButton = true;
break;
return PanelKindTraits{ListPanelStyle(true, false), L"均值滤波", false,
&Lane::meanFilterScroll, &Lane::meanFilterPanel,
&Lane::meanFilter};
case EdgePanelKind::PEAK:
style.listRows = true;
style.buildButton = true;
break;
return PanelKindTraits{ListPanelStyle(true, false), kWeakPeakTitle, false,
&Lane::weakPeakScroll, &Lane::weakPeakPanel, &Lane::weakPeak};
case EdgePanelKind::MARK:
style.markerGroups = true;
style.markerFooter = true;
break;
return PanelKindTraits{MarkerPanelStyle(), L"自由标记", false,
&Lane::freeMarkerScroll, &Lane::freeMarkerPanel,
&Lane::freeMarker};
case EdgePanelKind::RESAMPLE:
style.listRows = true;
style.buildButton = true;
break;
return PanelKindTraits{ListPanelStyle(true, false), kResampleTitle, false,
&Lane::resampleScroll, &Lane::resamplePanel, &Lane::resample};
case EdgePanelKind::CLIP:
style.listRows = true;
style.twoButtons = true;
break;
return PanelKindTraits{ListPanelStyle(false, true), kClipTitle, false,
&Lane::clipScroll, &Lane::clipPanel, &Lane::clip};
}
return style;
// 枚举值越界(只可能来自非法强制转换):退到「均值滤波」这份,保证指针非空。
return PanelKindTraits{ListPanelStyle(true, false), L"", false, &Lane::meanFilterScroll,
&Lane::meanFilterPanel, &Lane::meanFilter};
}
/** @brief 取某种面板的排版类别(唯一分类来源)。 */
inline PanelStyle PanelStyleOf(EdgePanelKind kind)
{
return PanelTraitsOf(kind).style;
}
/** @brief 取某种面板的 UI 状态(展开/折起/位置)——可写版本。 */
inline LanePanelState& LanePanelStateOf(Lane& lane, EdgePanelKind kind)
{
return lane.*(PanelTraitsOf(kind).panel);
}
/** @brief 取某种面板的 UI 状态——只读版本。 */
inline const LanePanelState& LanePanelStateOf(const Lane& lane, EdgePanelKind kind)
{
return LanePanelStateOf(const_cast<Lane&>(lane), kind);
}
/** @brief 取某种面板的**内容滚动偏移**(客户区像素)——可写版本。 */
inline float& LanePanelScrollOf(Lane& lane, EdgePanelKind kind)
{
return lane.*(PanelTraitsOf(kind).scroll);
}
/** @brief 取某种面板的**内容滚动偏移**——只读版本。 */
inline const float& LanePanelScrollOf(const Lane& lane, EdgePanelKind kind)
{
return LanePanelScrollOf(const_cast<Lane&>(lane), kind);
}
/** @brief 该面板对应的功能是否已启用(右键菜单里的开关)。 */
inline bool LanePanelEnabled(const Lane& lane, EdgePanelKind kind)
{
return lane.*(PanelTraitsOf(kind).enabled);
}
/**
+365 -327
View File
@@ -592,347 +592,385 @@ namespace jdi
}
/**
* @brief Marker panel content: per-signal group bars (collapsible) + table header +
* fixed-point rows, with sticky group bars.
// ---------------------------------------------------------------------------
// 「自由标记」面板内容(第 41 轮拆分)
//
// 原实现是一个约 340 行的单函数,内含 5 段互不相干的绘制(分组栏 / 列宽测量 /
// 条目行 / 表头 / 透明度滑块)。任何一段出问题都要在 300 行里找,而"表头与吸顶分组栏
// 必须最后画"这条**唯一真实的顺序约束**被埋在一个循环中间。现在拆成 6 个具名函数,
// `PaintPanelMarkerGroups` 只负责顺序,判据 = `panels -mode compare` +
// `rowgeom -mode compare`(两者都必须零差异)。
// ---------------------------------------------------------------------------
/**
* @brief 把「自由标记」六列的实测文本宽度写回 `Lane::freeMarkerColumnWidths`。
*
* Paint order is rows -> header -> sticky group bar (the last one covers rows that
* scroll up). Rows hidden under a sticky bar are already excluded by the layout.
* 含滚出视界的条目(列宽必须稳定,否则滚动时列会跳动);下一步的布局
* (`EdgePanelWidth` / `EdgePanelLayoutFor`)直接读这份宽度。
*/
void PlotView::MeasureMarkerColumnWidths(const Lane& tabLane,
const std::vector<FreeMarkerContentEntry>& contents,
std::array<float, 6>& stored)
{
float widest[6] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
const auto measure = [this, &widest](std::size_t column, const std::wstring& text) {
if (text.empty() || column >= 6)
{
return;
}
widest[column] = std::max(widest[column], UiMeasureTextWidth(m_fonts->monoSmall, text));
};
// 「序号」列:取本网格所有分组里最大的序号文本宽度。
std::size_t maxSerial = 0;
std::vector<std::size_t> serialOrder;
for (const SignalRef& ref : tabLane.signals)
{
SortedFreeMarkerIndices(tabLane, ref, serialOrder);
maxSerial = std::max(maxSerial, serialOrder.size());
}
if (maxSerial > 0)
{
measure(EdgePanelLayout::MARKER_COL_SERIAL, FormatInteger(maxSerial));
}
for (const FreeMarkerContentEntry& entry : contents)
{
measure(EdgePanelLayout::MARKER_COL_VALUE, entry.valueText);
measure(EdgePanelLayout::MARKER_COL_ENUM, entry.enumText);
measure(EdgePanelLayout::MARKER_COL_DELTA, entry.deltaText);
measure(EdgePanelLayout::MARKER_COL_TIME, entry.timeText);
measure(EdgePanelLayout::MARKER_COL_DT, entry.deltaTimeText);
}
for (std::size_t column = 0; column < 6; ++column)
{
// +2px 余量:`DrawTextW` 的 `DT_END_ELLIPSIS` 对"刚好装满"的文本
// 有时仍会截成省略号(亚像素/字形伸部),留一点余量可彻底避免。
const float want = (widest[column] > 0.0f) ? (widest[column] + 2.0f * m_scale) : 0.0f;
if (std::fabs(stored[column] - want) > 0.5f)
{
stored[column] = want;
}
}
}
/**
* @brief 「自由标记」列分隔竖线(数据行与表头共用同一套列边界)。
*
* 在每列右边界(加上半个列间隙)画一条淡竖线,让「序号 / 标记值 / 枚举文本 / 增量 /
* 时间点 / 时间间隔」有明显的列分界(用户 2026-10-23 要求)。
* 最后一列不画(它右边紧接行尾 ✕)。
*/
void PlotView::PaintMarkerColumnSeparators(gd::Graphics& g, const EdgePanelLayout& layout,
float y, float height)
{
int lastVisible = -1;
for (int column = 0; column < 6; ++column)
{
if (layout.markerColumns[column].width > 0.0f)
{
lastVisible = column;
}
}
for (int column = 0; column < lastVisible; ++column)
{
const EdgePanelLayout::MarkerColumn& cell = layout.markerColumns[column];
if (cell.width <= 0.0f)
{
continue;
}
const float lineX = cell.left + cell.width + kFreeMarkerColumnGap * m_scale * 0.5f;
UiDrawLine(g, lineX, y + 2.0f * m_scale, lineX, y + height - 2.0f * m_scale,
WithAlpha(m_theme->borderStrong, 0x6C), 1.0f);
}
}
/**
* @brief 「自由标记」一个信号分组栏(三角 + 名称 + `N 标记点` + ✕)。
*
* 底色 = 信号色;文字一律用 `UiContrastInk` 算出的自适应对比色。吸顶时这一栏要压住
* 下面滚动的条目,因此底色先铺**完全不透明**的同色、再叠 0xE8(观感不变)。
*/
void PlotView::PaintMarkerGroupBar(gd::Graphics& g, const EdgePanelLayout& layout,
const Lane& tabLane,
const EdgePanelLayout::MarkerGroup& group,
std::size_t laneIndex)
{
const ResolvedSignal resolved = ResolveSignal(group.ref);
if (resolved.data == nullptr)
{
return;
}
const std::uint32_t signalColor =
(resolved.style != nullptr) ? StyleColor(*resolved.style) : m_theme->border;
const UiInk ink = UiContrastInk(signalColor);
UiFillRoundRect(g, group.header, 3.0f * m_scale, signalColor);
UiFillRoundRect(g, group.header, 3.0f * m_scale, WithAlpha(signalColor, 0xE8));
// 左侧三角(与信号选择区域组名同款):朝下 = 展开、朝右 = 收起。
const bool triangleHot = (m_freeMarkerTriangleHoverLane == static_cast<int>(laneIndex) &&
m_freeMarkerTriangleHoverSignal == group.ref);
UiDrawTriangleIcon(g, group.triangle, triangleHot ? m_theme->accentSoft : ink.text,
group.collapsed ? IconDirection::RIGHT : IconDirection::DOWN);
// 右侧:`N 标记点` + ✕(清除该信号全部固定点)。
std::size_t markerCount = 0;
for (const Lane::FreeMarkerItem& entry : tabLane.freeMarkerItems)
{
if (entry.ref == group.ref)
{
++markerCount;
}
}
const std::wstring countText = FormatInteger(markerCount) + L" 标记点";
const float countWidth = UiMeasureTextWidth(m_fonts->monoSmall, countText) + 2.0f * m_scale;
const float countRight = group.close.X - 5.0f * m_scale;
UiTextClipped(g, countText, m_fonts->monoSmall,
gd::RectF(countRight - countWidth, group.header.Y, countWidth,
group.header.Height),
ink.text, TextAlign::RIGHT, layout.content);
const float nameLeft = group.triangle.X + group.triangle.Width + 6.0f * m_scale;
const float nameWidth = std::max(10.0f, countRight - countWidth - 6.0f * m_scale - nameLeft);
UiTextClipped(g, UiEllipsize(resolved.data->name, m_fonts->uiSmall, nameWidth),
m_fonts->uiSmall,
gd::RectF(nameLeft, group.header.Y, nameWidth, group.header.Height),
ink.text, TextAlign::LEFT, layout.content);
const bool closeHot = (m_freeMarkerPanelHoverLane == static_cast<int>(laneIndex) &&
m_freeMarkerPanelHoverSignal == group.ref &&
m_freeMarkerPanelHoverItem == static_cast<std::size_t>(-1));
if (closeHot)
{
UiFillRoundRect(g, group.close, 3.0f * m_scale, WithAlpha(m_theme->danger, 0x77));
}
const float crossPad = 3.5f * m_scale;
const std::uint32_t crossColor = closeHot ? m_theme->danger : ink.text;
UiDrawLine(g, group.close.X + crossPad, group.close.Y + crossPad,
group.close.X + group.close.Width - crossPad,
group.close.Y + group.close.Height - crossPad, crossColor, 1.5f * m_scale);
UiDrawLine(g, group.close.X + group.close.Width - crossPad, group.close.Y + crossPad,
group.close.X + crossPad, group.close.Y + group.close.Height - crossPad,
crossColor, 1.5f * m_scale);
}
/**
* @brief 「自由标记」一个固定点条目(六列 + 分隔线 + 行尾 ✕)。
*
* ⚠️ 面板里的数据列**不得省略**(用户 2026-10-21 明确要求):列宽已按实测文本宽度撑开。
* ⚠️ 文字必须用 `UiTextClipped`(GDI 裁剪):`UiText` 走 `DrawTextW`,**不认 GDI+ 裁剪**
* ⇒ 滚动到内容区上方的条目会把文字画到标题栏上(用户 2026-10-22 报告的显示错误)。
*/
void PlotView::PaintMarkerRow(gd::Graphics& g, const EdgePanelLayout& layout,
const Lane& tabLane,
const EdgePanelLayout::MarkerRow& markerRow,
const FreeMarkerContentEntry& entry, std::size_t laneIndex)
{
// ---- 行的选中 / 悬停高亮(用户 2026-10-22 要求)----
const bool selected = (tabLane.freeMarkerSelected == static_cast<int>(markerRow.itemIndex));
const bool rowHot = (m_freeMarkerRowHoverLane == static_cast<int>(laneIndex) &&
m_freeMarkerRowHoverItem == markerRow.itemIndex);
if (selected || rowHot)
{
UiFillRect(g,
gd::RectF(markerRow.rect.X, markerRow.rect.Y + 1.0f * m_scale,
markerRow.rect.Width, markerRow.rect.Height - 2.0f * m_scale),
selected ? WithAlpha(m_theme->accent, 0x3C) : WithAlpha(m_theme->accent, 0x1E));
}
const auto columnRect = [&](EdgePanelLayout::MarkerColumnIndex column) {
return gd::RectF(layout.markerColumns[column].left, markerRow.rect.Y,
layout.markerColumns[column].width, markerRow.rect.Height);
};
const auto paintCell = [&](EdgePanelLayout::MarkerColumnIndex column,
const std::wstring& text, std::uint32_t color) {
if (layout.markerColumns[column].width <= 0.0f || text.empty())
{
return;
}
UiTextClipped(g, text, m_fonts->monoSmall, columnRect(column), color, TextAlign::LEFT,
layout.content);
};
paintCell(EdgePanelLayout::MARKER_COL_SERIAL, FormatInteger(markerRow.serial),
m_theme->textMuted);
paintCell(EdgePanelLayout::MARKER_COL_VALUE, entry.valueText, m_theme->textStrong);
if (layout.markerHasEnumColumn)
{
paintCell(EdgePanelLayout::MARKER_COL_ENUM, entry.enumText, m_theme->text);
}
paintCell(EdgePanelLayout::MARKER_COL_DELTA, entry.deltaText, m_theme->textMuted);
paintCell(EdgePanelLayout::MARKER_COL_TIME, entry.timeText, m_theme->textMuted);
paintCell(EdgePanelLayout::MARKER_COL_DT, entry.deltaTimeText, m_theme->textMuted);
// 列分隔竖线(用户 2026-10-23 要求:各列之间要有明显分界)。
PaintMarkerColumnSeparators(g, layout, markerRow.rect.Y, markerRow.rect.Height);
// 行尾 ✕:移除该固定点(与之前一致)。
const gd::RectF& closeRect = markerRow.close;
const bool hot = (m_freeMarkerPanelHoverLane == static_cast<int>(laneIndex) &&
m_freeMarkerPanelHoverItem == markerRow.itemIndex);
UiFillRoundRect(g, closeRect, 3.0f * m_scale,
hot ? WithAlpha(m_theme->danger, 0x66) : m_theme->cardBg);
UiDrawRoundRect(g, closeRect, 3.0f * m_scale, hot ? m_theme->danger : m_theme->border, 1.0f);
const float crossPad = 3.5f * m_scale;
const std::uint32_t crossColor = hot ? m_theme->danger : m_theme->textMuted;
UiDrawLine(g, closeRect.X + crossPad, closeRect.Y + crossPad,
closeRect.X + closeRect.Width - crossPad,
closeRect.Y + closeRect.Height - crossPad, crossColor, 1.2f * m_scale);
UiDrawLine(g, closeRect.X + closeRect.Width - crossPad, closeRect.Y + crossPad,
closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad, crossColor,
1.2f * m_scale);
}
/**
* @brief 「自由标记」一个展开分组的表头行(不透明底衬 + 六列标题)。
*
* 不透明底衬是必须的:表头画在条目**之后**,用来盖住向上滚动的条目。
*/
void PlotView::PaintMarkerTableHeader(gd::Graphics& g, const EdgePanelLayout& layout,
const EdgePanelLayout::MarkerGroup& group)
{
if (group.tableHeader.Width <= 0.0f || group.tableHeader.Height <= 0.0f)
{
return;
}
// 不透明底衬(与面板底色 + 少量边框色调混合,观感是“一条浅灰带”)。
UiFillRect(g, group.tableHeader, MixColor(m_theme->cardBg, m_theme->borderStrong, 0.22));
UiDrawLine(g, group.tableHeader.X, group.tableHeader.Y + group.tableHeader.Height - 1.0f,
group.tableHeader.X + group.tableHeader.Width,
group.tableHeader.Y + group.tableHeader.Height - 1.0f,
WithAlpha(m_theme->borderStrong, 0x99), 1.0f);
for (int column = 0; column < 6; ++column)
{
const EdgePanelLayout::MarkerColumn& cell = layout.markerColumns[column];
if (cell.width <= 0.0f)
{
continue;
}
UiTextClipped(g, kFreeMarkerColumnLabels[column], m_fonts->uiSmall,
gd::RectF(cell.left, group.tableHeader.Y, cell.width,
group.tableHeader.Height),
m_theme->textMuted, TextAlign::LEFT, layout.content);
}
// 表头的列分隔竖线(与数据行同一套列边界)。
PaintMarkerColumnSeparators(g, layout, group.tableHeader.Y, group.tableHeader.Height);
}
/**
* @brief 「自由标记」底部「标记透明度」滑块行。
*
* 这一行**总是显示**(与「自由标记点选取」是否开启无关),几何全部取自布局
* (与命中测试同源);外观与信号样式区的「透明度」滑块完全相同(同一 `UiDrawSlider`)。
*/
void PlotView::PaintMarkerOpacityRow(gd::Graphics& g, const EdgePanelLayout& layout,
const Lane& tabLane, std::size_t laneIndex)
{
if (layout.opacityTrack.Width <= 0.0f)
{
return;
}
const gd::RectF& panelRect = layout.panel;
const float labelGap = kEdgePanelPad * m_scale;
UiText(g, kFreeMarkerOpacityLabel, m_fonts->uiSmall,
gd::RectF(panelRect.X + labelGap, layout.opacityValue.Y,
layout.opacityTrack.X - panelRect.X - labelGap,
layout.opacityValue.Height),
m_theme->textMuted, TextAlign::LEFT);
const bool sliderActive =
(m_dragMode == PlotDragMode::MARKER_OPACITY &&
m_markerOpacityDragLane == static_cast<int>(laneIndex)) ||
UiPointInRect(layout.opacityTrack, static_cast<float>(m_mouseX),
static_cast<float>(m_mouseY));
// 外观与信号样式区的「透明度」滑动条完全相同(同一 `UiDrawSlider` 口径)。
UiDrawSlider(g, layout.opacityTrack, static_cast<double>(tabLane.freeMarkerOpacity),
m_theme->accent, sliderActive, m_scale);
const int percent = static_cast<int>(tabLane.freeMarkerOpacity * 100.0f + 0.5f);
UiText(g, FormatInteger(percent) + L"%", m_fonts->monoSmall, layout.opacityValue,
m_theme->textStrong, TextAlign::RIGHT);
}
/**
* @brief 「自由标记」面板的**内容区**(分组栏 + 表头 + 条目 + 吸顶)。
*
* 绘制顺序(硬约束):
* 普通分组栏 → 条目 → 表头 → 吸顶分组栏 → 滚动条 → 透明度滑块
* 后两段必须压在条目之上,否则向上滚动的条目会盖住表头/吸顶栏。
*/
void PlotView::PaintPanelMarkerGroups(gd::Graphics& g, const EdgePanelLayout& layout,
const Lane& tabLane, std::vector<Lane>& tabLanes,
std::size_t laneIndex) {
const Lane& tabLane, std::vector<Lane>& tabLanes,
std::size_t laneIndex)
{
const std::size_t index = laneIndex;
const float groupRowHeight = kFreeMarkerNameRowHeight * m_scale;
bool markerAny = false;
for (const Lane::FreeMarkerItem& item : tabLane.freeMarkerItems)
{
markerAny = markerAny || item.ref.IsValid();
}
const float groupRowHeight = kFreeMarkerNameRowHeight * m_scale;
bool markerAny = false;
for (const Lane::FreeMarkerItem& item : tabLane.freeMarkerItems)
{
markerAny = markerAny || item.ref.IsValid();
}
const int contentState = g.Save();
g.SetClip(layout.content, gd::CombineModeIntersect);
const int contentState = g.Save();
g.SetClip(layout.content, gd::CombineModeIntersect);
if (!markerAny)
{
// ⚠️ 未开启「自由标记点选取」时鼠标是普通鼠标,**单击不会固定**任何数据点
//(用户 2026-10-23 要求)⇒ 提示语必须说清前提,否则用户会以为 bug。
const wchar_t* emptyHint = tabLane.freeMarkerCursor
? L"在网格内单击以固定数据点"
: L"开启「自由标记点选取」后单击网格可固定数据点";
UiTextClipped(g, emptyHint, m_fonts->uiSmall,
gd::RectF(layout.content.X, layout.content.Y, layout.content.Width,
groupRowHeight),
m_theme->textMuted, TextAlign::LEFT, layout.content);
}
if (!markerAny)
{
// ⚠️ 未开启「自由标记点选取」时鼠标是普通鼠标,**单击不会固定**任何数据点
//(用户 2026-10-23 要求)⇒ 提示语必须说清前提,否则用户会以为 bug。
const wchar_t* emptyHint = tabLane.freeMarkerCursor
? L"在网格内单击以固定数据点"
: L"开启「自由标记点选取」后单击网格可固定数据点";
UiTextClipped(g, emptyHint, m_fonts->uiSmall,
gd::RectF(layout.content.X, layout.content.Y, layout.content.Width,
groupRowHeight),
m_theme->textMuted, TextAlign::LEFT, layout.content);
}
// ---- 分组栏 ----
// ⚠️ 抽取成 lambda:**吸顶**的分组栏必须在条目之后重画一次(否则正在向上滚的
// 条目会盖住它)。见下方“条目 → 表头 → 吸顶分组栏”的绘制顺序。
const auto paintGroupBar = [this, &g, &layout, &tabLane, index](
const EdgePanelLayout::MarkerGroup& group)
{
const ResolvedSignal resolved = ResolveSignal(group.ref);
if (resolved.data == nullptr)
{
return;
}
const std::uint32_t signalColor =
(resolved.style != nullptr) ? StyleColor(*resolved.style) : m_theme->border;
// 托底 = 信号色;文字一律用**自适应对比色**(与信号面板的组名徽标同规则),
// 因此无论信号是亮黄还是深蓝都能看清分组栏上的所有内容。
const UiInk ink = UiContrastInk(signalColor);
// 先铺一层**完全不透明**的同色,再叠 0xE8(观感不变):
// 吸顶时它要压住下面滚动的条目,半透明会让条目透出来。
UiFillRoundRect(g, group.header, 3.0f * m_scale, signalColor);
UiFillRoundRect(g, group.header, 3.0f * m_scale, WithAlpha(signalColor, 0xE8));
// 取值与各列文本来自**与视界无关的内容缓存**(`FreeMarkerContents`):
// 大量固定点时逐帧重新采样会让"拖光标 / 平移视界"明显变卡。
const std::vector<FreeMarkerContentEntry>& contents =
FreeMarkerContents(static_cast<int>(index));
// 六列的实测宽度存回 `Lane`:下一步的布局(`EdgePanelWidth` / `EdgePanelLayoutFor`)要用。
MeasureMarkerColumnWidths(tabLane, contents, tabLanes[index].freeMarkerColumnWidths);
// 左侧三角(与信号选择区域组名同款):朝下 = 展开、朝右 = 收起。
const bool triangleHot =
(m_freeMarkerTriangleHoverLane == static_cast<int>(index) &&
m_freeMarkerTriangleHoverSignal == group.ref);
UiDrawTriangleIcon(g, group.triangle,
triangleHot ? m_theme->accentSoft : ink.text,
group.collapsed ? IconDirection::RIGHT
: IconDirection::DOWN);
// ---- 普通分组栏(吸顶的那些留到最后画)----
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
{
if (!group.pinned)
{
PaintMarkerGroupBar(g, layout, tabLane, group, index);
}
}
// 右侧:`N 标记点` + ✕(清除该信号全部固定点)。
std::size_t markerCount = 0;
for (const Lane::FreeMarkerItem& entry : tabLane.freeMarkerItems)
{
if (entry.ref == group.ref)
{
++markerCount;
}
}
const std::wstring countText = FormatInteger(markerCount) + L" 标记点";
const float countWidth =
UiMeasureTextWidth(m_fonts->monoSmall, countText) + 2.0f * m_scale;
const float countRight = group.close.X - 5.0f * m_scale;
UiTextClipped(g, countText, m_fonts->monoSmall,
gd::RectF(countRight - countWidth, group.header.Y, countWidth,
group.header.Height),
ink.text, TextAlign::RIGHT, layout.content);
// ---- 固定点条目(六列,列起点由布局给定 ⇒ 纵向严格对齐)----
for (const EdgePanelLayout::MarkerRow& markerRow : layout.markerRows)
{
if (markerRow.itemIndex >= contents.size())
{
continue;
}
PaintMarkerRow(g, layout, tabLane, markerRow, contents[markerRow.itemIndex], index);
}
const float nameLeft = group.triangle.X + group.triangle.Width + 6.0f * m_scale;
const float nameWidth =
std::max(10.0f, countRight - countWidth - 6.0f * m_scale - nameLeft);
UiTextClipped(g, UiEllipsize(resolved.data->name, m_fonts->uiSmall, nameWidth),
m_fonts->uiSmall,
gd::RectF(nameLeft, group.header.Y, nameWidth, group.header.Height),
ink.text, TextAlign::LEFT, layout.content);
// ---- 表头(每个展开分组一行,与条目共用列边界)----
// ⚠️ 必须在条目**之后**绘制:表头底衬要不透明地盖住向上滚动的条目。
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
{
PaintMarkerTableHeader(g, layout, group);
}
const bool closeHot = (m_freeMarkerPanelHoverLane == static_cast<int>(index) &&
m_freeMarkerPanelHoverSignal == group.ref &&
m_freeMarkerPanelHoverItem == static_cast<std::size_t>(-1));
if (closeHot)
{
UiFillRoundRect(g, group.close, 3.0f * m_scale,
WithAlpha(m_theme->danger, 0x77));
}
const float crossPad = 3.5f * m_scale;
const std::uint32_t crossColor = closeHot ? m_theme->danger : ink.text;
UiDrawLine(g, group.close.X + crossPad, group.close.Y + crossPad,
group.close.X + group.close.Width - crossPad,
group.close.Y + group.close.Height - crossPad, crossColor,
1.5f * m_scale);
UiDrawLine(g, group.close.X + group.close.Width - crossPad,
group.close.Y + crossPad, group.close.X + crossPad,
group.close.Y + group.close.Height - crossPad, crossColor,
1.5f * m_scale);
};
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
{
if (!group.pinned)
{
paintGroupBar(group);
}
}
// ---- 吸顶的分组栏(最后画:压住向上滚动的条目)----
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
{
if (group.pinned)
{
PaintMarkerGroupBar(g, layout, tabLane, group, index);
}
}
// ---- 固定点条目(六列,列起点由布局给定 ⇒ 纵向严格对齐)----
// ⚠️ 取值与各列文本来自**与视界无关的内容缓存**(`FreeMarkerContents`):
// 大量固定点时逐帧重新采样会让"拖光标 / 平移视界"明显变卡。
const std::vector<FreeMarkerContentEntry>& contents =
FreeMarkerContents(static_cast<int>(index));
// 六列的**实测宽度**:含滚出视界的条目,保证列宽稳定、文本不被省略。
// 存回 `Lane` 供下一步的布局(`EdgePanelWidth` / `EdgePanelLayoutFor`)使用。
{
float widest[6] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
const auto measure = [this, &widest](std::size_t column, const std::wstring& text) {
if (text.empty() || column >= 6)
{
return;
}
widest[column] = std::max(widest[column],
UiMeasureTextWidth(m_fonts->monoSmall, text));
};
// 「序号」列:取本网格所有分组里最大的序号文本宽度。
std::size_t maxSerial = 0;
std::vector<std::size_t> serialOrder;
for (const SignalRef& ref : tabLane.signals)
{
SortedFreeMarkerIndices(tabLane, ref, serialOrder);
maxSerial = std::max(maxSerial, serialOrder.size());
}
if (maxSerial > 0)
{
measure(EdgePanelLayout::MARKER_COL_SERIAL, FormatInteger(maxSerial));
}
for (const FreeMarkerContentEntry& entry : contents)
{
measure(EdgePanelLayout::MARKER_COL_VALUE, entry.valueText);
measure(EdgePanelLayout::MARKER_COL_ENUM, entry.enumText);
measure(EdgePanelLayout::MARKER_COL_DELTA, entry.deltaText);
measure(EdgePanelLayout::MARKER_COL_TIME, entry.timeText);
measure(EdgePanelLayout::MARKER_COL_DT, entry.deltaTimeText);
}
std::array<float, 6>& stored =
tabLanes[index].freeMarkerColumnWidths;
for (std::size_t column = 0; column < 6; ++column)
{
// +2px 余量:`DrawTextW` 的 `DT_END_ELLIPSIS` 对"刚好装满"的文本
// 有时仍会截成省略号(亚像素/字形伸部),留一点余量可彻底避免。
const float want =
(widest[column] > 0.0f) ? (widest[column] + 2.0f * m_scale) : 0.0f;
if (std::fabs(stored[column] - want) > 0.5f)
{
stored[column] = want;
}
}
}
// 列分隔竖线:在每列右边界(加上半个列间隙)画一条淡竖线,
// 让「序号 / 标记值 / 枚举文本 / 增量 / 时间点 / 时间间隔」有明显的列分界
//(用户 2026-10-23 要求)。最后一列不画(它右边紧接行尾 ✕)。
const auto paintColumnSeparators = [this, &g, &layout](float y, float height) {
int lastVisible = -1;
for (int column = 0; column < 6; ++column)
{
if (layout.markerColumns[column].width > 0.0f)
{
lastVisible = column;
}
}
for (int column = 0; column < lastVisible; ++column)
{
const EdgePanelLayout::MarkerColumn& cell = layout.markerColumns[column];
if (cell.width <= 0.0f)
{
continue;
}
const float lineX =
cell.left + cell.width + kFreeMarkerColumnGap * m_scale * 0.5f;
UiDrawLine(g, lineX, y + 2.0f * m_scale, lineX, y + height - 2.0f * m_scale,
WithAlpha(m_theme->borderStrong, 0x6C), 1.0f);
}
};
for (const EdgePanelLayout::MarkerRow& markerRow : layout.markerRows)
{
if (markerRow.itemIndex >= contents.size())
{
continue;
}
const FreeMarkerContentEntry& entry = contents[markerRow.itemIndex];
g.Restore(contentState);
// ---- 行的选中 / 悬停高亮(用户 2026-10-22 要求)----
const bool selected =
(tabLane.freeMarkerSelected == static_cast<int>(markerRow.itemIndex));
const bool rowHot = (m_freeMarkerRowHoverLane == static_cast<int>(index) &&
m_freeMarkerRowHoverItem == markerRow.itemIndex);
if (selected || rowHot)
{
UiFillRect(g,
gd::RectF(markerRow.rect.X, markerRow.rect.Y + 1.0f * m_scale,
markerRow.rect.Width,
markerRow.rect.Height - 2.0f * m_scale),
selected ? WithAlpha(m_theme->accent, 0x3C)
: WithAlpha(m_theme->accent, 0x1E));
}
// ---- 内容滚动条(只在内容超高时出现;按钮与滑块不参与滚动)----
PaintPanelScrollBar(g, layout, *m_theme);
const auto columnRect = [&](EdgePanelLayout::MarkerColumnIndex column) {
return gd::RectF(layout.markerColumns[column].left, markerRow.rect.Y,
layout.markerColumns[column].width,
markerRow.rect.Height);
};
// ⚠️ 面板里的数据列**不得省略**(用户 2026-10-21 明确要求):
// 列宽已按实测文本宽度撑开,因此这里直接整段输出,不再 `UiEllipsize`。
// ⚠️ 必须用 `UiTextClipped`(GDI 裁剪):`UiText` 走 `DrawTextW`,
// **不认 GDI+ 裁剪** ⇒ 滚动到内容区上方的条目会把文字画到标题栏上
//(用户 2026-10-22 报告的显示错误)。
const auto paintCell = [&](EdgePanelLayout::MarkerColumnIndex column,
const std::wstring& text, std::uint32_t color) {
if (layout.markerColumns[column].width <= 0.0f || text.empty())
{
return;
}
UiTextClipped(g, text, m_fonts->monoSmall, columnRect(column), color,
TextAlign::LEFT, layout.content);
};
paintCell(EdgePanelLayout::MARKER_COL_SERIAL, FormatInteger(markerRow.serial),
m_theme->textMuted);
paintCell(EdgePanelLayout::MARKER_COL_VALUE, entry.valueText,
m_theme->textStrong);
if (layout.markerHasEnumColumn)
{
paintCell(EdgePanelLayout::MARKER_COL_ENUM, entry.enumText, m_theme->text);
}
paintCell(EdgePanelLayout::MARKER_COL_DELTA, entry.deltaText,
m_theme->textMuted);
paintCell(EdgePanelLayout::MARKER_COL_TIME, entry.timeText, m_theme->textMuted);
paintCell(EdgePanelLayout::MARKER_COL_DT, entry.deltaTimeText,
m_theme->textMuted);
// 列分隔竖线(用户 2026-10-23 要求:各列之间要有明显分界)。
paintColumnSeparators(markerRow.rect.Y, markerRow.rect.Height);
// 行尾 ✕:移除该固定点(与之前一致)。
const gd::RectF& closeRect = markerRow.close;
const bool hot = (m_freeMarkerPanelHoverLane == static_cast<int>(index) &&
m_freeMarkerPanelHoverItem == markerRow.itemIndex);
UiFillRoundRect(g, closeRect, 3.0f * m_scale,
hot ? WithAlpha(m_theme->danger, 0x66) : m_theme->cardBg);
UiDrawRoundRect(g, closeRect, 3.0f * m_scale,
hot ? m_theme->danger : m_theme->border, 1.0f);
const float crossPad = 3.5f * m_scale;
const std::uint32_t crossColor = hot ? m_theme->danger : m_theme->textMuted;
UiDrawLine(g, closeRect.X + crossPad, closeRect.Y + crossPad,
closeRect.X + closeRect.Width - crossPad,
closeRect.Y + closeRect.Height - crossPad, crossColor, 1.2f * m_scale);
UiDrawLine(g, closeRect.X + closeRect.Width - crossPad, closeRect.Y + crossPad,
closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad,
crossColor, 1.2f * m_scale);
}
// ---- 表头(每个展开分组一行,与条目共用列边界)----
// ⚠️ 必须在条目**之后**绘制:表头底衬要不透明地盖住向上滚动的条目。
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
{
if (group.tableHeader.Width <= 0.0f || group.tableHeader.Height <= 0.0f)
{
continue;
}
// 不透明底衬(与面板底色 + 少量边框色调混合,观感是“一条浅灰带”)。
UiFillRect(g, group.tableHeader,
MixColor(m_theme->cardBg, m_theme->borderStrong, 0.22));
UiDrawLine(g, group.tableHeader.X, group.tableHeader.Y + group.tableHeader.Height - 1.0f,
group.tableHeader.X + group.tableHeader.Width,
group.tableHeader.Y + group.tableHeader.Height - 1.0f,
WithAlpha(m_theme->borderStrong, 0x99), 1.0f);
for (int column = 0; column < 6; ++column)
{
const EdgePanelLayout::MarkerColumn& cell =
layout.markerColumns[column];
if (cell.width <= 0.0f)
{
continue;
}
UiTextClipped(g, kFreeMarkerColumnLabels[column], m_fonts->uiSmall,
gd::RectF(cell.left, group.tableHeader.Y, cell.width,
group.tableHeader.Height),
m_theme->textMuted, TextAlign::LEFT, layout.content);
}
// 表头的列分隔竖线(与数据行同一套列边界)。
paintColumnSeparators(group.tableHeader.Y, group.tableHeader.Height);
}
// ---- 吸顶的分组栏(最后画:压住向上滚动的条目)----
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
{
if (group.pinned)
{
paintGroupBar(group);
}
}
g.Restore(contentState);
// ---- 内容滚动条(只在内容超高时出现;按钮与滑块不参与滚动)----
PaintPanelScrollBar(g, layout, *m_theme);
// ---- 「标记透明度」滑动条(固定在「自由标记点选取」按钮上方)----
if (layout.opacityTrack.Width > 0.0f)
{
// 几何全部取自布局(与命中测试同源)。
const gd::RectF& panelRect = layout.panel;
const float labelGap = kEdgePanelPad * m_scale;
UiText(g, kFreeMarkerOpacityLabel, m_fonts->uiSmall,
gd::RectF(panelRect.X + labelGap, layout.opacityValue.Y,
layout.opacityTrack.X - panelRect.X - labelGap,
layout.opacityValue.Height),
m_theme->textMuted, TextAlign::LEFT);
const bool sliderActive =
(m_dragMode == PlotDragMode::MARKER_OPACITY &&
m_markerOpacityDragLane == static_cast<int>(index)) ||
UiPointInRect(layout.opacityTrack, static_cast<float>(m_mouseX),
static_cast<float>(m_mouseY));
// 外观与信号样式区的「透明度」滑动条完全相同(同一 `UiDrawSlider` 口径)。
UiDrawSlider(g, layout.opacityTrack,
static_cast<double>(tabLane.freeMarkerOpacity), m_theme->accent,
sliderActive, m_scale);
const int percent =
static_cast<int>(tabLane.freeMarkerOpacity * 100.0f + 0.5f);
UiText(g, FormatInteger(percent) + L"%", m_fonts->monoSmall,
layout.opacityValue, m_theme->textStrong, TextAlign::RIGHT);
}
// ---- 「标记透明度」滑动条(固定在「自由标记点选取」按钮上方)----
PaintMarkerOpacityRow(g, layout, tabLane, index);
}
} // namespace jdi
+9 -17
View File
@@ -19,25 +19,17 @@ namespace jdi
{
std::wstring PlotView::PanelTitle(EdgePanelKind kind) const
{
// ⚠️ 穷举且不写 `default:`(`-Wswitch` 会在新增面板时报警)。
switch (kind)
// ⚠️ 标题前缀来自**唯一登记表**(`PanelTraitsOf`):新增面板时不需要改这个函数。
// 只有「边沿计数」「中心轴边沿计数」需要追加运行时后缀(`EdgeRangeSuffix()` 取决于
// 双光标是否启用),所以表里存的是"前缀 + 标志"而不是 `std::wstring`(无法 constexpr)
// 或函数指针(会把"数据"重新变成"代码",新增面板又要写一个函数)。
const PanelKindTraits traits = PanelTraitsOf(kind);
std::wstring title(traits.title);
if (traits.rangeSuffix)
{
case EdgePanelKind::COUNT:
return std::wstring(L"边沿计数") + EdgeRangeSuffix();
case EdgePanelKind::CENTER:
return std::wstring(L"中心轴边沿计数") + EdgeRangeSuffix();
case EdgePanelKind::MEAN:
return std::wstring(L"均值滤波");
case EdgePanelKind::PEAK:
return std::wstring(kWeakPeakTitle);
case EdgePanelKind::MARK:
return std::wstring(L"自由标记");
case EdgePanelKind::RESAMPLE:
return std::wstring(kResampleTitle);
case EdgePanelKind::CLIP:
return std::wstring(kClipTitle);
title += EdgeRangeSuffix();
}
return std::wstring();
return title;
}
PlotView::PanelColumnMetrics PlotView::MeasurePanelColumns(const Lane& lane,