Files
Just-DataInspector/Components/Application/Inc/ApplicationInternal.hpp
T
DeepSeek V4.1 Flash 97b361dedd fix(ui): 采样统计显示一致化、GX 拖拽在布局边界下可用、信号「移动到…」与拖拽排序
一、采样间隔/频率显示不一致(用户报告 1ms vs 1000us、100us 0ns)
* FormatDurationCompound 改为「先量化到整数皮秒、再整数分解」,
  彻底消除同一数值因浮点噪声落到不同单位/留下零尾数的问题;
  秒级分支与亚秒分支共用 totalPs 方案,次级进位/单位升级集中在一处。
* FormatFrequency 的单位选择改为按「舍入后的显示值」判定(999.9999999Hz
  与 1000Hz 均为 1.000kHz),并做向下校正避免 0.001kHz。

二、选区截取面板
* 提示文本改为用户指定原文「启用双光标模式以进行选区截取」。
* EdgePanelWidth 计入该提示的实际文本宽度;绘制改用 UiTextClipped 不再省略。
* viewHeight 补回漏减的 bottomPad,修掉条目区与按钮之间的空白。

三、未展开的信号条目栏悬停不再变成手型光标。

四、开启「显示布局边界」后 GX 标签可正常拖拽排序
* 抽出 PaintLaneReorderOverlay(吸附槽位 + 被拖轮廓),贴图与整幅重绘共用。
* 整幅重绘在 LANE_REORDER 时按 LaneDragOffset 逐格平移重画(被拖网格最后画),
  修掉调试模式下不跟随鼠标、无吸附提示、只能盲拖的问题。

五、信号右键新增「移动到…」二级菜单(位于「仅显示该信号」之上)
* 条目为「分组 N(组名)」,当前组打勾;无标题行;底部「新建分组」。
* MenuItem::submenuMaxRows + PopupMenu 独立子菜单滚动(滚动条、滚轮、视口命中过滤),
  IsScrollable 计入子菜单可滚状态。
* 数据层 DataSet::MoveSignalToGroup/MoveSignal 让时间轴随信号搬运
  (独占则移动、共享则复制);TabConfig::RemapAfterSignalMove 改写引用而非先删后插。
* Application::MoveSignalCore 统一入口,保证派生计算暂停、引用修正、选中迁移与同步一致。

六、信号列表支持按住拖拽排序(组内重排 / 跨组移动)
* 4px 阈值进入拖拽;条目脱离原位跟随鼠标(阴影 + 强调色描边 + 插入指示线)。
* 其它信号行/分组行按插入位置做避让位移动画(RowRect 统一叠加插值位移,
  宿主 kScrollTimerId 驱动 AdvanceSignalDragAnimation)。
* 数据层 DataSet::MoveSignal 支持任意位置;TabConfig::RemapAfterSignalReorder 线性重映射。
* 拖拽中禁用组名悬浮;双击改名与 InvalidateRows 均正确终止拖拽态。

验证:删 .o 全量重编译零 error / 零 warning;产物 873,984 B(< 1MB 预警阈值)。
本轮未使用鼠标/键盘/屏幕,未运行任何 GUI 探针,仅编译验证 + 源码级核对。
2026-11-03 19:27:43 +08:00

372 lines
15 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 "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; /**< 面板请求克隆单个信号的自定义消息。 */
/**
* @brief 面板请求「克隆该信号(带调整)」的自定义消息。
*
* `wParam = groupIndex`、`lParam = signalIndex`(与 `kMessageCloneSignal` 同一参数约定)。
* 新信号**沿用源信号的全部调整参数**(公式映射 / 系数 / 偏移 / 镜像 / 绝对值),
* 不烘值 ⇒ 两条曲线逐像素重合,且新信号的「调整」面板与源信号完全一致。
*/
inline const UINT kMessageCloneSignalAdjusted = WM_APP + 6; /**< 面板请求「克隆该信号(带调整)」的自定义消息。 */
/**
* @brief 面板请求「把信号移动到另一个分组」的自定义消息。
*
* `wParam = 源分组下标`;`lParam` 低 16 位 = 目标分组下标(`0xFFFF` 表示**新建分组**),
* 高 16 位 = 源分组内的信号下标。
* 新分组名固定为「新建分组」(重名时自动加序号),见 `Application::MoveSignalToGroup`。
*/
inline const UINT kMessageMoveSignalToGroup = WM_APP + 7;
inline const int kMoveSignalNewGroupMarker = 0xFFFF; /**< `kMessageMoveSignalToGroup` 中「新建分组」的 lParam 标记。 */
/**
* @brief 面板请求「信号列表拖拽排序」的自定义消息。
*
* `wParam` = `(源分组下标 << 16) | 源组内信号下标`;
* `lParam` 低 16 位 = 目标分组下标,高 16 位 = 目标组内下标(`0xFFFF` 表示末尾)。
*/
inline const UINT kMessageReorderSignal = WM_APP + 8;
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 绘图区矩形(客户区坐标)。
* @param [OUT] plotPaintValid `Application::m_plotPaintValid`:置假,表示后备缓冲区里的
* 绘图区像素已不可信(面板动画帧不得跳过绘图区重绘)。
*/
inline void InvalidatePlotRect(HWND hwnd, const gd::RectF& plotRect, bool& plotPaintValid)
{
plotPaintValid = false;
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 应用设置菜单(工具栏齿轮)命令 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<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__