Files
Just-DataInspector/Components/Application/Inc/Application.hpp
T
DeepSeek V4.1 Flash 0871eb0cba 分组折起热区与改名输入框修正 + 数据模型去冗余(单一名称)+ 新增 Components/Shared 组件 + 过时注释/命名大清理 + 二进制瘦身 25.6%
## 一、UI 交互修复(用户报告)

① 分组折起热区(用户要求):点分组**名称**不再折起,只有**折叠三角**或**「组X」标签**
   才折起。RowPart 新增 GROUP_TITLE;RowHitPart() 为分组行判出三角 / 复选框 / 关闭 ✕ /
   「组X」标签矩形 / 其余 BODY;OnLButtonDown 只在这两个热区调用 ToggleGroupFromRow()。
   附带:双击「组X」不再进入改名(该区域现为折起热区),双击分组名仍可改名。
② 分组改名输入框不再吞掉「N 项」:RowNameRect() 对分组行的右边界改为「项数左边缘 − 6*scale」;
   新增 GroupLabelText()/GroupStatsText() 作为文本与宽度的唯一来源(PaintGroupRow 与
   RowNameRect 共用),避免名称可用宽度与实际绘制文本漂移。

## 二、数据模型去冗余(用户指出的重点)

- SignalData / SignalGroup **只保留一个名称字段** name:删除 displayName、删除 Title()
  (31 处调用点改为直接用 name);DataSet::SetSignalDisplayName / SetGroupDisplayName
  → RenameSignal / RenameGroup,且**空串 / 纯空白一律拒绝**(名称不再有回退值)。
  依据:信号靠 SignalRef 下标定位、从不按名字匹配 ⇒ 两个字段是同一语义的两份副本。
  收益:每条信号 / 每个分组少一个 std::wstring(24B + 堆分配)与一条 Title() 分支。
- 删除 Application::m_loadingBytes(全仓既不读也不写的死成员,注释却称“用于展示体积”)。

## 三、新增 Components/Shared 组件(通用 UI 控件)

- 从 Utilities/PaintUtil.* 迁出全部「控件类」绘制/命中:图标(+ / ✕ / 三角)、复选框、
  滑块(UiSliderRatioFromX / UiDrawSlider)、内联输入框(UiDrawEditField /
  UiDrawEditFieldContent / UiEditTextLeft / UiEditOffsetAt / UiEditFieldMouseDown /
  UiEditFieldMouseMove)、颜色样块(UiDrawSwatch / UiSwatchNeedsOutline)、
  取值胶囊配色(UiInk / UiColorLuminance / UiContrastInk)。
