一、选区截取面板「条目区与按钮之间的空白」(连续三轮未修好的那个) 根因不是内容区几何,而是**底部固定区高度算了两行**:footerHeight 用 `buttonHeight * 2 + 6 + pad` 预留,而布局把「截取为新信号 / 原始信号截取」 两个按钮并排画在同一行 ⇒ 内容区下边界与按钮上边界之间空出一整行。 改为按一行算(两处:高度算式分支 + 布局几何分支同步修改)。 数值核对(scale=1.25、1 条可见信号):GAP 53.8px → 8.8px(= 一个 pad 的预期留白)。 二、拖拽插入指示线不再盖住被拖条目 PaintSignalDragGhost 的绘制顺序改为「插入指示线 → 阴影 → 幽灵行 → 描边」, 指示线始终位于跟随鼠标的那部分之下。 三、「移动到…」二级菜单 * 条目文案改为「组号 | 组名」(不再是括号形式)。 * 修复「新建分组」点了没反应:DataSet::AddGroup 拒收「无信号且无时间轴」的空分组, 旧实现「先建空组再搬信号」必然在建组那一步失败。新增 DataSet::MoveSignalToNewGroup 把建组 + 搬运时间轴 + 搬运信号做成原子操作(失败回滚)。 * TabConfig::RemapAfterSignalMove 在目标分组是新建时自行扩展 m_visible (调用方不再先 SyncToDataSet —— 那次同步会按旧下标清理引用)。 * Application::MoveSignalCore 增加 newGroupName 参数并返回 bool,避免失败仍报"已移动"。 四、信号拖拽避让动画更流畅 * 插值系数按真实帧间隔计算:1 - exp(-dt/22ms),dt 夹在 1~60ms,帧间隔抖动时不再忽快忽慢。 * 鼠标移动事件里也推进一次插值,快速拖动时让位效果跟得上指针。 验证:删 .o 全量重编译零 error / 零 warning,产物 875,008 B(< 1MB 预警阈值)。 本轮未占用鼠标/键盘/屏幕,未运行任何 GUI 探针;面板空白改为纯数值算式核对。 规则已同步到 Documents/agent_mem.md(含"改面板几何必须数值验证"的教训条目)。
794 lines
33 KiB
C++
794 lines
33 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"
|
||
#include "UiComponent.hpp"
|
||
#include "UiBox.hpp"
|
||
#include "UiSurface.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);
|
||
|
||
/**
|
||
* @brief 设置错误反馈是否使用模态对话框。
|
||
*
|
||
* 模态错误框会抢断用户当前操作;宿主在不需要打断(或无法交互)的场景下
|
||
* 应关闭它,此时错误信息改为在绘图区浮层中非模态展示。
|
||
*
|
||
* @param [IN] enabled true 表示允许弹出模态错误框。
|
||
*/
|
||
void SetModalErrorsEnabled(bool enabled) { m_modalErrorsEnabled = enabled; }
|
||
|
||
/**
|
||
* @brief 查询错误反馈是否使用模态对话框。
|
||
* @return 允许弹出模态错误框返回 true。
|
||
*/
|
||
bool ModalErrorsEnabled() const { return m_modalErrorsEnabled; }
|
||
|
||
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 取"整个应用共享的那张后台位图"作为一段可绘制表面。
|
||
*
|
||
* 这是 P5 的基础设施:后备缓冲是 **top-down 32bpp DIB 段**,因此除了 `dc` 之外
|
||
* 还能给出 `bits`(像素直写)与 `version`(位图代次;`WM_SIZE` / `WM_DPICHANGED`
|
||
* 重建后旧句柄必须作废)。
|
||
* ⚠️ 绝大多数绘制只需要 `dc + rect`;`bits` 仅用于 CPU 侧合成,**不要**用它画控件。
|
||
*/
|
||
UiSurface BackBufferSurface() const;
|
||
|
||
/**
|
||
* @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 把一条信号移动到另一个分组(`toGroup < 0` 表示新建分组)。
|
||
*
|
||
* 用户从信号右键菜单的「移动到…」调用。跨分组移动会让 `SignalData::axisIndex`
|
||
* 失效(轴按分组存放),因此数据层会把该信号的时间轴一并带走
|
||
* (独占则移动、共享则复制),详见 `DataSet::MoveSignalToGroup`。
|
||
*
|
||
* 移动后:各标签页先 `RemapAfterSignalMove` 修正引用(信号**留在原网格**、
|
||
* 曲线继续显示),共享选中项同步改写,最后 `SyncTabsToDataSet` 整体对齐。
|
||
*
|
||
* @param [IN] fromGroup 源分组下标。
|
||
* @param [IN] fromSignal 源分组内的信号下标。
|
||
* @param [IN] toGroup 目标分组下标,负数表示新建一个名为「新建分组」的分组。
|
||
*/
|
||
void MoveSignalToGroup(int fromGroup, int fromSignal, int toGroup);
|
||
|
||
/**
|
||
* @brief 生成一个在现有分组中不重复的分组名(`base` / `base (2)` / `base (3)` …)。
|
||
* @param [IN] base 期望名称。
|
||
* @return 实际可用名称。
|
||
*/
|
||
std::wstring UniqueGroupName(const std::wstring& base) const;
|
||
|
||
/**
|
||
* @brief 把一条信号移动到指定分组的指定位置(组内重排 / 跨组移动的统一实现)。
|
||
*
|
||
* `MoveSignalToGroup` 与信号列表的拖拽排序都汇到这里,保证两处的引用修正、
|
||
* 选中项迁移与同步流程完全一致。
|
||
*
|
||
* @param [IN] fromGroup 源分组下标。
|
||
* @param [IN] fromSignal 源分组内的信号下标。
|
||
* @param [IN] toGroup 目标分组下标(负数 + `newGroupName` 非空 = 新建分组)。
|
||
* @param [IN] toIndex 目标组内下标(负数表示追加到末尾)。
|
||
* @param [IN] newGroupName 非空且 `toGroup < 0` 时,新建该名称的分组并把信号搬进去。
|
||
* @return 确实发生了移动返回 true。
|
||
*/
|
||
bool MoveSignalCore(int fromGroup, int fromSignal, int toGroup, int toIndex,
|
||
const std::wstring* newGroupName);
|
||
|
||
/**
|
||
* @brief 信号列表拖拽排序的提交入口(组内重排或跨组移动)。
|
||
* @param [IN] fromGroup 源分组下标。
|
||
* @param [IN] fromSignal 源分组内的信号下标。
|
||
* @param [IN] toGroup 目标分组下标。
|
||
* @param [IN] toIndex 目标组内下标(-1 表示末尾)。
|
||
*/
|
||
void ReorderSignal(int fromGroup, int fromSignal, int toGroup, int toIndex);
|
||
|
||
/**
|
||
* @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 应用信号面板提交的「把绘图区滚动到目标网格」的联动请求。
|
||
*
|
||
* 用户 2026-10-16 要求:点击信号选择区域的**信号名**(或样式区「目标」行的信号名)时,
|
||
* 若该信号所在的网格不在绘图区可视区内,就自动调节绘图区的滚动条让该网格完整出现
|
||
*(上百个独立网格时,仅靠网格高亮无法定位到画面之外的网格)。悬停不触发。
|
||
*
|
||
* @return 存在请求并已应用返回 true。
|
||
*/
|
||
bool ApplyPendingPlotLaneScroll();
|
||
|
||
/**
|
||
* @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 把一条信号按固定间隔重采样,并插入为一个新信号(带独立的新时间轴)。
|
||
*
|
||
* 采样值先按该信号的「公式映射 + 系数 / 绝对值 / 垂直镜像 / Y 偏移」变换(与绘制一致,
|
||
* 时间轴偏移除外),再在 `[tMin, tMax]` 上按 `interval` 等间隔线性插值。
|
||
* 新信号名 = `<原信号> (重采样 <自适应单位的频率>)`,插入到源信号下方。
|
||
*
|
||
* @param [IN] groupIndex 源信号所属分组下标。
|
||
* @param [IN] signalIndex 源信号下标。
|
||
* @param [IN] interval 重采样间隔(秒,> 0)。
|
||
* @return 构建并插入成功返回 true。
|
||
*/
|
||
bool BuildResampledSignal(int groupIndex, int signalIndex, double interval);
|
||
|
||
/**
|
||
* @brief 「选区截取 → 截取为新信号」:按 `[begin, end]` 区间截取出**新信号**。
|
||
*
|
||
* 语义(用户 2026-11-03 明确要求):
|
||
* * 采样值 = 源信号在该区间内的**原始数据**(不烘焙任何调整);
|
||
* * 名称 = `<源信号名> (AAAA - BBBB)`,其中 AAAA / BBBB 是选区左右端时刻,
|
||
* 以秒为单位、**保留三位小数**并带 `s` 单位(如 `(1.234s - 2.345s)`);
|
||
* * 插入到源信号下方,并**继承源信号的整套调整**(公式/系数/偏移/镜像/绝对值)。
|
||
*
|
||
* @param [IN] groupIndex 源信号所属分组下标。
|
||
* @param [IN] signalIndex 源信号下标。
|
||
* @param [IN] begin 选区左端(显示时刻,秒)。
|
||
* @param [IN] end 选区右端(显示时刻,秒)。
|
||
* @return 构建并插入成功返回 true。
|
||
*/
|
||
bool BuildClippedSignal(int groupIndex, int signalIndex, double begin, double end);
|
||
|
||
/**
|
||
* @brief 「选区截取 → 原始信号截取」:**破坏性**丢弃 `[begin, end]` 之外的采样。
|
||
*
|
||
* 与 `BuildClippedSignal` 不同,这里直接改写源信号的采样数据与所属时间轴:
|
||
* * 保留区间内的采样,区间外的采样与对应时间全部丢弃并释放(`shrink_to_fit`);
|
||
* * 名称不变,也不新增信号;
|
||
* * 重算统计(min/max/finiteCount/粗粒度包络)。
|
||
*
|
||
* ⚠️ 该操作**不可撤销**,因此面板按钮语义上等价于"就地截断原始信号"。
|
||
*
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @param [IN] signalIndex 信号下标。
|
||
* @param [IN] begin 选区左端(显示时刻,秒)。
|
||
* @param [IN] end 选区右端(显示时刻,秒)。
|
||
* @return 成功返回 true。
|
||
*/
|
||
bool ClipSignalInPlace(int groupIndex, int signalIndex, double begin, double end);
|
||
|
||
/**
|
||
* @brief 把一条信号插入到「源信号」的下方,并同步各标签页的引用与可见性。
|
||
*
|
||
* 派生(均值滤波 / 弱峰调整)与克隆共用这一段:插入 → 各标签页下标重映射 →
|
||
* 在当前标签页勾选并把它所在的网格挪到源信号网格下方。**不做** `SyncTabsToDataSet`,
|
||
* 好让调用方在同步之前再改新信号的外观。
|
||
*
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @param [IN] sourceIndex 源信号在组内的下标。
|
||
* @param [IN] signal 要插入的信号(移动语义)。
|
||
* @param [IN] makeVisible 是否在当前标签页中勾选新信号。
|
||
* @param [OUT] newIndex 新信号在组内的下标。
|
||
* @return 插入成功返回 true。
|
||
*/
|
||
bool InsertSignalBelowSource(int groupIndex, int sourceIndex, SignalData&& signal,
|
||
bool makeVisible, int& newIndex);
|
||
|
||
/**
|
||
* @brief 克隆一条信号(**不烘值**,只沿用源信号的时间轴偏移)。
|
||
*
|
||
* 语义(用户 2026-11-03 明确要求):
|
||
* * 采样值 = 源信号的**原始数据**(不做任何烘焙);
|
||
* * 名称 = `*` + 源信号名,插入到原信号下方;
|
||
* * 样式:沿用源信号的外观(颜色 / 透明度 / 线宽 / 绘制样式)**与时间轴偏移**
|
||
* (`offsetTime` 只能留在样式上,无法烘进采样值),
|
||
* 而**公式映射 / 系数 / 绝对值 / 镜像 / Y 偏移一律恢复默认**。
|
||
*
|
||
* 于是「克隆该信号」得到的是一条“去掉所有数值调整、但仍在同一个时刻显示”的原始信号,
|
||
* 便于与源信号叠加对比。
|
||
*
|
||
* @param [IN] sourceRef 源信号引用。
|
||
* @return 克隆成功返回 true。
|
||
*/
|
||
bool CloneSignal(const SignalRef& sourceRef);
|
||
|
||
/**
|
||
* @brief 克隆一条信号(**带调整**:完整沿用源信号的调整参数,不烘值)。
|
||
*
|
||
* 语义(用户 2026-11-03 明确要求):
|
||
* * 采样值 = 源信号的**原始数据**(不做任何烘焙);
|
||
* * 名称 = `*` + 源信号名,插入到原信号下方;
|
||
* * 样式:**完整复制**源信号的调整参数(公式映射 / 系数 / Y 偏移 / 时间轴偏移 /
|
||
* 水平与垂直镜像 / 绝对值)以及外观(颜色 / 透明度 / 线宽 / 绘制样式)。
|
||
*
|
||
* 因此新信号与源信号**逐像素重合**,且可以在新信号上继续独立修改调整而不影响源信号。
|
||
*
|
||
* @param [IN] sourceRef 源信号引用。
|
||
* @return 克隆成功返回 true。
|
||
*/
|
||
bool CloneSignalAdjusted(const SignalRef& sourceRef);
|
||
/**
|
||
* @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] groups 构建好的信号分组(多数文件只有 1 个;`.mat` 的嵌套数据集会有多个)。
|
||
* @param [OUT] error 失败原因描述。
|
||
* @return 加载成功返回 true;用户取消或失败返回 false。
|
||
*/
|
||
bool LoadFileAsync(const std::wstring& path, std::vector<SignalGroup>& groups, 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 展开「应用设置」菜单(工具栏齿轮按钮)。
|
||
*
|
||
* 锚点取齿轮按钮的**实际矩形**(由控件实测),因此菜单总能贴在按钮下方,
|
||
* 不会因按钮文本宽度变化而错位。
|
||
*/
|
||
void OpenSettingsMenu();
|
||
|
||
/**
|
||
* @brief 切换主题配色,并作废全部**像素级**缓存后整窗重绘。
|
||
*
|
||
* 主题色被烘进了多处位图缓存(场景缓存、每网格宽基底图层、装饰层、浮动面板层、
|
||
* 信号列表行位图),换主题后必须整体作废,否则界面会停留在旧配色上。
|
||
*
|
||
* @param [IN] kind 目标主题。
|
||
*/
|
||
void ApplyTheme(ThemeKind kind);
|
||
|
||
/**
|
||
* @brief 绘制全部 UI 控件的布局边界(调试开关开启时在 Render 的最后一步调用)。
|
||
*
|
||
* 内容分两路:
|
||
* * **组件结构**:遍历组件表(底 → 顶)让每个组件上报内部结构,再补上不属于任何
|
||
* 组件的区域(客户区外框、面板分隔条、菜单 / 拾色器);
|
||
* * **文本**:绘制期间由 `UiSetTextRectSink(&m_debugBoxes)` 自动收集(见 `PaintUtil`),
|
||
* 因为文本是本项目唯一"没有几何身份"的元素,它在 60+ 个调用点现算矩形、画完即弃。
|
||
*
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] width 客户区宽度。
|
||
* @param [IN] height 客户区高度。
|
||
*/
|
||
void PaintLayoutBounds(gd::Graphics& g, int width, int height);
|
||
|
||
/**
|
||
* @brief 取某个组件的**绘制层号**(越小越靠下)。
|
||
*
|
||
* 层号 = 组件在 `m_components` 里的下标 × `kLayersPerComponent`,因此
|
||
* “文本收集层”与 `PaintLayoutBounds` 里给几何 box 分配的层号**完全同源**。
|
||
*
|
||
* ⚠️ **为什么要乘以间隔而不是直接用下标**:组件内部可能还需要再压一层
|
||
*(`PlotView` 的浮动面板就在自己之上再加一层)。若直接用下标,
|
||
* 那个子层会与“下一个组件的基层”撞号,遮挡比较就失去意义。
|
||
*
|
||
* @param [IN] component 组件实例。
|
||
* @return 层号;不在组件表里返回 0。
|
||
*/
|
||
int ComponentLayer(const UiComponent* component) const;
|
||
|
||
/**
|
||
* @brief 每个组件占用的层号个数。
|
||
*
|
||
* * `+0`:组件自己的基层(网格 / 悬浮构件 / 光标读数…);
|
||
* * `+1`:组件内部的不透明浮层(`PlotView` 的实验性功能浮动面板);
|
||
* * `+2`:绘制在那些浮层**之上**的叠加层(滚动条 / 回到最上方 / 内联输入框)。
|
||
*/
|
||
static constexpr int kLayersPerComponent = 3;
|
||
|
||
/**
|
||
* @brief 取**不属于组件表**的浮层 / 装饰的绘制层号。
|
||
*
|
||
* 它们接在组件表之后:`offset = 0` = 面板分隔条,`1` = 弹出菜单,`2` = 浮动拾色器。
|
||
* @param [IN] offset 相对组件表末尾的偏移。
|
||
* @return 层号。
|
||
*/
|
||
int OverlayLayer(int offset) const
|
||
{
|
||
return static_cast<int>(m_components.size()) * kLayersPerComponent +
|
||
offset * kLayersPerComponent;
|
||
}
|
||
|
||
/**
|
||
* @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; /**< 主题配色。 */
|
||
/**
|
||
* @brief 当前选中的主题种类(设置菜单 → 主题配色里的勾选项)。
|
||
*
|
||
* 默认值是 **Solarised Light**:这是项目历史默认,用户 2026-10-29 明确要求
|
||
* 新增其它主题时"默认的主题色维持现状"。
|
||
*/
|
||
ThemeKind m_themeKind = ThemeKind::LIGHT;
|
||
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; /**< 波形绘图区。 */
|
||
|
||
/**
|
||
* @brief 顶层组件表的一项。
|
||
*
|
||
* 宿主对“设置缩放 / 摆放 / 悬停离开 / 光标仲裁 / 绘制装饰区”都**只按这张表遍历**,
|
||
* 不再在每条消息里逐个点名组件(旧实现新增一个组件要改 5 处)。
|
||
*/
|
||
struct ComponentEntry
|
||
{
|
||
UiComponent* component = nullptr; /**< 组件实例。 */
|
||
Target target = Target::NONE; /**< 按下时写入 `m_activeTarget` 的值。 */
|
||
bool chrome = false; /**< 是否属于“装饰区”(标签栏/工具栏/面板):
|
||
决定悬停离开后是全帧重绘还是只重绘绘图区。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 组件表(**底 → 顶**顺序)。
|
||
*
|
||
* 命中与光标仲裁从**后往前**遍历(末尾优先),与旧实现“菜单 > 拾色器 > …… > 绘图区”的
|
||
* 优先级方向一致;由于四个组件的区域互不重叠,实际结果与旧的手写链完全等价。
|
||
*/
|
||
std::vector<ComponentEntry> m_components;
|
||
|
||
/**
|
||
* @brief 是否显示**布局边界**(设置菜单里的调试开关)。
|
||
*
|
||
* 开启时 `Render` 在最后一步遍历组件表让每个组件上报边界(见 `PaintLayoutBounds`),
|
||
* 并强制整帧回送(局部回送只会更新绘图区,装饰区的边界会留在旧帧上)。
|
||
* 关闭时**完全不执行**,对正常绘制零影响。
|
||
*/
|
||
bool m_showLayoutBounds = false;
|
||
|
||
/**
|
||
* @brief 调试 box 容器(只在 `m_showLayoutBounds` 为真时使用)。
|
||
*
|
||
* 作为成员而不是局部变量:避免开关打开时每帧重新分配,
|
||
* 也让它能直接充当 `PaintUtil` 的文本收集器(`UiTextRectSink`)。
|
||
*/
|
||
UiBoxTable m_debugBoxes;
|
||
|
||
/** @brief 填充组件表(`Initialize` 里 `Setup` 之后调用一次)。 */
|
||
void BuildComponentTable();
|
||
|
||
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; /**< 后备缓冲位图(top-down 32bpp DIB 段)。 */
|
||
HGDIOBJ m_backOldBitmap = nullptr; /**< 后备缓冲原始位图。 */
|
||
int m_backWidth = 0; /**< 后备缓冲宽度(设备像素)。 */
|
||
int m_backHeight = 0; /**< 后备缓冲高度(设备像素)。 */
|
||
std::uint32_t* m_backBits = nullptr; /**< 后备缓冲像素首地址(仅 CPU 侧合成时使用)。 */
|
||
int m_backStride = 0; /**< 后备缓冲行字节数(**不可**假设 = 宽 × 4)。 */
|
||
std::uint64_t m_backVersion = 0; /**< 后备缓冲代次(重建位图时递增,用于作废旧句柄)。 */
|
||
|
||
Target m_activeTarget = Target::NONE; /**< 当前鼠标作用目标。 */
|
||
bool m_trackingLeave = false; /**< 是否已注册鼠标离开通知。 */
|
||
bool m_hintTimerActive = false; /**< 提示浮层淡出定时器是否已启动。 */
|
||
/** 信号列表「滚动到目标信号」动画的定时器是否已启动(见 `SignalPanel::AdvanceScrollAnimation`)。 */
|
||
bool m_scrollTimerActive = false;
|
||
/**
|
||
* 错误反馈是否使用模态对话框(默认开启;`Initialize` 遇到隐藏窗口时自动关闭)。
|
||
*
|
||
* 隐藏窗口(无人可点确定)下弹模态框等于把进程卡死,因此按启动方式自动降级为
|
||
* 绘图区非模态浮层提示;调用方也可用 `SetModalErrorsEnabled` 显式控制。
|
||
*/
|
||
bool m_modalErrorsEnabled = true;
|
||
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;
|
||
/**
|
||
* @brief 「只有信号面板(与分隔条)需要重绘」。
|
||
*
|
||
* 面板滚动动画(「滚动到目标信号」)每帧只改变面板像素,而 `m_chromeDirty` 会
|
||
* 连**标签栏 / 工具栏 / 整窗底色**一起重绘 —— 实测 129 个网格时这一份装饰重绘
|
||
* 约 **3~4ms/帧**,会把动画挤出 15.6ms 的预算(用户 2026-10-17 报告:滚动过渡动画卡顿)。
|
||
*
|
||
* ✓ 由滚动定时器在**只有面板动画在跑**时置位:`Render` 只重绘面板 + 分隔条
|
||
*(标签栏/工具栏/整窗底色与上一帧逐像素相同,后备缓冲区里的旧像素本来就是对的)。
|
||
* ⚠️ 回送仍是 `if (fullFrame) {整窗} else {绘图区}` 的**无条件分支**,
|
||
* 与 §9.41 的教训一致(**不**按更新区域决定回送范围)。
|
||
*/
|
||
bool m_panelDirty = false;
|
||
bool m_mouseOverPlot = false; /**< 上一次鼠标移动是否位于绘图区内。 */
|
||
|
||
/**
|
||
* @brief 本次 `WM_PAINT` 是不是「面板动画帧」(由滚动定时器置位)。
|
||
*
|
||
* 面板动画每帧都会把**整个窗口**标脏,而 `WM_PAINT` 里"更新区域不在绘图区内 ⇒
|
||
* 置 `m_chromeDirty`"这条启发式会因此把整帧判定为脏 ⇒ 每帧都重绘标签栏/工具栏/
|
||
* **绘图区**。绘图区一帧要 12~46ms(283 个信号同网格、双光标时实测 46ms),
|
||
* 面板动画于是被拖到 43ms/帧(用户 2026-10-30 报告:组名展开/收起非常卡顿,
|
||
* 而网格里根本没有任何需要重绘的变化)。
|
||
* ✓ 定时器置位后,`WM_PAINT` 在"没有其它任何原因要求整帧脏"时**不**再置 `m_chromeDirty`。
|
||
*/
|
||
bool m_panelAnimationFrame = false;
|
||
|
||
/**
|
||
* @brief 后备缓冲区里的**绘图区像素是否仍然有效**(可跳过重绘)。
|
||
*
|
||
* 只有「面板动画帧」且该标志为真时才跳过 `PlotView::Paint`:此时面板不覆盖绘图区,
|
||
* 上一帧画好的绘图区像素原样保留在缓冲区里,回送整窗也不会错。
|
||
* 任何"绘图区需要重绘"的来源都必须把它置假(见 `InvalidatePlotRect` 的各调用点、
|
||
* `WM_PAINT` 里"更新区域就在绘图区内"的分支、以及后备缓冲区重建)。
|
||
*/
|
||
bool m_plotPaintValid = false;
|
||
};
|
||
|
||
} // namespace jdi
|
||
|
||
#endif // __APPLICATION_HPP__
|