// 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 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"已加载 组" + std::to_wstring(lastGroupIndex + 1) + L"|" + loadedGroup->name + L"(" + FormatCount(static_cast(rowCount)) + L" 行采样," + std::to_wstring(loadedGroup->signals.size()) + L" 条信号," + std::to_wstring(loadedGroup->axes.size()) + L" 条时间轴:" + timeNames + L")"; if (anyNonMonotonic) { message += L" 提示:时间列非单调递增,光标取值使用最近邻策略。"; } SetStatusMessage(message, false); } return true; } bool Application::LoadFileAsync(const std::wstring& path, std::vector& 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; 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(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; 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(width), static_cast(height)), 0x66000000u); const float cardWidth = std::min(420.0f * m_scale, static_cast(width) - 40.0f * m_scale); const float cardHeight = 132.0f * m_scale; const gd::RectF card((static_cast(width) - cardWidth) * 0.5f, (static_cast(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"已加载 " + std::to_wstring(loaded) + L" 个文件,请在左侧勾选需要查看的信号。", false); } } } // namespace jdi