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

+612
View File
@@ -0,0 +1,612 @@
#include "SignalSet.hpp"
#include "Theme.hpp"
#include "Utf8Util.hpp"
#include <algorithm>
#include <cmath>
#include <cwctype>
#include <limits>
namespace jdi
{
namespace
{
const double kMissing = std::numeric_limits<double>::quiet_NaN(); /**< 缺失值占位。 */
const double kDefaultLineWidth = 1.6; /**< 默认线宽。 */
/**
* @brief 判断列名是否为时间轴候选列。
* @param [IN] header 原始列名。
* @return 可能为时间轴返回 true。
*/
bool LooksLikeTimeHeader(const std::wstring& header)
{
const std::wstring text = ToLowerAscii(Trim(header));
if (text.empty())
{
return false;
}
if (text == L"t" || text == L"time" || text == L"timestamp" || text == L"simulation time" ||
text == L"时间" || text == L"时间轴")
{
return true;
}
return ContainsInsensitive(text, L"time") || ContainsInsensitive(text, L"时间");
}
/**
* @brief 剥离列名中的 "[n]" 序号前缀,得到显示名称。
* @param [IN] header 原始列名。
* @return 显示名称。
*/
std::wstring MakeDisplayName(const std::wstring& header)
{
const std::wstring text = Trim(header);
if (text.size() >= 3 && text[0] == L'[')
{
const std::size_t close = text.find(L']');
if (close != std::wstring::npos && close > 1 && (close + 1) < text.size())
{
bool numeric = true;
for (std::size_t i = 1; i < close; ++i)
{
if (!iswdigit(text[i]))
{
numeric = false;
break;
}
}
if (numeric)
{
const std::wstring rest = Trim(text.substr(close + 1));
if (!rest.empty())
{
return rest;
}
}
}
}
return text.empty() ? header : text;
}
/**
* @brief 从列名末尾的括号中提取单位(仅接受形如 mA / V / deg/s 的短标记)。
* @param [IN] header 原始列名。
* @return 单位文本,无法识别时返回空字符串。
*/
std::wstring ExtractUnit(const std::wstring& header)
{
const std::wstring text = Trim(header);
if (text.size() < 3 || text.back() != L')')
{
return std::wstring();
}
const std::size_t open = text.rfind(L'(');
if (open == std::wstring::npos || open + 2 >= text.size())
{
return std::wstring();
}
const std::wstring inner = Trim(text.substr(open + 1, text.size() - open - 2));
if (inner.empty() || inner.size() > 10)
{
return std::wstring();
}
for (const wchar_t c : inner)
{
const bool allowed = iswalpha(c) || iswdigit(c) || c == L'%' || c == L'/' || c == L'^' ||
c == L'-' || c == L'.' || c == L'\u00B0' || c == L'\u00B5' ||
c == L'\u03A9' || c == L'\u00B2' || c == L'\u00B3' || c == L'\u00B7';
if (!allowed)
{
return std::wstring();
}
}
return inner;
}
/**
* @brief 为网格范围补充留白,避免波形紧贴边界。
* @param [IN, OUT] yMin 范围下限。
* @param [IN, OUT] yMax 范围上限。
*/
void PadRange(double& yMin, double& yMax)
{
if (!std::isfinite(yMin) || !std::isfinite(yMax))
{
yMin = 0.0;
yMax = 1.0;
return;
}
const double span = yMax - yMin;
if (span <= 0.0)
{
const double magnitude = std::max(1.0, std::fabs(yMin) * 0.1);
yMin -= magnitude;
yMax += magnitude;
return;
}
const double pad = span * 0.06;
yMin -= pad;
yMax += pad;
}
}
const wchar_t* DrawModeName(DrawMode mode)
{
switch (mode)
{
case DrawMode::LINE:
return L"线段";
case DrawMode::SCATTER:
return L"散点";
case DrawMode::LINE_SCATTER:
return L"点线";
case DrawMode::STEP:
return L"阶梯";
default:
return L"线段";
}
}
SignalSet::SignalSet() = default;
void SignalSet::Clear()
{
m_time.clear();
m_signals.clear();
m_lanes.clear();
m_timeColumnName.clear();
m_filePath.clear();
m_fileName.clear();
m_minTime = 0.0;
m_maxTime = 0.0;
m_timeMonotonic = true;
m_selectedSignal = -1;
}
bool SignalSet::BuildFromTable(const CsvTable& table, const std::wstring& filePath, std::wstring& error)
{
Clear();
error.clear();
if (!table.HasData())
{
error = L"数据表为空。";
return false;
}
const std::size_t columnCount = table.headers.size();
const std::size_t rowCount = table.rows.size();
// 1. 识别时间轴列:优先取列名匹配的列,否则回退到第一列。
int timeColumn = -1;
for (std::size_t column = 0; column < columnCount; ++column)
{
if (LooksLikeTimeHeader(table.headers[column]))
{
timeColumn = static_cast<int>(column);
break;
}
}
if (timeColumn < 0)
{
timeColumn = 0;
}
m_time.resize(rowCount);
std::size_t finiteTimeCount = 0;
bool monotonic = true;
double previousTime = 0.0;
bool hasPrevious = false;
for (std::size_t row = 0; row < rowCount; ++row)
{
const double value = table.rows[row][static_cast<std::size_t>(timeColumn)];
m_time[row] = value;
if (std::isfinite(value))
{
++finiteTimeCount;
if (hasPrevious && value < previousTime)
{
monotonic = false;
}
previousTime = value;
hasPrevious = true;
}
}
if (finiteTimeCount < 2)
{
error = L"时间轴列(" + table.headers[static_cast<std::size_t>(timeColumn)] + L")有效数值不足。";
return false;
}
m_timeColumnName = table.headers[static_cast<std::size_t>(timeColumn)];
m_timeMonotonic = monotonic;
m_filePath = filePath;
const std::size_t slashPos = filePath.find_last_of(L"\\/");
m_fileName = (slashPos == std::wstring::npos) ? filePath : filePath.substr(slashPos + 1);
// 2. 构建信号列。
m_signals.reserve(columnCount);
for (std::size_t column = 0; column < columnCount; ++column)
{
if (static_cast<int>(column) == timeColumn)
{
continue;
}
Signal signal;
signal.rawHeader = table.headers[column];
signal.name = MakeDisplayName(signal.rawHeader);
signal.unit = ExtractUnit(signal.rawHeader);
signal.color = PickDefaultColor(m_signals.size());
signal.lineWidth = static_cast<float>(kDefaultLineWidth);
signal.mode = DrawMode::LINE;
signal.visible = true;
signal.values.resize(rowCount);
double minValue = std::numeric_limits<double>::infinity();
double maxValue = -std::numeric_limits<double>::infinity();
std::size_t finiteCount = 0;
for (std::size_t row = 0; row < rowCount; ++row)
{
const double value = table.rows[row][column];
signal.values[row] = value;
if (std::isfinite(value))
{
++finiteCount;
minValue = std::min(minValue, value);
maxValue = std::max(maxValue, value);
}
}
if (finiteCount == 0)
{
signal.minValue = 0.0;
signal.maxValue = 0.0;
}
else
{
signal.minValue = minValue;
signal.maxValue = maxValue;
}
signal.finiteCount = finiteCount;
m_signals.push_back(std::move(signal));
}
if (m_signals.empty())
{
error = L"除时间轴外未发现任何数据列。";
Clear();
return false;
}
// 3. 统计时间范围。
double tMin = std::numeric_limits<double>::infinity();
double tMax = -std::numeric_limits<double>::infinity();
for (const double value : m_time)
{
if (std::isfinite(value))
{
tMin = std::min(tMin, value);
tMax = std::max(tMax, value);
}
}
m_minTime = tMin;
m_maxTime = tMax;
// 4. 默认每个信号独占一个网格,便于区分量纲差异较大的数据。
m_lanes.clear();
m_lanes.resize(m_signals.size());
for (std::size_t index = 0; index < m_signals.size(); ++index)
{
m_lanes[index].name = L"网格 " + std::to_wstring(index + 1);
m_lanes[index].signalIndices.push_back(static_cast<int>(index));
m_lanes[index].autoRange = true;
}
m_selectedSignal = m_signals.empty() ? -1 : 0;
return true;
}
void SignalSet::SetSelectedSignal(int index)
{
if (index < 0 || index >= static_cast<int>(m_signals.size()))
{
m_selectedSignal = -1;
return;
}
m_selectedSignal = index;
}
int SignalSet::LaneOfSignal(int signalIndex) const
{
for (std::size_t lane = 0; lane < m_lanes.size(); ++lane)
{
const std::vector<int>& indices = m_lanes[lane].signalIndices;
if (std::find(indices.begin(), indices.end(), signalIndex) != indices.end())
{
return static_cast<int>(lane);
}
}
return -1;
}
int SignalSet::MoveSignalToLane(int signalIndex, int laneIndex)
{
if (signalIndex < 0 || signalIndex >= static_cast<int>(m_signals.size()))
{
return -1;
}
for (Lane& lane : m_lanes)
{
std::vector<int>& indices = lane.signalIndices;
indices.erase(std::remove(indices.begin(), indices.end(), signalIndex), indices.end());
}
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
{
Lane lane;
lane.name = L"网格 " + std::to_wstring(m_lanes.size() + 1);
lane.autoRange = true;
lane.signalIndices.push_back(signalIndex);
m_lanes.push_back(std::move(lane));
}
else
{
m_lanes[static_cast<std::size_t>(laneIndex)].signalIndices.push_back(signalIndex);
}
PruneEmptyLanes();
return LaneOfSignal(signalIndex);
}
void SignalSet::MergeIntoSingleLane()
{
m_lanes.clear();
Lane lane;
lane.name = L"网格 1";
lane.autoRange = true;
lane.signalIndices.reserve(m_signals.size());
for (std::size_t index = 0; index < m_signals.size(); ++index)
{
lane.signalIndices.push_back(static_cast<int>(index));
}
m_lanes.push_back(std::move(lane));
}
void SignalSet::SplitIntoLanes()
{
m_lanes.clear();
m_lanes.resize(m_signals.size());
for (std::size_t index = 0; index < m_signals.size(); ++index)
{
Lane& lane = m_lanes[index];
lane.name = L"网格 " + std::to_wstring(index + 1);
lane.autoRange = true;
lane.signalIndices.push_back(static_cast<int>(index));
}
}
void SignalSet::SetAllVisible(bool visible)
{
for (Signal& signal : m_signals)
{
signal.visible = visible;
}
}
void SignalSet::SoloSignal(int signalIndex)
{
for (std::size_t index = 0; index < m_signals.size(); ++index)
{
m_signals[index].visible = (static_cast<int>(index) == signalIndex);
}
}
void SignalSet::ComputeLaneRange(int laneIndex, double& yMin, double& yMax) const
{
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
{
yMin = 0.0;
yMax = 1.0;
return;
}
const Lane& lane = m_lanes[static_cast<std::size_t>(laneIndex)];
if (!lane.autoRange)
{
yMin = lane.yMin;
yMax = lane.yMax;
if (!(yMax > yMin))
{
yMin = 0.0;
yMax = 1.0;
}
return;
}
double low = std::numeric_limits<double>::infinity();
double high = -std::numeric_limits<double>::infinity();
for (const int signalIndex : lane.signalIndices)
{
if (signalIndex < 0 || signalIndex >= static_cast<int>(m_signals.size()))
{
continue;
}
const Signal& signal = m_signals[static_cast<std::size_t>(signalIndex)];
if (!signal.visible || signal.finiteCount == 0)
{
continue;
}
low = std::min(low, signal.minValue);
high = std::max(high, signal.maxValue);
}
if (!std::isfinite(low) || !std::isfinite(high))
{
low = 0.0;
high = 1.0;
}
PadRange(low, high);
yMin = low;
yMax = high;
}
int SignalSet::FindNearestSample(double time) const
{
const std::size_t count = m_time.size();
if (count == 0)
{
return -1;
}
if (m_timeMonotonic)
{
const std::vector<double>::const_iterator lower =
std::lower_bound(m_time.begin(), m_time.end(), time);
if (lower == m_time.begin())
{
return 0;
}
if (lower == m_time.end())
{
return static_cast<int>(count - 1);
}
const std::size_t rightIndex = static_cast<std::size_t>(lower - m_time.begin());
const std::size_t leftIndex = rightIndex - 1;
const double leftDistance = std::fabs(time - m_time[leftIndex]);
const double rightDistance = std::fabs(m_time[rightIndex] - time);
return (leftDistance <= rightDistance) ? static_cast<int>(leftIndex) : static_cast<int>(rightIndex);
}
int best = 0;
double bestDistance = std::numeric_limits<double>::infinity();
for (std::size_t index = 0; index < count; ++index)
{
if (!std::isfinite(m_time[index]))
{
continue;
}
const double distance = std::fabs(m_time[index] - time);
if (distance < bestDistance)
{
bestDistance = distance;
best = static_cast<int>(index);
}
}
return best;
}
bool SignalSet::SampleAt(int signalIndex, double time, bool interpolate, double& value) const
{
value = kMissing;
if (signalIndex < 0 || signalIndex >= static_cast<int>(m_signals.size()))
{
return false;
}
if (m_time.empty() || time < m_minTime || time > m_maxTime)
{
return false;
}
const std::vector<double>& samples = m_signals[static_cast<std::size_t>(signalIndex)].values;
if (!m_timeMonotonic)
{
const int nearest = FindNearestSample(time);
if (nearest < 0)
{
return false;
}
value = samples[static_cast<std::size_t>(nearest)];
return std::isfinite(value);
}
const std::vector<double>::const_iterator lower =
std::lower_bound(m_time.begin(), m_time.end(), time);
const std::size_t rightIndex = static_cast<std::size_t>(lower - m_time.begin());
if (rightIndex == 0)
{
value = samples[0];
return std::isfinite(value);
}
if (rightIndex >= m_time.size())
{
value = samples.back();
return std::isfinite(value);
}
const std::size_t leftIndex = rightIndex - 1;
const double t0 = m_time[leftIndex];
const double t1 = m_time[rightIndex];
const double v0 = samples[leftIndex];
const double v1 = samples[rightIndex];
if (!interpolate || !std::isfinite(v0) || !std::isfinite(v1) || !(t1 > t0))
{
const double leftDistance = std::fabs(time - t0);
const double rightDistance = std::fabs(t1 - time);
value = (leftDistance <= rightDistance) ? v0 : v1;
return std::isfinite(value);
}
const double ratio = (time - t0) / (t1 - t0);
value = v0 + (v1 - v0) * ratio;
return true;
}
void SignalSet::PruneEmptyLanes()
{
if (m_lanes.size() <= 1)
{
return;
}
std::vector<Lane> kept;
kept.reserve(m_lanes.size());
for (Lane& lane : m_lanes)
{
if (!lane.signalIndices.empty())
{
kept.push_back(std::move(lane));
}
}
if (kept.empty())
{
return;
}
m_lanes = std::move(kept);
for (std::size_t index = 0; index < m_lanes.size(); ++index)
{
m_lanes[index].name = L"网格 " + std::to_wstring(index + 1);
}
}
std::uint32_t SignalSet::PickDefaultColor(std::size_t index) const
{
int paletteCount = 0;
const std::uint32_t* palette = SignalPalette(paletteCount);
if (palette == nullptr || paletteCount <= 0)
{
return 0xFF268BD2;
}
return palette[index % static_cast<std::size_t>(paletteCount)];
}
} // namespace jdi