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

+212
View File
@@ -0,0 +1,212 @@
#ifndef __SIGNAL_SET_HPP__
#define __SIGNAL_SET_HPP__
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include "CsvTable.hpp"
namespace jdi
{
/**
* @brief 波形绘制方式。
*/
enum class DrawMode : int
{
LINE = 0, /**< 相邻采样点之间以插值直线连接。 */
SCATTER = 1, /**< 仅绘制采样点散点。 */
LINE_SCATTER = 2, /**< 同时绘制插值直线与采样点散点。 */
STEP = 3 /**< 阶梯保持(零阶保持)连线。 */
};
/**
* @brief 将绘制方式转换为中文显示名称。
* @param [IN] mode 绘制方式。
* @return 显示名称。
*/
const wchar_t* DrawModeName(DrawMode mode);
/**
* @brief 单条信号(一列数据及其显示属性)。
*/
struct Signal
{
std::wstring name; /**< 显示名称(已剥离 "[n]" 序号前缀)。 */
std::wstring rawHeader; /**< CSV 原始列名。 */
std::wstring unit; /**< 从列名括号中提取的单位,可能为空。 */
std::vector<double> values; /**< 采样值,缺失处为 NaN。 */
bool visible = true; /**< 是否参与绘制。 */
std::uint32_t color = 0xFF268BD2; /**< 波形颜色,格式 0xAARRGGBB。 */
float lineWidth = 1.6f; /**< 线宽(逻辑像素,未乘 DPI 缩放)。 */
DrawMode mode = DrawMode::LINE; /**< 绘制方式。 */
double minValue = 0.0; /**< 有限采样值最小值。 */
double maxValue = 0.0; /**< 有限采样值最大值。 */
std::size_t finiteCount = 0; /**< 有限采样点数量。 */
};
/**
* @brief 一个波形网格(纵向泳道),可容纳多条信号叠加显示。
*/
struct Lane
{
std::wstring name; /**< 网格名称。 */
std::vector<int> signalIndices; /**< 该网格包含的信号下标(按加入顺序)。 */
bool autoRange = true; /**< 是否依据信号取值范围自动计算 Y 轴。 */
double yMin = -1.0; /**< 手动 Y 轴下限。 */
double yMax = 1.0; /**< 手动 Y 轴上限。 */
};
/**
* @brief 仿真数据集合:时间轴 + 多条信号 + 网格划分。
*/
class SignalSet
{
public:
SignalSet();
/**
* @brief 清空全部数据与显示状态。
*/
void Clear();
/**
* @brief 依据 CSV 解析结果构建数据集(自动识别时间列与信号列)。
* @param [IN] table CSV 解析结果。
* @param [IN] filePath 源文件路径,用于界面展示。
* @param [OUT] error 失败原因描述。
* @return 构建成功返回 true。
*/
bool BuildFromTable(const CsvTable& table, const std::wstring& filePath, std::wstring& error);
/**
* @brief 判断数据集是否已加载数据。
* @return 已加载返回 true。
*/
bool IsLoaded() const { return !m_signals.empty(); }
/// @brief 信号集合(可读写)。
std::vector<Signal>& Signals() { return m_signals; }
/// @brief 信号集合(只读)。
const std::vector<Signal>& Signals() const { return m_signals; }
/// @brief 网格集合(可读写)。
std::vector<Lane>& Lanes() { return m_lanes; }
/// @brief 网格集合(只读)。
const std::vector<Lane>& Lanes() const { return m_lanes; }
/// @brief 时间轴数值(只读)。
const std::vector<double>& Time() const { return m_time; }
/// @brief 时间轴列名(只读)。
const std::wstring& TimeColumnName() const { return m_timeColumnName; }
/// @brief 源文件路径(只读)。
const std::wstring& FilePath() const { return m_filePath; }
/// @brief 源文件名称(只读)。
const std::wstring& FileName() const { return m_fileName; }
/// @brief 采样点数量(只读)。
std::size_t SampleCount() const { return m_time.size(); }
/// @brief 时间轴最小值(只读)。
double MinTime() const { return m_minTime; }
/// @brief 时间轴最大值(只读)。
double MaxTime() const { return m_maxTime; }
/// @brief 时间轴是否单调不减(只读)。
bool TimeMonotonic() const { return m_timeMonotonic; }
/// @brief 当前选中信号下标,-1 表示未选中(只读)。
int SelectedSignal() const { return m_selectedSignal; }
/**
* @brief 设置当前选中信号。
* @param [IN] index 信号下标,传入越界值等价于取消选择。
*/
void SetSelectedSignal(int index);
/**
* @brief 查询某信号所属网格下标。
* @param [IN] signalIndex 信号下标。
* @return 网格下标,未找到返回 -1。
*/
int LaneOfSignal(int signalIndex) const;
/**
* @brief 将信号移动到指定网格,必要时创建新网格并回收空网格。
* @param [IN] signalIndex 信号下标。
* @param [IN] laneIndex 目标网格下标,传入负值表示新建网格。
* @return 移动后该信号所在的网格下标。
*/
int MoveSignalToLane(int signalIndex, int laneIndex);
/**
* @brief 将所有可见信号合并到同一个网格中。
*/
void MergeIntoSingleLane();
/**
* @brief 为每条信号创建独立网格。
*/
void SplitIntoLanes();
/**
* @brief 设置全部信号的可见状态。
* @param [IN] visible 目标可见状态。
*/
void SetAllVisible(bool visible);
/**
* @brief 仅显示指定信号,其余全部隐藏。
* @param [IN] signalIndex 需要保持可见的信号下标。
*/
void SoloSignal(int signalIndex);
/**
* @brief 计算某网格的 Y 轴显示范围。
* @param [IN] laneIndex 网格下标。
* @param [OUT] yMin 输出下限。
* @param [OUT] yMax 输出上限。
*/
void ComputeLaneRange(int laneIndex, double& yMin, double& yMax) const;
/**
* @brief 在当前时间轴上采样指定信号的值。
* @param [IN] signalIndex 信号下标。
* @param [IN] time 采样时刻。
* @param [IN] interpolate 是否在相邻采样点之间做线性插值。
* @param [OUT] value 输出采样值,缺失时为 NaN。
* @return 时间点落在数据范围内且取到有效值返回 true。
*/
bool SampleAt(int signalIndex, double time, bool interpolate, double& value) const;
/**
* @brief 在当前数据集中查找时间最为接近的采样点下标。
* @param [IN] time 采样时刻。
* @return 采样点下标,数据集为空时返回 -1。
*/
int FindNearestSample(double time) const;
private:
/**
* @brief 移除没有包含任何信号的网格(至少保留一个)。
*/
void PruneEmptyLanes();
/**
* @brief 为新增信号分配默认颜色。
* @param [IN] index 信号序号。
* @return 0xAARRGGBB 颜色值。
*/
std::uint32_t PickDefaultColor(std::size_t index) const;
std::vector<double> m_time; /**< 时间轴数值。 */
std::vector<Signal> m_signals; /**< 信号集合。 */
std::vector<Lane> m_lanes; /**< 网格集合。 */
std::wstring m_timeColumnName; /**< 时间列列名。 */
std::wstring m_filePath; /**< 源文件完整路径。 */
std::wstring m_fileName; /**< 源文件名称。 */
double m_minTime = 0.0; /**< 时间轴最小值。 */
double m_maxTime = 0.0; /**< 时间轴最大值。 */
bool m_timeMonotonic = true; /**< 时间轴是否单调不减。 */
int m_selectedSignal = -1; /**< 当前选中信号下标。 */
};
} // namespace jdi
#endif // __SIGNAL_SET_HPP__