网格菜单重构、大文件异步加载与内存/解析优化、修复应用图标

网格右键菜单:
- 菜单顺序改为: 网格 N / Y 轴最大值 / Y 轴最小值 / 选区 Y 轴自适应 / --分隔线-- /
  Y 轴自适应 / 时间轴自适应 / 全部自适应
  (恢复自适应移到分隔线以下、时间轴自适应上方)
- Y 轴最大值/最小值改为菜单内联输入框:点击就地编辑、回车提交,菜单不关闭,
  处理方式与拾色器的 RGB 内联编辑一致;提交后按真实夹取结果回写菜单显示值,
  并同步刷新自适应项的文案/对勾/可用状态
- 新增"选区 Y 轴自适应":只按当前显示时间段内的数据做幅值自适应
  (时间单调时二分定位区间,非单调时全扫并按区间过滤)

大文件加载:
- 加载改为工作线程执行(解析 + 按列重组),主线程抽送消息并播放过渡动画
  (旋转弧线 + 进度条 + 文件名 + 百分比 + 按 Esc 取消),界面不再整块卡死
- 加载完成后主线程只做一次 O(1) 的移动入队,不会产生可见卡顿
- 新增 DataModel::BuildDataFile / DataSet::AddPreparedFile:不依赖 DataSet,
  因此可以安全地在线程上运行

内存与解析效率:
- CsvTable 由"行优先二维数组"改为"列优先":百万行文件原先会产生上百万次独立堆分配
  (每行还有 vector 控制块与容量对齐开销),现在每列一整块连续内存
- 解析改为流式分块读取(4MB),不再把整个文件读入内存,也没有体积上限
  (原先超过 512MB 直接拒绝);实测 51MB 文件峰值内存约 126MB(数据本身 113MB)
- BuildDataFile 直接把整列移动进 SignalData(零拷贝),不再逐元素复制
- TryParseDouble 增加十进制快速路径:ucrt strtod 实测约 335ns/次,手写路径约 13ns
  (快 25 倍);非十进制写法(十六进制浮点、inf/nan、带尾部杂字符)原样交回 strtod,
  输入接受语义完全不变(实测与 strtod 的差异不超过 1 ULP)
  → 640MB 测试文件加载时间由约 40s 降到 2s 以内

交互流畅性:
- 拖拽(平移/框选/拖动信号)期间关闭波形抗锯齿并压缩顶点预算(2 像素一列包络),
  松开后立即恢复全质量绘制;实测 BIG_DATA 10 信号全显示的帧耗时由
  105~168ms 降到 75~90ms(其中折线绘制 55ms→23ms)
- 折线包络抽稀改为"列边界单调推进"的二分,避免每列在全区间二分造成缓存缺失
- 未对波形构建引入多线程:构建已由几何键缓存消除,且拖拽降级后仅约 1~2ms/帧,
  并行化收益不足以抵消线程与同步的复杂度(加载路径的多线程收益明确,故只在该处引入)

图标:
- 窗口类与 WM_SETICON 原先使用系统默认图标 IDI_APPLICATION,导致标题栏、
  任务栏与 Alt+Tab 缩略图都显示成通用应用图标;改为按 SM_CXICON/SM_CXSMICON
  从应用的 IDI_APP_ICON 资源加载(多尺寸 ICO 会挑选最接近的一帧)

其他:
- PopupMenu 新增 EDIT 项类型(菜单内联输入框)与 UpdateItemValue/UpdateItemLabel
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-09-30 18:46:56 +08:00
1 parent d6a81839bb
commit 45b44eae57
11 files changed
+1637 -222

No files matched your search

