#ifndef __APPLICATION_HPP__ #define __APPLICATION_HPP__ #include #include #include #include #include #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 新建标签页并切换为激活页。 */ void NewTab(); /** * @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] fileIndex 文件下标。 */ void RemoveFile(int fileIndex); /** * @brief 从工作区移除某分组中的一条信号,并立即释放其采样内存。 * * 用户点击信号行左侧的 ✕ 时调用:该信号一旦被确认不再需要,就应从数据层删除 * 并释放内存(而非只是隐藏)。若它是所属分组中最后一条信号,则连同分组一起移除。 * * @param [IN] fileIndex 信号所属文件下标。 * @param [IN] signalIndex 文件内信号下标。 */ void RemoveSignal(int fileIndex, 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 点均值滤波信号,命名为 * `<原信号名称> (N点均值)`,插入到原信号所在组的原信号位置**下方**, * 并把新信号所在的网格移动到原信号网格的下方。 * * @return 实际构建了新信号返回 true。 */ bool ApplyPendingDerivedBuild(); /** * @brief 把一条多阶均值滤波派生信号插入数据层并同步各视图。 * @param [IN] fileIndex 原信号所属文件下标。 * @param [IN] signalIndex 原信号下标。 * @param [IN] points 均值点数 X(滑平半径)。 * @param [IN] order 阶数 N(迭代次数)。 * @param [IN] makeVisible 是否在当前标签页中勾选新信号。 * @return 构建并插入成功返回 true。 */ bool BuildDerivedSignal(int fileIndex, int signalIndex, int points, int order, bool diffAmp, bool makeVisible); /** * @brief 弹出文件选择对话框并加载所选文件。 */ void OpenFileDialog(); /** * @brief 加载指定 CSV 文件并刷新界面。 * @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] file 构建好的数据文件。 * @param [OUT] error 失败原因描述。 * @return 加载成功返回 true;用户取消或失败返回 false。 */ bool LoadFileAsync(const std::wstring& path, DataFile& file, 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 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; /**< 提示浮层淡出定时器是否已启动。 */ 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)。 */ unsigned long long m_loadingBytesRead = 0; /**< 已读取字节数(用于进度浮层)。 */ unsigned long long m_loadingTotalBytes = 0; /**< 文件总字节数(0 表示未知)。 */ float m_loadingSpin = 0.0f; /**< 加载动画旋转相位(度)。 */ unsigned long long m_loadingBytes = 0; /**< 当前文件字节数(用于展示体积)。 */ /** * @brief 装饰区域(标签栏 / 工具栏 / 信号面板 / 分隔条)是否需要重绘。 * * 这些区域的绘制成本与数据量无关但与控件数量成正比,且只在鼠标位于其中 * 或发生交互时才会变化。绘图区拖拽(如双光标左右移动)期间持续置为 * false,从而每帧只重绘、只回送绘图区所在的矩形,避免整窗重绘的开销。 */ bool m_chromeDirty = true; bool m_mouseOverPlot = false; /**< 上一次鼠标移动是否位于绘图区内。 */ }; } // namespace jdi #endif // __APPLICATION_HPP__