按组件目录结构把四个巨型实现文件拆分为多个协作文件(PlotView / SignalPanel / Application / DataModel)
承接上一条提交(0871eb0)—— 该提交只完成了 Components/Shared 与注释/命名清理,并**主动未做**
大文件拆分;本提交把大文件拆分补齐,至此“按功能拆分逻辑块到各组件自己的 Inc/Src”已落地。
## 一、拆分结果
| 原文件 | 拆分后 |
| --- | --- |
| Components/PlotView/Src/PlotView.cpp(13 695 行 / 544 KB) | PlotView.cpp(787) + PlotViewPaint.cpp(6 367) + PlotViewInteraction.cpp(2 715) + PlotViewFeatures.cpp(3 005) + PlotViewInternal.hpp(936) |
| Components/SignalPanel/Src/SignalPanel.cpp(4 184 行) | SignalPanel.cpp(1 141) + SignalPanelPaint.cpp(697) + SignalPanelInteraction.cpp(2 095) + SignalPanelInternal.hpp |
| Components/Application/Src/Application.cpp(3 135 行) | Application.cpp(1 427) + ApplicationCommands.cpp(1 184) + ApplicationLoading.cpp(270) + ApplicationInternal.hpp |
| Utilities/Src/DataModel.cpp(2 615 行) | DataModel.cpp(1 583,含自由函数区) + DataModelTab.cpp(732) + DataModelInternal.hpp |
分片职责(以 PlotView 为例):
core 视图核心:构造/析构、上下文、布局、坐标映射、视界与平移、网格结构与归属
paint 渲染与像素缓存:全部绘制、场景/装饰缓存、每网格图层、宽基底与滚动补条
interaction 交互与编辑:命中测试、鼠标/键盘、光标形状、内联输入框、浮动面板层序
features 视图特性与派生数据:光标读数/提示/缩放、自适应、吸附、单点、派生曲线线程池、
边沿统计、实验性功能参数
(SignalPanel 为 core/paint/interaction;Application 为 core/commands/loading;
DataModel 为 core/tab。)
## 二、关键手法(这是能安全拆开的前提)
原文件的辅助区都是**匿名命名空间**(PlotView 约 870 行常量与工具函数),被所有成员函数广泛引用。
直接拆文件会让各 .cpp 拿不到这些工具,因此:
1. 把该块整块搬进 `<组件>Internal.hpp`,从 `namespace { }` 改为**具名**命名空间
(pvdetail / spdetail / appdetail / dmdetail);
2. 块内**每个函数与常量都标 inline** ⇒ 多个翻译单元共用同一个定义:既不重复符号,
也不产生 N 份函数定义;
3. 头部末尾 `using namespace <ns>;` ⇒ 各 .cpp 函数体里原有的非限定调用(IntersectRect /
NiceStep / ClampDouble …)**一行都不用改**;
4. 成员定义按「方法名 → 分片」归类后**整块搬移** —— 全程**纯代码搬移**,没有改动任何一行函数体。
## 三、拆分过程中踩到并修正的四个坑(已固化到 .preflight/perf/split_component.ps1)
1. `[[maybe_unused]]` 等属性**必须排在 inline 之前**(`inline [[x]] T` 非法);
单独的 `{`/`}` 行、注释行、`struct`/`enum`/`using`/`template` 行不能加 inline。
2. 匿名命名空间的**结束行可能带注释**(`} // namespace`),只认 `^\t}$` 会一路匹配到后面
某个成员函数的结束括号,把大段成员定义误当成“辅助区”搬走。
3. **首个成员定义之前的所有行**必须跟着进核心文件:DataModel.cpp 在匿名命名空间之后、
首个成员定义之前还有约 390 行命名空间外的自由函数(SignalStats / ComputeStats /
CacheSparseSamples / ApplySinglePointDefaultStyle …),漏掉会被静默丢弃。
4. 内部头必须带上原文件真正用到的全部头:DataModel 需要 Theme.hpp / <cstring> / <cstdio> /
<deque> / <atomic> / <cwctype> / <new>(原文件靠这些头才编译得过)。
## 四、附带完成的命名审计
PlotView 中残留的 File 语义命名一并修正:LaneSpansMultipleFiles → LaneSpansMultipleGroups、
firstFile → firstGroup、multiFile → multiGroup、SceneCacheKey::haloFile → haloGroup。
## 五、验证(全部离屏:SW_HIDE + 不注入真实输入 + 进程/线程 BELOW_NORMAL)
- .preflight/perf/verify.exe(PlotView 渲染/交互,含逐像素断言)49/49
- .preflight/perf/panelanim.exe(信号面板:卷尺式折起/展开、筛选态、悬浮组名、目标行)156/156
- .preflight/perf/datacheck.exe(数据层:单轴/多时间轴 CSV、MF4、重命名语义)69/69
- .preflight/perf/edgecheck.exe(边沿统计与面板排版)46/46
- make release / make debug 零警告。
即:拆分后**行为与拆分前完全一致**(纯代码搬移,四套探针全绿)。
## 六、体积(诚实记录)
Target.exe: 754 176 → 764 416 B(+10 240 B,+1.4%)
因为 inline 工具函数会在每个引用它的翻译单元里各生成一份(PE/COFF 链接器没有 ICF 合并)。
这再次印证:拆分解决的是**可维护性**,不是体积(本轮的 −24.6% 全部来自 ICO 256×256 条目改 PNG)。
## 七、文档
Documents/agent_mem.md:§9.37 新增“七、多文件拆分(已完成)”与“八、仍未完成”(只剩
Lane → LanePanelState、模态提示解耦、MF4 压缩块三项);§11.2 新增第 91、92 条(多文件化的
唯一安全手法 + 体积代价)。Documents/refactor_report_2026-10-16.md 同步更新 §五/§七/§八。
This commit is contained in:
1 parent
0871eb0cba
commit
e6b7bf66ec
19 files changed
+19130
-18771
No files matched your search
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,312 @@
|
||||
#ifndef __APPLICATION_INTERNAL_HPP__
|
||||
#define __APPLICATION_INTERNAL_HPP__
|
||||
|
||||
// Application 组件内部共享的常量与工具函数(私有头,仅本组件的 .cpp 使用)。
|
||||
//
|
||||
// 本组件的实现按功能拆成多个协作文件,它们共用这一份内部工具集。
|
||||
//
|
||||
// ⚠️ 为让多个翻译单元都能使用,原匿名命名空间改为**具名**命名空间 `appdetail`,
|
||||
// 并把其中每个函数与常量标为 `inline` —— 单一定义、单一副本,不会产生代码重复。
|
||||
// 各实现文件在包含本头之后用 `using namespace appdetail;` 引入这些工具,
|
||||
// 因此函数体里原有的非限定调用一行都不用改。
|
||||
|
||||
#include <windows.h>
|
||||
#include <objidl.h>
|
||||
#include <gdiplus.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <commdlg.h>
|
||||
#include <psapi.h>
|
||||
#include <shellapi.h>
|
||||
#include <windowsx.h>
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "DataLoader.hpp"
|
||||
#include "ResourceIds.h"
|
||||
#include "Utf8Util.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
namespace appdetail
|
||||
{
|
||||
/**
|
||||
* @brief Application 内部工具集(常量与自由函数)。
|
||||
*
|
||||
* 全部符号都是 `inline` ⇒ 多个翻译单元共用同一个定义:既不重复符号,也不产生多份代码。
|
||||
* 不要把它们移回匿名命名空间(那样每个 .cpp 都会各自复制一份)。
|
||||
*/
|
||||
inline const wchar_t* kWindowClassName = L"JustDataInspectorMainWindow"; /**< 窗口类名。 */
|
||||
inline const wchar_t* kWindowTitle = L"Just DataInspector"; /**< 窗口标题中的应用名称部分。 */
|
||||
/**
|
||||
* @brief 应用名称与内存占用之间的分隔竖线。
|
||||
*
|
||||
* 与信号分组标题里「组X | 组名」的分隔方式一致:竖线两侧各留出空隙
|
||||
*(`kGroupSeparatorGap` 是那个分隔线的左右间距,这里用两个空格达到同等观感)。
|
||||
*/
|
||||
inline const wchar_t* kTitleSeparator = L" | ";
|
||||
/** @brief 内存占用的显示前缀(与具体数值之间保留一个空格)。 */
|
||||
inline const wchar_t* kRamTitlePrefix = L"RAM : ";
|
||||
inline const float kSplitterWidth = 4.0f; /**< 分隔条宽度(逻辑像素)。 */
|
||||
inline const float kMinPanelWidth = 260.0f; /**< 面板最小宽度(逻辑像素)。 */
|
||||
inline const float kMaxPanelWidth = 400.0f; /**< 面板最大宽度(逻辑像素)。 */
|
||||
inline const int kMinClientWidth = 860; /**< 窗口最小客户区宽度。 */
|
||||
inline const int kMinClientHeight = 540; /**< 窗口最小客户区高度。 */
|
||||
inline const UINT kMessageRemoveGroup = WM_APP + 1; /**< 面板请求移除分组的自定义消息。 */
|
||||
inline const UINT kMessageRemoveSignal = WM_APP + 2; /**< 面板请求移除单个信号的自定义消息。 */
|
||||
inline const UINT_PTR kHintTimerId = 1; /**< 提示浮层淡出用的定时器 ID。 */
|
||||
inline const UINT kHintTimerPeriodMs = 33; /**< 提示浮层淡出的重绘间隔(约 30fps)。 */
|
||||
inline const UINT_PTR kRamTimerId = 2; /**< 标题栏内存占用刷新的定时器 ID。 */
|
||||
inline const UINT kRamTimerPeriodMs = 1000; /**< 标题栏内存占用的刷新间隔(1 秒)。 */
|
||||
/**
|
||||
* @brief 信号列表「滚动到目标信号」动画的定时器 ID。
|
||||
*
|
||||
* 用户 2026-10-15 要求:联动时列表要**平滑滚动**(不是立刻翻到目标位置),
|
||||
* 且"整个动画过程不要太久"。动画本身由 `SignalPanel::AdvanceScrollAnimation()`
|
||||
* 按经过时间推进,这里只负责按帧驱动它(约 60fps)。
|
||||
*/
|
||||
inline const UINT_PTR kScrollTimerId = 3;
|
||||
inline const UINT kScrollTimerPeriodMs = 16;
|
||||
|
||||
/**
|
||||
* @brief 查询本进程当前占用的内存(**私有工作集**,与任务管理器"内存"列同口径)。
|
||||
*
|
||||
* ⚠️ 原先取的是 `PROCESS_MEMORY_COUNTERS::WorkingSetSize`(**总工作集**),它把映射进来的
|
||||
* DLL / 共享页面以及已经释放但尚未被系统回收的驻留页面都算进去 —— 加载一个大文件再
|
||||
* 关闭分组后,实际占用的私有内存已经回到几 MB,标题栏却还显示几十 MB,
|
||||
* 与任务管理器(资源管理器里看到的"内存")对不上(用户报告)。
|
||||
* `PROCESS_MEMORY_COUNTERS_EX2::PrivateWorkingSetSize` 正是任务管理器"内存"列的口径:
|
||||
* 只统计**属于本进程的私有、且驻留在物理内存中**的页面。
|
||||
*
|
||||
* 结构体在这里就地声明(MinGW 的 psapi.h 未提供 `PROCESS_MEMORY_COUNTERS_EX2`),
|
||||
* 布局与 Win32 文档一致;EX2 查询失败(老系统)时退回 `PrivateUsage`(私有提交),
|
||||
* 再不行退回 `WorkingSetSize`,保证标题栏永远有值。
|
||||
*
|
||||
* @return 内存占用字节数;查询失败返回 0。
|
||||
*/
|
||||
inline unsigned long long ProcessPrivateWorkingSetBytes()
|
||||
{
|
||||
struct MemoryCountersEx2
|
||||
{
|
||||
DWORD cb;
|
||||
DWORD PageFaultCount;
|
||||
SIZE_T PeakWorkingSetSize;
|
||||
SIZE_T WorkingSetSize;
|
||||
SIZE_T QuotaPeakPagedPoolUsage;
|
||||
SIZE_T QuotaPagedPoolUsage;
|
||||
SIZE_T QuotaPeakNonPagedPoolUsage;
|
||||
SIZE_T QuotaNonPagedPoolUsage;
|
||||
SIZE_T PagefileUsage;
|
||||
SIZE_T PeakPagefileUsage;
|
||||
SIZE_T PrivateUsage;
|
||||
SIZE_T PrivateWorkingSetSize;
|
||||
SIZE_T SharedCommitUsage;
|
||||
};
|
||||
|
||||
MemoryCountersEx2 ex2 = {};
|
||||
ex2.cb = sizeof(ex2);
|
||||
if (K32GetProcessMemoryInfo(GetCurrentProcess(),
|
||||
reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&ex2),
|
||||
sizeof(ex2)) != FALSE &&
|
||||
ex2.PrivateWorkingSetSize > 0)
|
||||
{
|
||||
return static_cast<unsigned long long>(ex2.PrivateWorkingSetSize);
|
||||
}
|
||||
|
||||
PROCESS_MEMORY_COUNTERS_EX counters = {};
|
||||
counters.cb = sizeof(counters);
|
||||
if (K32GetProcessMemoryInfo(GetCurrentProcess(),
|
||||
reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&counters),
|
||||
sizeof(counters)) == FALSE)
|
||||
{
|
||||
return 0ull;
|
||||
}
|
||||
if (counters.PrivateUsage > 0)
|
||||
{
|
||||
return static_cast<unsigned long long>(counters.PrivateUsage);
|
||||
}
|
||||
return static_cast<unsigned long long>(counters.WorkingSetSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 只失效绘图区所在的矩形。
|
||||
*
|
||||
* 配合 WM_PAINT 中对更新区域的判断,可以走“只重绘、只回送绘图区”的快路径;
|
||||
* 绘图区矩形无效时退化为整窗失效,保证不会漏画。
|
||||
*
|
||||
* @param [IN] hwnd 窗口句柄。
|
||||
* @param [IN] plotRect 绘图区矩形(客户区坐标)。
|
||||
*/
|
||||
inline void InvalidatePlotRect(HWND hwnd, const gd::RectF& plotRect)
|
||||
{
|
||||
RECT invalid = {};
|
||||
invalid.left = static_cast<LONG>(std::floor(plotRect.X));
|
||||
invalid.top = static_cast<LONG>(std::floor(plotRect.Y));
|
||||
invalid.right = static_cast<LONG>(std::ceil(plotRect.X + plotRect.Width));
|
||||
invalid.bottom = static_cast<LONG>(std::ceil(plotRect.Y + plotRect.Height));
|
||||
if (invalid.right > invalid.left && invalid.bottom > invalid.top)
|
||||
{
|
||||
InvalidateRect(hwnd, &invalid, FALSE);
|
||||
}
|
||||
else
|
||||
{
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief 标签页右键菜单命令 ID。 */
|
||||
inline const int kMenuTabNew = 1;
|
||||
inline const int kMenuTabClose = 2;
|
||||
inline const int kMenuTabCloseOthers = 3;
|
||||
|
||||
/**
|
||||
* @brief 从应用资源中按指定尺寸加载图标。
|
||||
* @param [IN] cx 期望宽度(像素)。
|
||||
* @param [IN] cy 期望高度(像素)。
|
||||
* @return 图标句柄,加载失败返回 nullptr。
|
||||
*/
|
||||
inline HICON LoadAppIcon(int cx, int cy)
|
||||
{
|
||||
HINSTANCE module = GetModuleHandleW(nullptr);
|
||||
// LR_DEFAULTSIZE 之外的显式尺寸请求会挑选 ICO 中最接近的一帧并由系统缩放,
|
||||
// 因此 256/64/48/32/16 的多尺寸资源在任意 DPI 下都能取到合适的一帧。
|
||||
HICON icon = static_cast<HICON>(LoadImageW(module, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON,
|
||||
cx, cy, LR_DEFAULTCOLOR));
|
||||
if (icon == nullptr)
|
||||
{
|
||||
icon = static_cast<HICON>(LoadImageW(module, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON, 0, 0,
|
||||
LR_DEFAULTCOLOR | LR_DEFAULTSIZE));
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 后台加载任务的共享状态。
|
||||
*
|
||||
* `done` 是唯一的同步点:工作线程写完其它字段后以 release 语义置位,
|
||||
* 主线程以 acquire 语义读取,因此无需额外锁。
|
||||
*/
|
||||
struct AsyncLoadState
|
||||
{
|
||||
std::atomic<bool> done{false}; /**< 工作线程是否已结束。 */
|
||||
std::atomic<bool> cancelled{false}; /**< 是否请求取消。 */
|
||||
std::atomic<float> progress{0.0f}; /**< 0-1 进度。 */
|
||||
std::atomic<unsigned long long> bytesRead{0}; /**< 已读取字节数(解析阶段)。 */
|
||||
std::atomic<unsigned long long> totalBytes{0}; /**< 文件总字节数(0 表示未知)。 */
|
||||
bool ok = false; /**< 加载是否成功。 */
|
||||
std::wstring error; /**< 失败原因。 */
|
||||
SignalGroup group; /**< 构建结果。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 工作线程主体:按扩展名选择加载器读入文件并构建信号分组(不接触 DataSet)。
|
||||
* @param [IN,OUT] state 共享状态。
|
||||
* @param [IN] path 文件路径。
|
||||
* @param [IN] paletteBase 调色板起始序号。
|
||||
*/
|
||||
inline void RunAsyncLoad(AsyncLoadState* state, std::wstring path, std::size_t paletteBase)
|
||||
{
|
||||
// 解析阶段占总进度的 0-75%,列重组与统计占 75-100%。
|
||||
LoadProgress parseProgress = [state](const LoadProgressInfo& info)
|
||||
{
|
||||
state->progress.store(static_cast<float>(info.ratio * 0.75), std::memory_order_relaxed);
|
||||
if (info.hasBytes)
|
||||
{
|
||||
state->bytesRead.store(info.bytesRead, std::memory_order_relaxed);
|
||||
state->totalBytes.store(info.totalBytes, std::memory_order_relaxed);
|
||||
}
|
||||
return !state->cancelled.load(std::memory_order_relaxed);
|
||||
};
|
||||
|
||||
LoadedSource source;
|
||||
std::wstring error;
|
||||
if (!LoadSignalSource(path, source, error, parseProgress))
|
||||
{
|
||||
state->error = error;
|
||||
state->done.store(true, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
|
||||
LoadProgress buildProgress = [state](const LoadProgressInfo& info)
|
||||
{
|
||||
state->progress.store(static_cast<float>(0.75 + info.ratio * 0.25),
|
||||
std::memory_order_relaxed);
|
||||
return !state->cancelled.load(std::memory_order_relaxed);
|
||||
};
|
||||
|
||||
SignalGroup group;
|
||||
if (!BuildSignalGroup(std::move(source), paletteBase, group, error, buildProgress))
|
||||
{
|
||||
state->error = error;
|
||||
state->done.store(true, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
|
||||
state->group = std::move(group);
|
||||
state->ok = true;
|
||||
state->progress.store(1.0f, std::memory_order_relaxed);
|
||||
state->done.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 开启进程级 DPI 感知(优先 Per-Monitor V2)。
|
||||
*/
|
||||
inline void EnableDpiAwareness()
|
||||
{
|
||||
HMODULE user32 = GetModuleHandleW(L"user32.dll");
|
||||
if (user32 != nullptr)
|
||||
{
|
||||
typedef BOOL(WINAPI * SetProcessDpiAwarenessContextFn)(HANDLE);
|
||||
SetProcessDpiAwarenessContextFn setContext =
|
||||
reinterpret_cast<SetProcessDpiAwarenessContextFn>(
|
||||
reinterpret_cast<void*>(GetProcAddress(user32, "SetProcessDpiAwarenessContext")));
|
||||
if (setContext != nullptr && setContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) != FALSE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
SetProcessDPIAware();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 查询屏幕 DPI。
|
||||
* @return DPI 数值,失败时返回 96。
|
||||
*/
|
||||
inline UINT QuerySystemDpi()
|
||||
{
|
||||
HDC hdc = GetDC(nullptr);
|
||||
if (hdc == nullptr)
|
||||
{
|
||||
return 96u;
|
||||
}
|
||||
const int dpi = GetDeviceCaps(hdc, LOGPIXELSX);
|
||||
ReleaseDC(nullptr, hdc);
|
||||
return (dpi > 0) ? static_cast<UINT>(dpi) : 96u;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 依据 DPI 缩放逻辑像素。
|
||||
* @param [IN] value 逻辑像素值。
|
||||
* @param [IN] scale 缩放比例。
|
||||
* @return 物理像素值。
|
||||
*/
|
||||
inline float ScaleOf(float value, float scale)
|
||||
{
|
||||
return value * scale;
|
||||
}
|
||||
} // namespace appdetail
|
||||
|
||||
using namespace appdetail;
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __APPLICATION_INTERNAL_HPP__
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
// Application 实现分片 —— 本文件只包含 Application 的**成员函数定义**(纯代码搬移,行为与拆分前完全一致)。
|
||||
//
|
||||
// 共用的常量与自由函数在 ApplicationInternal.hpp(命名空间 `appdetail`,全部 inline)。
|
||||
// 分片按“职责”划分:核心(状态/布局/几何)、渲染、交互与编辑,见各文件名。
|
||||
#include "ApplicationInternal.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
bool Application::LoadFile(const std::wstring& path)
|
||||
{
|
||||
if (path.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 大文件在工作线程解析并构建,主线程播放加载过渡动画,界面不再整块卡住。
|
||||
SignalGroup group;
|
||||
std::wstring error;
|
||||
if (!LoadFileAsync(path, group, error))
|
||||
{
|
||||
if (!error.empty())
|
||||
{
|
||||
SetStatusMessage(L"解析失败:" + error, true);
|
||||
}
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
return false;
|
||||
}
|
||||
|
||||
int groupIndex = -1;
|
||||
// 插入新分组会移动 SignalGroup/SignalData 的存储位置,必须先暂停后台派生曲线计算,
|
||||
// 否则后台线程可能正在读取即将被移动的采样数据。
|
||||
m_plot.PauseDerivedAsync();
|
||||
// 此处只做一次指针/缓冲移动,不拷贝任何采样数据,因此不会造成可见卡顿。
|
||||
if (!m_dataSet.AddGroup(std::move(group), groupIndex))
|
||||
{
|
||||
SetStatusMessage(L"解析失败:数据为空。", true);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 新分组的信号在全部标签页中默认均为未勾选状态,由用户自行决定查看内容。
|
||||
SyncTabsToDataSet();
|
||||
UpdateWindowTitle();
|
||||
|
||||
const SignalGroup* loaded = m_dataSet.GroupAt(groupIndex);
|
||||
if (loaded != nullptr)
|
||||
{
|
||||
// 行数取各时间轴的**最大值**(多时间轴文件的各轴长度可能不同)。
|
||||
std::size_t rowCount = 0;
|
||||
bool anyNonMonotonic = false;
|
||||
std::wstring timeNames;
|
||||
for (const TimeAxis& axis : loaded->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(groupIndex + 1) + L"|" + loaded->name +
|
||||
L"(" + FormatCount(static_cast<unsigned long long>(rowCount)) +
|
||||
L" 行采样," + std::to_wstring(loaded->signals.size()) +
|
||||
L" 条信号," + std::to_wstring(loaded->axes.size()) + L" 条时间轴:" +
|
||||
timeNames + L")";
|
||||
if (anyNonMonotonic)
|
||||
{
|
||||
message += L" 提示:时间列非单调递增,光标取值使用最近邻策略。";
|
||||
}
|
||||
SetStatusMessage(message, false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::LoadFileAsync(const std::wstring& path, SignalGroup& group, 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<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;
|
||||
|
||||
if (state.cancelled.load(std::memory_order_relaxed))
|
||||
{
|
||||
// 用户主动取消:不弹错误框(与 Esc 取消缩放选区的静默语义一致)。
|
||||
InvalidateRect(m_hwnd, nullptr, TRUE);
|
||||
return false;
|
||||
}
|
||||
if (!state.ok)
|
||||
{
|
||||
error = state.error;
|
||||
return false;
|
||||
}
|
||||
|
||||
group = std::move(state.group);
|
||||
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"已加载 " + std::to_wstring(loaded) +
|
||||
L" 个文件,请在左侧勾选需要查看的信号。",
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
Reference in new issue
Block a user