+102 -47
View File
@@ -244,18 +244,55 @@ namespace jdi
}
}
// ============================ DataSet ============================
// ============================ DataFile 构建 ============================
void DataSet::Clear()
/**
* @brief 单条信号的取值范围统计结果。
*/
struct SignalStats
{
m_files.clear();
double minValue = 0.0; /**< 有限采样值最小值。 */
double maxValue = 0.0; /**< 有限采样值最大值。 */
std::size_t finiteCount = 0; /**< 有限采样点数量。 */
};
/**
* @brief 统计一列采样的最小/最大值与有效点数量。
* @param [IN] values 采样列。
* @return 统计结果。
*/
SignalStats ComputeStats(const std::vector<double>& values)
{
SignalStats stats;
double low = std::numeric_limits<double>::infinity();
double high = -std::numeric_limits<double>::infinity();
for (const double value : values)
{
if (std::isfinite(value))
{
++stats.finiteCount;
low = std::min(low, value);
high = std::max(high, value);
}
}
if (stats.finiteCount == 0)
{
stats.minValue = 0.0;
stats.maxValue = 0.0;
}
else
{
stats.minValue = low;
stats.maxValue = high;
}
return stats;
}
bool DataSet::AddFromTable(const CsvTable& table, const std::wstring& filePath, std::wstring& error,
int& fileIndex)
bool BuildDataFile(CsvTable&& table, const std::wstring& filePath, std::size_t paletteBase, DataFile& file,
std::wstring& error, const CsvProgress& progress)
{
error.clear();
fileIndex = -1;
file = DataFile();
if (!table.HasData())
{
@@ -264,9 +301,12 @@ namespace jdi
}
const std::size_t columnCount = table.headers.size();
const std::size_t rowCount = table.rows.size();
DataFile file;
const std::size_t rowCount = table.rowCount;
if (table.columns.size() < columnCount)
{
error = L"数据表列结构不完整。";
return false;
}
// 1. 识别时间轴列:优先取列名匹配的列,否则回退到第一列。
int timeColumn = -1;
@@ -283,16 +323,15 @@ namespace jdi
timeColumn = 0;
}
file.time.resize(rowCount);
// 时间列整列移动,不做逐元素拷贝。
file.time = std::move(table.columns[static_cast<std::size_t>(timeColumn)]);
std::size_t finiteTimeCount = 0;
bool monotonic = true;
double previousTime = 0.0;
bool hasPrevious = false;
for (std::size_t row = 0; row < rowCount; ++row)
for (const double value : file.time)
{
const double value = table.rows[row][static_cast<std::size_t>(timeColumn)];
file.time[row] = value;
if (std::isfinite(value))
{
++finiteTimeCount;
@@ -318,7 +357,9 @@ namespace jdi
const std::size_t slashPos = filePath.find_last_of(L"\\/");
file.fileName = (slashPos == std::wstring::npos) ? filePath : filePath.substr(slashPos + 1);
// 2. 构建信号列。
// 2. 构建信号列:每列整块移动,随后就地统计取值范围。
int paletteCount = 0;
const std::uint32_t* palette = SignalPalette(paletteCount);
file.signals.reserve(columnCount);
for (std::size_t column = 0; column < columnCount; ++column)
{
@@ -335,41 +376,16 @@ namespace jdi
signal.unit = ExtractUnit(signal.name);
}
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;
signal.values = std::move(table.columns[column]);
const SignalStats stats = ComputeStats(signal.values);
signal.minValue = stats.minValue;
signal.maxValue = stats.maxValue;
signal.finiteCount = stats.finiteCount;
// 显示外观随信号一起初始化:调色板按「全局信号序号」取色,
// 使同一信号在任意标签页中都保持同一默认颜色。
{
int paletteCount = 0;
const std::uint32_t* palette = SignalPalette(paletteCount);
const std::size_t globalIndex = SignalCount() + file.signals.size();
const std::size_t globalIndex = paletteBase + file.signals.size();
signal.style.color =
(palette != nullptr && paletteCount > 0)
? palette[globalIndex % static_cast<std::size_t>(paletteCount)]
@@ -381,6 +397,13 @@ namespace jdi
}
file.signals.push_back(std::move(signal));
if (progress && !progress(0.75 + 0.25 * static_cast<double>(column + 1) /
static_cast<double>(columnCount)))
{
error = L"已取消加载。";
return false;
}
}
if (file.signals.empty())
@@ -389,7 +412,7 @@ namespace jdi
return false;
}
// 3. 统计时间范围。
// 3. 统计时间范围(时间列为空或全部无效时回退到行数上界)。
double tMin = std::numeric_limits<double>::infinity();
double tMax = -std::numeric_limits<double>::infinity();
for (const double value : file.time)
@@ -402,12 +425,44 @@ namespace jdi
}
file.minTime = tMin;
file.maxTime = tMax;
(void)rowCount;
return true;
}
// ============================ DataSet ============================
void DataSet::Clear()
{
m_files.clear();
}
bool DataSet::AddPreparedFile(DataFile&& file, int& fileIndex)
{
fileIndex = -1;
if (file.signals.empty())
{
return false;
}
m_files.push_back(std::move(file));
fileIndex = static_cast<int>(m_files.size()) - 1;
return true;
}
bool DataSet::AddFromTable(const CsvTable& table, const std::wstring& filePath, std::wstring& error,
int& fileIndex)
{
fileIndex = -1;
// 兼容入口:复制一份表格后走与后台加载完全一致的构建流程。
CsvTable local = table;
DataFile file;
if (!BuildDataFile(std::move(local), filePath, SignalCount(), file, error))
{
return false;
}
return AddPreparedFile(std::move(file), fileIndex);
}
bool DataSet::RemoveFile(int fileIndex)
{
if (fileIndex < 0 || fileIndex >= static_cast<int>(m_files.size()))