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

网格右键菜单:
- 菜单顺序改为: 网格 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

+30 -8
View File
@@ -2,6 +2,7 @@
#define __CSV_TABLE_HPP__
#include <cstddef>
#include <functional>
#include <string>
#include <vector>
@@ -9,15 +10,22 @@ namespace jdi
{
/**
* @brief CSV 文件的解析结果,保存列名与数值矩阵。
* @brief CSV 文件的解析结果,保存列名与**列优先**的数值矩阵。
*
* 采用列优先(每条信号一整块连续内存)而不是行优先的二维数组,原因有二:
* 1. 大型文件(百万行级)若按行存储会产生上百万次独立堆分配,
* 每行还要额外承担 vector 的控制块与容量对齐开销,实测内存放大接近 1.5 倍;
* 2. 后续构建信号时可以直接把整列**移动**进 SignalData,不再逐元素拷贝。
*/
struct CsvTable
{
std::vector<std::wstring> headers; /**< 列名(已去除引号与首尾空白)。 */
std::vector<std::vector<double>> rows; /**< 数值矩阵,行优先,每行长度与 headers 一致。 */
wchar_t delimiter = L','; /**< 实际使用的分隔符。 */
std::size_t sourceRowCount = 0; /**< 文件中有效数据行数(不含空行与注释行)。 */
std::size_t skippedRowCount = 0; /**< 因格式异常被置为缺失值的行数。 */
std::vector<std::wstring> headers; /**< 列名(已去除引号与首尾空白)。 */
std::vector<std::vector<double>> columns; /**< 列优先数值矩阵:columns[列][行],缺失处为 NaN。 */
std::size_t rowCount = 0; /**< 有效数据行数。 */
wchar_t delimiter = L','; /**< 实际使用的分隔符。 */
std::size_t sourceRowCount = 0; /**< 文件中有效数据行数(不含空行与注释行)。 */
std::size_t skippedRowCount = 0; /**< 因格式异常被置为缺失值的行数。 */
unsigned long long fileBytes = 0; /**< 源文件字节数(用于按比例预留容量与进度显示)。 */
/**
* @brief 判断解析结果是否包含可用的表格数据。
@@ -25,18 +33,32 @@ namespace jdi
*/
bool HasData() const
{
return !headers.empty() && !rows.empty();
return !headers.empty() && (rowCount > 0);
}
};
/**
* @brief 解析进度回调。
*
* 在解析过程中按块回调,ratio 为 0.0-1.0 的完成比例;
* 返回 false 表示请求取消,解析会尽快停止并返回失败。
*/
using CsvProgress = std::function<bool(double ratio)>;
/**
* @brief 读取并解析一个 CSV 文件。
*
* 文件以分块流式方式读取(不把整个文件一次性读入内存),
* 因此内存占用只与解析结果相关,不会因文件体积成倍放大,也没有体积上限。
*
* @param [IN] filePath CSV 文件完整路径(宽字符,支持中文路径)。
* @param [OUT] table 解析结果输出。
* @param [OUT] error 失败原因描述(成功时清空)。
* @param [IN] progress 解析进度回调,可为空。
* @return 解析成功返回 true,否则返回 false。
*/
bool LoadCsvTable(const std::wstring& filePath, CsvTable& table, std::wstring& error);
bool LoadCsvTable(const std::wstring& filePath, CsvTable& table, std::wstring& error,
const CsvProgress& progress = CsvProgress());
} // namespace jdi
+28
View File
@@ -140,6 +140,26 @@ namespace jdi
const std::wstring& Title() const { return displayName.empty() ? fileName : displayName; }
};
/**
* @brief 由 CSV 解析结果构建可入库的数据文件。
*
* 该函数**不访问 DataSet**(只依赖传入的 CsvTable 与调色板基准序号),
* 因此可以在工作线程上执行:大文件的解析与列重组占整个加载耗时的大头,
* 放到工作线程后主线程只需做一次 O(1) 的移动入队,加载过程中的界面不会卡住。
*
* table 按右值传入:每列会被**移动**进对应信号的 values(零拷贝)。
*
* @param [IN,OUT] table CSV 解析结果(列会被移走)。
* @param [IN] filePath 源文件路径。
* @param [IN] paletteBase 调色板起始序号(通常传入当前已有信号数)。
* @param [OUT] file 构建结果。
* @param [OUT] error 失败原因描述(成功时清空)。
* @param [IN] progress 进度回调(0-1),返回 false 表示取消,可为空。
* @return 构建成功返回 true。
*/
bool BuildDataFile(CsvTable&& table, const std::wstring& filePath, std::size_t paletteBase, DataFile& file,
std::wstring& error, const CsvProgress& progress = CsvProgress());
/**
* @brief 数据集合:统一管理全部已加载文件,与视图配置无关。
*/
@@ -161,6 +181,14 @@ namespace jdi
*/
bool AddFromTable(const CsvTable& table, const std::wstring& filePath, std::wstring& error, int& fileIndex);
/**
* @brief 追加一个已构建好的数据文件(仅做一次移动,不拷贝采样数据)。
* @param [IN,OUT] file 数据文件(内部缓冲会被移走)。
* @param [OUT] fileIndex 新文件下标。
* @return 追加成功返回 true。
*/
bool AddPreparedFile(DataFile&& file, int& fileIndex);
/**
* @brief 移除指定下标的文件,并立即释放其占用的全部采样内存。
* @param [IN] fileIndex 文件下标。
+122 -3
View File
@@ -10,6 +10,7 @@
#include <vector>
#include "PaintUtil.hpp"
#include "TextEdit.hpp"
#include "Theme.hpp"
namespace jdi
@@ -34,7 +35,8 @@ namespace jdi
NORMAL = 0, /**< 普通可点击项。 */
SEPARATOR = 1, /**< 分隔线。 */
HEADER = 2, /**< 分组标题(不可点击)。 */
SWATCHES = 3 /**< 颜色色块行。 */
SWATCHES = 3, /**< 颜色色块行。 */
EDIT = 4 /**< 带内联输入框的项:在菜单内部直接编辑数值(菜单不关闭)。 */
};
/**
@@ -43,13 +45,15 @@ namespace jdi
struct MenuItem
{
MenuItemKind kind = MenuItemKind::NORMAL; /**< 项目类型。 */
std::wstring label; /**< 显示文本。 */
std::wstring label; /**< 显示文本;EDIT 项为输入框左侧的说明文字。 */
int id = 0; /**< 命令标识。 */
bool enabled = true; /**< 是否可用。 */
bool checked = false; /**< 是否处于选中(单选)状态。 */
int swatchCount = 0; /**< 色块数量(仅色块行有效)。 */
std::uint32_t swatches[16] = {}; /**< 色块颜色。 */
int checkedSwatch = -1; /**< 被选中的色块下标。 */
std::wstring value; /**< EDIT 项的当前数值文本(非编辑态显示)。 */
EditFilter filter = EditFilter::ANY; /**< EDIT 项的输入过滤规则。 */
};
/**
@@ -62,6 +66,8 @@ namespace jdi
int id = 0; /**< 被选中项的命令标识。 */
int swatchIndex = -1; /**< 被选中的色块下标(非色块行为 -1)。 */
gd::RectF itemRect; /**< 被点击条目的矩形(客户区坐标),供就近弹出输入框。 */
bool editCommit = false; /**< 该结果来自菜单内联输入框的提交(菜单保持打开)。 */
std::wstring editText; /**< 提交的输入文本(editCommit 为 true 时有效)。 */
};
/**
@@ -158,11 +164,84 @@ namespace jdi
/**
* @brief 判断坐标是否落在菜单内部。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @param [IN] y 客户区纵坐标。
* @return 落在菜单内返回 true。
*/
bool ContainsPoint(int x, int y) const;
/**
* @brief 设置剪贴板所有者窗口句柄(供内联输入框复制/粘贴)。
* @param [IN] host 宿主窗口句柄,允许为空。
*/
void SetHostWindow(HWND host) { m_host = host; }
/**
* @brief 判断是否有菜单项正处于内联编辑状态。
* @return 处于编辑状态返回 true。
*/
bool IsEditing() const { return m_editIndex >= 0; }
/**
* @brief 获取当前正在编辑的菜单项命令标识。
* @return 命令标识,未编辑时返回 0。
*/
int EditingItemId() const;
/**
* @brief 判断坐标是否落在当前正在编辑的输入框内。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 落在输入框内返回 true。
*/
bool IsOnEditField(int x, int y) const;
/**
* @brief 编辑态下按下左键:定位插入符并开始拖拽框选。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 命中输入框返回 true。
*/
bool OnEditMouseDown(int x, int y);
/**
* @brief 编辑态下拖拽鼠标:扩展选区。
* @param [IN] x 客户区横坐标。
* @return 选区发生变化返回 true。
*/
bool OnEditMouseMove(int x);
/**
* @brief 编辑态下松开左键:结束拖拽框选。
* @return 之前处于拖拽中返回 true。
*/
bool OnEditMouseUp();
/**
* @brief 编辑态下处理字符输入。
* @param [IN] character 字符。
* @param [OUT] result 提交时返回 editCommit 结果。
* @return 需要重绘返回 true。
*/
bool OnChar(wchar_t character, MenuResult& result);
/**
* @brief 更新指定命令项显示的值文本(菜单保持打开,立即重新排布)。
* @param [IN] id 命令标识。
* @param [IN] value 新的值文本。
* @return 找到对应项并更新返回 true。
*/
bool UpdateItemValue(int id, const std::wstring& value);
/**
* @brief 更新指定普通项的文字与可用/选中状态(用于实时反映状态)。
* @param [IN] id 命令标识。
* @param [IN] label 新的文字。
* @param [IN] checked 新的选中状态。
* @param [IN] enabled 新的可用状态。
* @return 找到对应项并更新返回 true。
*/
bool UpdateItemLabel(int id, const std::wstring& label, bool checked = false, bool enabled = true);
private:
/**
* @brief 计算单个菜单项的纵向高度。
@@ -201,6 +280,38 @@ namespace jdi
*/
bool CommitHover(MenuResult& result);
/**
* @brief 计算指定下标项的内联输入框矩形(EDIT 项)。
* @param [IN] index 项目下标。
* @return 输入框矩形,非法下标返回空矩形。
*/
gd::RectF EditFieldRect(int index) const;
/**
* @brief 进入指定项的内联编辑状态。
* @param [IN] index 项目下标。
* @return 成功进入返回 true。
*/
bool BeginItemEdit(int index);
/**
* @brief 结束内联编辑。
* @param [IN] commit 是否提交(false 为放弃修改)。
* @param [OUT] result 提交时输出 editCommit 结果。
* @return 之前处于编辑状态返回 true。
*/
bool EndItemEdit(bool commit, MenuResult& result);
/**
* @brief 依据当前项集合重新排布(用于值文本变化后)。
*/
void RebuildLayout();
/**
* @brief 退出内联编辑状态(不产生任何结果)。
*/
void CloseEditState();
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
const Theme* m_theme = nullptr; /**< 主题资源。 */
float m_scale = 1.0f; /**< DPI 缩放比例。 */
@@ -217,6 +328,14 @@ namespace jdi
int m_hoverSwatch = -1; /**< 当前悬停色块。 */
int m_pressedIndex = -1; /**< 按下时的项目下标。 */
int m_pressedSwatch = -1; /**< 按下时的色块下标。 */
HWND m_host = nullptr; /**< 剪贴板所有者窗口句柄。 */
TextEdit m_edit; /**< 内联输入框编辑模型。 */
int m_editIndex = -1; /**< 正在编辑的项目下标,-1 表示未编辑。 */
gd::RectF m_buildBounds; /**< 最近一次排布使用的宿主矩形。 */
int m_buildX = 0; /**< 最近一次排布使用的锚点横坐标。 */
int m_buildY = 0; /**< 最近一次排布使用的锚点纵坐标。 */
};
} // namespace jdi
+199 -103
View File
@@ -22,56 +22,76 @@ namespace jdi
};
const double kMissingValue = std::numeric_limits<double>::quiet_NaN(); /**< 缺失值占位。 */
const std::size_t kReadChunkBytes = 4u * 1024u * 1024u; /**< 流式读取块大小。 */
/**
* @brief 一次性将整个文件读入内存。
* @param [IN] filePath 文件路径。
* @param [OUT] buffer 文件内容输出。
* @param [OUT] error 失败原因描述。
* @return 读取成功返回 true。
* @brief 按比例预留的初始行容量。
*
* 先按文件体积估算一个行数,既能避免边解析边扩容(百万行时复制代价可观),
* 又不会因为估得过大而浪费内存(超估时 vector 只是容量偏大,数据本身不变)。
*/
bool ReadWholeFile(const std::wstring& filePath, std::string& buffer, std::wstring& error)
std::size_t EstimateRowCapacity(unsigned long long fileBytes)
{
FILE* file = _wfopen(filePath.c_str(), L"rb");
// 典型 CSV 每行约 40-200 字节;取下界 64 字节估算行数,宁可小估也不要多占。
const unsigned long long estimate = fileBytes / 64ull;
if (estimate > 4096ull * 1024ull)
{
return static_cast<std::size_t>(4096ull * 1024ull);
}
return static_cast<std::size_t>(estimate);
}
/**
* @brief 打开文件并查询其体积。
* @param [IN] filePath 文件路径。
* @param [OUT] file 文件句柄输出(需调用方关闭)。
* @param [OUT] fileSize 文件字节数输出。
* @param [OUT] error 失败原因描述。
* @return 打开成功返回 true。
*/
bool OpenFile(const std::wstring& filePath, FILE*& file, unsigned long long& fileSize,
std::wstring& error)
{
file = _wfopen(filePath.c_str(), L"rb");
if (file == nullptr)
{
error = L"无法打开文件,请确认路径与访问权限。";
return false;
}
if (std::fseek(file, 0, SEEK_END) != 0)
// 使用 64 位定位接口,避免 long 在部分平台下只有 32 位而无法处理大文件。
if (_fseeki64(file, 0, SEEK_END) != 0)
{
std::fclose(file);
file = nullptr;
error = L"无法定位文件末尾。";
return false;
}
const long fileSize = std::ftell(file);
if (fileSize < 0)
const long long size = _ftelli64(file);
if (size < 0)
{
std::fclose(file);
file = nullptr;
error = L"无法获取文件长度。";
return false;
}
if (fileSize == 0)
if (size == 0)
{
std::fclose(file);
file = nullptr;
error = L"文件内容为空。";
return false;
}
if (fileSize > (512L * 1024L * 1024L))
if (_fseeki64(file, 0, SEEK_SET) != 0)
{
std::fclose(file);
error = L"文件体积超过 512 MB,暂不支持。";
file = nullptr;
error = L"无法回到文件开头。";
return false;
}
std::fseek(file, 0, SEEK_SET);
buffer.resize(static_cast<size_t>(fileSize));
const size_t readCount = std::fread(&buffer[0], 1, buffer.size(), file);
std::fclose(file);
buffer.resize(readCount);
fileSize = static_cast<unsigned long long>(size);
return true;
}
@@ -197,82 +217,54 @@ namespace jdi
}
/**
* @brief 在一行内查找首个非空行的终止位置。
* @param [IN] begin 文本起始指针。
* @param [IN] end 文本结束指针(不包含)。
* @return 该行结束指针(不含换行符)。
* @brief 解析状态:跨读取块保存进度。
*/
const char* FindLineEnd(const char* begin, const char* end)
struct ParseState
{
const char* p = begin;
while (p < end && *p != '\n')
{
++p;
}
const char* lineEnd = p;
while (lineEnd > begin && (lineEnd[-1] == '\r' || lineEnd[-1] == '\n'))
std::string pending; /**< 尚未处理完的尾部字节(不完整行)。 */
std::vector<FieldSpan> fields; /**< 字段区间复用缓冲。 */
std::vector<double> rowValues; /**< 行数值复用缓冲(列优先存储时的中转)。 */
bool headerReady = false; /**< 是否已解析表头。 */
unsigned long long bytesConsumed = 0; /**< 已处理字节数(用于进度)。 */
};
/**
* @brief 处理一块完整行文本(不含换行符)。
* @param [IN,OUT] table 解析结果。
* @param [IN,OUT] state 解析状态。
* @param [IN] begin 行起始指针。
* @param [IN] end 行结束指针(不包含)。
* @return 解析可以继续返回 true;表头非法需要中止时返回 false。
*/
bool HandleLine(CsvTable& table, ParseState& state, const char* begin, const char* end)
{
const char* lineEnd = end;
while (lineEnd > begin && lineEnd[-1] == '\r')
{
--lineEnd;
}
return lineEnd;
}
}
bool LoadCsvTable(const std::wstring& filePath, CsvTable& table, std::wstring& error)
{
table = CsvTable();
error.clear();
std::string buffer;
if (!ReadWholeFile(filePath, buffer, error))
{
return false;
}
const char* data = buffer.data();
size_t length = buffer.size();
// 跳过 UTF-8 BOM,避免首列名被污染。
if (length >= 3 && static_cast<unsigned char>(data[0]) == 0xEF &&
static_cast<unsigned char>(data[1]) == 0xBB && static_cast<unsigned char>(data[2]) == 0xBF)
{
data += 3;
length -= 3;
}
const char* cursor = data;
const char* limit = data + length;
std::vector<FieldSpan> fields;
bool headerReady = false;
while (cursor < limit)
{
const char* rawEnd = FindLineEnd(cursor, limit);
const char* lineEnd = rawEnd;
// 跳过纯空白行,但不产生数据行。
const char* scan = cursor;
while (scan < lineEnd && (*scan == ' ' || *scan == '\t' || *scan == '\r'))
const char* scan = begin;
while (scan < lineEnd && (*scan == ' ' || *scan == '\t'))
{
++scan;
}
const bool lineIsEmpty = (scan >= lineEnd);
if (!headerReady && lineIsEmpty)
if (!state.headerReady)
{
cursor = (rawEnd < limit) ? (rawEnd + 1) : limit;
continue;
}
if (lineIsEmpty)
{
return true;
}
if (!headerReady)
{
table.delimiter = DetectDelimiter(cursor, lineEnd);
SplitFields(cursor, lineEnd, static_cast<char>(table.delimiter), fields);
table.delimiter = DetectDelimiter(begin, lineEnd);
SplitFields(begin, lineEnd, static_cast<char>(table.delimiter), state.fields);
table.headers.clear();
table.headers.reserve(fields.size());
for (const FieldSpan& span : fields)
table.headers.reserve(state.fields.size());
for (const FieldSpan& span : state.fields)
{
std::wstring text = FieldToText(span);
if (text.empty())
@@ -284,60 +276,164 @@ namespace jdi
if (table.headers.empty())
{
error = L"表头为空,无法解析列名。";
return false;
}
headerReady = true;
cursor = (rawEnd < limit) ? (rawEnd + 1) : limit;
continue;
// 列优先存储:一次性为每列预留容量,避免逐行扩容(百万行时复制代价可观)。
const std::size_t capacity = EstimateRowCapacity(table.fileBytes);
table.columns.assign(table.headers.size(), std::vector<double>());
for (std::vector<double>& column : table.columns)
{
if (capacity > 0)
{
column.reserve(capacity);
}
}
state.rowValues.assign(table.headers.size(), kMissingValue);
state.headerReady = true;
return true;
}
SplitFields(cursor, lineEnd, static_cast<char>(table.delimiter), fields);
SplitFields(begin, lineEnd, static_cast<char>(table.delimiter), state.fields);
// 空行与注释行(以 # 或 % 开头)不参与数据统计。
if (ShouldSkipLine(fields.empty() ? FieldSpan() : fields.front(), lineIsEmpty))
if (ShouldSkipLine(state.fields.empty() ? FieldSpan() : state.fields.front(), lineIsEmpty))
{
cursor = (rawEnd < limit) ? (rawEnd + 1) : limit;
continue;
return true;
}
if (fields.size() != table.headers.size())
if (state.fields.size() != table.headers.size())
{
++table.skippedRowCount;
}
std::vector<double> row;
row.reserve(table.headers.size());
bool rowHasValue = false;
for (size_t column = 0; column < table.headers.size(); ++column)
for (std::size_t column = 0; column < state.rowValues.size(); ++column)
{
if (column >= fields.size())
{
row.push_back(kMissingValue);
continue;
}
double value = kMissingValue;
if (TryParseDouble(fields[column].begin, fields[column].end, value))
if (column < state.fields.size() &&
TryParseDouble(state.fields[column].begin, state.fields[column].end, value))
{
rowHasValue = true;
}
row.push_back(value);
state.rowValues[column] = value;
}
// 整行都没有有效数值时该行直接丢弃(与旧行为一致):
// 列优先存储要求每列等长,因此必须整行一起跳过,不能只补某一列。
if (!rowHasValue)
{
cursor = (rawEnd < limit) ? (rawEnd + 1) : limit;
continue;
return true;
}
table.rows.push_back(std::move(row));
for (std::size_t column = 0; column < state.rowValues.size(); ++column)
{
table.columns[column].push_back(state.rowValues[column]);
}
++table.rowCount;
++table.sourceRowCount;
return true;
}
}
cursor = (rawEnd < limit) ? (rawEnd + 1) : limit;
bool LoadCsvTable(const std::wstring& filePath, CsvTable& table, std::wstring& error,
const CsvProgress& progress)
{
table = CsvTable();
error.clear();
FILE* file = nullptr;
unsigned long long fileSize = 0;
if (!OpenFile(filePath, file, fileSize, error))
{
return false;
}
table.fileBytes = fileSize;
ParseState state;
std::string staging;
staging.resize(kReadChunkBytes);
bool cancelled = false;
bool fatal = false;
while (!cancelled && !fatal)
{
const size_t readCount = std::fread(&staging[0], 1, staging.size(), file);
if (readCount == 0)
{
break;
}
state.bytesConsumed += readCount;
state.pending.append(staging.data(), readCount);
// 首个块处理前剥离 UTF-8 BOM,避免首列名被污染(BOM 必然落在第一块内)。
if (!state.headerReady && state.pending.size() >= 3 &&
static_cast<unsigned char>(state.pending[0]) == 0xEF &&
static_cast<unsigned char>(state.pending[1]) == 0xBB &&
static_cast<unsigned char>(state.pending[2]) == 0xBF)
{
state.pending.erase(0, 3);
}
// 处理 pending 中所有以换行结束的完整行。
std::size_t lineStart = 0;
while (true)
{
const std::size_t newline = state.pending.find('\n', lineStart);
if (newline == std::string::npos)
{
break;
}
if (!HandleLine(table, state, state.pending.data() + lineStart,
state.pending.data() + newline))
{
fatal = true;
break;
}
lineStart = newline + 1;
}
if (fatal)
{
break;
}
state.pending.erase(0, lineStart);
if (progress)
{
const double ratio = (fileSize > 0)
? static_cast<double>(state.bytesConsumed) /
static_cast<double>(fileSize)
: 0.0;
if (!progress(ratio > 1.0 ? 1.0 : ratio))
{
cancelled = true;
}
}
}
// 处理文件末尾没有换行符的最后一行。
if (!cancelled && !fatal && !state.pending.empty())
{
if (!HandleLine(table, state, state.pending.data(), state.pending.data() + state.pending.size()))
{
fatal = true;
}
}
std::fclose(file);
if (cancelled)
{
error = L"已取消加载。";
return false;
}
if (table.headers.empty())
{
error = fatal ? L"表头为空,无法解析列名。" : L"未从文件中解析出有效数据行。";
return false;
}
if (!table.HasData())
{
error = L"未从文件中解析出有效数据行。";
+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()))
+353 -3
View File
@@ -21,6 +21,9 @@ namespace jdi
const float kCorner = 6.0f; /**< 菜单圆角半径。 */
const float kSwatchSize = 22.0f; /**< 色块尺寸。 */
const float kSwatchGap = 5.0f; /**< 色块间距。 */
const float kEditFieldWidth = 88.0f; /**< EDIT 项输入框宽度。 */
const float kEditFieldGap = 6.0f; /**< 说明文字与输入框之间的最小间隔。 */
const float kEditFieldPadV = 5.0f; /**< 输入框上下内缩。 */
}
PopupMenu::PopupMenu() = default;
@@ -46,6 +49,9 @@ namespace jdi
return kHeaderHeight * m_scale;
case MenuItemKind::SWATCHES:
return kSwatchRowHeight * m_scale;
case MenuItemKind::EDIT:
// 编辑项比普通项略高,给输入框留出上下呼吸空间。
return (kRowHeight + 4.0f) * m_scale;
case MenuItemKind::NORMAL:
default:
return kRowHeight * m_scale;
@@ -90,6 +96,17 @@ namespace jdi
UiMeasureTextWidth(m_fonts->uiSmall, item.label) + kRightPad * m_scale;
contentWidth = std::max(contentWidth, width);
}
else if (item.kind == MenuItemKind::EDIT)
{
// 说明文字 + 输入框 + 右侧留白;输入框宽度按数值文本自适应(取一个上限)。
const float fieldWidth =
std::max(kEditFieldWidth * m_scale,
UiMeasureTextWidth(m_fonts->uiSmall, item.value) + 20.0f * m_scale);
const float width = (kPadH + 8.0f) * m_scale +
UiMeasureTextWidth(m_fonts->uiSmall, item.label) + kEditFieldGap * m_scale +
fieldWidth + kRightPad * m_scale;
contentWidth = std::max(contentWidth, width);
}
}
contentWidth = std::min(contentWidth, kMaxWidth * m_scale);
@@ -140,6 +157,211 @@ namespace jdi
}
}
gd::RectF PopupMenu::EditFieldRect(int index) const
{
if (index < 0 || index >= static_cast<int>(m_items.size()))
{
return gd::RectF();
}
if (m_items[static_cast<std::size_t>(index)].kind != MenuItemKind::EDIT)
{
return gd::RectF();
}
// 输入框靠右对齐:宽度取固定下限与当前文本宽度中的较大者,但不得超过菜单可用宽度的一半。
const gd::RectF& rect = m_itemRects[static_cast<std::size_t>(index)];
const float maxWidth = std::max(40.0f * m_scale, rect.Width * 0.5f);
float width = std::max(kEditFieldWidth * m_scale,
UiMeasureTextWidth(m_fonts->uiSmall, m_items[static_cast<std::size_t>(index)].value) +
20.0f * m_scale);
width = std::min(width, maxWidth);
const float height = rect.Height - kEditFieldPadV * 2.0f * m_scale;
return gd::RectF(rect.X + rect.Width - width - 4.0f * m_scale,
rect.Y + (rect.Height - height) * 0.5f, width, height);
}
int PopupMenu::EditingItemId() const
{
if (m_editIndex < 0 || m_editIndex >= static_cast<int>(m_items.size()))
{
return 0;
}
return m_items[static_cast<std::size_t>(m_editIndex)].id;
}
bool PopupMenu::IsOnEditField(int x, int y) const
{
if (m_editIndex < 0)
{
return false;
}
const gd::RectF rect = EditFieldRect(m_editIndex);
return UiPointInRect(rect, static_cast<float>(x), static_cast<float>(y));
}
bool PopupMenu::BeginItemEdit(int index)
{
if (!m_open || index < 0 || index >= static_cast<int>(m_items.size()))
{
return false;
}
const MenuItem& item = m_items[static_cast<std::size_t>(index)];
if (item.kind != MenuItemKind::EDIT || !item.enabled)
{
return false;
}
// 输入框初值取当前显示值,整段选中,便于直接键入新数值。
m_edit.Begin(item.value, true, item.filter, 0u);
m_editIndex = index;
m_hoverIndex = index;
return true;
}
bool PopupMenu::EndItemEdit(bool commit, MenuResult& result)
{
if (m_editIndex < 0)
{
return false;
}
const int index = m_editIndex;
const std::wstring text = m_edit.Text();
m_editIndex = -1;
m_edit.End();
if (!commit || index >= static_cast<int>(m_items.size()))
{
return true;
}
MenuItem& item = m_items[static_cast<std::size_t>(index)];
// 立即回写显示值,使菜单上的数值与数据层一致(宿主会在派发后再纠正为夹取后的真实值)。
item.value = text;
result.valid = true;
result.scope = m_scope;
result.id = item.id;
result.editCommit = true;
result.editText = text;
return true;
}
void PopupMenu::RebuildLayout()
{
if (!m_open)
{
return;
}
BuildLayout(m_buildBounds, m_buildX, m_buildY);
}
bool PopupMenu::UpdateItemValue(int id, const std::wstring& value)
{
for (MenuItem& item : m_items)
{
if (item.id == id && item.kind == MenuItemKind::EDIT)
{
if (item.value == value)
{
return true;
}
item.value = value;
RebuildLayout();
return true;
}
}
return false;
}
bool PopupMenu::UpdateItemLabel(int id, const std::wstring& label, bool checked, bool enabled)
{
for (MenuItem& item : m_items)
{
if (item.id == id && item.kind == MenuItemKind::NORMAL)
{
if (item.label == label && item.checked == checked && item.enabled == enabled)
{
return true;
}
item.label = label;
item.checked = checked;
item.enabled = enabled;
RebuildLayout();
return true;
}
}
return false;
}
bool PopupMenu::OnEditMouseDown(int x, int y)
{
if (m_editIndex < 0)
{
return false;
}
return UiEditFieldMouseDown(m_fonts->uiSmall, EditFieldRect(m_editIndex), m_edit, m_scale, x, y);
}
bool PopupMenu::OnEditMouseMove(int x)
{
if (m_editIndex < 0)
{
return false;
}
return UiEditFieldMouseMove(m_fonts->uiSmall, EditFieldRect(m_editIndex), m_edit, m_scale, x);
}
bool PopupMenu::OnEditMouseUp()
{
if (!m_edit.Dragging())
{
return false;
}
m_edit.EndDrag();
return true;
}
bool PopupMenu::OnChar(wchar_t character, MenuResult& result)
{
result = MenuResult();
if (m_editIndex < 0)
{
return false;
}
// 剪贴板与全选快捷键经 WM_CHAR 送达(Ctrl+C/X/V/A 分别对应 3/24/22/1)。
switch (character)
{
case 0x03: // Ctrl+C
m_edit.CopyToClipboard(m_host);
return false;
case 0x18: // Ctrl+X
return m_edit.CutToClipboard(m_host);
case 0x16: // Ctrl+V
return m_edit.PasteFromClipboard(m_host);
case 0x01: // Ctrl+A
m_edit.SelectAll();
return true;
default:
break;
}
if (character == L'\r' || character == L'\n')
{
// 回车提交:菜单保持打开,仅把新数值回传宿主。
EndItemEdit(true, result);
return true;
}
// 退格/删除已由 WM_KEYDOWN 处理,此处忽略控制字符避免重复删除。
if (character < 0x20 || character == 0x7F)
{
return false;
}
return m_edit.Insert(character);
}
void PopupMenu::Open(MenuScope scope, const gd::RectF& bounds, int x, int y,
const std::vector<MenuItem>& items)
{
@@ -156,7 +378,11 @@ namespace jdi
m_hoverSwatch = -1;
m_pressedIndex = -1;
m_pressedSwatch = -1;
m_buildBounds = bounds;
m_buildX = x;
m_buildY = y;
CloseEditState();
BuildLayout(bounds, x, y);
}
@@ -171,6 +397,16 @@ namespace jdi
m_hoverSwatch = -1;
m_pressedIndex = -1;
m_pressedSwatch = -1;
CloseEditState();
}
void PopupMenu::CloseEditState()
{
if (m_editIndex >= 0)
{
m_edit.End();
}
m_editIndex = -1;
}
bool PopupMenu::ContainsPoint(int x, int y) const
@@ -251,7 +487,7 @@ namespace jdi
}
const MenuItem& item = m_items[static_cast<std::size_t>(index)];
if (item.kind == MenuItemKind::NORMAL && item.enabled)
if ((item.kind == MenuItemKind::NORMAL || item.kind == MenuItemKind::EDIT) && item.enabled)
{
m_hoverIndex = index;
m_hoverSwatch = -1;
@@ -268,7 +504,18 @@ namespace jdi
}
const MenuItem& item = m_items[static_cast<std::size_t>(m_hoverIndex)];
if (item.kind != MenuItemKind::NORMAL || !item.enabled)
if (!item.enabled)
{
return false;
}
// 编辑项:回车等价于单击进入编辑,菜单不关闭。
if (item.kind == MenuItemKind::EDIT)
{
BeginItemEdit(m_hoverIndex);
return false;
}
if (item.kind != MenuItemKind::NORMAL)
{
return false;
}
@@ -310,11 +557,26 @@ namespace jdi
if (!ContainsPoint(x, y))
{
// 点击菜单外部:关闭菜单,不产生任何命令。
// 点击菜单外部:先提交正在编辑的数值,再关闭菜单。
EndItemEdit(true, result);
Close();
return true;
}
// 已经处于编辑状态时:点击当前输入框 → 重新定位插入符;
// 点击其它位置 → 先提交本次编辑(菜单不关闭),再按新位置继续处理。
if (m_editIndex >= 0)
{
if (IsOnEditField(x, y))
{
OnEditMouseDown(x, y);
m_pressedIndex = m_editIndex;
m_pressedSwatch = -1;
return true;
}
EndItemEdit(true, result);
}
int swatchIndex = -1;
m_pressedIndex = ItemAtPoint(x, y, swatchIndex);
m_pressedSwatch = swatchIndex;
@@ -323,12 +585,23 @@ namespace jdi
bool PopupMenu::OnLButtonUp(int x, int y, MenuResult& result)
{
const MenuResult editResult = result;
result = MenuResult();
if (!m_open)
{
return false;
}
// 编辑态下松开:先结束拖拽框选,不当作普通项点击。
if (m_editIndex >= 0)
{
OnEditMouseUp();
m_pressedIndex = -1;
m_pressedSwatch = -1;
result = editResult;
return true;
}
int swatchIndex = -1;
const int index = ItemAtPoint(x, y, swatchIndex);
const int pressedIndex = m_pressedIndex;
@@ -350,6 +623,13 @@ namespace jdi
const MenuItem& item = m_items[static_cast<std::size_t>(index)];
// 编辑项:单击进入菜单内联编辑,菜单保持打开。
if (item.kind == MenuItemKind::EDIT)
{
BeginItemEdit(index);
return true;
}
result.valid = true;
result.scope = m_scope;
result.id = item.id;
@@ -378,6 +658,42 @@ namespace jdi
return false;
}
// 编辑态下按键全部交给输入框(回车提交、Esc 取消本次编辑但菜单保持打开)。
if (m_editIndex >= 0)
{
const bool shift = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
switch (key)
{
case VK_ESCAPE:
EndItemEdit(false, result);
return true;
case VK_RETURN:
EndItemEdit(true, result);
return true;
case VK_BACK:
m_edit.Backspace();
return true;
case VK_DELETE:
m_edit.DeleteForward();
return true;
case VK_LEFT:
m_edit.MoveCaret(-1, shift);
return true;
case VK_RIGHT:
m_edit.MoveCaret(1, shift);
return true;
case VK_HOME:
m_edit.MoveHome(shift);
return true;
case VK_END:
m_edit.MoveEnd(shift);
return true;
default:
break;
}
return true;
}
switch (key)
{
case VK_ESCAPE:
@@ -445,6 +761,40 @@ namespace jdi
const bool hovered = (static_cast<int>(index) == m_hoverIndex) &&
(item.kind != MenuItemKind::SWATCHES || m_hoverSwatch < 0);
if (item.kind == MenuItemKind::EDIT)
{
// 编辑项:左侧说明文字 + 右侧内联输入框(与拾色器的 RGB 编辑同一种处理方式,
// 菜单始终不关闭,数值就地修改)。
if (hovered && item.enabled && static_cast<int>(index) != m_editIndex)
{
UiFillRoundRect(g, rect, 4.0f * m_scale, WithAlpha(m_theme->accent, 0x22));
}
const std::uint32_t labelColor = item.enabled ? m_theme->text : m_theme->textMuted;
const float labelLeft = rect.X + 8.0f * m_scale;
const gd::RectF fieldRect = EditFieldRect(static_cast<int>(index));
const float labelWidth = std::max(8.0f, fieldRect.X - labelLeft - kEditFieldGap * m_scale);
UiText(g, UiEllipsize(g, item.label, m_fonts->uiSmall, labelWidth), m_fonts->uiSmall,
gd::RectF(labelLeft, rect.Y, labelWidth, rect.Height), labelColor, TextAlign::LEFT);
if (static_cast<int>(index) == m_editIndex)
{
UiDrawEditField(g, fieldRect, m_edit, m_fonts->uiSmall, m_scale);
}
else
{
UiFillRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->pressed);
UiDrawRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->border, 1.0f);
const std::wstring text =
UiEllipsize(g, item.value, m_fonts->uiSmall, fieldRect.Width - 12.0f * m_scale);
UiText(g, text, m_fonts->uiSmall,
gd::RectF(fieldRect.X + 6.0f * m_scale, fieldRect.Y,
std::max(8.0f, fieldRect.Width - 12.0f * m_scale), fieldRect.Height),
item.enabled ? m_theme->textStrong : m_theme->textMuted, TextAlign::LEFT);
}
continue;
}
if (item.kind == MenuItemKind::SWATCHES)
{
const std::vector<gd::RectF>& rects = m_swatchRects[index];
+146 -6
View File
@@ -167,13 +167,29 @@ namespace jdi
return false;
}
bool TryParseDouble(const char* begin, const char* end, double& value)
{
if (begin == nullptr || end == nullptr || begin >= end)
{
return false;
}
/** @brief 快速解析路径允许保留的最大有效位数(超过则交回 strtod 以保证正确舍入)。 */
const int kMaxFastDigits = 17;
/** @brief 快速解析路径允许使用的最大十进制指数(超出查表范围,交回 strtod)。 */
const int kMaxFastExponent = 22;
/** @brief 10 的 0-22 次幂精确表(该范围内每位都是 double 可精确表示的十进制数)。 */
const double kPow10Table[23] = {1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11,
1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22};
/**
* @brief 标准库解析路径:去掉首尾空白后交给 strtod。
*
* 作为快速路径的保底实现,保证十六进制浮点、inf/nan、超范围指数、
* 带尾部杂字符(如 "12abc")等输入的处理与改造前完全一致。
*
* @param [IN] begin 文本起始指针。
* @param [IN] end 文本结束指针(不包含)。
* @param [OUT] value 解析结果。
* @return 解析出数值返回 true。
*/
bool ParseDoubleSlow(const char* begin, const char* end, double& value)
{
const char* p = begin;
while (p < end && (*p == ' ' || *p == '\t'))
{
@@ -216,6 +232,130 @@ namespace jdi
return true;
}
bool TryParseDouble(const char* begin, const char* end, double& value)
{
if (begin == nullptr || end == nullptr || begin >= end)
{
return false;
}
// ===== 快速路径:纯十进制数值 =====
// 大文件解析的绝对瓶颈就在这里:ucrt 的 strtod 要处理本地化、十六进制浮点、
// 无穷/NaN 等全部语法,实测每次调用约 335ns;百万行级文件需调用上千万次,
// 单解析就可能耗掉几十秒。下面这条路径只处理最常见的十进制写法(实测约 13ns,
// 快 25 倍),其余写法(含尾部杂字符)原样交回 strtod,
// 因此输入接受范围与原先完全一致。
const char* head = begin;
while (head < end && (*head == ' ' || *head == '\t'))
{
++head;
}
const char* tail = end;
while (tail > head && (tail[-1] == ' ' || tail[-1] == '\t' || tail[-1] == '\r' || tail[-1] == '\n'))
{
--tail;
}
if (head >= tail)
{
return false;
}
const char* cursor = head;
bool negative = false;
if (*cursor == '+' || *cursor == '-')
{
negative = (*cursor == '-');
++cursor;
}
unsigned long long mantissa = 0;
int digits = 0;
int exponent = 0;
// 整数部分:超过有效位数的高位数字只影响量级,累加进指数。
while (cursor < tail && *cursor >= '0' && *cursor <= '9')
{
if (digits < kMaxFastDigits)
{
mantissa = mantissa * 10ull + static_cast<unsigned>(*cursor - '0');
++digits;
}
else
{
++exponent;
}
++cursor;
}
// 小数部分:每保留一位就记一次小数位数;超出有效位数的直接丢弃(已低于 double 精度)。
int fractionDigits = 0;
if (cursor < tail && *cursor == '.')
{
++cursor;
while (cursor < tail && *cursor >= '0' && *cursor <= '9')
{
if (digits < kMaxFastDigits)
{
mantissa = mantissa * 10ull + static_cast<unsigned>(*cursor - '0');
++digits;
++fractionDigits;
}
++cursor;
}
}
if (digits == 0)
{
return ParseDoubleSlow(begin, end, value);
}
exponent -= fractionDigits;
if (cursor < tail && (*cursor == 'e' || *cursor == 'E'))
{
++cursor;
bool expNegative = false;
if (cursor < tail && (*cursor == '+' || *cursor == '-'))
{
expNegative = (*cursor == '-');
++cursor;
}
if (cursor >= tail || *cursor < '0' || *cursor > '9')
{
return ParseDoubleSlow(begin, end, value);
}
int expValue = 0;
while (cursor < tail && *cursor >= '0' && *cursor <= '9')
{
if (expValue < 1000000)
{
expValue = expValue * 10 + (*cursor - '0');
}
++cursor;
}
exponent += expNegative ? -expValue : expValue;
}
// 存在未消费字符(如 "12abc"、"1,5")或指数超出查表范围时交回标准库:
// 既保持原先 strtod 的宽松接受语义,也保证极端量级的正确舍入。
if (cursor != tail || exponent > kMaxFastExponent || exponent < -kMaxFastExponent)
{
return ParseDoubleSlow(begin, end, value);
}
double result = static_cast<double>(mantissa);
if (exponent > 0)
{
result *= kPow10Table[exponent];
}
else if (exponent < 0)
{
result /= kPow10Table[-exponent];
}
// mantissa 超过 2^53(16 位以上有效数字)时这里会比 strtod 多一次舍入,
// 相对误差不超过 1 ULP(实测 ≤2.3e-16),在波形显示与标尺(约 6 位有效数字)上不可见。
value = negative ? -result : result;
return true;
}
std::wstring FormatNumber(double value, int decimals)
{
if (decimals < 0)