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 链接库
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-09-29 15:05:29 +08:00
commit 132b305544
22 files changed
+7146

No files matched your search

+218
View File
@@ -0,0 +1,218 @@
#ifndef __SIGNAL_PANEL_HPP__
#define __SIGNAL_PANEL_HPP__
#include <windows.h>
#include <objidl.h>
#include <gdiplus.h>
#include <array>
#include <string>
#include "PaintUtil.hpp"
#include "SignalSet.hpp"
#include "Theme.hpp"
namespace jdi
{
/**
* @brief 左侧信号面板:信号列表、可见性开关与样式编辑。
*/
class SignalPanel
{
public:
SignalPanel();
/**
* @brief 绑定宿主窗口、字体与主题资源。
* @param [IN] host 宿主窗口句柄,用于弹出上下文菜单。
* @param [IN] fonts 字体集合,需在面板生命周期内保持有效。
* @param [IN] theme 主题配色,需在面板生命周期内保持有效。
*/
void Setup(HWND host, const UiFonts* fonts, const Theme* theme);
/**
* @brief 绑定数据集。
* @param [IN] document 数据集指针,允许为空。
*/
void SetDocument(SignalSet* document);
/**
* @brief 设置 DPI 缩放比例。
* @param [IN] scale 缩放比例(1.0 对应 96 DPI)。
*/
void SetScale(float scale);
/**
* @brief 按给定区域重新排布面板内容。
* @param [IN] area 面板可用区域。
*/
void Layout(const gd::RectF& area);
/**
* @brief 绘制面板。
* @param [IN,OUT] g 绘图上下文。
*/
void Paint(gd::Graphics& g);
/**
* @brief 处理鼠标移动。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 悬停状态发生变化返回 true。
*/
bool OnMouseMove(int x, int y);
/**
* @brief 处理鼠标左键按下。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 需要重绘返回 true。
*/
bool OnLButtonDown(int x, int y);
/**
* @brief 处理鼠标左键抬起。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 视觉状态发生变化返回 true。
*/
bool OnLButtonUp(int x, int y);
/**
* @brief 处理鼠标左键双击(切换信号可见性)。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 需要重绘返回 true。
*/
bool OnDoubleClick(int x, int y);
/**
* @brief 处理鼠标右键按下(弹出信号上下文菜单)。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 需要重绘返回 true。
*/
bool OnRButtonDown(int x, int y);
/**
* @brief 处理滚轮滚动(列表纵向滚动)。
* @param [IN] delta 滚轮增量。
* @return 需要重绘返回 true。
*/
bool OnMouseWheel(int delta);
/**
* @brief 处理鼠标离开面板。
* @return 视觉状态发生变化返回 true。
*/
bool OnMouseLeave();
/**
* @brief 判断坐标是否落在面板区域内。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 落在区域内返回 true。
*/
bool HitTest(int x, int y) const;
private:
/**
* @brief 样式编辑区中的可点击条目类型。
*/
enum class StyleHit : int
{
NONE = 0, /**< 未命中。 */
COLOR, /**< 颜色色块。 */
WIDTH, /**< 线宽按钮。 */
MODE, /**< 绘制方式按钮。 */
LANE, /**< 网格选择按钮。 */
TOGGLE_ALL /**< 全部显示/隐藏按钮。 */
};
/**
* @brief 计算列表区域内容高度。
* @return 内容高度。
*/
float ContentHeight() const;
/**
* @brief 限制滚动偏移量到合法范围。
*/
void ClampScroll();
/**
* @brief 查询指定坐标对应的信号行下标。
* @param [IN] y 客户区纵坐标。
* @return 行下标,未命中返回 -1。
*/
int RowAtY(int y) const;
/**
* @brief 计算行的纵向矩形。
* @param [IN] row 行下标。
* @return 行矩形。
*/
gd::RectF RowRect(int row) const;
/**
* @brief 绘制列表区域的单行。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] row 行下标。
*/
void PaintRow(gd::Graphics& g, int row);
/**
* @brief 绘制样式编辑区。
* @param [IN,OUT] g 绘图上下文。
*/
void PaintStyleSection(gd::Graphics& g);
/**
* @brief 弹出信号右键菜单。
* @param [IN] signalIndex 目标信号下标。
* @param [IN] screenX 屏幕坐标横坐标。
* @param [IN] screenY 屏幕坐标纵坐标。
*/
void ShowSignalMenu(int signalIndex, int screenX, int screenY);
/**
* @brief 弹出网格分配菜单。
*/
void ShowLaneMenu();
/**
* @brief 判断全部信号是否均为可见状态。
* @return 全部可见返回 true。
*/
bool AreAllSignalsVisible() const;
HWND m_host = nullptr; /**< 宿主窗口句柄。 */
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
const Theme* m_theme = nullptr; /**< 主题资源。 */
SignalSet* m_document = nullptr; /**< 数据集。 */
float m_scale = 1.0f; /**< DPI 缩放比例。 */
gd::RectF m_area; /**< 面板整体区域。 */
gd::RectF m_headerRect; /**< 标题区域。 */
gd::RectF m_listRect; /**< 列表区域。 */
gd::RectF m_styleRect; /**< 样式编辑区域。 */
gd::RectF m_toggleAllRect; /**< 全部显示/隐藏按钮。 */
gd::RectF m_laneButtonRect; /**< 网格选择按钮。 */
std::array<gd::RectF, 12> m_colorRects; /**< 颜色色块矩形。 */
std::array<gd::RectF, 4> m_widthRects; /**< 线宽按钮矩形。 */
std::array<gd::RectF, 4> m_modeRects; /**< 绘制方式按钮矩形。 */
int m_colorCount = 0; /**< 实际可用的颜色数量。 */
float m_widthRowLabelX = 0.0f; /**< 样式标签列起始横坐标。 */
float m_styleRowHeight = 0.0f; /**< 样式行高。 */
float m_scrollOffset = 0.0f; /**< 列表滚动偏移量。 */
int m_hoverRow = -1; /**< 当前悬停行。 */
StyleHit m_hoverStyle = StyleHit::NONE; /**< 当前悬停的样式条目类型。 */
int m_hoverStyleIndex = -1; /**< 当前悬停的样式条目下标。 */
};
} // namespace jdi
#endif // __SIGNAL_PANEL_HPP__
+889
View File
@@ -0,0 +1,889 @@
#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