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:
commit
132b305544
22 files changed
+7146
No files matched your search
@@ -0,0 +1,370 @@
|
||||
#include "Utf8Util.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
/** @brief 判断字符是否为需要裁掉的空白字符。 */
|
||||
bool IsSpaceChar(wchar_t c)
|
||||
{
|
||||
return c == L' ' || c == L'\t' || c == L'\r' || c == L'\n' || c == L'\v' || c == L'\f' ||
|
||||
c == static_cast<wchar_t>(0x00A0);
|
||||
}
|
||||
|
||||
/** @brief 判断字符是否为 ASCII 大写字母。 */
|
||||
bool IsUpperAscii(wchar_t c)
|
||||
{
|
||||
return c >= L'A' && c <= L'Z';
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring Utf8ToWide(const char* text, int length)
|
||||
{
|
||||
if (text == nullptr)
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
int byteCount = (length < 0) ? static_cast<int>(std::strlen(text)) : length;
|
||||
if (byteCount <= 0)
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
int wideCount = MultiByteToWideChar(CP_UTF8, 0, text, byteCount, nullptr, 0);
|
||||
if (wideCount <= 0)
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
std::wstring result(static_cast<size_t>(wideCount), L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, text, byteCount, &result[0], wideCount);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string WideToUtf8(const wchar_t* text)
|
||||
{
|
||||
if (text == nullptr)
|
||||
{
|
||||
return std::string();
|
||||
}
|
||||
|
||||
int wideCount = static_cast<int>(std::wcslen(text));
|
||||
if (wideCount <= 0)
|
||||
{
|
||||
return std::string();
|
||||
}
|
||||
|
||||
int byteCount = WideCharToMultiByte(CP_UTF8, 0, text, wideCount, nullptr, 0, nullptr, nullptr);
|
||||
if (byteCount <= 0)
|
||||
{
|
||||
return std::string();
|
||||
}
|
||||
|
||||
std::string result(static_cast<size_t>(byteCount), '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, text, wideCount, &result[0], byteCount, nullptr, nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::wstring Trim(const std::wstring& text)
|
||||
{
|
||||
size_t begin = 0;
|
||||
size_t end = text.size();
|
||||
while (begin < end && IsSpaceChar(text[begin]))
|
||||
{
|
||||
++begin;
|
||||
}
|
||||
while (end > begin && IsSpaceChar(text[end - 1]))
|
||||
{
|
||||
--end;
|
||||
}
|
||||
return text.substr(begin, end - begin);
|
||||
}
|
||||
|
||||
std::wstring StripQuotes(const std::wstring& text)
|
||||
{
|
||||
std::wstring trimmed = Trim(text);
|
||||
if (trimmed.size() >= 2)
|
||||
{
|
||||
const wchar_t first = trimmed.front();
|
||||
const wchar_t last = trimmed.back();
|
||||
if ((first == L'"' && last == L'"') || (first == L'\'' && last == L'\''))
|
||||
{
|
||||
trimmed = trimmed.substr(1, trimmed.size() - 2);
|
||||
}
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
std::wstring ToLowerAscii(const std::wstring& text)
|
||||
{
|
||||
std::wstring result = text;
|
||||
for (wchar_t& c : result)
|
||||
{
|
||||
if (IsUpperAscii(c))
|
||||
{
|
||||
c = static_cast<wchar_t>(c - L'A' + L'a');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool StartsWith(const std::wstring& text, const std::wstring& prefix)
|
||||
{
|
||||
if (prefix.size() > text.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return text.compare(0, prefix.size(), prefix) == 0;
|
||||
}
|
||||
|
||||
bool ContainsInsensitive(const std::wstring& text, const std::wstring& needle)
|
||||
{
|
||||
if (needle.empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (needle.size() > text.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t last = text.size() - needle.size();
|
||||
for (size_t i = 0; i <= last; ++i)
|
||||
{
|
||||
size_t j = 0;
|
||||
while (j < needle.size())
|
||||
{
|
||||
wchar_t a = text[i + j];
|
||||
wchar_t b = needle[j];
|
||||
if (IsUpperAscii(a))
|
||||
{
|
||||
a = static_cast<wchar_t>(a - L'A' + L'a');
|
||||
}
|
||||
if (IsUpperAscii(b))
|
||||
{
|
||||
b = static_cast<wchar_t>(b - L'A' + L'a');
|
||||
}
|
||||
if (a != b)
|
||||
{
|
||||
break;
|
||||
}
|
||||
++j;
|
||||
}
|
||||
if (j == needle.size())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TryParseDouble(const char* begin, const char* end, double& value)
|
||||
{
|
||||
if (begin == nullptr || end == nullptr || begin >= end)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* p = begin;
|
||||
while (p < end && (*p == ' ' || *p == '\t'))
|
||||
{
|
||||
++p;
|
||||
}
|
||||
const char* q = end;
|
||||
while (q > p && (q[-1] == ' ' || q[-1] == '\t' || q[-1] == '\r' || q[-1] == '\n'))
|
||||
{
|
||||
--q;
|
||||
}
|
||||
if (p >= q)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
char buffer[96];
|
||||
const size_t length = static_cast<size_t>(q - p);
|
||||
const char* text = nullptr;
|
||||
std::string dynamic;
|
||||
if (length < sizeof(buffer))
|
||||
{
|
||||
std::memcpy(buffer, p, length);
|
||||
buffer[length] = '\0';
|
||||
text = buffer;
|
||||
}
|
||||
else
|
||||
{
|
||||
dynamic.assign(p, q);
|
||||
text = dynamic.c_str();
|
||||
}
|
||||
|
||||
char* stop = nullptr;
|
||||
const double parsed = std::strtod(text, &stop);
|
||||
if (stop == text)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::wstring FormatNumber(double value, int decimals)
|
||||
{
|
||||
if (decimals < 0)
|
||||
{
|
||||
decimals = 0;
|
||||
}
|
||||
if (decimals > 12)
|
||||
{
|
||||
decimals = 12;
|
||||
}
|
||||
|
||||
wchar_t buffer[96];
|
||||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.*f", decimals, value);
|
||||
return std::wstring(buffer);
|
||||
}
|
||||
|
||||
std::wstring FormatValue(double value)
|
||||
{
|
||||
if (std::isnan(value))
|
||||
{
|
||||
return L"NaN";
|
||||
}
|
||||
if (std::isinf(value))
|
||||
{
|
||||
return (value > 0.0) ? L"+Inf" : L"-Inf";
|
||||
}
|
||||
|
||||
wchar_t buffer[96];
|
||||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.6g", value);
|
||||
return std::wstring(buffer);
|
||||
}
|
||||
|
||||
std::wstring FormatTick(double value, double step)
|
||||
{
|
||||
if (value == 0.0)
|
||||
{
|
||||
return L"0";
|
||||
}
|
||||
if (!std::isfinite(value))
|
||||
{
|
||||
return FormatValue(value);
|
||||
}
|
||||
|
||||
int decimals = 0;
|
||||
if (step > 0.0 && std::isfinite(step))
|
||||
{
|
||||
const double magnitude = std::ceil(-std::log10(step) - 1.0e-9);
|
||||
if (magnitude > 0.0)
|
||||
{
|
||||
decimals = static_cast<int>(magnitude);
|
||||
}
|
||||
}
|
||||
if (decimals > 9)
|
||||
{
|
||||
decimals = 9;
|
||||
}
|
||||
|
||||
wchar_t buffer[96];
|
||||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.*f", decimals, value);
|
||||
std::wstring text(buffer);
|
||||
if (text.size() > 15)
|
||||
{
|
||||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.4e", value);
|
||||
text.assign(buffer);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
std::wstring FormatCount(unsigned long long value)
|
||||
{
|
||||
wchar_t buffer[32];
|
||||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%llu", value);
|
||||
|
||||
std::wstring digits(buffer);
|
||||
std::wstring result;
|
||||
result.reserve(digits.size() + digits.size() / 3);
|
||||
|
||||
const size_t length = digits.size();
|
||||
for (size_t i = 0; i < length; ++i)
|
||||
{
|
||||
if (i > 0 && ((length - i) % 3u) == 0u)
|
||||
{
|
||||
result.push_back(L',');
|
||||
}
|
||||
result.push_back(digits[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
double NiceStep(double range, int targetTicks)
|
||||
{
|
||||
if (!(range > 0.0) || !std::isfinite(range) || targetTicks <= 0)
|
||||
{
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
const double raw = range / static_cast<double>(targetTicks);
|
||||
const double magnitude = std::pow(10.0, std::floor(std::log10(raw)));
|
||||
const double normalized = raw / magnitude;
|
||||
|
||||
double nice = 10.0;
|
||||
if (normalized <= 1.0)
|
||||
{
|
||||
nice = 1.0;
|
||||
}
|
||||
else if (normalized <= 2.0)
|
||||
{
|
||||
nice = 2.0;
|
||||
}
|
||||
else if (normalized <= 5.0)
|
||||
{
|
||||
nice = 5.0;
|
||||
}
|
||||
|
||||
return nice * magnitude;
|
||||
}
|
||||
|
||||
void BuildTicks(double minValue, double maxValue, double step, std::vector<double>& ticks)
|
||||
{
|
||||
ticks.clear();
|
||||
if (!std::isfinite(minValue) || !std::isfinite(maxValue) || !(step > 0.0) || !std::isfinite(step))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (maxValue < minValue)
|
||||
{
|
||||
const double temp = minValue;
|
||||
minValue = maxValue;
|
||||
maxValue = temp;
|
||||
}
|
||||
|
||||
const long long first = static_cast<long long>(std::ceil(minValue / step - 1.0e-9));
|
||||
const long long last = static_cast<long long>(std::floor(maxValue / step + 1.0e-9));
|
||||
if (last < first)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const long long count = last - first + 1;
|
||||
if (count > 512)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ticks.reserve(static_cast<size_t>(count));
|
||||
for (long long index = first; index <= last; ++index)
|
||||
{
|
||||
ticks.push_back(static_cast<double>(index) * step);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
Reference in new issue
Block a user