Files
Just-DataInspector/Components/SignalPanel/Src/SignalPanel.cpp
T
DeepSeek V4.1 Flash 132b305544 feat: 搭建 Just DataInspector 波形数据查看器基础功能
基于 Win32 API + GDI+ 从零实现仿真数据检查器,用于替代 Simulink.sdi.view 查看带时间轴的 CSV 数据。

- 新增 Utilities 层:UTF-8/宽字符与数值格式化工具、CSV 解析器(自动识别分隔符与引号)、
  信号数据集模型(时间轴识别、信号名解析、取值采样)、Solarised Light 主题色板与绘图基础工具
- 新增 Toolbar 组件:打开 CSV、适应窗口、适应时间、放大、缩小、合并网格、每信号一格
- 新增 SignalPanel 组件:信号列表(可见性开关、颜色标识、所属网格标记)、样式编辑区
  (颜色、线宽、线段/散点/点线/阶梯显示方式、所属网格),支持右键上下文菜单与滚轮滚动
- 新增 PlotView 组件:多网格波形绘制、自适应时间轴与幅值轴、网格线、信号图例、
  时间光标与实时数值读数、纵向滚动条
- 新增 Application 组件:DPI 感知(Per-Monitor V2)、双缓冲绘制、区域布局与分隔条拖拽、
  鼠标与键盘消息分发、文件拖放、状态栏提示
- 交互能力:滚轮缩放、Shift+滚轮平移、Ctrl+滚轮缩放幅值、拖拽平移、幅值轴纵向缩放、
  右键框选缩放、单击固定/取消时间光标、双击自适应、方向键平移与滚动
- 补齐 Makefile 的源文件与头文件索引,并新增 gdiplus、comdlg32 链接库
2026-09-29 15:05:29 +08:00

