背景:Code Review 报告 §10.3 / §11.5 建议「替换 std::to_wstring / std::to_string,
可去掉 gdtoa 链(≈33 KB)」。本轮把它做完,并给出这条建议的**真实边界**。
一、新增 `Utf8Util::FormatInteger*`(手写十进制展开,不经 printf)
* `FormatInteger(long long)` / `FormatIntegerUnsigned(unsigned long long)` /
`FormatIntegerNarrow(long long)`(后者供 MatLoader 拼 HDF5 成员路径)。
* 语义与 `std::to_wstring` / `std::to_string` 完全一致:`0` → `"0"`、负数带 `-`、
`LLONG_MIN` 用模 2^64 语义取绝对值(不会溢出)。
* 这样做的直接理由:预编译 libstdc++ 的 `to_string` 内部转调 `__mingw_swprintf`,
工程里只要留一处,MinGW 的 printf 引擎(`mingw_wpformat.o`)+ gdtoa 就必然被链入。
二、全仓替换(76 处 `to_wstring` + 2 处 `to_string`,20 个文件)
* `MatLoader.cpp` 的 `TrimNumber` 整数分支也改为 `FormatInteger`(它曾是产物里
**最后一个**引用 MinGW printf 的调用点)。
* 需要的地方补 `#include "Utf8Util.hpp"`(仅 `MatLoader.cpp` 一个新 include,其余由
`PlotViewInternal` / `SignalPanelInternal` / `ApplicationInternal` / `DataModelInternal`
已经包含)。
三、⚠️ 实测结论:gdtoa **去不掉**(把这件事彻底查清并写进 Makefile 注释)
* 只替换 `to_wstring`/`to_string`:**−1,024 B**(787,968 → 786,944)。
* 再叠加 `-D__USE_MINGW_ANSI_STDIO=0`(把 `swprintf` 让给系统 UCRT):**0 字节**。
* 根因(定位到文件与行):libstdc++ 的
`lib/gcc/.../include/c++/x86_64-w64-mingw32/bits/os_defines.h:48-51` **无条件**执行
`#undef __USE_MINGW_ANSI_STDIO` + `#define __USE_MINGW_ANSI_STDIO 1`
(注释:libstdc++ depends on POSIX-definitions of those functions)。
`g++ -E -dM` 实测最终值恒为 1 ⇒ **任何 C++ TU 的 `swprintf` / `strtod` 都是 MinGW 版本**,
命令行无法覆盖。
* 要真省这 33 KB 只能自己实现「浮点 → 十进制(正确舍入)」与「浮点解析的
十六进制/inf/nan 分支」,等价于重写 gdtoa。**判定:不做**(风险 >> 收益)。
* 该开关已回退,Makefile 里保留一段说明,避免后续接手者重复这两次实验。
四、验证
* 新增单元测试 `integer_replacement_matches_std`:12 个有符号样本(含 LLONG_MIN/MAX)
+ 5 个无符号样本(含 ULLONG_MAX),与 `std::to_wstring` **逐字符对照**,
并校验窄/宽版本内容一致。
* `UnitTests`:`checks=260 failed=0 cases=19`(新增 41 条断言)。
* `OffscreenChecks -all`:6 个检查全 PASS,`checks=87 failed=0`。
* `panels -mode compare`:7 个面板像素哈希与几何**逐项不变**
(整数文本从 `to_wstring` 换到手写实现后,渲染结果零差异 —— 这是最强的语义证据)。
* release 全量零 error / 零 warning;产物 787,968 → **786,944 B**。
292 lines
9.8 KiB
C++
292 lines
9.8 KiB
C++
// Application 实现分片 —— 本文件只包含 Application 的**成员函数定义**(纯代码搬移,行为与拆分前完全一致)。
|
||
//
|
||
// 共用的常量与自由函数在 ApplicationInternal.hpp(命名空间 `appdetail`,全部 inline)。
|
||
// 分片按“职责”划分:核心(状态/布局/几何)、渲染、交互与编辑,见各文件名。
|
||
#include "ApplicationInternal.hpp"
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
bool Application::LoadFile(const std::wstring& path)
|
||
{
|
||
if (path.empty())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 大文件在工作线程解析并构建,主线程播放加载过渡动画,界面不再整块卡住。
|
||
std::vector<SignalGroup> loaded;
|
||
std::wstring error;
|
||
if (!LoadFileAsync(path, loaded, error))
|
||
{
|
||
if (!error.empty())
|
||
{
|
||
SetStatusMessage(L"解析失败:" + error, true);
|
||
}
|
||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||
return false;
|
||
}
|
||
if (loaded.empty())
|
||
{
|
||
SetStatusMessage(L"解析失败:数据为空。", true);
|
||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||
return false;
|
||
}
|
||
|
||
// 插入新分组会移动 SignalGroup/SignalData 的存储位置,必须先暂停后台派生曲线计算,
|
||
// 否则后台线程可能正在读取即将被移动的采样数据。
|
||
m_plot.PauseDerivedAsync();
|
||
int lastGroupIndex = -1;
|
||
for (SignalGroup& group : loaded)
|
||
{
|
||
int groupIndex = -1;
|
||
// 此处只做一次指针/缓冲移动,不拷贝任何采样数据,因此不会造成可见卡顿。
|
||
if (!m_dataSet.AddGroup(std::move(group), groupIndex))
|
||
{
|
||
continue;
|
||
}
|
||
lastGroupIndex = groupIndex;
|
||
}
|
||
if (lastGroupIndex < 0)
|
||
{
|
||
SetStatusMessage(L"解析失败:数据为空。", true);
|
||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||
return false;
|
||
}
|
||
|
||
// 新分组的信号在全部标签页中默认均为未勾选状态,由用户自行决定查看内容。
|
||
SyncTabsToDataSet();
|
||
UpdateWindowTitle();
|
||
|
||
const SignalGroup* loadedGroup = m_dataSet.GroupAt(lastGroupIndex);
|
||
if (loadedGroup != nullptr)
|
||
{
|
||
// 行数取各时间轴的**最大值**(多时间轴文件的各轴长度可能不同)。
|
||
std::size_t rowCount = 0;
|
||
bool anyNonMonotonic = false;
|
||
std::wstring timeNames;
|
||
for (const TimeAxis& axis : loadedGroup->axes)
|
||
{
|
||
rowCount = std::max(rowCount, axis.Count());
|
||
anyNonMonotonic = anyNonMonotonic || !axis.monotonic;
|
||
if (!timeNames.empty())
|
||
{
|
||
timeNames += L"、";
|
||
}
|
||
timeNames += axis.name;
|
||
}
|
||
std::wstring message =
|
||
L"已加载 组" + FormatInteger(lastGroupIndex + 1) + L"|" + loadedGroup->name +
|
||
L"(" + FormatCount(static_cast<unsigned long long>(rowCount)) + L" 行采样," +
|
||
FormatInteger(loadedGroup->signals.size()) + L" 条信号," +
|
||
FormatInteger(loadedGroup->axes.size()) + L" 条时间轴:" + timeNames + L")";
|
||
if (anyNonMonotonic)
|
||
{
|
||
message += L" 提示:时间列非单调递增,光标取值使用最近邻策略。";
|
||
}
|
||
SetStatusMessage(message, false);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
bool Application::LoadFileAsync(const std::wstring& path, std::vector<SignalGroup>& groups,
|
||
std::wstring& error)
|
||
{
|
||
error.clear();
|
||
|
||
AsyncLoadState state;
|
||
const std::size_t paletteBase = m_dataSet.SignalCount();
|
||
|
||
m_loading = true;
|
||
m_loadingProgress = 0.0f;
|
||
m_loadingBytesRead = 0;
|
||
m_loadingTotalBytes = 0;
|
||
// ⚠️ 告知绘图区“正在加载”:暂停边沿统计推进与统计面板绘制。
|
||
// 本函数的主循环会不断 `UpdateWindow` 触发整帧绘制,若面板继续扫统计 + 重绘上百行,
|
||
// 加载进度会被拖得很慢(用户报告“统计没算完就无法开始加载文件”)。
|
||
m_plot.SetLoadingBusy(true);
|
||
const std::size_t slashPos = path.find_last_of(L"\\/");
|
||
m_loadingText = (slashPos == std::wstring::npos) ? path : path.substr(slashPos + 1);
|
||
|
||
std::thread worker(RunAsyncLoad, &state, path, paletteBase);
|
||
|
||
// 主线程循环:抽送消息 + 刷新动画,直到后台完成或用户取消。
|
||
while (!state.done.load(std::memory_order_acquire))
|
||
{
|
||
MSG message = {};
|
||
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE))
|
||
{
|
||
if (message.message == WM_QUIT)
|
||
{
|
||
// 退出请求:让后台停下并重新投递 WM_QUIT,由主循环正常收尾。
|
||
state.cancelled.store(true, std::memory_order_relaxed);
|
||
PostQuitMessage(static_cast<int>(message.wParam));
|
||
break;
|
||
}
|
||
// 加载期间屏蔽鼠标与其它按键,避免重入加载或误改数据;
|
||
// 保留 Esc 作为取消入口。
|
||
if (message.message >= WM_MOUSEFIRST && message.message <= WM_MOUSELAST)
|
||
{
|
||
continue;
|
||
}
|
||
if (message.message == WM_KEYDOWN && message.wParam == VK_ESCAPE)
|
||
{
|
||
state.cancelled.store(true, std::memory_order_relaxed);
|
||
continue;
|
||
}
|
||
if (message.message >= WM_KEYFIRST && message.message <= WM_KEYLAST)
|
||
{
|
||
continue;
|
||
}
|
||
TranslateMessage(&message);
|
||
DispatchMessageW(&message);
|
||
}
|
||
|
||
m_loadingProgress = state.progress.load(std::memory_order_relaxed);
|
||
m_loadingBytesRead = state.bytesRead.load(std::memory_order_relaxed);
|
||
m_loadingTotalBytes = state.totalBytes.load(std::memory_order_relaxed);
|
||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||
UpdateWindow(m_hwnd);
|
||
Sleep(15);
|
||
}
|
||
|
||
m_loadingProgress = 1.0f;
|
||
m_loadingBytesRead = m_loadingTotalBytes;
|
||
worker.join();
|
||
m_loading = false;
|
||
m_plot.SetLoadingBusy(false);
|
||
|
||
if (state.cancelled.load(std::memory_order_relaxed))
|
||
{
|
||
// 用户主动取消:不弹错误框(与 Esc 取消缩放选区的静默语义一致)。
|
||
InvalidateRect(m_hwnd, nullptr, TRUE);
|
||
return false;
|
||
}
|
||
if (!state.ok)
|
||
{
|
||
error = state.error;
|
||
return false;
|
||
}
|
||
|
||
groups = std::move(state.groups);
|
||
return true;
|
||
}
|
||
|
||
void Application::PaintLoadingOverlay(gd::Graphics& g, int width, int height)
|
||
{
|
||
if (!m_loading)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 半透明遮罩压暗整个界面,形成明确的"正在加载"状态。
|
||
UiFillRect(g, gd::RectF(0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height)),
|
||
0x66000000u);
|
||
|
||
const float cardWidth = std::min(420.0f * m_scale, static_cast<float>(width) - 40.0f * m_scale);
|
||
const float cardHeight = 132.0f * m_scale;
|
||
const gd::RectF card((static_cast<float>(width) - cardWidth) * 0.5f,
|
||
(static_cast<float>(height) - cardHeight) * 0.5f, cardWidth, cardHeight);
|
||
|
||
UiFillRoundRect(g, card, 8.0f * m_scale, m_theme.cardBg);
|
||
UiDrawRoundRect(g, card, 8.0f * m_scale, m_theme.borderStrong, 1.0f);
|
||
|
||
// 旋转弧线:用时间驱动的起始角度形成持续的加载动画。
|
||
const float spinnerRadius = 11.0f * m_scale;
|
||
const gd::RectF spinner(card.X + 26.0f * m_scale, card.Y + 30.0f * m_scale - spinnerRadius,
|
||
spinnerRadius * 2.0f, spinnerRadius * 2.0f);
|
||
m_loadingSpin += 12.0f;
|
||
if (m_loadingSpin >= 360.0f)
|
||
{
|
||
m_loadingSpin -= 360.0f;
|
||
}
|
||
gd::Pen trackPen(ToGdiColor(m_theme.border), 3.0f * m_scale);
|
||
gd::Pen spinPen(ToGdiColor(m_theme.accent), 3.0f * m_scale);
|
||
g.DrawArc(&trackPen, spinner, 0.0f, 359.0f);
|
||
g.DrawArc(&spinPen, spinner, m_loadingSpin, 110.0f);
|
||
|
||
const float textLeft = card.X + 52.0f * m_scale;
|
||
const float textWidth = cardWidth - 52.0f * m_scale - 20.0f * m_scale;
|
||
|
||
UiText(g, L"正在加载数据…", m_fonts.uiBold,
|
||
gd::RectF(textLeft, card.Y + 18.0f * m_scale, textWidth, 22.0f * m_scale), m_theme.text,
|
||
TextAlign::LEFT);
|
||
|
||
// 超长文件名用省略号截断,避免撑破卡片。
|
||
const std::wstring nameText =
|
||
UiEllipsize(m_loadingText, m_fonts.uiSmall, textWidth);
|
||
UiText(g, nameText, m_fonts.uiSmall,
|
||
gd::RectF(textLeft, card.Y + 42.0f * m_scale, textWidth, 20.0f * m_scale),
|
||
m_theme.textMuted, TextAlign::LEFT);
|
||
|
||
// 进度条(复用样式区滑块以外的简单圆角矩形,填充宽度严格按比例)。
|
||
const gd::RectF track(card.X + 26.0f * m_scale, card.Y + 76.0f * m_scale,
|
||
cardWidth - 52.0f * m_scale, 8.0f * m_scale);
|
||
UiFillRoundRect(g, track, track.Height * 0.5f, m_theme.pressed);
|
||
float ratio = m_loadingProgress;
|
||
ratio = std::max(0.0f, std::min(ratio, 1.0f));
|
||
const float fillWidth = track.Width * ratio;
|
||
if (fillWidth > 1.0f)
|
||
{
|
||
UiFillRoundRect(g, gd::RectF(track.X, track.Y, fillWidth, track.Height), track.Height * 0.5f,
|
||
m_theme.accent);
|
||
}
|
||
|
||
wchar_t buffer[64];
|
||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.2f%%", ratio * 100.0);
|
||
|
||
// 已读取 / 总大小:两者都只显示数字与自适应单位(GB / MB / KB),均保留两位小数;
|
||
// 百分比同样是两位小数。总大小未知(如列重组阶段)时只显示百分比。
|
||
{
|
||
std::wstring statusText = buffer;
|
||
if (m_loadingTotalBytes > 0)
|
||
{
|
||
statusText = FormatByteSize(m_loadingBytesRead) + L" / " +
|
||
FormatByteSize(m_loadingTotalBytes) + L" " + buffer;
|
||
}
|
||
UiText(g, statusText, m_fonts.monoSmall,
|
||
gd::RectF(track.X, card.Y + 92.0f * m_scale, track.Width, 20.0f * m_scale),
|
||
m_theme.text, TextAlign::LEFT);
|
||
}
|
||
|
||
UiText(g, L"按 Esc 取消", m_fonts.uiSmall,
|
||
gd::RectF(track.X, card.Y + 92.0f * m_scale, track.Width, 20.0f * m_scale),
|
||
m_theme.textMuted, TextAlign::RIGHT);
|
||
}
|
||
|
||
void Application::LoadCommandLineFiles()
|
||
{
|
||
int argumentCount = 0;
|
||
LPWSTR* arguments = CommandLineToArgvW(GetCommandLineW(), &argumentCount);
|
||
if (arguments == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
int loaded = 0;
|
||
for (int index = 1; index < argumentCount; ++index)
|
||
{
|
||
const std::wstring argument = arguments[index];
|
||
if (argument.empty() || argument[0] == L'-' || argument[0] == L'/')
|
||
{
|
||
continue;
|
||
}
|
||
if (LoadFile(argument))
|
||
{
|
||
++loaded;
|
||
}
|
||
}
|
||
LocalFree(arguments);
|
||
|
||
if (loaded > 0)
|
||
{
|
||
SetStatusMessage(L"已加载 " + FormatInteger(loaded) +
|
||
L" 个文件,请在左侧勾选需要查看的信号。",
|
||
false);
|
||
}
|
||
}
|
||
|
||
|
||
} // namespace jdi
|
||
|