Files
Just-DataInspector/Utilities/Inc/DataModel.hpp
T
DeepSeek V4.1 Flash dbe9b904ab fix: 重构样式区交互与布局,修复拾色浮窗穿透与自定义颜色记忆
样式编辑区统一为固定四行网格(颜色 / 样式 / 透明度 / 线宽):
- 四行共用同一内容列宽度,宽度带上下限(面板拉宽时不再无限延伸),
  标签列与内容列作为整体在面板内居中,行标签统一按整行垂直居中,
  修复“颜色”字样与色块存在高低差的问题。
- 颜色行的色块间距按剩余空间自适应,使该行与其它三行等宽对齐。
- 线宽改为与透明度同款的滑块(0.5–10 px),从原先的“预设 + 数值旋钮”
  调整为置于样式区最下方的滑块,保留双击数值直接键入的能力。

自定义颜色浮窗(ColorPickerPopup):
- 拖动滑块时不再与下方图层(尤其是绘图区平移/框选)产生交互:
  拾色浮窗内的按下会被完整消费并抓取鼠标,不再下传。
- 打开浮窗时光标正确显示为横向调整(IDC_SIZEWE),其余区域为箭头。
- 新增自定义颜色记忆:SignalStyle 记录 customColor,从自定义色切换到
  预设色后再次点击加号方格即可直接回到上次的自定义颜色;
  加号方格始终以该颜色填充(与预设色块同尺寸、同外框)。
- 打开浮窗时以记忆色为初始取值,不再重新从预设色起步。

透明度:
- 预设色块与自定义颜色方格同样叠加不透明度显示,
  修复此前透明度只影响自定义颜色预览的问题。

其他:
- 左侧面板宽度上限由 560 收紧至 400,避免样式控件被拉得过长。
- 移除只写不读的色块尺寸缓存与已废弃的线宽步进接口。

验证:真实鼠标输入逐项核对(拾色浮窗拖拽后绘图区视图范围与时间轴完全
不变、滑块光标 SIZEWE、自定义色 #0071C4 在切换预设后仍可一键恢复、
透明度 21% 时全部预设色块同步变淡、线宽滑块 8.1 px / 双击键入 5 设为
5.0 px、面板最窄与最宽两种极限下四行均不溢出且等宽)。
make clean && make all 零错误零警告。
2026-09-29 18:06:34 +08:00

496 lines
16 KiB
C++