- PaintUtil.* 只保留低层绘制原语(矩形 / 圆角 / 直线 / 批量实线 / 颜色混合 / 文本与测量)。
- 依赖方向固定:Components/* → Components/Shared → Utilities(不得反向,Utilities 不再含控件)。
- Makefile 与 .preflight/perf/build.ps1 已登记新目录;调用方改 include "UiControls.hpp"。

## 四、过时注释 / 命名批量修正(承接上一轮未做完的部分)

- 注释:文件下标→分组下标、文件内信号下标→组内信号下标、所属文件下标→所属分组下标;
  措辞统一 文件分组→分组、移除该文件→移除该分组、打开 CSV 文件→打开数据文件 等一批。
- 文不对题的形参名:VisibleIndexRange(const TimeAxis& file,…) → axis;
  Application::LoadFileAsync(…, SignalGroup& file,…) → group。
- 函数名里的 File 语义(上一轮只改了 RowKind::FILE):PaintFileRow→PaintGroupRow、
  FileCloseRect→GroupCloseRect、OpenFileMenu→OpenGroupMenu、FileGroupLabelWidth→GroupLabelWidth、
  StyleEditField::FILE_NAME→GROUP_TITLE;局部变量 fileRow/fileHeight/isFile →
  groupRow/groupHeight/isGroup。
- 修复 DataModel.hpp 两处被压扁的注释块(/**<TAB><TAB>* @brief …)与 Application::NewTab
  上方重复的文档块。
- ⚠️ 踩坑:GROUP_NAME 撞上 nb30.h(windows.h 引入)的 #define GROUP_NAME 0x80,
  导致枚举编译失败,改名 GROUP_TITLE 解决。

## 五、二进制瘦身:1 013 248 → 754 176 B(−25.6%,省 259 KB)

- 真因不是 PlotView.cpp,而是 Resources/JustDataInspector.ico 里 **256×256@32bpp 以未压缩
  BMP 存储的条目(270 376 B,占整包 27%)**。改为 PNG 存储(Vista+ 标准做法,观感零变化)
  ⇒ 该条目 270 376 → 11 280 B、整个 ico 302 430 → 43 334 B(小尺寸仍保留 BMP)。
- 实测结论:“拆分大文件 ⇒ 瘦身”不成立(同一批机器码仍要进二进制);
  -ffunction-sections/-fdata-sections/-Wl,--gc-sections 在本工程反而 +9 216 B(已回退)。
- ⚠️ .o 体积受节对齐/重定位表主导,判断“代码搬去哪了”要用 nm,不要看 .o 字节数。

## 六、验证(全部离屏:SW_HIDE + 不注入真实输入 + 进程/线程 BELOW_NORMAL)

- panelanim 156/156(连跑 5 次稳定;新增/更新:点分组名不折起也不滚动、扫掠定位「组X」热区、
  筛选态与普通态折叠三角逐像素一致、改名输入框右边界早于项数文本);verify 49/49;
  datacheck 69/69(含“重命名写入唯一名称字段且空名被拒”);edgecheck 46/46。
- A/B:回退两处 UI 修复 ⇒ 10 项如实失败;恢复后全绿。
- make release / make debug 零警告。

## 七、本轮未完成(已在报告与 agent_mem.md 中如实记录)

- PlotView.cpp(13 695 行 / 544 KB)按功能拆分为多个协作文件:未执行。
  前置改造是把第 13–888 行的匿名命名空间(~870 行常量与工具函数)改成具名命名空间并逐个
  inline,再逐个文件搬移并反复验证;时间不足以安全完成,故主动不做(附可执行方案)。
- SignalPanel.cpp / Application.cpp / DataModel.cpp 的多文件化、Lane 抽出 LanePanelState、
  SetStatusMessage 模态阻塞解耦:均未执行。

## 八、文档

- 新增 Documents/refactor_report_2026-10-16.md(本轮详细报告);
  删除 Documents/refactor_report_2026-10-15.md(有效内容已并入 agent_mem.md 与本报告)。
- Documents/agent_mem.md 新增 §9.37 与 §11.2 第 85–90 条;更新数据层架构摘要(单一名称字段);
  §12 历史附录加“时效声明”。
2026-10-16 03:25:48 +08:00

443 lines
15 KiB
C++

#ifndef __APPLICATION_HPP__
#define __APPLICATION_HPP__
#include <windows.h>
#include <objidl.h>
#include <gdiplus.h>
#include <string>
#include <vector>
#include "ColorPickerPopup.hpp"
#include "DataModel.hpp"
#include "PaintUtil.hpp"
#include "PlotView.hpp"
#include "PopupMenu.hpp"
#include "SignalPanel.hpp"
#include "TabBar.hpp"
#include "Theme.hpp"
#include "Toolbar.hpp"
namespace jdi
{
/**
* @brief 应用主框架:负责窗口、布局、消息分发、标签页管理与绘制合成。
*
* 数据层(DataSet)全局共享,标签页仅保存各自的视图配置(TabConfig),
* 因此新标签页可以独立勾选信号、独立配色与分组,互不影响。
*/
class Application
{
public:
/**
* @brief 获取全局唯一应用实例。
* @return 应用实例引用。
*/
static Application& Instance();
/**
* @brief 初始化环境、创建窗口并进入消息循环。
* @param [IN] instance 模块实例句柄。
* @param [IN] showCommand 窗口显示方式。
* @return 进程退出码。
*/
int Run(HINSTANCE instance, int showCommand);
/**
* @brief 窗口过程中转处理函数。
* @param [IN] hwnd 窗口句柄。
* @param [IN] message 消息标识。
* @param [IN] wParam 消息参数。
* @param [IN] lParam 消息参数。
* @return 消息处理结果。
*/
LRESULT HandleMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
private:
/**
* @brief 鼠标交互的作用目标。
*/
enum class Target : int
{
NONE = 0, /**< 无目标。 */
TAB_BAR, /**< 标签栏。 */
TOOLBAR, /**< 工具栏。 */
PANEL, /**< 信号面板。 */
PLOT, /**< 波形绘图区。 */
SPLITTER, /**< 面板分隔条。 */
EDIT_FIELD /**< 内联输入框(拖拽框选选区)。 */
};
Application() = default;
~Application() = default;
Application(const Application&) = delete;
Application& operator=(const Application&) = delete;
/**
* @brief 窗口过程静态回调。
* @param [IN] hwnd 窗口句柄。
* @param [IN] message 消息标识。
* @param [IN] wParam 消息参数。
* @param [IN] lParam 消息参数。
* @return 消息处理结果。
*/
static LRESULT CALLBACK WindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
/**
* @brief 创建主窗口与相关资源。
* @param [IN] instance 模块实例句柄。
* @param [IN] showCommand 窗口显示方式。
* @return 创建成功返回 true。
*/
bool Initialize(HINSTANCE instance, int showCommand);
/**
* @brief 释放窗口与绘图资源。
*/
void Shutdown();
/**
* @brief 依据 DPI 重建字体与布局。
* @param [IN] dpi 目标 DPI。
*/
void ApplyDpi(UINT dpi);
/**
* @brief 重新计算各区域的布局。
*/
void Relayout();
/**
* @brief 创建或调整后备缓冲区尺寸。
* @param [IN] width 客户区宽度。
* @param [IN] height 客户区高度。
*/
void EnsureBackBuffer(int width, int height);
/**
* @brief 释放后备缓冲区。
*/
void ReleaseBackBuffer();
/**
* @brief 完整重绘客户区并呈现到窗口。
*/
void Render();
/**
* @brief 获取当前激活标签页配置(可写)。
* @return 激活标签页配置引用。
*/
TabConfig& ActiveTab();
/**
* @brief 获取当前激活标签页配置(只读)。
* @return 激活标签页配置引用。
*/
const TabConfig& ActiveTab() const;
/**
* @brief 新建一个标签页并切换到它。
* @param [IN] collapseGroups 是否把信号列表里的分组**默认折起**
*(用户新建的标签页传 true;启动时的首个标签页传 false,便于直接勾选信号)。
*/
void NewTab(bool collapseGroups);
/**
* @brief 生成当前未被占用的最小标签页名称(关闭标签页后编号可被复用)。
* @return 形如「视图 N」的名称。
*/
std::wstring NextTabName() const;
/**
* @brief 关闭指定标签页。
* @param [IN] index 标签页下标。
*/
void CloseTab(int index);
/**
* @brief 切换激活标签页并重新绑定各组件上下文。
* @param [IN] index 标签页下标。
*/
void SetActiveTab(int index);
/**
* @brief 将标签栏内容与当前标签页集合同步。
*/
void RefreshTabBar();
/**
* @brief 检测激活标签页的可见性/网格变化,并同步绘图区与布局。
* @param [IN] force 是否强制同步(忽略缓存比对)。
*/
void SynchronizeView(bool force);
/**
* @brief 将全部标签页配置与数据层对齐(分组增删后调用)。
*/
void SyncTabsToDataSet();
/**
* @brief 从工作区移除指定分组并修正各标签页配置。
* @param [IN] groupIndex 分组下标。
*/
void RemoveGroup(int groupIndex);
/**
* @brief 从工作区移除某分组中的一条信号,并立即释放其采样内存。
*
* 用户点击信号行左侧的 ✕ 时调用:该信号一旦被确认不再需要,就应从数据层删除
* 并释放内存(而非只是隐藏)。若它是所属分组中最后一条信号,则连同分组一起移除。
*
* @param [IN] groupIndex 信号所属分组下标。
* @param [IN] signalIndex 组内信号下标。
*/
void RemoveSignal(int groupIndex, int signalIndex);
/**
* @brief 处理标签栏交互结果。
* @param [IN] result 交互结果。
*/
void OnTabBarResult(const TabBarResult& result);
/**
* @brief 弹出标签页右键菜单(主题化自绘菜单)。
* @param [IN] result 标签栏交互结果。
*/
void ShowTabContextMenu(const TabBarResult& result);
/**
* @brief 弹出绘图区网格右键菜单(设置 Y 轴上下限 / 恢复自适应)。
* @param [IN] laneIndex 目标网格下标。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
*/
void ShowPlotContextMenu(int laneIndex, int x, int y);
/**
* @brief 把网格菜单上的 Y 轴上下限显示值刷新为数据层的真实值(菜单保持打开)。
* @param [IN] laneIndex 网格下标。
*/
void RefreshPlotMenuValues(int laneIndex);
/**
* @brief 派发弹出菜单的选择结果。
* @param [IN] result 菜单选择结果。
*/
void DispatchMenuResult(const MenuResult& result);
/**
* @brief 调整标签页顺序(拖拽重排)。
* @param [IN] from 被移动的标签页下标。
* @param [IN] target 目标插入位置(插入到该下标之前)。
*/
void MoveTab(int from, int target);
/**
* @brief 应用绘图区提交的信号重命名请求。
* @return 应用了重命名返回 true。
*/
bool ApplyPendingRename();
/**
* @brief 应用绘图区提交的「实验性功能 → 构建为新信号」请求。
*
* 为指定网格内的每条被勾选信号各构建一条派生信号(多次迭代均值滤波或弱峰调整),
* 命名为 `<原信号名称> (N点均值…)` / `<原信号名称> (弱峰调整 M%)`,
* 插入到原信号所在组的原信号位置**下方**,并把新信号所在的网格移动到原信号网格的下方。
*
* @return 实际构建了新信号返回 true。
*/
bool ApplyPendingDerivedBuild();
/**
* @brief 应用绘图区提交的「把信号列表滚动到目标信号」的联动请求。
*
* 用户 2026-10-15 要求:在绘图区触发联动(点击图例 / 点击信号曲线等把某条信号选为
* 当前目标)时,信号选择区域要**平滑滚动**到该信号并完整显示它
*(信号很多时否则找不见选中的那一条)。
*
* @return 存在并应用了请求返回 true。
*/
bool ApplyPendingPanelScroll();
/**
* @brief 派生信号构建请求(多次迭代均值滤波 / 弱峰调整共用同一套插入流程)。
*/
struct DerivedSignalRequest
{
int groupIndex = -1; /**< 原信号所属分组下标。 */
int signalIndex = -1; /**< 原信号下标。 */
int points = 5; /**< 多次迭代均值滤波:均值点数 X(滑平半径)。 */
int iterations = 1; /**< 多次迭代均值滤波:迭代次数 N(至少 1)。 */
int amountPercent = 0; /**< 弱峰调整:调整幅度(-400–400,默认 0)。 */
bool weakPeak = false; /**< true = 弱峰调整,false = 多次迭代均值滤波。 */
};
/**
* @brief 把一条派生信号(多次迭代均值滤波 / 弱峰调整)插入数据层并同步各视图。
* @param [IN] request 构建请求。
* @param [IN] makeVisible 是否在当前标签页中勾选新信号。
* @return 构建并插入成功返回 true。
*/
bool BuildDerivedSignal(const DerivedSignalRequest& request, bool makeVisible);
/**
* @brief 弹出“打开数据文件”对话框并加载所选文件。
*/
void OpenFileDialog();
/**
* @brief 加载指定的数据文件并刷新界面。
* @param [IN] path 文件完整路径。
* @return 加载成功返回 true。
*/
bool LoadFile(const std::wstring& path);
/**
* @brief 执行工具栏动作。
* @param [IN] action 动作标识。
*/
void ExecuteToolbarAction(ToolbarAction action);
/**
* @brief 处理键盘按键。
* @param [IN] key 虚拟键码。
* @return 已处理返回 true。
*/
bool HandleKeyDown(WPARAM key);
/**
* @brief 报告操作结果信息。
* @param [IN] text 提示文本。
* @param [IN] isError 是否为错误提示。
* @note 界面不再保留常驻状态栏:错误以对话框提示,普通提示被忽略,避免无用信息干扰。
*/
void SetStatusMessage(const std::wstring& text, bool isError);
/**
* @brief 刷新窗口标题。
*/
void UpdateWindowTitle();
/**
* @brief 解析命令行中的文件参数并加载。
*/
void LoadCommandLineFiles();
/**
* @brief 在后台线程解析并构建分组数据,同时播放加载过渡动画(界面不卡死)。
*
* 大文件的瓶颈在“解析 CSV”与“按列重组采样数据”,这两步都不依赖 DataSet,
* 因此放在工作线程执行;主线程循环抽送消息并重绘加载遮罩层,
* 直到后台完成后再把结果移动进 DataSet(O(1),不会引起卡顿)。
*
* @param [IN] path 文件路径。
* @param [OUT] group 构建好的信号分组。
* @param [OUT] error 失败原因描述。
* @return 加载成功返回 true;用户取消或失败返回 false。
*/
bool LoadFileAsync(const std::wstring& path, SignalGroup& group, std::wstring& error);
/**
* @brief 绘制加载过渡动画遮罩(在 Render 的最后一步调用,覆盖整个客户区)。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] width 客户区宽度。
* @param [IN] height 客户区高度。
*/
void PaintLoadingOverlay(gd::Graphics& g, int width, int height);
/**
* @brief 处理客户区鼠标移动。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @param [IN] buttonDown 是否有按键处于按下状态。
* @return 需要重绘返回 true。
*/
bool HandleMouseMove(int x, int y, bool buttonDown);
/**
* @brief 拖动(鼠标捕获)期间主动刷新光标形状。
*
* Windows 在鼠标被捕获时不再派发 `WM_SETCURSOR`,若不主动设置,
* 光标会一直停留在按下瞬间的形状(例如选区放大时看不到十字/方向箭头)。
*
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
*/
void UpdateDragCursor(int x, int y);
HINSTANCE m_instance = nullptr; /**< 模块实例句柄。 */
HWND m_hwnd = nullptr; /**< 主窗口句柄。 */
UINT m_dpi = 96; /**< 当前 DPI。 */
float m_scale = 1.0f; /**< DPI 缩放比例。 */
ULONG_PTR m_gdiplusToken = 0; /**< GDI+ 运行时令牌。 */
bool m_gdiplusReady = false; /**< GDI+ 是否初始化成功。 */
Theme m_theme; /**< 主题配色。 */
UiFonts m_fonts; /**< 界面字体。 */
PopupMenu m_menu; /**< 与主题一致的自绘弹出菜单。 */
ColorPickerPopup m_colorPicker; /**< 与主题一致的浮动拾色器。 */
DataSet m_dataSet; /**< 全局共享的数据集合。 */
std::vector<TabConfig> m_tabs; /**< 标签页配置集合。 */
int m_activeTab = -1; /**< 当前激活标签页下标。 */
TabBar m_tabBar; /**< 顶部标签栏。 */
Toolbar m_toolbar; /**< 顶部工具栏。 */
SignalPanel m_panel; /**< 左侧信号面板。 */
PlotView m_plot; /**< 波形绘图区。 */
gd::RectF m_tabBarRect; /**< 标签栏区域。 */
gd::RectF m_toolbarRect; /**< 工具栏区域。 */
gd::RectF m_panelRect; /**< 信号面板区域。 */
gd::RectF m_splitterRect; /**< 面板分隔条区域。 */
gd::RectF m_plotRect; /**< 绘图区区域。 */
float m_panelWidth = 366.0f; /**< 面板宽度(逻辑像素)。 */
bool m_draggingSplitter = false; /**< 是否正在拖动分隔条。 */
bool m_middlePanning = false; /**< 是否正在用鼠标中键自由平移绘图区。 */
int m_splitterGrabOffset = 0; /**< 分隔条拖动的抓取偏移。 */
HDC m_backDc = nullptr; /**< 后备缓冲设备上下文。 */
HBITMAP m_backBitmap = nullptr; /**< 后备缓冲位图。 */
HGDIOBJ m_backOldBitmap = nullptr; /**< 后备缓冲原始位图。 */
int m_backWidth = 0; /**< 后备缓冲宽度。 */
int m_backHeight = 0; /**< 后备缓冲高度。 */
Target m_activeTarget = Target::NONE; /**< 当前鼠标作用目标。 */
bool m_trackingLeave = false; /**< 是否已注册鼠标离开通知。 */
bool m_hintTimerActive = false; /**< 提示浮层淡出定时器是否已启动。 */
/** 信号列表「滚动到目标信号」动画的定时器是否已启动(见 `SignalPanel::AdvanceScrollAnimation`)。 */
bool m_scrollTimerActive = false;
std::wstring m_windowTitleText; /**< 上次写入窗口标题的完整文本(用于避免重复设置)。 */
int m_tabMenuIndex = -1; /**< 标签栏右键菜单的目标标签页下标。 */
int m_plotMenuLane = -1; /**< 绘图区右键菜单的目标网格下标。 */
int m_cachedVisibleCount = -1; /**< 上次同步时的可见信号数量。 */
std::size_t m_cachedLaneCount = 0; /**< 上次同步时的网格数量。 */
bool m_cachedAnyVisible = false; /**< 上次同步时是否存在可见信号。 */
bool m_loading = false; /**< 是否正在显示加载过渡动画。 */
std::wstring m_loadingText; /**< 加载遮罩显示的文件名。 */
float m_loadingProgress = 0.0f; /**< 加载进度(0-1)。 */
/** 已读取字节数(用于进度浮层);与 `m_loadingTotalBytes` 一起展示“已读 / 总量”。 */
unsigned long long m_loadingBytesRead = 0;
/** 文件总字节数(0 表示未知)。 */
unsigned long long m_loadingTotalBytes = 0;
float m_loadingSpin = 0.0f; /**< 加载动画旋转相位(度)。 */
/**
* @brief 装饰区域(标签栏 / 工具栏 / 信号面板 / 分隔条)是否需要重绘。
*
* 这些区域的绘制成本与数据量无关但与控件数量成正比,且只在鼠标位于其中
* 或发生交互时才会变化。绘图区拖拽(如双光标左右移动)期间持续置为
* false,从而每帧只重绘、只回送绘图区所在的矩形,避免整窗重绘的开销。
*/
bool m_chromeDirty = true;
bool m_mouseOverPlot = false; /**< 上一次鼠标移动是否位于绘图区内。 */
};
} // namespace jdi
#endif // __APPLICATION_HPP__