890 lines
29 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "SignalPanel.hpp"
#include <algorithm>
#include <cmath>
#include "Utf8Util.hpp"
namespace jdi
{
namespace
{
const float kHeaderHeight = 36.0f; /**< 标题区高度。 */
const float kRowHeight = 30.0f; /**< 列表行高。 */
const float kStyleHeight = 182.0f; /**< 样式编辑区高度。 */
const float kPadding = 12.0f; /**< 通用内边距。 */
const float kCheckSize = 15.0f; /**< 复选框尺寸。 */
const float kSwatchSize = 12.0f; /**< 颜色小方块尺寸。 */
const float kBadgeWidth = 30.0f; /**< 网格标签宽度。 */
const float kBadgeHeight = 17.0f; /**< 网格标签高度。 */
const float kCorner = 4.0f; /**< 通用圆角半径。 */
const float kLineLabelWidth = 40.0f; /**< 样式行标签列宽度。 */
/** @brief 上下文菜单命令 ID 基准值。 */
const int kMenuToggleVisible = 1;
const int kMenuSolo = 2;
const int kMenuShowAll = 3;
const int kMenuModeBase = 100;
const int kMenuWidthBase = 200;
const int kMenuColorBase = 300;
const int kMenuLaneBase = 400;
const int kMenuLaneNew = 500;
/** @brief 可选线宽档位。 */
const float kWidthOptions[] = {1.0f, 1.5f, 2.0f, 3.0f};
/** @brief 可选绘制方式顺序。 */
const DrawMode kModeOptions[] = {DrawMode::LINE, DrawMode::SCATTER, DrawMode::LINE_SCATTER, DrawMode::STEP};
}
SignalPanel::SignalPanel()
{
m_colorRects.fill(gd::RectF());
m_widthRects.fill(gd::RectF());
m_modeRects.fill(gd::RectF());
}
void SignalPanel::Setup(HWND host, const UiFonts* fonts, const Theme* theme)
{
m_host = host;
m_fonts = fonts;
m_theme = theme;
}
void SignalPanel::SetDocument(SignalSet* document)
{
m_document = document;
m_scrollOffset = 0.0f;
}
void SignalPanel::SetScale(float scale)
{
m_scale = (scale > 0.1f) ? scale : 1.0f;
}
bool SignalPanel::HitTest(int x, int y) const
{
return UiPointInRect(m_area, static_cast<float>(x), static_cast<float>(y));
}
float SignalPanel::ContentHeight() const
{
if (m_document == nullptr)
{
return 0.0f;
}
return static_cast<float>(m_document->Signals().size()) * kRowHeight * m_scale;
}
void SignalPanel::ClampScroll()
{
const float visibleHeight = m_listRect.Height;
const float contentHeight = ContentHeight();
const float maxOffset = std::max(0.0f, contentHeight - visibleHeight);
m_scrollOffset = std::max(0.0f, std::min(m_scrollOffset, maxOffset));
}
int SignalPanel::RowAtY(int y) const
{
if (m_document == nullptr || m_listRect.Height <= 0.0f)
{
return -1;
}
const float fy = static_cast<float>(y);
if (fy < m_listRect.Y || fy > m_listRect.Y + m_listRect.Height)
{
return -1;
}
const float local = fy - m_listRect.Y + m_scrollOffset;
if (local < 0.0f)
{
return -1;
}
const int row = static_cast<int>(local / (kRowHeight * m_scale));
if (row < 0 || row >= static_cast<int>(m_document->Signals().size()))
{
return -1;
}
return row;
}
gd::RectF SignalPanel::RowRect(int row) const
{
const float rowHeight = kRowHeight * m_scale;
const float top = m_listRect.Y + static_cast<float>(row) * rowHeight - m_scrollOffset;
return gd::RectF(m_listRect.X, top, m_listRect.Width, rowHeight);
}
void SignalPanel::Layout(const gd::RectF& area)
{
m_area = area;
m_headerRect = gd::RectF(area.X, area.Y, area.Width, kHeaderHeight * m_scale);
const float styleHeight = kStyleHeight * m_scale;
m_styleRect = gd::RectF(area.X, area.Y + area.Height - styleHeight, area.Width, styleHeight);
m_listRect = gd::RectF(area.X, m_headerRect.Y + m_headerRect.Height,
area.Width, std::max(0.0f, m_styleRect.Y - m_headerRect.Y - m_headerRect.Height));
// 标题区右侧的“全部显示/隐藏”按钮。
const float toggleWidth = 68.0f * m_scale;
const float toggleHeight = 22.0f * m_scale;
m_toggleAllRect = gd::RectF(area.X + area.Width - kPadding * m_scale - toggleWidth,
m_headerRect.Y + (m_headerRect.Height - toggleHeight) * 0.5f,
toggleWidth, toggleHeight);
// 样式编辑区布局。
const float labelWidth = kLineLabelWidth * m_scale;
m_widthRowLabelX = m_styleRect.X + kPadding * m_scale + labelWidth;
m_styleRowHeight = 30.0f * m_scale;
const float contentWidth = m_styleRect.Width - kPadding * 2.0f * m_scale - labelWidth;
const float contentLeft = m_widthRowLabelX;
int paletteCount = 0;
const std::uint32_t* palette = SignalPalette(paletteCount);
(void)palette;
m_colorCount = std::min(paletteCount, static_cast<int>(m_colorRects.size()));
// 色块尺寸按可用宽度自适应,避免超出面板。
const float colorGap = 4.0f * m_scale;
float swatchSize = 20.0f * m_scale;
if (m_colorCount > 1)
{
const float fitted = (contentWidth - colorGap * static_cast<float>(m_colorCount - 1)) /
static_cast<float>(m_colorCount);
if (fitted < swatchSize)
{
swatchSize = std::max(10.0f * m_scale, fitted);
}
}
const float colorRowTop = m_styleRect.Y + 30.0f * m_scale;
for (int index = 0; index < m_colorCount; ++index)
{
const float x = contentLeft + static_cast<float>(index) * (swatchSize + colorGap);
m_colorRects[static_cast<std::size_t>(index)] =
gd::RectF(x, colorRowTop, swatchSize, swatchSize);
}
const float widthRowTop = colorRowTop + swatchSize + 12.0f * m_scale;
const float widthGap = 6.0f * m_scale;
const float widthButtonWidth = std::min(56.0f * m_scale,
(contentWidth - widthGap * 3.0f) / 4.0f);
for (int index = 0; index < 4; ++index)
{
const float x = contentLeft + static_cast<float>(index) * (widthButtonWidth + widthGap);
m_widthRects[static_cast<std::size_t>(index)] =
gd::RectF(x, widthRowTop, widthButtonWidth, 26.0f * m_scale);
}
const float modeRowTop = widthRowTop + 26.0f * m_scale + 10.0f * m_scale - 2.0f * m_scale;
const float modeGap = 6.0f * m_scale;
const float modeButtonWidth = (contentWidth - modeGap * 3.0f) / 4.0f;
for (int index = 0; index < 4; ++index)
{
const float x = contentLeft + static_cast<float>(index) * (modeButtonWidth + modeGap);
m_modeRects[static_cast<std::size_t>(index)] =
gd::RectF(x, modeRowTop, modeButtonWidth, 26.0f * m_scale);
}
const float laneRowTop = modeRowTop + 26.0f * m_scale + 14.0f * m_scale;
m_laneButtonRect = gd::RectF(contentLeft, laneRowTop,
std::min(contentWidth, 150.0f * m_scale), 26.0f * m_scale);
ClampScroll();
}
void SignalPanel::Paint(gd::Graphics& g)
{
if (m_theme == nullptr || m_fonts == nullptr)
{
return;
}
UiFillRect(g, m_area, m_theme->panelBg);
// 标题区。
{
const std::wstring title =
(m_document == nullptr)
? std::wstring(L"信号")
: (L"信号 · " + FormatCount(static_cast<unsigned long long>(m_document->Signals().size())));
gd::RectF titleRect(m_headerRect.X + kPadding * m_scale, m_headerRect.Y,
m_headerRect.Width - kPadding * 2.0f * m_scale, m_headerRect.Height);
UiText(g, title, m_fonts->uiBold, titleRect, m_theme->textStrong, TextAlign::LEFT);
const bool allVisible = AreAllSignalsVisible();
const std::wstring toggleLabel = allVisible ? L"全部隐藏" : L"全部显示";
const bool toggleHover = (m_hoverStyle == StyleHit::TOGGLE_ALL);
if (toggleHover)
{
UiFillRoundRect(g, m_toggleAllRect, kCorner * m_scale, m_theme->hover);
}
UiDrawRoundRect(g, m_toggleAllRect, kCorner * m_scale, m_theme->border, 1.0f);
UiText(g, toggleLabel, m_fonts->uiSmall, m_toggleAllRect,
(m_document == nullptr) ? m_theme->textMuted : m_theme->text, TextAlign::CENTER);
}
// 列表区。
{
const int saveState = g.Save();
g.SetClip(gd::RectF(m_listRect.X, m_listRect.Y, m_listRect.Width, m_listRect.Height));
if (m_document == nullptr || m_document->Signals().empty())
{
UiText(g, L"尚未加载数据,请打开 CSV 文件", m_fonts->uiSmall, m_listRect, m_theme->textMuted,
TextAlign::CENTER);
}
else
{
const int rowCount = static_cast<int>(m_document->Signals().size());
for (int row = 0; row < rowCount; ++row)
{
const gd::RectF rect = RowRect(row);
if (rect.Y + rect.Height < m_listRect.Y || rect.Y > m_listRect.Y + m_listRect.Height)
{
continue;
}
PaintRow(g, row);
}
}
g.Restore(saveState);
}
PaintStyleSection(g);
// 分隔线。
UiDrawLine(g, m_area.X + m_area.Width - 1.0f, m_area.Y, m_area.X + m_area.Width - 1.0f,
m_area.Y + m_area.Height, m_theme->border, 1.0f);
UiDrawLine(g, m_headerRect.X, m_headerRect.Y + m_headerRect.Height - 1.0f, m_headerRect.X + m_headerRect.Width,
m_headerRect.Y + m_headerRect.Height - 1.0f, m_theme->border, 1.0f);
if (m_styleRect.Height > 0.0f)
{
UiDrawLine(g, m_styleRect.X, m_styleRect.Y, m_styleRect.X + m_styleRect.Width, m_styleRect.Y,
m_theme->border, 1.0f);
}
}
void SignalPanel::PaintRow(gd::Graphics& g, int row)
{
if (m_document == nullptr || row < 0 || row >= static_cast<int>(m_document->Signals().size()))
{
return;
}
const Signal& signal = m_document->Signals()[static_cast<std::size_t>(row)];
const gd::RectF rect = RowRect(row);
const bool selected = (m_document->SelectedSignal() == row);
if (selected)
{
UiFillRect(g, rect, m_theme->selection);
UiFillRect(g, gd::RectF(rect.X, rect.Y, 2.0f * m_scale, rect.Height), m_theme->accent);
}
else if (m_hoverRow == row)
{
UiFillRect(g, rect, m_theme->hover);
}
const float centerY = rect.Y + rect.Height * 0.5f;
// 复选框。
const gd::RectF checkRect(rect.X + kPadding * m_scale, centerY - kCheckSize * m_scale * 0.5f,
kCheckSize * m_scale, kCheckSize * m_scale);
if (signal.visible)
{
UiFillRoundRect(g, checkRect, 3.0f * m_scale, m_theme->accent);
gd::Pen pen(ToGdiColor(m_theme->textOnAccent), 1.6f * m_scale);
pen.SetStartCap(gd::LineCapRound);
pen.SetEndCap(gd::LineCapRound);
g.DrawLine(&pen, checkRect.X + 3.0f * m_scale, centerY,
checkRect.X + 6.0f * m_scale, checkRect.Y + checkRect.Height - 4.0f * m_scale);
g.DrawLine(&pen, checkRect.X + 6.0f * m_scale, checkRect.Y + checkRect.Height - 4.0f * m_scale,
checkRect.X + checkRect.Width - 3.0f * m_scale, checkRect.Y + 4.0f * m_scale);
}
else
{
UiFillRoundRect(g, checkRect, 3.0f * m_scale, m_theme->cardBg);
UiDrawRoundRect(g, checkRect, 3.0f * m_scale, m_theme->borderStrong, 1.0f);
}
// 颜色块。
const gd::RectF swatchRect(checkRect.X + checkRect.Width + 10.0f * m_scale,
centerY - kSwatchSize * m_scale * 0.5f, kSwatchSize * m_scale,
kSwatchSize * m_scale);
UiFillRoundRect(g, swatchRect, 2.5f * m_scale,
signal.visible ? signal.color : WithAlpha(signal.color, 0x55));
// 名称。
const float badgeOffset = kBadgeWidth * m_scale + 8.0f * m_scale;
const float nameLeft = swatchRect.X + swatchRect.Width + 10.0f * m_scale;
const float nameWidth = rect.X + rect.Width - kPadding * m_scale - badgeOffset - nameLeft;
const std::wstring nameText = UiEllipsize(g, signal.name, m_fonts->ui, nameWidth);
UiText(g, nameText, m_fonts->ui, gd::RectF(nameLeft, rect.Y, nameWidth, rect.Height),
signal.visible ? m_theme->text : m_theme->textMuted, TextAlign::LEFT);
// 网格标记。
const int laneIndex = m_document->LaneOfSignal(row);
if (laneIndex >= 0)
{
const gd::RectF badgeRect(rect.X + rect.Width - kPadding * m_scale - kBadgeWidth * m_scale,
centerY - kBadgeHeight * m_scale * 0.5f, kBadgeWidth * m_scale,
kBadgeHeight * m_scale);
const bool laneActive = (selected && m_hoverStyle == StyleHit::LANE);
UiFillRoundRect(g, badgeRect, kBadgeHeight * m_scale * 0.5f,
laneActive ? m_theme->accentSoft : m_theme->cardBg);
UiDrawRoundRect(g, badgeRect, kBadgeHeight * m_scale * 0.5f, m_theme->border, 1.0f);
UiText(g, L"G" + std::to_wstring(laneIndex + 1), m_fonts->monoSmall, badgeRect, m_theme->text,
TextAlign::CENTER);
}
}
void SignalPanel::PaintStyleSection(gd::Graphics& g)
{
if (m_styleRect.Height <= 0.0f)
{
return;
}
UiFillRect(g, m_styleRect, m_theme->panelBg);
const int selectedIndex = (m_document == nullptr) ? -1 : m_document->SelectedSignal();
if (m_document == nullptr || selectedIndex < 0 ||
selectedIndex >= static_cast<int>(m_document->Signals().size()))
{
UiText(g, L"请选择信号以编辑样式", m_fonts->uiSmall, m_styleRect, m_theme->textMuted, TextAlign::CENTER);
return;
}
const Signal& signal = m_document->Signals()[static_cast<std::size_t>(selectedIndex)];
// 标题行。
const gd::RectF titleRect(m_styleRect.X + kPadding * m_scale, m_styleRect.Y,
m_styleRect.Width - kPadding * 2.0f * m_scale, 28.0f * m_scale);
UiText(g, L"样式 · " + signal.name, m_fonts->uiBold, titleRect, m_theme->textStrong, TextAlign::LEFT);
const float labelX = m_styleRect.X + kPadding * m_scale;
const float labelWidth = kLineLabelWidth * m_scale;
// 颜色。
{
const float rowTop = m_styleRect.Y + 30.0f * m_scale;
UiText(g, L"颜色", m_fonts->uiSmall,
gd::RectF(labelX, rowTop, labelWidth, 20.0f * m_scale), m_theme->textMuted, TextAlign::LEFT);
int paletteCount = 0;
const std::uint32_t* palette = SignalPalette(paletteCount);
for (int index = 0; index < m_colorCount; ++index)
{
const gd::RectF& rect = m_colorRects[static_cast<std::size_t>(index)];
const std::uint32_t color = palette[index % paletteCount];
const bool hovered = (m_hoverStyle == StyleHit::COLOR && m_hoverStyleIndex == index);
const bool active = (signal.color == color);
UiFillRoundRect(g, rect, 4.0f * m_scale, color);
if (active || hovered)
{
const gd::RectF ring(rect.X - 2.0f * m_scale, rect.Y - 2.0f * m_scale,
rect.Width + 4.0f * m_scale, rect.Height + 4.0f * m_scale);
UiDrawRoundRect(g, ring, 5.0f * m_scale, active ? m_theme->textStrong : m_theme->borderStrong,
active ? 1.6f : 1.0f);
}
}
}
// 线宽。
{
const gd::RectF& first = m_widthRects[0];
UiText(g, L"线宽", m_fonts->uiSmall, gd::RectF(labelX, first.Y, labelWidth, first.Height),
m_theme->textMuted, TextAlign::LEFT);
for (int index = 0; index < 4; ++index)
{
const gd::RectF& rect = m_widthRects[static_cast<std::size_t>(index)];
const bool active = std::fabs(signal.lineWidth - kWidthOptions[index]) < 0.01f;
const bool hovered = (m_hoverStyle == StyleHit::WIDTH && m_hoverStyleIndex == index);
UiFillRoundRect(g, rect, kCorner * m_scale,
active ? m_theme->accentSoft : (hovered ? m_theme->hover : m_theme->cardBg));
UiDrawRoundRect(g, rect, kCorner * m_scale, active ? m_theme->accent : m_theme->border, 1.0f);
// 以实际线宽绘制示意线段。
const float centerY = rect.Y + rect.Height * 0.5f;
const float lineWidth = kWidthOptions[index] * m_scale;
UiDrawLine(g, rect.X + 8.0f * m_scale, centerY, rect.X + rect.Width - 8.0f * m_scale, centerY,
active ? m_theme->accent : m_theme->text, lineWidth);
}
}
// 绘制方式。
{
const gd::RectF& first = m_modeRects[0];
UiText(g, L"方式", m_fonts->uiSmall, gd::RectF(labelX, first.Y, labelWidth, first.Height),
m_theme->textMuted, TextAlign::LEFT);
for (int index = 0; index < 4; ++index)
{
const gd::RectF& rect = m_modeRects[static_cast<std::size_t>(index)];
const bool active = (signal.mode == kModeOptions[index]);
const bool hovered = (m_hoverStyle == StyleHit::MODE && m_hoverStyleIndex == index);
UiFillRoundRect(g, rect, kCorner * m_scale,
active ? m_theme->accentSoft : (hovered ? m_theme->hover : m_theme->cardBg));
UiDrawRoundRect(g, rect, kCorner * m_scale, active ? m_theme->accent : m_theme->border, 1.0f);
UiText(g, DrawModeName(kModeOptions[index]), m_fonts->uiSmall, rect,
active ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
}
}
// 所属网格。
{
UiText(g, L"网格", m_fonts->uiSmall,
gd::RectF(labelX, m_laneButtonRect.Y, labelWidth, m_laneButtonRect.Height), m_theme->textMuted,
TextAlign::LEFT);
const bool hovered = (m_hoverStyle == StyleHit::LANE);
UiFillRoundRect(g, m_laneButtonRect, kCorner * m_scale, hovered ? m_theme->hover : m_theme->cardBg);
UiDrawRoundRect(g, m_laneButtonRect, kCorner * m_scale, m_theme->border, 1.0f);
const int laneIndex = m_document->LaneOfSignal(selectedIndex);
std::wstring label = (laneIndex >= 0) ? (L"网格 " + std::to_wstring(laneIndex + 1)) : L"未分配";
const gd::RectF textRect(m_laneButtonRect.X + 8.0f * m_scale, m_laneButtonRect.Y,
m_laneButtonRect.Width - 26.0f * m_scale, m_laneButtonRect.Height);
UiText(g, label, m_fonts->uiSmall, textRect, m_theme->text, TextAlign::LEFT);
// 下拉箭头。
const float arrowX = m_laneButtonRect.X + m_laneButtonRect.Width - 16.0f * m_scale;
const float arrowY = m_laneButtonRect.Y + m_laneButtonRect.Height * 0.5f;
gd::PointF points[3];
points[0] = gd::PointF(arrowX - 4.0f * m_scale, arrowY - 2.0f * m_scale);
points[1] = gd::PointF(arrowX + 4.0f * m_scale, arrowY - 2.0f * m_scale);
points[2] = gd::PointF(arrowX, arrowY + 3.0f * m_scale);
gd::SolidBrush brush(ToGdiColor(m_theme->textMuted));
g.FillPolygon(&brush, points, 3);
}
}
bool SignalPanel::AreAllSignalsVisible() const
{
if (m_document == nullptr || m_document->Signals().empty())
{
return false;
}
for (const Signal& signal : m_document->Signals())
{
if (!signal.visible)
{
return false;
}
}
return true;
}
bool SignalPanel::OnMouseMove(int x, int y)
{
const int hoverRow = RowAtY(y);
StyleHit hoverStyle = StyleHit::NONE;
int hoverStyleIndex = -1;
const float fx = static_cast<float>(x);
const float fy = static_cast<float>(y);
if (UiPointInRect(m_toggleAllRect, fx, fy))
{
hoverStyle = StyleHit::TOGGLE_ALL;
}
else if (m_document != nullptr && m_document->SelectedSignal() >= 0)
{
for (int index = 0; index < m_colorCount; ++index)
{
if (UiPointInRect(m_colorRects[static_cast<std::size_t>(index)], fx, fy))
{
hoverStyle = StyleHit::COLOR;
hoverStyleIndex = index;
break;
}
}
if (hoverStyle == StyleHit::NONE)
{
for (int index = 0; index < 4; ++index)
{
if (UiPointInRect(m_widthRects[static_cast<std::size_t>(index)], fx, fy))
{
hoverStyle = StyleHit::WIDTH;
hoverStyleIndex = index;
break;
}
}
}
if (hoverStyle == StyleHit::NONE)
{
for (int index = 0; index < 4; ++index)
{
if (UiPointInRect(m_modeRects[static_cast<std::size_t>(index)], fx, fy))
{
hoverStyle = StyleHit::MODE;
hoverStyleIndex = index;
break;
}
}
}
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_laneButtonRect, fx, fy))
{
hoverStyle = StyleHit::LANE;
}
}
if (hoverRow == m_hoverRow && hoverStyle == m_hoverStyle && hoverStyleIndex == m_hoverStyleIndex)
{
return false;
}
m_hoverRow = hoverRow;
m_hoverStyle = hoverStyle;
m_hoverStyleIndex = hoverStyleIndex;
return true;
}
bool SignalPanel::OnLButtonDown(int x, int y)
{
if (m_document == nullptr)
{
return false;
}
const float fx = static_cast<float>(x);
const float fy = static_cast<float>(y);
// 全部显示/隐藏。
if (UiPointInRect(m_toggleAllRect, fx, fy))
{
m_document->SetAllVisible(!AreAllSignalsVisible());
return true;
}
// 样式编辑区。
const int selectedIndex = m_document->SelectedSignal();
if (selectedIndex >= 0 && selectedIndex < static_cast<int>(m_document->Signals().size()))
{
Signal& signal = m_document->Signals()[static_cast<std::size_t>(selectedIndex)];
int paletteCount = 0;
const std::uint32_t* palette = SignalPalette(paletteCount);
for (int index = 0; index < m_colorCount; ++index)
{
if (UiPointInRect(m_colorRects[static_cast<std::size_t>(index)], fx, fy))
{
signal.color = palette[index % paletteCount];
return true;
}
}
for (int index = 0; index < 4; ++index)
{
if (UiPointInRect(m_widthRects[static_cast<std::size_t>(index)], fx, fy))
{
signal.lineWidth = kWidthOptions[index];
return true;
}
}
for (int index = 0; index < 4; ++index)
{
if (UiPointInRect(m_modeRects[static_cast<std::size_t>(index)], fx, fy))
{
signal.mode = kModeOptions[index];
return true;
}
}
if (UiPointInRect(m_laneButtonRect, fx, fy))
{
ShowLaneMenu();
return true;
}
}
// 列表行。
const int row = RowAtY(y);
if (row >= 0)
{
const gd::RectF rect = RowRect(row);
const gd::RectF checkRect(rect.X + kPadding * m_scale, rect.Y + (rect.Height - kCheckSize * m_scale) * 0.5f,
kCheckSize * m_scale, kCheckSize * m_scale);
if (UiPointInRect(checkRect, fx, fy))
{
Signal& signal = m_document->Signals()[static_cast<std::size_t>(row)];
signal.visible = !signal.visible;
m_document->SetSelectedSignal(row);
return true;
}
m_document->SetSelectedSignal(row);
return true;
}
return false;
}
bool SignalPanel::OnLButtonUp(int /*x*/, int /*y*/)
{
return false;
}
bool SignalPanel::OnDoubleClick(int x, int y)
{
if (m_document == nullptr)
{
return false;
}
const int row = RowAtY(y);
if (row < 0)
{
return false;
}
Signal& signal = m_document->Signals()[static_cast<std::size_t>(row)];
signal.visible = !signal.visible;
m_document->SetSelectedSignal(row);
return true;
}
bool SignalPanel::OnRButtonDown(int x, int y)
{
if (m_document == nullptr)
{
return false;
}
const int row = RowAtY(y);
if (row < 0)
{
return false;
}
m_document->SetSelectedSignal(row);
POINT cursor = {x, y};
if (m_host != nullptr)
{
ClientToScreen(m_host, &cursor);
}
ShowSignalMenu(row, cursor.x, cursor.y);
return true;
}
bool SignalPanel::OnMouseWheel(int delta)
{
if (m_document == nullptr)
{
return false;
}
const float previous = m_scrollOffset;
m_scrollOffset -= static_cast<float>(delta) / 120.0f * 54.0f * m_scale;
ClampScroll();
return std::fabs(previous - m_scrollOffset) > 0.01f;
}
bool SignalPanel::OnMouseLeave()
{
if (m_hoverRow < 0 && m_hoverStyle == StyleHit::NONE)
{
return false;
}
m_hoverRow = -1;
m_hoverStyle = StyleHit::NONE;
m_hoverStyleIndex = -1;
return true;
}
void SignalPanel::ShowLaneMenu()
{
if (m_document == nullptr || m_host == nullptr)
{
return;
}
const int selectedIndex = m_document->SelectedSignal();
if (selectedIndex < 0)
{
return;
}
HMENU menu = CreatePopupMenu();
if (menu == nullptr)
{
return;
}
const int currentLane = m_document->LaneOfSignal(selectedIndex);
const std::vector<Lane>& lanes = m_document->Lanes();
for (std::size_t index = 0; index < lanes.size(); ++index)
{
std::wstring label = L"网格 " + std::to_wstring(index + 1) + L"(" +
std::to_wstring(lanes[index].signalIndices.size()) + L" 条信号)";
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuLaneBase + index), label.c_str());
}
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuLaneNew), L"新建网格");
if (currentLane >= 0 && currentLane < static_cast<int>(lanes.size()))
{
CheckMenuRadioItem(menu, static_cast<UINT>(kMenuLaneBase),
static_cast<UINT>(kMenuLaneBase + lanes.size() - 1),
static_cast<UINT>(kMenuLaneBase + currentLane), MF_BYCOMMAND);
}
POINT anchor = {static_cast<LONG>(m_laneButtonRect.X), static_cast<LONG>(m_laneButtonRect.Y)};
ClientToScreen(m_host, &anchor);
SetForegroundWindow(m_host);
const int command = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_LEFTALIGN | TPM_TOPALIGN, anchor.x,
anchor.y + static_cast<int>(m_laneButtonRect.Height), 0, m_host, nullptr);
DestroyMenu(menu);
if (command >= kMenuLaneBase && command < kMenuLaneNew)
{
m_document->MoveSignalToLane(selectedIndex, command - kMenuLaneBase);
}
else if (command == kMenuLaneNew)
{
m_document->MoveSignalToLane(selectedIndex, -1);
}
}
void SignalPanel::ShowSignalMenu(int signalIndex, int screenX, int screenY)
{
if (m_document == nullptr || m_host == nullptr)
{
return;
}
if (signalIndex < 0 || signalIndex >= static_cast<int>(m_document->Signals().size()))
{
return;
}
Signal& signal = m_document->Signals()[static_cast<std::size_t>(signalIndex)];
HMENU menu = CreatePopupMenu();
if (menu == nullptr)
{
return;
}
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuToggleVisible),
signal.visible ? L"隐藏该信号" : L"显示该信号");
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuSolo), L"仅显示该信号");
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuShowAll), L"显示全部信号");
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
HMENU colorMenu = CreatePopupMenu();
int paletteCount = 0;
const std::uint32_t* palette = SignalPalette(paletteCount);
const wchar_t* colorNames[] = {L"蓝", L"绿", L"红", L"紫", L"黄", L"青", L"品红", L"橙", L"深灰", L"灰"};
for (int index = 0; index < SignalColorCount(); ++index)
{
const std::uint32_t color = palette[index % paletteCount];
std::wstring label = (index < 10) ? std::wstring(colorNames[index]) : (L"颜色 " + std::to_wstring(index + 1));
AppendMenuW(colorMenu, MF_STRING, static_cast<UINT_PTR>(kMenuColorBase + index), label.c_str());
if (signal.color == color)
{
CheckMenuRadioItem(colorMenu, static_cast<UINT>(kMenuColorBase),
static_cast<UINT>(kMenuColorBase + SignalColorCount() - 1),
static_cast<UINT>(kMenuColorBase + index), MF_BYCOMMAND);
}
}
AppendMenuW(menu, MF_POPUP, reinterpret_cast<UINT_PTR>(colorMenu), L"颜色");
HMENU widthMenu = CreatePopupMenu();
for (int index = 0; index < 4; ++index)
{
const std::wstring label = FormatNumber(kWidthOptions[index], 1) + L" px";
AppendMenuW(widthMenu, MF_STRING, static_cast<UINT_PTR>(kMenuWidthBase + index), label.c_str());
if (std::fabs(signal.lineWidth - kWidthOptions[index]) < 0.01f)
{
CheckMenuRadioItem(widthMenu, static_cast<UINT>(kMenuWidthBase),
static_cast<UINT>(kMenuWidthBase + 3),
static_cast<UINT>(kMenuWidthBase + index), MF_BYCOMMAND);
}
}
AppendMenuW(menu, MF_POPUP, reinterpret_cast<UINT_PTR>(widthMenu), L"线宽");
HMENU modeMenu = CreatePopupMenu();
for (int index = 0; index < 4; ++index)
{
AppendMenuW(modeMenu, MF_STRING, static_cast<UINT_PTR>(kMenuModeBase + index),
DrawModeName(kModeOptions[index]));
if (signal.mode == kModeOptions[index])
{
CheckMenuRadioItem(modeMenu, static_cast<UINT>(kMenuModeBase), static_cast<UINT>(kMenuModeBase + 3),
static_cast<UINT>(kMenuModeBase + index), MF_BYCOMMAND);
}
}
AppendMenuW(menu, MF_POPUP, reinterpret_cast<UINT_PTR>(modeMenu), L"显示方式");
HMENU laneMenu = CreatePopupMenu();
const std::vector<Lane>& lanes = m_document->Lanes();
const int currentLane = m_document->LaneOfSignal(signalIndex);
for (std::size_t index = 0; index < lanes.size(); ++index)
{
const std::wstring laneLabel = L"网格 " + std::to_wstring(index + 1);
AppendMenuW(laneMenu, MF_STRING, static_cast<UINT_PTR>(kMenuLaneBase + index), laneLabel.c_str());
}
AppendMenuW(laneMenu, MF_SEPARATOR, 0, nullptr);
AppendMenuW(laneMenu, MF_STRING, static_cast<UINT_PTR>(kMenuLaneNew), L"新建网格");
if (currentLane >= 0 && !lanes.empty())
{
CheckMenuRadioItem(laneMenu, static_cast<UINT>(kMenuLaneBase),
static_cast<UINT>(kMenuLaneBase + lanes.size() - 1),
static_cast<UINT>(kMenuLaneBase + currentLane), MF_BYCOMMAND);
}
AppendMenuW(menu, MF_POPUP, reinterpret_cast<UINT_PTR>(laneMenu), L"所属网格");
SetForegroundWindow(m_host);
const int command = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_LEFTALIGN | TPM_TOPALIGN, screenX, screenY, 0,
m_host, nullptr);
DestroyMenu(menu);
if (command == kMenuToggleVisible)
{
signal.visible = !signal.visible;
}
else if (command == kMenuSolo)
{
m_document->SoloSignal(signalIndex);
}
else if (command == kMenuShowAll)
{
m_document->SetAllVisible(true);
}
else if (command >= kMenuModeBase && command < kMenuModeBase + 4)
{
signal.mode = kModeOptions[command - kMenuModeBase];
}
else if (command >= kMenuWidthBase && command < kMenuWidthBase + 4)
{
signal.lineWidth = kWidthOptions[command - kMenuWidthBase];
}
else if (command >= kMenuColorBase && command < kMenuColorBase + SignalColorCount())
{
signal.color = palette[(command - kMenuColorBase) % paletteCount];
}
else if (command >= kMenuLaneBase && command < kMenuLaneNew)
{
m_document->MoveSignalToLane(signalIndex, command - kMenuLaneBase);
}
else if (command == kMenuLaneNew)
{
m_document->MoveSignalToLane(signalIndex, -1);
}
}
} // namespace jdi