#ifndef __DATA_MODEL_HPP__
#define __DATA_MODEL_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 /**< 同时绘制插值直线与采样点散点。 */
};
/**
* @brief 将绘制方式转换为中文显示名称。
* @param [IN] mode 绘制方式。
* @return 显示名称。
*/
const wchar_t* DrawModeName(DrawMode mode);
/**
* @brief 网格划分策略:决定信号可见性与网格数量的联动方式。
*/
enum class LaneMode : int
{
PER_SIGNAL = 0, /**< 每条可见信号独占一个网格(随可见性自动增删)。 */
MERGED = 1, /**< 全部可见信号叠加在同一网格。 */
CUSTOM = 2 /**< 由用户手动指定归属;可见性变化时仅清理空网格。 */
};
/**
* @brief 全局信号引用:文件下标 + 文件内信号下标。
*/
struct SignalRef
{
int fileIndex = -1; /**< 所属文件下标。 */
int signalIndex = -1; /**< 文件内信号下标。 */
/**
* @brief 判断引用是否指向有效位置。
* @return 两个下标均非负返回 true。
*/
bool IsValid() const { return fileIndex >= 0 && signalIndex >= 0; }
/**
* @brief 相等比较。
* @param [IN] other 另一引用。
* @return 指向同一信号返回 true。
*/
bool operator==(const SignalRef& other) const
{
return fileIndex == other.fileIndex && signalIndex == other.signalIndex;
}
/**
* @brief 不等比较。
* @param [IN] other 另一引用。
* @return 指向不同信号返回 true。
*/
bool operator!=(const SignalRef& other) const { return !(*this == other); }
};
/**
* @brief 单条信号的元数据与采样值(纯数据,不含显示配置)。
*/
struct SignalData
{
std::wstring name; /**< 显示名称(已剥离 "[n]" 序号前缀)。 */
std::wstring rawHeader; /**< 原始列名。 */
std::wstring unit; /**< 从列名括号中提取的单位,可能为空。 */
std::vector<double> values; /**< 采样值,缺失处为 NaN。 */
double minValue = 0.0; /**< 有限采样值最小值。 */
double maxValue = 0.0; /**< 有限采样值最大值。 */
std::size_t finiteCount = 0; /**< 有限采样点数量。 */
};
/**
* @brief 一个已加载的数据文件及其自有时间轴。
*/
struct DataFile
{
std::wstring filePath; /**< 文件完整路径。 */
std::wstring fileName; /**< 文件名称。 */
std::wstring timeColumnName; /**< 时间列列名。 */
std::vector<double> time; /**< 时间轴数值。 */
std::vector<SignalData> signals; /**< 该文件包含的信号。 */
double minTime = 0.0; /**< 时间轴最小值。 */
double maxTime = 0.0; /**< 时间轴最大值。 */
bool timeMonotonic = true; /**< 时间轴是否单调不减。 */
};
/**
* @brief 数据集合:统一管理全部已加载文件,与视图配置无关。
*/
class DataSet
{
public:
/**
* @brief 清空全部已加载文件。
*/
void Clear();
/**
* @brief 依据 CSV 解析结果追加一个数据文件。
* @param [IN] table CSV 解析结果。
* @param [IN] filePath 源文件路径。
* @param [OUT] error 失败原因描述(成功时清空)。
* @param [OUT] fileIndex 新文件下标,失败时为 -1。
* @return 追加成功返回 true。
*/
bool AddFromTable(const CsvTable& table, const std::wstring& filePath, std::wstring& error, int& fileIndex);
/**
* @brief 移除指定下标的文件。
* @param [IN] fileIndex 文件下标。
* @return 移除成功返回 true。
*/
bool RemoveFile(int fileIndex);
/**
* @brief 判断数据集合是否为空。
* @return 未加载任何文件返回 true。
*/
bool Empty() const { return m_files.empty(); }
/**
* @brief 获取文件数量。
* @return 文件数量。
*/
std::size_t FileCount() const { return m_files.size(); }
/**
* @brief 获取信号总数。
* @return 信号总数。
*/
std::size_t SignalCount() const;
/**
* @brief 获取全部文件(只读)。
* @return 文件集合。
*/
const std::vector<DataFile>& Files() const { return m_files; }
/**
* @brief 按下标获取文件。
* @param [IN] fileIndex 文件下标。
* @return 文件指针,下标非法时返回 nullptr。
*/
const DataFile* FileAt(int fileIndex) const;
/**
* @brief 解析信号引用。
* @param [IN] ref 信号引用。
* @return 信号数据指针,引用无效时返回 nullptr。
*/
const SignalData* Resolve(const SignalRef& ref) const;
/**
* @brief 按路径查找已加载文件下标。
* @param [IN] filePath 文件完整路径。
* @return 文件下标,未找到返回 -1。
*/
int IndexOfFile(const std::wstring& filePath) const;
/**
* @brief 计算全部文件时间范围的并集下限。
* @return 时间下限。
*/
double MinTime() const;
/**
* @brief 计算全部文件时间范围的并集上限。
* @return 时间上限。
*/
double MaxTime() const;
private:
std::vector<DataFile> m_files; /**< 已加载文件集合。 */
};
/**
* @brief 单个信号的显示配置(属于某个标签页)。
*/
struct SignalStyle
{
bool visible = false; /**< 是否参与绘制。默认关闭,由用户自行勾选。 */
std::uint32_t color = 0xFF268BD2; /**< 波形颜色,格式 0xAARRGGBB。 */
std::uint32_t customColor = 0xFF6C71C4; /**< 自定义颜色记忆值(切换预设色后仍保留)。 */
float opacity = 1.0f; /**< 不透明度,取值 0.0-1.0,默认 1.0。 */
float lineWidth = 1.6f; /**< 线宽(逻辑像素,未乘 DPI 缩放)。 */
DrawMode mode = DrawMode::LINE; /**< 绘制方式。 */
};
/**
* @brief 一个波形网格(纵向泳道),可容纳多条信号叠加显示。
*/
struct Lane
{
std::wstring name; /**< 网格名称。 */
std::vector<SignalRef> signals; /**< 该网格包含的信号(按加入顺序)。 */
bool autoRange = true; /**< 是否依据信号取值范围自动计算 Y 轴。 */
double yMin = -1.0; /**< 手动 Y 轴下限。 */
double yMax = 1.0; /**< 手动 Y 轴上限。 */
};
/**
* @brief 计算某条信号在给定时刻的取值。
* @param [IN] file 信号所属文件。
* @param [IN] signal 目标信号。
* @param [IN] time 采样时刻。
* @param [IN] interpolate 是否在相邻采样点之间做线性插值。
* @param [OUT] value 输出采样值,缺失时为 NaN。
* @return 时间点落在数据范围内且取到有效值返回 true。
*/
bool SampleFileAt(const DataFile& file, const SignalData& signal, double time, bool interpolate, double& value);
/**
* @brief 在指定文件的时间轴上查找与给定时刻最接近的采样点。
* @param [IN] file 目标文件。
* @param [IN] time 采样时刻。
* @return 采样点下标,文件为空时返回 -1。
*/
int FindNearestSample(const DataFile& file, double time);
/**
* @brief 单个标签页的独立视图配置(信号可见性、样式、网格划分与视图范围)。
*/
class TabConfig
{
public:
TabConfig();
/**
* @brief 获取标签页名称。
* @return 名称。
*/
const std::wstring& Name() const { return m_name; }
/**
* @brief 设置标签页名称。
* @param [IN] name 新名称。
*/
void SetName(const std::wstring& name) { m_name = name; }
/**
* @brief 依据数据集合调整配置容量(新增文件补默认配置,移除文件清理配置)。
* @param [IN] dataSet 数据集合。
*/
void SyncToDataSet(const DataSet& dataSet);
/**
* @brief 在数据层移除某文件之前,按旧下标修正本配置(不重建网格)。
* @param [IN] fileIndex 即将被移除的文件下标。
* @note 调用顺序必须为:各标签页 RemapAfterFileRemoval → 数据层 RemoveFile → 各标签页 SyncToDataSet。
*/
void RemapAfterFileRemoval(int fileIndex);
/**
* @brief 获取指定信号的显示配置(只读)。
* @param [IN] ref 信号引用。
* @return 配置指针,引用无效时返回 nullptr。
*/
const SignalStyle* Style(const SignalRef& ref) const;
/**
* @brief 获取指定信号的显示配置(可写)。
* @param [IN] ref 信号引用。
* @return 配置指针,引用无效时返回 nullptr。
*/
SignalStyle* Style(const SignalRef& ref);
/**
* @brief 判断指定信号是否可见。
* @param [IN] ref 信号引用。
* @return 可见返回 true。
*/
bool IsVisible(const SignalRef& ref) const;
/**
* @brief 设置指定信号的可见性,并同步网格划分。
* @param [IN] dataSet 数据集合。
* @param [IN] ref 信号引用。
* @param [IN] visible 目标可见状态。
*/
void SetVisible(const DataSet& dataSet, const SignalRef& ref, bool visible);
/**
* @brief 切换指定信号的可见性,并同步网格划分。
* @param [IN] dataSet 数据集合。
* @param [IN] ref 信号引用。
*/
void ToggleVisible(const DataSet& dataSet, const SignalRef& ref);
/**
* @brief 设置某个文件全部信号的可见性,并同步网格划分。
* @param [IN] dataSet 数据集合。
* @param [IN] fileIndex 文件下标。
* @param [IN] visible 目标可见状态。
*/
void SetFileVisible(const DataSet& dataSet, int fileIndex, bool visible);
/**
* @brief 设置全部信号可见性,并同步网格划分。
* @param [IN] dataSet 数据集合。
* @param [IN] visible 目标可见状态。
*/
void SetAllVisible(const DataSet& dataSet, bool visible);
/**
* @brief 仅显示指定信号,其余全部隐藏。
* @param [IN] dataSet 数据集合。
* @param [IN] ref 需要保持可见的信号。
*/
void SoloSignal(const DataSet& dataSet, const SignalRef& ref);
/**
* @brief 判断某文件是否存在可见信号。
* @param [IN] dataSet 数据集合。
* @param [IN] fileIndex 文件下标。
* @return 存在可见信号返回 true。
*/
bool FileHasVisible(const DataSet& dataSet, int fileIndex) const;
/**
* @brief 判断某文件是否全部信号可见。
* @param [IN] dataSet 数据集合。
* @param [IN] fileIndex 文件下标。
* @return 全部可见且至少有一条信号返回 true。
*/
bool FileAllVisible(const DataSet& dataSet, int fileIndex) const;
/**
* @brief 判断是否存在任何可见信号。
* @return 存在返回 true。
*/
bool AnyVisible() const;
/**
* @brief 统计可见信号数量。
* @return 可见信号数量。
*/
int VisibleCount() const;
/**
* @brief 获取网格划分策略。
* @return 策略枚举。
*/
LaneMode Mode() const { return m_laneMode; }
/**
* @brief 设置网格划分策略并同步网格。
* @param [IN] dataSet 数据集合。
* @param [IN] mode 目标策略。
*/
void SetMode(const DataSet& dataSet, LaneMode mode);
/**
* @brief 依据当前可见性与策略重新生成网格划分。
* @param [IN] dataSet 数据集合。
*/
void SyncLanes(const DataSet& dataSet);
/**
* @brief 获取网格集合(只读)。
* @return 网格集合。
*/
const std::vector<Lane>& Lanes() const { return m_lanes; }
/**
* @brief 获取网格集合(可写)。
* @return 网格集合。
*/
std::vector<Lane>& Lanes() { return m_lanes; }
/**
* @brief 查询某信号所属网格下标。
* @param [IN] ref 信号引用。
* @return 网格下标,未找到返回 -1。
*/
int LaneOfSignal(const SignalRef& ref) const;
/**
* @brief 将信号移动到指定网格,并切换为自定义划分策略。
* @param [IN] dataSet 数据集合。
* @param [IN] ref 信号引用。
* @param [IN] laneIndex 目标网格下标,传入负值表示新建网格。
* @return 移动后该信号所在的网格下标。
*/
int MoveSignalToLane(const DataSet& dataSet, const SignalRef& ref, int laneIndex);
/**
* @brief 查询某文件的分组是否处于折叠状态。
* @param [IN] fileIndex 文件下标。
* @return 折叠状态。
*/
bool IsCollapsed(int fileIndex) const;
/**
* @brief 切换某文件分组的折叠状态。
* @param [IN] fileIndex 文件下标。
*/
void ToggleCollapsed(int fileIndex);
/**
* @brief 获取当前选中的信号引用。
* @return 信号引用。
*/
SignalRef Selected() const { return m_selected; }
/**
* @brief 设置当前选中的信号。
* @param [IN] ref 信号引用,无效值表示取消选择。
*/
void SetSelected(const SignalRef& ref) { m_selected = ref; }
/**
* @brief 计算某网格的 Y 轴显示范围。
* @param [IN] dataSet 数据集合。
* @param [IN] laneIndex 网格下标。
* @param [OUT] yMin 输出下限。
* @param [OUT] yMax 输出上限。
*/
void ComputeLaneRange(const DataSet& dataSet, int laneIndex, double& yMin, double& yMax) const;
/**
* @brief 计算全部可见信号覆盖的时间范围。
* @param [IN] dataSet 数据集合。
* @param [OUT] minTime 输出下限。
* @param [OUT] maxTime 输出上限。
* @return 存在可见信号返回 true。
*/
bool ComputeVisibleTimeBounds(const DataSet& dataSet, double& minTime, double& maxTime) const;
/// @brief 视图左边界时间(可读写)。
double& ViewMin() { return m_viewMin; }
/// @brief 视图左边界时间(只读)。
double ViewMin() const { return m_viewMin; }
/// @brief 视图右边界时间(可读写)。
double& ViewMax() { return m_viewMax; }
/// @brief 视图右边界时间(只读)。
double ViewMax() const { return m_viewMax; }
/// @brief 视图范围是否已初始化(可读写)。
bool& ViewInitialized() { return m_viewInitialized; }
/// @brief 网格纵向滚动偏移量(可读写)。
float& LaneScroll() { return m_laneScroll; }
/// @brief 时间光标是否可见(可读写)。
bool& CursorVisible() { return m_cursorVisible; }
/// @brief 时间光标是否可见(只读)。
bool CursorVisible() const { return m_cursorVisible; }
/// @brief 时间光标是否已固定(可读写)。
bool& CursorPinned() { return m_cursorPinned; }
/// @brief 时间光标是否已固定(只读)。
bool CursorPinned() const { return m_cursorPinned; }
/// @brief 时间光标位置(可读写)。
double& CursorTime() { return m_cursorTime; }
/// @brief 时间光标位置(只读)。
double CursorTime() const { return m_cursorTime; }
private:
/**
* @brief 为新增信号补默认样式(含调色板取色)。
* @param [IN] dataSet 数据集合。
* @param [IN] fileIndex 文件下标。
* @param [IN] signalIndex 信号下标。
*/
void EnsureStyle(const DataSet& dataSet, int fileIndex, int signalIndex);
std::wstring m_name; /**< 标签页名称。 */
std::vector<std::vector<SignalStyle>> m_styles; /**< 显示配置,按 [文件][信号] 索引。 */
std::vector<bool> m_collapsed; /**< 文件分组折叠状态,按文件索引。 */
std::vector<Lane> m_lanes; /**< 网格划分。 */
LaneMode m_laneMode = LaneMode::PER_SIGNAL; /**< 网格划分策略。 */
SignalRef m_selected; /**< 当前选中信号。 */
double m_viewMin = 0.0; /**< 视图左边界时间。 */
double m_viewMax = 1.0; /**< 视图右边界时间。 */
bool m_viewInitialized = false; /**< 视图范围是否已初始化。 */
float m_laneScroll = 0.0f; /**< 网格纵向滚动偏移量。 */
bool m_cursorVisible = false; /**< 时间光标是否可见。 */
bool m_cursorPinned = false; /**< 时间光标是否已固定。 */
double m_cursorTime = 0.0; /**< 时间光标位置。 */
};
} // namespace jdi
#endif // __DATA_MODEL_HPP__