#ifndef __APPLICATION_INTERNAL_HPP__ #define __APPLICATION_INTERNAL_HPP__ // Application 组件内部共享的常量与工具函数(私有头,仅本组件的 .cpp 使用)。 // // 本组件的实现按功能拆成多个协作文件,它们共用这一份内部工具集。 // // ⚠️ 为让多个翻译单元都能使用,原匿名命名空间改为**具名**命名空间 `appdetail`, // 并把其中每个函数与常量标为 `inline` —— 单一定义、单一副本,不会产生代码重复。 // 各实现文件在包含本头之后用 `using namespace appdetail;` 引入这些工具, // 因此函数体里原有的非限定调用一行都不用改。 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Application.hpp" #include "DataLoader.hpp" #include "MemProbe.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 kMessageCloneSignal = WM_APP + 5; /**< 面板请求克隆单个信号的自定义消息。 */ 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; /** * @brief 信号 / 绘图区动画的定时器周期(毫秒)。 * * ⚠️⚠️ **必须是「不超过一个系统 tick」的值**:`SetTimer` 的间隔会被量化到系统 tick *(默认 15.6ms)的整数倍上,因此请求 16ms 实际会等到**下一个** tick ⇒ 实测帧距 * **≈ 26ms(只有约 38fps)**,动画明显卡顿(用户 2026-10-17 报告:所有过渡动画都不平顺)。 * 请求 10ms 时量化结果正好是一个 tick ⇒ 实测帧距 **≈ 15.5ms(约 64fps)**, * 回到“约 60fps 平滑过渡”的设计目标。 */ inline const UINT kScrollTimerPeriodMs = 10; /** * @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(&ex2), sizeof(ex2)) != FALSE && ex2.PrivateWorkingSetSize > 0) { return static_cast(ex2.PrivateWorkingSetSize); } PROCESS_MEMORY_COUNTERS_EX counters = {}; counters.cb = sizeof(counters); if (K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&counters), sizeof(counters)) == FALSE) { return 0ull; } if (counters.PrivateUsage > 0) { return static_cast(counters.PrivateUsage); } return static_cast(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(std::floor(plotRect.X)); invalid.top = static_cast(std::floor(plotRect.Y)); invalid.right = static_cast(std::ceil(plotRect.X + plotRect.Width)); invalid.bottom = static_cast(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 应用设置菜单(工具栏齿轮)命令 ID。 */ inline const int kAppCmdShowLayoutBounds = 1; /** * @brief 「主题配色」二级菜单的命令 ID 基准。 * * 第 i 个主题的 ID = `kAppCmdThemeBase + i`,`i` 即 `ThemeKind` 的整型值 * (顺序就是菜单里的显示顺序:Light / Solarised Light / Dark / Solarised Dark)。 */ inline const int kAppCmdThemeBase = 100; /** * @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(LoadImageW(module, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON, cx, cy, LR_DEFAULTCOLOR)); if (icon == nullptr) { icon = static_cast(LoadImageW(module, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR | LR_DEFAULTSIZE)); } return icon; } /** * @brief 后台加载任务的共享状态。 * * `done` 是唯一的同步点:工作线程写完其它字段后以 release 语义置位, * 主线程以 acquire 语义读取,因此无需额外锁。 */ struct AsyncLoadState { std::atomic done{false}; /**< 工作线程是否已结束。 */ std::atomic cancelled{false}; /**< 是否请求取消。 */ std::atomic progress{0.0f}; /**< 0-1 进度。 */ std::atomic bytesRead{0}; /**< 已读取字节数(解析阶段)。 */ std::atomic totalBytes{0}; /**< 文件总字节数(0 表示未知)。 */ bool ok = false; /**< 加载是否成功。 */ std::wstring error; /**< 失败原因。 */ std::vector groups; /**< 构建结果(多分组格式可有多个)。 */ }; /** * @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(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); }; std::vector sources; std::wstring error; if (!LoadSignalSourceGroups(path, sources, error, parseProgress)) { state->error = error; state->done.store(true, std::memory_order_release); return; } const std::size_t count = std::max(sources.size(), 1); for (std::size_t index = 0; index < sources.size(); ++index) { LoadProgress buildProgress = [state, index, count](const LoadProgressInfo& info) { const double base = 0.75 + 0.25 * (static_cast(index) / static_cast(count)); state->progress.store( static_cast(base + 0.25 * info.ratio / static_cast(count)), std::memory_order_relaxed); return !state->cancelled.load(std::memory_order_relaxed); }; SignalGroup group; if (!BuildSignalGroup(std::move(sources[index].source), paletteBase, group, error, buildProgress)) { state->error = error; state->done.store(true, std::memory_order_release); return; } // 分组名:`.mat` 的嵌套路径由加载器给出(如 `A-A1`);其它格式为空 ⇒ 用文件名。 if (!sources[index].name.empty()) { group.name = sources[index].name; } state->groups.push_back(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( reinterpret_cast(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(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__