Files
Just-DataInspector/Components/Application/Inc/ApplicationInternal.hpp
T
DeepSeek V4.1 Flash affb9cc340 目录规范化(Inc/Src)+「自由标记」第六轮交互调整
1) 目录规范化:所有 .hpp 只放 Inc,Src 只放 .cpp
   - 4 个 *Internal.hpp 从 Src 迁到 Inc(DataModel/Application/PlotView/SignalPanel)
   - Makefile 固化该约定(注释必须单独成行:写在同一行会把 CXX_INCLUDES_PATHS 整条注释掉)
   - include 全是扁平形式,搬家无需改任何 include

2) 常规模式 = 普通鼠标(未开启「自由标记点选取」)
   - ProbeFreeMarker 早退:不预览、不画纵向光标、单击不固定
   - 补 if (!hitCurve) continue:否则整列光标下 probe.ref 恒有效,任意位置都能固定
   - 固定点仍恒吸附到真实采样时刻;「已吸附」圆环只在稀疏判据成立时画

3) 两个显示开关:按钮改名「自由标记点选取」,其下新增「显示增量 | 显示枚举文本」
   - 默认关闭,只影响已固定点的胶囊文本;预览胶囊恒为「值 = 枚举」
   - 开关行计入面板高度与内容区高度

4) 面板六列:新增恒存在的「序号」列(组内 1 起)
   - 枚举列存在时,无枚举/对不上任何枚举项显示 N/A
   - 列与列之间画竖线;悬停某行时其它固定点封顶 50% 不透明度,移出恢复
   - 删除固定点 ✕ 的悬停浮动提示(PaintFreeMarkerTooltip 实现与声明全部移除)
   - 开启选取模式的网格上,信号悬停提示只显示信号名

5) 缓存键补齐
   - FreeMarkerContents 混入枚举表指纹(旧键会让加上枚举表后仍显示 N/A)
   - PanelSlotKey 混入两个新按钮矩形与开关状态

6) 注释/文档
   - 修正 FreeMarkerProbe、SetLaneFreeMarkerCursor、标记透明度等处的过期说明
   - 空态提示按是否开启选取模式区分文案

验证(全离屏,未占用鼠标/键盘/屏幕)
   - 新增 .preflight/mark5check 52/52
   - 回归:markcheck 67/67、mark4check 40/40、fixcheck 37/37、round3check 31/31、
     round4check 34/34、round5check 5/5、editcheck 20/20、convcheck 18/18、diag 19/19
   - check 136/137(唯一失败项为既有不稳定项,drift=15818 与改动前逐位相同)
   - make release / make debug 零警告;Release 产物 TargetCheck.exe 857,600 字节
2026-10-23 15:50:55 +08:00

333 lines
13 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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;
/**
* @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<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; /**< 失败原因。 */
std::vector<SignalGroup> 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<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);
};
std::vector<LoadedSourceGroup> 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<std::size_t>(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<double>(index) / static_cast<double>(count));
state->progress.store(
static_cast<float>(base + 0.25 * info.ratio / static_cast<double>(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<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__