From 542aa51e4015983ede4b5d99e9e69ae57e8d039c Mon Sep 17 00:00:00 2001 From: "DeepSeek V4.1 Flash" <> Date: Tue, 29 Sep 2026 20:49:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E7=BB=98=E5=9B=BE?= =?UTF-8?q?=E5=8C=BA=E7=BD=91=E6=A0=BC=E5=B8=83=E5=B1=80=E4=B8=8E=E4=BF=A1?= =?UTF-8?q?=E5=8F=B7=E6=8B=96=E6=8B=BD=EF=BC=8C=E7=B2=BE=E7=AE=80=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E4=BF=A1=E6=81=AF=E5=B9=B6=E6=96=B0=E5=A2=9E=20Y=20?= =?UTF-8?q?=E8=BD=B4=E7=B2=BE=E7=A1=AE=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 网格与标尺: - 消除网格之间无意义的间隔,改用较重的横向分界线明确区分相邻网格 - 纵轴标尺从绘图区左侧外侧移入网格内侧,并改为带半透明衬底的覆盖层绘制, 使其在波形之上依然清晰可读;绘图区左侧空间因此完全还给波形 - 继续保证底部时间轴预留高度,并把时间列名贴左显示、避开刻度标签 常规模式拖拽: - 常规模式下水平拖动平移时间轴,纵向拖动偏移当前网格的幅值显示位置 - 绘图区右键菜单新增「恢复 Y 轴自适应」「设置 Y 轴最大值…」「设置 Y 轴最小值…」 「全部自适应」,上下限通过内联输入框直接键入(与线宽输入一致的交互) 信号拖拽: - 鼠标贴近信号曲线时给出光晕高亮与提示气泡(与时间光标的提示方式一致) - 仅在出现提示时按左键才会进入拖拽;拖拽过程中在目标网格内绘制曲线虚影、 高亮目标网格边框并提示「移动到网格 N」,松开左键确认归属变更 - 命中检测基于折线到光标的精确点到线段距离,散点模式按采样点距离判定 界面精简: - 移除底部状态栏(左下角统计与右下角提示均不再占用空间),错误改由对话框提示 - 标题栏只显示应用名称 - 移除工具栏右上角的视图时间范围文本;按钮除「导入文件」外整体靠右排列 其他: - 新增 PlotView::PaintEditField 统一绘制时间光标与网格 Y 轴上下限的输入框 - 移除已无引用的 UiDrawDashedLine 与绘图区标尺占位宽度 --- Components/Application/Inc/Application.hpp | 21 +- Components/Application/Src/Application.cpp | 184 +++-- Components/PlotView/Inc/PlotView.hpp | 141 +++- Components/PlotView/Src/PlotView.cpp | 883 +++++++++++++++++---- Components/Toolbar/Inc/Toolbar.hpp | 7 - Components/Toolbar/Src/Toolbar.cpp | 53 +- Utilities/Inc/PaintUtil.hpp | 14 - Utilities/Inc/PopupMenu.hpp | 3 +- Utilities/Src/PaintUtil.cpp | 10 - 9 files changed, 981 insertions(+), 335 deletions(-) diff --git a/Components/Application/Inc/Application.hpp b/Components/Application/Inc/Application.hpp index fe523b2..eb4bb7d 100644 --- a/Components/Application/Inc/Application.hpp +++ b/Components/Application/Inc/Application.hpp @@ -124,12 +124,6 @@ namespace jdi */ void Render(); - /** - * @brief 绘制底部状态栏。 - * @param [IN,OUT] g 绘图上下文。 - */ - void PaintStatusBar(gd::Graphics& g); - /** * @brief 获取当前激活标签页配置(可写)。 * @return 激活标签页配置引用。 @@ -199,6 +193,14 @@ namespace jdi */ 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 派发弹出菜单的选择结果。 * @param [IN] result 菜单选择结果。 @@ -231,9 +233,10 @@ namespace jdi bool HandleKeyDown(WPARAM key); /** - * @brief 设置状态栏提示信息。 + * @brief 报告操作结果信息。 * @param [IN] text 提示文本。 * @param [IN] isError 是否为错误提示。 + * @note 界面不再保留常驻状态栏:错误以对话框提示,普通提示被忽略,避免无用信息干扰。 */ void SetStatusMessage(const std::wstring& text, bool isError); @@ -281,7 +284,6 @@ namespace jdi gd::RectF m_panelRect; /**< 信号面板区域。 */ gd::RectF m_splitterRect; /**< 面板分隔条区域。 */ gd::RectF m_plotRect; /**< 绘图区区域。 */ - gd::RectF m_statusRect; /**< 状态栏区域。 */ float m_panelWidth = 366.0f; /**< 面板宽度(逻辑像素)。 */ bool m_draggingSplitter = false; /**< 是否正在拖动分隔条。 */ @@ -296,8 +298,7 @@ namespace jdi Target m_activeTarget = Target::NONE; /**< 当前鼠标作用目标。 */ bool m_trackingLeave = false; /**< 是否已注册鼠标离开通知。 */ int m_tabMenuIndex = -1; /**< 标签栏右键菜单的目标标签页下标。 */ - std::wstring m_statusMessage; /**< 状态栏提示文本。 */ - bool m_statusIsError = false; /**< 状态提示是否为错误。 */ + int m_plotMenuLane = -1; /**< 绘图区右键菜单的目标网格下标。 */ int m_cachedVisibleCount = -1; /**< 上次同步时的可见信号数量。 */ std::size_t m_cachedLaneCount = 0; /**< 上次同步时的网格数量。 */ diff --git a/Components/Application/Src/Application.cpp b/Components/Application/Src/Application.cpp index 9d222f1..3d97f8e 100644 --- a/Components/Application/Src/Application.cpp +++ b/Components/Application/Src/Application.cpp @@ -24,16 +24,13 @@ namespace jdi namespace { const wchar_t* kWindowClassName = L"JustDataInspectorMainWindow"; /**< 窗口类名。 */ - const wchar_t* kWindowTitle = L"Just DataInspector"; /**< 默认窗口标题。 */ - const float kStatusBarHeight = 26.0f; /**< 状态栏高度(逻辑像素)。 */ + const wchar_t* kWindowTitle = L"Just DataInspector"; /**< 窗口标题(仅显示应用名称)。 */ const float kSplitterWidth = 4.0f; /**< 分隔条宽度(逻辑像素)。 */ const float kMinPanelWidth = 260.0f; /**< 面板最小宽度(逻辑像素)。 */ const float kMaxPanelWidth = 400.0f; /**< 面板最大宽度(逻辑像素)。 */ const int kMinClientWidth = 860; /**< 窗口最小客户区宽度。 */ const int kMinClientHeight = 540; /**< 窗口最小客户区高度。 */ const UINT kMessageRemoveFile = WM_APP + 1; /**< 面板请求移除文件的自定义消息。 */ - const wchar_t* kStatusHint = - L"滚轮纵向滚动网格 · Shift+滚轮平移 · 双击绘图区恢复自适应 · Esc 关闭光标"; /** @brief 标签页右键菜单命令 ID。 */ const int kMenuTabNew = 1; @@ -459,15 +456,14 @@ namespace jdi const float tabBarHeight = m_tabBar.DesiredHeight(); const float toolbarHeight = m_toolbar.DesiredHeight(); - const float statusHeight = ScaleOf(kStatusBarHeight, m_scale); const float splitterWidth = ScaleOf(kSplitterWidth, m_scale); m_tabBarRect = gd::RectF(0.0f, 0.0f, width, tabBarHeight); m_toolbarRect = gd::RectF(0.0f, m_tabBarRect.Y + m_tabBarRect.Height, width, toolbarHeight); - m_statusRect = gd::RectF(0.0f, height - statusHeight, width, statusHeight); + // 不再保留底部状态栏,内容区一直延伸到窗口底部。 const float contentTop = m_toolbarRect.Y + m_toolbarRect.Height; - const float contentHeight = std::max(0.0f, m_statusRect.Y - contentTop); + const float contentHeight = std::max(0.0f, height - contentTop); float panelWidth = ScaleOf(m_panelWidth, m_scale); const float maximum = std::min(ScaleOf(kMaxPanelWidth, m_scale), width * 0.45f); @@ -567,15 +563,6 @@ namespace jdi SynchronizeView(false); m_toolbar.SetDocumentLoaded(!m_dataSet.Empty()); - if (!m_dataSet.Empty()) - { - m_toolbar.SetStatusText(L"视图 " + FormatValue(m_plot.ViewMinTime()) + L" ~ " + - FormatValue(m_plot.ViewMaxTime())); - } - else - { - m_toolbar.SetStatusText(std::wstring()); - } // 工具栏可切换按钮的选中状态始终与当前标签页配置保持一致。 m_toolbar.SetToggleState(ToolbarAction::ZOOM_TIME, @@ -600,8 +587,6 @@ namespace jdi UiFillRect(g, m_splitterRect, m_theme.border); - PaintStatusBar(g); - // 弹出菜单与浮动拾色器始终绘制在最上层,保证与主界面风格一致。 m_menu.Paint(g); m_colorPicker.Paint(g); @@ -615,70 +600,20 @@ namespace jdi } } - void Application::PaintStatusBar(gd::Graphics& g) - { - UiFillRect(g, m_statusRect, m_theme.statusBg); - UiDrawLine(g, m_statusRect.X, m_statusRect.Y, m_statusRect.X + m_statusRect.Width, m_statusRect.Y, - m_theme.border, 1.0f); - - const float padding = ScaleOf(12.0f, m_scale); - const float halfWidth = m_statusRect.Width * 0.5f; - - std::wstring leftText; - if (!m_dataSet.Empty()) - { - const TabConfig& tab = ActiveTab(); - leftText = std::to_wstring(m_dataSet.FileCount()) + L" 个文件 · " + - FormatCount(static_cast(m_dataSet.SignalCount())) + L" 条信号 · " + - std::to_wstring(tab.VisibleCount()) + L" 条已选 · " + - std::to_wstring(tab.Lanes().size()) + L" 个网格"; - } - else - { - leftText = L"未加载数据 · 请点击「打开 CSV」或拖拽文件到窗口"; - } - - UiText(g, UiEllipsize(g, leftText, m_fonts.uiSmall, halfWidth - padding * 2.0f), m_fonts.uiSmall, - gd::RectF(m_statusRect.X + padding, m_statusRect.Y, halfWidth - padding, m_statusRect.Height), - m_theme.text, TextAlign::LEFT); - - std::wstring rightText = kStatusHint; - std::uint32_t rightColor = m_theme.textMuted; - if (!m_statusMessage.empty()) - { - rightText = m_statusMessage; - rightColor = m_statusIsError ? m_theme.danger : m_theme.textStrong; - } - else if (m_plot.CursorEnabled() && !m_dataSet.Empty()) - { - rightText = L"光标 · " + FormatValue(m_plot.CursorTime()); - rightColor = m_theme.text; - } - - UiText(g, UiEllipsize(g, rightText, m_fonts.uiSmall, halfWidth - padding * 2.0f), m_fonts.uiSmall, - gd::RectF(m_statusRect.X + halfWidth, m_statusRect.Y, halfWidth - padding, m_statusRect.Height), - rightColor, TextAlign::RIGHT); - } - void Application::SetStatusMessage(const std::wstring& text, bool isError) { - m_statusMessage = text; - m_statusIsError = isError; + // 界面不再占用底部空间显示常驻提示;仅把真实错误以对话框形式反馈给用户。 + if (!isError || text.empty()) + { + return; + } + MessageBoxW(m_hwnd, text.c_str(), kWindowTitle, MB_ICONWARNING | MB_OK); } void Application::UpdateWindowTitle() { - std::wstring title = kWindowTitle; - if (m_activeTab >= 0 && m_activeTab < static_cast(m_tabs.size())) - { - title += L" — "; - title += m_tabs[static_cast(m_activeTab)].Name(); - } - if (!m_dataSet.Empty()) - { - title += L"(" + std::to_wstring(m_dataSet.FileCount()) + L" 个文件)"; - } - SetWindowTextW(m_hwnd, title.c_str()); + // 标题栏只显示应用名称,不附带标签页、文件数量等无关信息。 + SetWindowTextW(m_hwnd, kWindowTitle); } void Application::OpenFileDialog() @@ -1071,6 +1006,58 @@ namespace jdi m_menu.Open(MenuScope::TAB_BAR, bounds, anchor.x, anchor.y, items); } + void Application::ShowPlotContextMenu(int laneIndex, int x, int y) + { + if (laneIndex < 0) + { + return; + } + + m_plotMenuLane = laneIndex; + + std::vector items; + + MenuItem header; + header.kind = MenuItemKind::HEADER; + header.label = L"网格 " + std::to_wstring(laneIndex + 1); + items.push_back(header); + + const bool isAuto = m_plot.LaneRangeIsAuto(laneIndex); + + MenuItem autoItem; + autoItem.label = isAuto ? L"Y 轴自适应(当前)" : L"恢复 Y 轴自适应"; + autoItem.id = PLOT_CMD_LANE_AUTO; + autoItem.checked = isAuto; + autoItem.enabled = !isAuto; + items.push_back(autoItem); + + MenuItem maxItem; + maxItem.label = L"设置 Y 轴最大值…"; + maxItem.id = PLOT_CMD_LANE_Y_MAX; + items.push_back(maxItem); + + MenuItem minItem; + minItem.label = L"设置 Y 轴最小值…"; + minItem.id = PLOT_CMD_LANE_Y_MIN; + items.push_back(minItem); + + MenuItem separator; + separator.kind = MenuItemKind::SEPARATOR; + items.push_back(separator); + + MenuItem fitItem; + fitItem.label = L"全部自适应"; + fitItem.id = PLOT_CMD_FIT_ALL; + items.push_back(fitItem); + + RECT client = {}; + GetClientRect(m_hwnd, &client); + const gd::RectF bounds(0.0f, 0.0f, static_cast(client.right - client.left), + static_cast(client.bottom - client.top)); + + m_menu.Open(MenuScope::PLOT_VIEW, bounds, x, y, items); + } + void Application::DispatchMenuResult(const MenuResult& result) { if (!result.valid) @@ -1090,6 +1077,31 @@ namespace jdi InvalidateRect(m_hwnd, nullptr, FALSE); break; } + case MenuScope::PLOT_VIEW: + { + const int laneIndex = m_plotMenuLane; + m_plotMenuLane = -1; + switch (result.id) + { + case PLOT_CMD_LANE_AUTO: + m_plot.ResetLaneRange(laneIndex); + break; + case PLOT_CMD_LANE_Y_MAX: + m_plot.BeginLaneValueEdit(laneIndex, true); + break; + case PLOT_CMD_LANE_Y_MIN: + m_plot.BeginLaneValueEdit(laneIndex, false); + break; + case PLOT_CMD_FIT_ALL: + m_plot.FitAll(); + break; + default: + break; + } + SetFocus(m_hwnd); + InvalidateRect(m_hwnd, nullptr, TRUE); + break; + } case MenuScope::TAB_BAR: { const int index = m_tabMenuIndex; @@ -1367,8 +1379,8 @@ namespace jdi const int x = GET_X_LPARAM(lParam); const int y = GET_Y_LPARAM(lParam); - // 时间光标处于精确输入态时,点击绘图区之外视为提交(与线宽输入一致)。 - if (m_plot.IsEditingCursor() && !m_plot.HitTest(x, y)) + // 内联数值输入处于活动态时,点击绘图区之外视为提交(与线宽输入一致)。 + if (m_plot.IsEditing() && !m_plot.HitTest(x, y)) { m_plot.OnEditKey(VK_RETURN); } @@ -1533,6 +1545,18 @@ namespace jdi m_activeTarget = Target::NONE; return 0; } + + // 绘图区右键:弹出该网格的 Y 轴设置菜单(含上下限输入)。 + if (m_plot.HitTest(x, y)) + { + const int laneIndex = m_plot.LaneIndexAt(x, y); + if (laneIndex >= 0) + { + ShowPlotContextMenu(laneIndex, x, y); + InvalidateRect(hwnd, nullptr, FALSE); + } + return 0; + } return 0; } case WM_RBUTTONUP: @@ -1600,7 +1624,7 @@ namespace jdi } return 0; } - if (m_plot.IsEditingCursor()) + if (m_plot.IsEditing()) { if (m_plot.OnEditKey(wParam)) { @@ -1625,7 +1649,7 @@ namespace jdi } return 0; } - if (m_plot.IsEditingCursor()) + if (m_plot.IsEditing()) { if (m_plot.OnEditChar(static_cast(wParam))) { diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index bcc5e80..f2ba23d 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -21,15 +21,38 @@ namespace jdi enum class PlotDragMode : int { NONE = 0, /**< 未拖拽。 */ - PAN_TIME, /**< 水平平移时间轴(常规模式下的左键拖拽)。 */ + PAN, /**< 常规模式:水平平移时间轴 + 纵向偏移当前网格幅值。 */ RUBBER_TIME, /**< 时间轴缩放选区(竖向贯穿全高)。 */ RUBBER_VALUE, /**< Y 轴缩放选区(限定在某个网格内)。 */ SCROLL_LANES, /**< 拖动网格滚动条。 */ - CURSOR_MOVE /**< 拖拽时间光标。 */ + CURSOR_MOVE, /**< 拖拽时间光标。 */ + MOVE_SIGNAL /**< 拖拽信号曲线到其它网格。 */ }; /** - * @brief 波形绘图区:网格、时间轴、光标、缩放与自适应。 + * @brief 绘图区内的内联数值输入目标。 + */ + enum class PlotEditTarget : int + { + NONE = 0, /**< 未处于输入状态。 */ + CURSOR_TIME, /**< 编辑时间光标位置。 */ + LANE_Y_MAX, /**< 编辑某网格的 Y 轴最大值。 */ + LANE_Y_MIN /**< 编辑某网格的 Y 轴最小值。 */ + }; + + /** + * @brief 绘图区右键菜单命令标识(与信号面板的命令空间相互独立)。 + */ + enum PlotMenuCommand : int + { + PLOT_CMD_LANE_AUTO = 1, /**< 该网格恢复 Y 轴自适应。 */ + PLOT_CMD_LANE_Y_MAX = 2, /**< 输入该网格 Y 轴最大值。 */ + PLOT_CMD_LANE_Y_MIN = 3, /**< 输入该网格 Y 轴最小值。 */ + PLOT_CMD_FIT_ALL = 4 /**< 全部网格与时间轴自适应。 */ + }; + + /** + * @brief 波形绘图区:网格、时间轴、光标、缩放、自适应与信号拖拽。 * * 绘图区同时依赖数据层(DataSet,只读)与当前标签页配置(TabConfig,可写), * 视图范围、网格滚动与时间光标均保存在标签页配置中,因此各标签页互不干扰。 @@ -201,10 +224,40 @@ namespace jdi PlotZoomMode CurrentZoomMode() const; /** - * @brief 判断是否已进入精确时间输入状态。 + * @brief 查询坐标命中的网格下标(用于右键菜单)。 + * @param [IN] x 客户区横坐标。 + * @param [IN] y 客户区纵坐标。 + * @return 网格下标,未命中返回 -1。 + */ + int LaneIndexAt(int x, int y) const; + + /** + * @brief 判断某网格的 Y 轴是否处于自适应状态。 + * @param [IN] laneIndex 网格下标。 + * @return 自适应返回 true。 + */ + bool LaneRangeIsAuto(int laneIndex) const; + + /** + * @brief 让某网格的 Y 轴恢复自适应。 + * @param [IN] laneIndex 网格下标。 + * @return 状态发生变化返回 true。 + */ + bool ResetLaneRange(int laneIndex); + + /** + * @brief 进入某网格 Y 轴上限/下限的内联输入状态。 + * @param [IN] laneIndex 网格下标。 + * @param [IN] editMax 为 true 编辑上限,为 false 编辑下限。 + * @return 进入成功返回 true。 + */ + bool BeginLaneValueEdit(int laneIndex, bool editMax); + + /** + * @brief 判断是否处于任意内联数值输入状态。 * @return 处于编辑态返回 true。 */ - bool IsEditingCursor() const { return m_cursorEditing; } + bool IsEditing() const { return m_editTarget != PlotEditTarget::NONE; } /** * @brief 处理编辑态下的字符输入。 @@ -373,6 +426,21 @@ namespace jdi */ bool ApplyValueSelection(int laneIndex, float y0, float y1); + /** + * @brief 以指定倍率缩放某网格的幅值轴(保持锚点幅值不动)。 + * @param [IN] laneIndex 目标网格下标。 + * @param [IN] factor 缩放倍率,大于 1 表示放大。 + * @param [IN] anchorValue 锚点幅值。 + */ + void ScaleLaneRange(int laneIndex, double factor, double anchorValue); + + /** + * @brief 平移某网格的幅值轴(偏移波形显示位置)。 + * @param [IN] laneIndex 目标网格下标。 + * @param [IN] deltaValue 幅值偏移量。 + */ + void ShiftLaneRange(int laneIndex, double deltaValue); + /** * @brief 计算时间光标当前的横坐标。 * @return 横坐标。 @@ -393,17 +461,34 @@ namespace jdi bool IsOnCursorLine(int x) const; /** - * @brief 进入时间光标的精确时间输入状态。 - * @return 进入成功返回 true。 + * @brief 精确检测鼠标位置是否命中某条可见信号曲线。 + * @param [IN] x 客户区横坐标。 + * @param [IN] y 客户区纵坐标。 + * @return 命中的信号引用,未命中返回无效引用。 */ - bool BeginCursorEdit(); + SignalRef HitTestSignal(int x, int y) const; /** - * @brief 结束时间光标的精确时间输入。 + * @brief 计算当前内联输入框的矩形(依编辑目标而定)。 + * @return 输入框矩形,未处于编辑态时返回空矩形。 + */ + gd::RectF EditFieldRect() const; + + /** + * @brief 以给定目标与初始文本进入内联输入状态。 + * @param [IN] target 编辑目标。 + * @param [IN] laneIndex 目标网格下标(光标编辑时忽略)。 + * @param [IN] initialText 初始文本。 + * @return 进入成功返回 true。 + */ + bool BeginEdit(PlotEditTarget target, int laneIndex, const std::wstring& initialText); + + /** + * @brief 结束内联输入。 * @param [IN] commit 是否提交当前输入。 * @return 状态发生变化返回 true。 */ - bool EndCursorEdit(bool commit); + bool EndEdit(bool commit); /** * @brief 构建单条信号在当前视图下的折线点序列。 @@ -421,10 +506,9 @@ namespace jdi * @param [IN] laneIndex 网格下标。 * @param [IN] lane 网格绘制信息。 * @param [IN] timeTicks 时间刻度序列。 - * @param [IN] timeStep 时间刻度步长。 */ void PaintLane(gd::Graphics& g, int laneIndex, const LaneVisual& lane, - const std::vector& timeTicks, double timeStep); + const std::vector& timeTicks); /** * @brief 绘制底部时间轴。 @@ -447,6 +531,18 @@ namespace jdi */ void PaintRubberBand(gd::Graphics& g); + /** + * @brief 绘制内联数值输入框(时间光标或网格 Y 轴上下限)。 + * @param [IN,OUT] g 绘图上下文。 + */ + void PaintEditField(gd::Graphics& g); + + /** + * @brief 绘制信号悬停提示与拖拽虚影。 + * @param [IN,OUT] g 绘图上下文。 + */ + void PaintSignalInteraction(gd::Graphics& g); + /** * @brief 绘制网格纵向滚动条。 * @param [IN,OUT] g 绘图上下文。 @@ -503,10 +599,9 @@ namespace jdi gd::RectF m_timeAxisRect; /**< 时间轴区域。 */ gd::RectF m_scrollBarRect; /**< 网格滚动条区域。 */ - float m_yAxisWidth = 74.0f; /**< 纵轴标签列宽度(逻辑像素)。 */ + float m_labelBandWidth = 66.0f; /**< 网格内侧纵轴标尺带宽度(逻辑像素)。 */ float m_timeAxisHeight = 32.0f; /**< 时间轴高度(逻辑像素)。 */ - float m_laneGap = 6.0f; /**< 网格间距(逻辑像素)。 */ - float m_minLaneHeight = 46.0f; /**< 网格最小高度(逻辑像素)。 */ + float m_minLaneHeight = 54.0f; /**< 网格最小高度(逻辑像素)。 */ float m_laneContentHeight = 0.0f; /**< 网格内容总高度。 */ bool m_scrollBarVisible = false; /**< 是否需要显示滚动条。 */ @@ -517,15 +612,21 @@ namespace jdi int m_dragStartY = 0; /**< 拖拽起点纵坐标。 */ int m_dragLastX = 0; /**< 上一次拖拽横坐标。 */ int m_dragLastY = 0; /**< 上一次拖拽纵坐标。 */ - gd::RectF m_rubberRect; /**< 拖拽选区矩形。 */ + gd::RectF m_rubberRect; /**< 拖拽选区矩形。 */ + + SignalRef m_hoverSignal; /**< 鼠标悬停命中的信号(曲线高亮)。 */ + SignalRef m_dragSignal; /**< 正在被拖拽的信号。 */ + int m_dragTargetLane = -1; /**< 拖拽信号时的目标网格下标。 */ bool m_cursorHover = false; /**< 鼠标是否悬停在时间光标上。 */ - bool m_cursorEditing = false; /**< 是否处于精确时间输入状态。 */ - bool m_cursorEditFresh = false; /**< 编辑框是否处于“首次输入整体替换”状态。 */ - std::wstring m_cursorEditText; /**< 编辑框当前文本。 */ - gd::RectF m_cursorEditRect; /**< 编辑框矩形(用于命中测试与绘制)。 */ + PlotEditTarget m_editTarget = PlotEditTarget::NONE; /**< 当前内联输入目标。 */ + bool m_editFresh = false; /**< 输入框是否处于“首次输入整体替换”状态。 */ + std::wstring m_editText; /**< 输入框当前文本。 */ + int m_editLane = -1; /**< 内联输入所作用的网格下标。 */ int m_hoverLane = -1; /**< 当前悬停网格下标。 */ + int m_mouseX = 0; /**< 最近一次鼠标横坐标(用于悬停提示定位)。 */ + int m_mouseY = 0; /**< 最近一次鼠标纵坐标(用于悬停提示定位)。 */ std::vector m_lanes; /**< 当前网格绘制信息集合。 */ }; diff --git a/Components/PlotView/Src/PlotView.cpp b/Components/PlotView/Src/PlotView.cpp index 35fa8b0..503a316 100644 --- a/Components/PlotView/Src/PlotView.cpp +++ b/Components/PlotView/Src/PlotView.cpp @@ -15,13 +15,16 @@ namespace jdi const int kYTickTarget = 4; /**< 幅值轴期望刻度数。 */ const float kScrollBarThickness = 9.0f; /**< 滚动条宽度(逻辑像素)。 */ const float kLanePadding = 8.0f; /**< 网格内边距。 */ - const float kAxisLabelGap = 10.0f; /**< 轴上文字与轴线的间距。 */ const size_t kMaxPolylinePoints = 20000; /**< 单条折线最大顶点数。 */ const size_t kMaxScatterPoints = 2500; /**< 单条散点最大绘制点数。 */ const float kMinScatterRadius = 1.6f; /**< 散点最小半径(逻辑像素)。 */ const float kMaxScatterRadius = 12.0f; /**< 散点最大半径(逻辑像素)。 */ const float kScatterRadiusScale = 1.5f; /**< 散点半径相对线宽的增长系数。 */ const double kMinTimeSpan = 1.0e-12; /**< 允许的最小时间跨度。 */ + const float kPlotSidePadding = 8.0f; /**< 绘图区左右留白(逻辑像素)。 */ + const float kLaneDividerWidth = 2.0f; /**< 网格之间分界线的宽度。 */ + const float kSignalHitTolerance = 6.0f; /**< 信号曲线命中容差(逻辑像素)。 */ + const float kLaneLabelPad = 6.0f; /**< 网格内侧标尺带的内边距。 */ /** * @brief 将数值限制到合法区间。 @@ -164,21 +167,19 @@ namespace jdi { m_area = area; - m_yAxisWidth = 74.0f * m_scale; - m_timeAxisHeight = 36.0f * m_scale; - m_laneGap = 9.0f * m_scale; + m_labelBandWidth = 66.0f * m_scale; + m_timeAxisHeight = 34.0f * m_scale; m_minLaneHeight = 54.0f * m_scale; // 先为时间轴保留固定高度,剩余空间才交给网格,保证时间轴不会被挤压。 const float topPadding = 6.0f * m_scale; const float availHeight = std::max(0.0f, area.Height - m_timeAxisHeight - topPadding); - // 预估是否需要纵向滚动条:仅取决于网格数量与最小网格高度。 + // 预估是否需要纵向滚动条:仅取决于网格数量与最小网格高度(网格之间不再留间隔)。 std::size_t laneCount = (m_tab != nullptr) ? m_tab->Lanes().size() : 0; if (laneCount > 0) { - const float needed = static_cast(laneCount) * m_minLaneHeight + - m_laneGap * static_cast(laneCount - 1); + const float needed = static_cast(laneCount) * m_minLaneHeight; m_scrollBarVisible = (needed > availHeight); } else @@ -186,11 +187,13 @@ namespace jdi m_scrollBarVisible = false; } - const float rightReserve = - m_scrollBarVisible ? (kScrollBarThickness * m_scale + 6.0f * m_scale) : (6.0f * m_scale); - const float plotWidth = std::max(0.0f, area.Width - m_yAxisWidth - rightReserve); + const float sidePadding = kPlotSidePadding * m_scale; + const float rightReserve = m_scrollBarVisible + ? (kScrollBarThickness * m_scale + sidePadding) + : sidePadding; + const float plotWidth = std::max(0.0f, area.Width - sidePadding - rightReserve); - m_plotRect = gd::RectF(area.X + m_yAxisWidth, area.Y + topPadding, plotWidth, availHeight); + m_plotRect = gd::RectF(area.X + sidePadding, area.Y + topPadding, plotWidth, availHeight); m_timeAxisRect = gd::RectF(m_plotRect.X, m_plotRect.Y + m_plotRect.Height, m_plotRect.Width, m_timeAxisHeight); m_scrollBarRect = gd::RectF(area.X + area.Width - kScrollBarThickness * m_scale - 3.0f * m_scale, @@ -255,14 +258,13 @@ namespace jdi return; } - const float gap = m_laneGap * m_scale; - float laneHeight = - (m_plotRect.Height - gap * static_cast(count - 1)) / static_cast(count); + const float gap = 0.0f; + float laneHeight = m_plotRect.Height / static_cast(count); if (laneHeight < m_minLaneHeight * m_scale) { laneHeight = m_minLaneHeight * m_scale; } - m_laneContentHeight = laneHeight * static_cast(count) + gap * static_cast(count - 1); + m_laneContentHeight = laneHeight * static_cast(count); ClampLaneScroll(); const float scrollOffset = Tab().LaneScroll(); @@ -444,10 +446,13 @@ namespace jdi Tab().ViewMax() = high; Tab().ViewInitialized() = true; Tab().LaneScroll() = 0.0f; - // 自适应即回到初始状态:退出拖拽缩放模式并放下进行中的选区。 + // 自适应即回到初始状态:退出拖拽缩放模式并放下进行中的选区与拖拽。 Tab().ZoomMode() = PlotZoomMode::NONE; m_rubberRect = gd::RectF(); m_dragMode = PlotDragMode::NONE; + m_dragSignal = SignalRef(); + m_dragTargetLane = -1; + m_hoverSignal = SignalRef(); if (m_tab != nullptr) { @@ -463,9 +468,10 @@ namespace jdi { Tab().CursorEnabled() = false; m_cursorHover = false; - m_cursorEditing = false; - m_cursorEditFresh = false; - m_cursorEditText.clear(); + if (m_editTarget == PlotEditTarget::CURSOR_TIME) + { + EndEdit(false); + } if (m_dragMode == PlotDragMode::CURSOR_MOVE) { m_dragMode = PlotDragMode::NONE; @@ -511,20 +517,9 @@ namespace jdi return gd::RectF(); } - // 编辑态下取编辑文本与显示文本中较宽者,保证输入过程中框体不跳动。 - std::wstring text = m_cursorEditing ? m_cursorEditText : FormatValue(Tab().CursorTime()); - if (m_cursorEditing) - { - const float displayWidth = UiMeasureTextWidth(m_fonts->monoSmall, FormatValue(Tab().CursorTime())); - const float editWidth = UiMeasureTextWidth(m_fonts->monoSmall, text); - if (editWidth < displayWidth) - { - text = FormatValue(Tab().CursorTime()); - } - } - - const float width = std::max(64.0f * m_scale, UiMeasureTextWidth(m_fonts->monoSmall, text) + - 16.0f * m_scale); + const std::wstring text = FormatValue(Tab().CursorTime()); + const float width = + std::max(64.0f * m_scale, UiMeasureTextWidth(m_fonts->monoSmall, text) + 16.0f * m_scale); const float height = 20.0f * m_scale; // 读数标签固定在绘图区顶部,避免遮挡底部时间轴刻度尺。 @@ -615,66 +610,357 @@ namespace jdi return true; } - bool PlotView::BeginCursorEdit() + int PlotView::LaneIndexAt(int x, int y) const { - if (!Tab().CursorEnabled() || m_cursorEditing) + if (!UiPointInRect(m_plotRect, static_cast(x), static_cast(y))) + { + return -1; + } + return LaneAtY(y); + } + + bool PlotView::LaneRangeIsAuto(int laneIndex) const + { + if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) + { + return true; + } + return m_tab->Lanes()[static_cast(laneIndex)].autoRange; + } + + bool PlotView::ResetLaneRange(int laneIndex) + { + if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) { return false; } - m_cursorEditText = FormatValue(Tab().CursorTime()); - m_cursorEditing = true; - m_cursorEditFresh = true; + + Lane& lane = m_tab->Lanes()[static_cast(laneIndex)]; + if (lane.autoRange) + { + return false; + } + lane.autoRange = true; + BuildLaneVisuals(); return true; } - bool PlotView::EndCursorEdit(bool commit) + void PlotView::ScaleLaneRange(int laneIndex, double factor, double anchorValue) { - if (!m_cursorEditing) + if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_lanes.size())) + { + return; + } + if (!(factor > 0.0) || !std::isfinite(anchorValue)) + { + return; + } + + const LaneVisual& visual = m_lanes[static_cast(laneIndex)]; + const double span = visual.yMax - visual.yMin; + if (!(span > 0.0)) + { + return; + } + + const double magnitude = std::max(std::fabs(visual.yMax), std::fabs(visual.yMin)); + const double minSpan = std::max(magnitude * 1.0e-9, 1.0e-12); + const double newSpan = ClampDouble(span / factor, minSpan, magnitude * 100.0 + 1.0); + const double ratio = (visual.yMax - anchorValue) / span; + + Lane& lane = m_tab->Lanes()[static_cast(laneIndex)]; + lane.autoRange = false; + lane.yMin = anchorValue - ratio * newSpan; + lane.yMax = lane.yMin + newSpan; + BuildLaneVisuals(); + } + + void PlotView::ShiftLaneRange(int laneIndex, double deltaValue) + { + if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_lanes.size()) || + !std::isfinite(deltaValue) || deltaValue == 0.0) + { + return; + } + + // 首次纵向拖动时先把当前自适应范围固化下来,再进行偏移。 + const LaneVisual& visual = m_lanes[static_cast(laneIndex)]; + Lane& lane = m_tab->Lanes()[static_cast(laneIndex)]; + if (lane.autoRange) + { + lane.autoRange = false; + lane.yMin = visual.yMin; + lane.yMax = visual.yMax; + } + lane.yMin += deltaValue; + lane.yMax += deltaValue; + BuildLaneVisuals(); + } + + bool PlotView::BeginLaneValueEdit(int laneIndex, bool editMax) + { + if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) + { + return false; + } + if (m_lanes.empty()) + { + BuildLaneVisuals(); + } + if (laneIndex >= static_cast(m_lanes.size())) { return false; } - if (commit) + const LaneVisual& visual = m_lanes[static_cast(laneIndex)]; + const double current = editMax ? visual.yMax : visual.yMin; + return BeginEdit(editMax ? PlotEditTarget::LANE_Y_MAX : PlotEditTarget::LANE_Y_MIN, laneIndex, + FormatValue(current)); + } + + SignalRef PlotView::HitTestSignal(int x, int y) const + { + SignalRef empty; + if (m_tab == nullptr || m_dataSet == nullptr || !HasDrawableContent()) { - const std::wstring trimmed = Trim(m_cursorEditText); - if (!trimmed.empty()) + return empty; + } + if (!UiPointInRect(m_plotRect, static_cast(x), static_cast(y))) + { + return empty; + } + + const int laneIndex = LaneAtY(y); + if (laneIndex < 0 || laneIndex >= static_cast(m_lanes.size())) + { + return empty; + } + + const LaneVisual& lane = m_lanes[static_cast(laneIndex)]; + const float tolerance = kSignalHitTolerance * m_scale; + const float fx = static_cast(x); + const float fy = static_cast(y); + + std::vector points; + std::vector breaks; + + SignalRef best; + float bestDistance = tolerance; + + // 只为当前网格内的可见信号做精确的点到线段距离检测,避免跨网格误命中。 + for (const SignalRef& ref : m_tab->Lanes()[static_cast(laneIndex)].signals) + { + const ResolvedSignal resolved = ResolveSignal(ref); + if (resolved.data == nullptr || resolved.style == nullptr || !resolved.style->visible) { - wchar_t* stop = nullptr; - const double parsed = std::wcstod(trimmed.c_str(), &stop); - if (stop != trimmed.c_str() && parsed == parsed) + continue; + } + + BuildPolyline(resolved, lane, points, breaks); + if (points.size() < 2) + { + continue; + } + + // 散点模式下把采样点本身也视为可命中目标。 + if (resolved.style->mode == DrawMode::SCATTER) + { + for (const gd::PointF& point : points) { - double low = 0.0; - double high = 1.0; - TimeBounds(low, high); - const double span = std::max(high - low, kMinTimeSpan); - Tab().CursorTime() = ClampDouble(parsed, low - span * 10.0, high + span * 10.0); + const float dx = point.X - fx; + const float dy = point.Y - fy; + const float distance = std::sqrt(dx * dx + dy * dy); + if (distance <= bestDistance) + { + bestDistance = distance; + best = ref; + } + } + continue; + } + + for (std::size_t k = 0; k < breaks.size(); ++k) + { + const std::size_t start = breaks[k]; + const std::size_t stop = (k + 1 < breaks.size()) ? breaks[k + 1] : points.size(); + for (std::size_t index = start + 1; index < stop; ++index) + { + const gd::PointF& a = points[index - 1]; + const gd::PointF& b = points[index]; + + const float vx = b.X - a.X; + const float vy = b.Y - a.Y; + const float wx = fx - a.X; + const float wy = fy - a.Y; + + const float lengthSquared = vx * vx + vy * vy; + float t = 0.0f; + if (lengthSquared > 1.0e-6f) + { + t = (wx * vx + wy * vy) / lengthSquared; + t = std::max(0.0f, std::min(1.0f, t)); + } + + const float dx = wx - t * vx; + const float dy = wy - t * vy; + const float distance = std::sqrt(dx * dx + dy * dy); + if (distance < bestDistance) + { + bestDistance = distance; + best = ref; + } } } } - m_cursorEditing = false; - m_cursorEditFresh = false; - m_cursorEditText.clear(); + return best; + } + + gd::RectF PlotView::EditFieldRect() const + { + if (m_editTarget == PlotEditTarget::NONE || m_fonts == nullptr || m_fonts->monoSmall == nullptr) + { + return gd::RectF(); + } + + const float height = 22.0f * m_scale; + float width = std::max(90.0f * m_scale, + UiMeasureTextWidth(m_fonts->monoSmall, m_editText) + 24.0f * m_scale); + + if (m_editTarget == PlotEditTarget::CURSOR_TIME) + { + // 与显示态标签取相同宽度,避免进入编辑时框体跳动。 + const gd::RectF displayRect = CursorLabelRect(); + width = std::max(width, displayRect.Width); + float left = CursorX() - width * 0.5f; + left = std::max(m_plotRect.X, std::min(left, m_plotRect.X + m_plotRect.Width - width)); + return gd::RectF(left, m_plotRect.Y + 2.0f * m_scale, width, height); + } + + // 网格 Y 轴上下限输入框贴在该网格内侧标尺带下方。 + if (m_editLane < 0 || m_editLane >= static_cast(m_lanes.size())) + { + return gd::RectF(); + } + const LaneVisual& lane = m_lanes[static_cast(m_editLane)]; + const float left = lane.rect.X + kLaneLabelPad * m_scale; + const float top = lane.rect.Y + kLaneLabelPad * m_scale; + return gd::RectF(left, top, std::min(width, lane.rect.Width - 2.0f * kLaneLabelPad * m_scale), height); + } + + bool PlotView::BeginEdit(PlotEditTarget target, int laneIndex, const std::wstring& initialText) + { + if (target == PlotEditTarget::NONE) + { + return false; + } + m_editTarget = target; + m_editLane = laneIndex; + m_editText = initialText; + m_editFresh = true; + return true; + } + + bool PlotView::EndEdit(bool commit) + { + if (m_editTarget == PlotEditTarget::NONE) + { + return false; + } + + const PlotEditTarget target = m_editTarget; + const int laneIndex = m_editLane; + const std::wstring text = Trim(m_editText); + + double parsed = 0.0; + bool parsedOk = false; + if (commit && !text.empty()) + { + wchar_t* stop = nullptr; + parsed = std::wcstod(text.c_str(), &stop); + parsedOk = (stop != text.c_str()) && (parsed == parsed) && std::isfinite(parsed); + } + + m_editTarget = PlotEditTarget::NONE; + m_editLane = -1; + m_editFresh = false; + m_editText.clear(); + + if (!parsedOk) + { + return true; + } + + if (target == PlotEditTarget::CURSOR_TIME) + { + double low = 0.0; + double high = 1.0; + TimeBounds(low, high); + const double span = std::max(high - low, kMinTimeSpan); + Tab().CursorTime() = ClampDouble(parsed, low - span * 10.0, high + span * 10.0); + return true; + } + + if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) + { + return true; + } + + // 直接输入上下限:先固化当前范围,再用新值替换对应端点(保证 max > min)。 + if (laneIndex >= static_cast(m_lanes.size())) + { + return true; + } + const LaneVisual& visual = m_lanes[static_cast(laneIndex)]; + Lane& lane = m_tab->Lanes()[static_cast(laneIndex)]; + if (lane.autoRange) + { + lane.autoRange = false; + lane.yMin = visual.yMin; + lane.yMax = visual.yMax; + } + + const double magnitude = std::max(std::fabs(lane.yMin), std::fabs(lane.yMax)); + const double minimumGap = std::max(magnitude * 1.0e-6, 1.0e-12); + + if (target == PlotEditTarget::LANE_Y_MAX) + { + if (parsed <= lane.yMin) + { + parsed = lane.yMin + minimumGap; + } + lane.yMax = parsed; + } + else + { + if (parsed >= lane.yMax) + { + parsed = lane.yMax - minimumGap; + } + lane.yMin = parsed; + } + + BuildLaneVisuals(); return true; } bool PlotView::OnEditChar(wchar_t character) { - if (!m_cursorEditing) + if (m_editTarget == PlotEditTarget::NONE) { return false; } if (character == L'\r' || character == L'\n') { - return EndCursorEdit(true); + return EndEdit(true); } if (character == L'\b') { - m_cursorEditFresh = false; - if (!m_cursorEditText.empty()) + m_editFresh = false; + if (!m_editText.empty()) { - m_cursorEditText.pop_back(); + m_editText.pop_back(); } return true; } @@ -684,39 +970,39 @@ namespace jdi character == L'E'); if (!digit && !special) { - // 时间输入只接受数字与科学计数法符号,其余字符吞掉。 + // 数值输入只接受数字与科学计数法符号,其余字符吞掉。 return true; } - // 首次键入直接替换原有数值,无需先手动删除(与线宽输入一致)。 - if (m_cursorEditFresh) + // 首次键入直接替换原有数值,无需先手动删除。 + if (m_editFresh) { - m_cursorEditText.clear(); - m_cursorEditFresh = false; + m_editText.clear(); + m_editFresh = false; } - if (m_cursorEditText.size() < 16) + if (m_editText.size() < 16) { if (special && character == L'.' && - (m_cursorEditText.find(L'.') != std::wstring::npos || - m_cursorEditText.find(L'e') != std::wstring::npos || - m_cursorEditText.find(L'E') != std::wstring::npos)) + (m_editText.find(L'.') != std::wstring::npos || + m_editText.find(L'e') != std::wstring::npos || + m_editText.find(L'E') != std::wstring::npos)) { return true; } - if ((character == L'-' || character == L'+') && !m_cursorEditText.empty() && - m_cursorEditText.back() != L'e' && m_cursorEditText.back() != L'E') + if ((character == L'-' || character == L'+') && !m_editText.empty() && + m_editText.back() != L'e' && m_editText.back() != L'E') { return true; } - m_cursorEditText.push_back(character); + m_editText.push_back(character); } return true; } bool PlotView::OnEditKey(WPARAM key) { - if (!m_cursorEditing) + if (m_editTarget == PlotEditTarget::NONE) { return false; } @@ -724,14 +1010,14 @@ namespace jdi switch (key) { case VK_RETURN: - return EndCursorEdit(true); + return EndEdit(true); case VK_ESCAPE: - return EndCursorEdit(false); + return EndEdit(false); case VK_BACK: - m_cursorEditFresh = false; - if (!m_cursorEditText.empty()) + m_editFresh = false; + if (!m_editText.empty()) { - m_cursorEditText.pop_back(); + m_editText.pop_back(); } return true; default: @@ -749,7 +1035,7 @@ namespace jdi { switch (m_dragMode) { - case PlotDragMode::PAN_TIME: + case PlotDragMode::PAN: return LoadCursorW(nullptr, IDC_SIZEALL); case PlotDragMode::RUBBER_TIME: return LoadCursorW(nullptr, IDC_SIZEWE); @@ -759,6 +1045,8 @@ namespace jdi return LoadCursorW(nullptr, IDC_SIZENS); case PlotDragMode::CURSOR_MOVE: return LoadCursorW(nullptr, IDC_SIZEWE); + case PlotDragMode::MOVE_SIGNAL: + return LoadCursorW(nullptr, IDC_SIZEALL); default: break; } @@ -776,6 +1064,12 @@ namespace jdi if (UiPointInRect(m_plotRect, static_cast(x), static_cast(y))) { + // 命中信号曲线时给出可拖拽提示光标(仅在常规模式下)。 + if (Tab().ZoomMode() == PlotZoomMode::NONE && HitTestSignal(x, y).IsValid()) + { + return LoadCursorW(nullptr, IDC_SIZEALL); + } + // 仅在缩放模式下才切换到对应的缩放光标,常规模式保持箭头(不出现十字指针)。 const PlotZoomMode mode = Tab().ZoomMode(); if (mode == PlotZoomMode::TIME) @@ -792,6 +1086,9 @@ namespace jdi bool PlotView::OnMouseMove(int x, int y) { + m_mouseX = x; + m_mouseY = y; + if (!HasDrawableContent()) { return false; @@ -801,18 +1098,37 @@ namespace jdi switch (m_dragMode) { - case PlotDragMode::PAN_TIME: + case PlotDragMode::PAN: { if (m_plotRect.Width <= 0.0f) { return false; } + + // 横向拖动平移时间轴;纵向拖动偏移当前网格的幅值显示位置。 const double span = Tab().ViewMax() - Tab().ViewMin(); const double delta = -static_cast(x - m_dragLastX) / static_cast(m_plotRect.Width) * span; - Tab().ViewMin() += delta; - Tab().ViewMax() += delta; - ClampTimeView(); + if (delta != 0.0) + { + Tab().ViewMin() += delta; + Tab().ViewMax() += delta; + ClampTimeView(); + } + + const int laneIndex = m_dragLane; + if (laneIndex >= 0 && laneIndex < static_cast(m_lanes.size()) && y != m_dragLastY) + { + const LaneVisual& visual = m_lanes[static_cast(laneIndex)]; + if (visual.rect.Height > 0.0f) + { + const double valueSpan = visual.yMax - visual.yMin; + const double deltaValue = static_cast(y - m_dragLastY) / + static_cast(visual.rect.Height) * valueSpan; + ShiftLaneRange(laneIndex, deltaValue); + } + } + m_dragLastX = x; m_dragLastY = y; if (std::abs(x - m_dragStartX) > 3 || std::abs(y - m_dragStartY) > 3) @@ -865,6 +1181,16 @@ namespace jdi m_cursorHover = true; return true; } + case PlotDragMode::MOVE_SIGNAL: + { + const int target = LaneAtY(y); + m_dragMoved = true; + if (target != m_dragTargetLane) + { + m_dragTargetLane = target; + } + return true; + } case PlotDragMode::NONE: default: break; @@ -886,6 +1212,18 @@ namespace jdi m_cursorHover = hover; changed = true; } + + // 常规模式下精确检测鼠标是否贴近某条信号曲线,用于给出可拖拽提示。 + SignalRef hoverSignal; + if (Tab().ZoomMode() == PlotZoomMode::NONE && !m_cursorHover) + { + hoverSignal = HitTestSignal(x, y); + } + if (hoverSignal != m_hoverSignal) + { + m_hoverSignal = hoverSignal; + changed = true; + } return changed; } @@ -913,12 +1251,12 @@ namespace jdi return true; } - // 精确时间输入中:编辑框外的点击视为提交。 - if (m_cursorEditing) + // 内联输入中:输入框之外的点击视为提交(与线宽输入一致)。 + if (IsEditing()) { - if (!UiPointInRect(m_cursorEditRect, fx, fy)) + if (!UiPointInRect(EditFieldRect(), fx, fy)) { - EndCursorEdit(true); + EndEdit(true); } return true; } @@ -959,7 +1297,18 @@ namespace jdi return true; } - m_dragMode = PlotDragMode::PAN_TIME; + // 常规模式下:先检测是否落在信号曲线上;只有命中提示时才允许拖拽该信号。 + const SignalRef hit = HitTestSignal(x, y); + if (hit.IsValid()) + { + m_dragSignal = hit; + m_dragTargetLane = Tab().LaneOfSignal(hit); + m_dragMode = PlotDragMode::MOVE_SIGNAL; + return true; + } + + m_dragLane = LaneAtY(y); + m_dragMode = PlotDragMode::PAN; return true; } @@ -986,6 +1335,20 @@ namespace jdi case PlotDragMode::CURSOR_MOVE: m_cursorHover = true; break; + case PlotDragMode::MOVE_SIGNAL: + { + const int target = LaneAtY(y); + const int source = (m_tab != nullptr) ? m_tab->LaneOfSignal(m_dragSignal) : -1; + if (m_dataSet != nullptr && m_tab != nullptr && m_dragSignal.IsValid() && target >= 0 && + target != source && m_dragMoved) + { + m_tab->MoveSignalToLane(*m_dataSet, m_dragSignal, target); + changed = true; + } + m_dragSignal = SignalRef(); + m_dragTargetLane = -1; + break; + } default: break; } @@ -995,7 +1358,6 @@ namespace jdi m_dragMoved = false; m_rubberRect = gd::RectF(); (void)x; - (void)y; return changed; } @@ -1041,7 +1403,7 @@ namespace jdi // 命中时间光标:进入精确时间输入(交互与线宽输入框一致)。 if (Tab().CursorEnabled() && IsOnCursorLine(x)) { - return BeginCursorEdit(); + return BeginEdit(PlotEditTarget::CURSOR_TIME, -1, FormatValue(Tab().CursorTime())); } // 其它位置:回到初始状态(自适应 + 退出缩放模式)。 @@ -1057,6 +1419,11 @@ namespace jdi m_hoverLane = -1; changed = true; } + if (m_hoverSignal.IsValid()) + { + m_hoverSignal = SignalRef(); + changed = true; + } if (m_cursorHover) { m_cursorHover = false; @@ -1194,17 +1561,24 @@ namespace jdi for (std::size_t index = 0; index < m_lanes.size(); ++index) { - PaintLane(g, static_cast(index), m_lanes[index], timeTicks, timeStep); + PaintLane(g, static_cast(index), m_lanes[index], timeTicks); } PaintTimeAxis(g, timeTicks, timeStep); PaintRubberBand(g); PaintCursor(g, m_lanes); + PaintSignalInteraction(g); + + // 网格 Y 轴上下限的输入框绘制在最上层。 + if (m_editTarget == PlotEditTarget::LANE_Y_MAX || m_editTarget == PlotEditTarget::LANE_Y_MIN) + { + PaintEditField(g); + } PaintScrollBar(g); - // 绘图区外框。 - gd::Pen borderPen(ToGdiColor(m_theme->border), 1.0f); + // 绘图区外框(比网格内部分界线更重,形成明确整体边界)。 + gd::Pen borderPen(ToGdiColor(m_theme->axisLine), 1.6f); g.DrawRectangle(&borderPen, m_plotRect.X, m_plotRect.Y, m_plotRect.Width, m_plotRect.Height); } @@ -1256,7 +1630,7 @@ namespace jdi } void PlotView::PaintLane(gd::Graphics& g, int laneIndex, const LaneVisual& lane, - const std::vector& timeTicks, double timeStep) + const std::vector& timeTicks) { // 网格内容必须裁剪在绘图区之内:纵向滚动时网格会部分移出可视范围, // 若不与 m_plotRect 求交,波形就会溢出到工具栏与时间轴区域。 @@ -1277,14 +1651,11 @@ namespace jdi UiFillRect(g, lane.rect, MixColor(m_theme->cardBg, m_theme->accent, 0.035)); } - // 幅值网格线与标签。 + // 幅值网格线(先画线,标尺稍后作为覆盖层绘制,避免被波形遮挡)。 const double yStep = NiceStep(lane.yMax - lane.yMin, kYTickTarget); std::vector yTicks; BuildTicks(lane.yMin, lane.yMax, yStep, yTicks); - const bool showLabels = (lane.rect.Height >= 34.0f * m_scale); - float lastLabelY = -1.0e9f; - for (const double tick : yTicks) { const float y = ValueToY(tick, lane); @@ -1296,22 +1667,6 @@ namespace jdi const bool zeroLine = (tick == 0.0); UiDrawLine(g, lane.rect.X, y, lane.rect.X + lane.rect.Width, y, zeroLine ? m_theme->gridStrong : m_theme->gridWeak, 1.0f); - - if (!showLabels || std::fabs(y - lastLabelY) < 15.0f * m_scale) - { - continue; - } - lastLabelY = y; - - const gd::RectF labelRect(m_area.X, y - 9.0f * m_scale, m_yAxisWidth - kAxisLabelGap * m_scale, - 18.0f * m_scale); - // 文本必须完整落在可见区内,否则会越过绘图区顶部(GDI 文本不受 GDI+ 裁剪约束)。 - if (!IsFullyVisible(labelRect, clipTop, clipBottom)) - { - continue; - } - UiText(g, FormatTick(tick, yStep), m_fonts->monoSmall, labelRect, - zeroLine ? m_theme->text : m_theme->textMuted, TextAlign::RIGHT); } // 时间网格线。 @@ -1354,6 +1709,27 @@ namespace jdi const bool drawLine = (mode != DrawMode::SCATTER); const bool drawScatter = (mode == DrawMode::SCATTER || mode == DrawMode::LINE_SCATTER); + // 悬停命中的信号先画一层浅色光晕,作为“可拖拽”的视觉提示。 + const bool highlighted = (ref == m_hoverSignal) || (ref == m_dragSignal); + if (highlighted && drawLine && !breaks.empty()) + { + gd::Pen halo(ToGdiColor(WithAlpha(m_theme->accent, 0x66)), + (resolved.style->lineWidth + 4.0f) * m_scale); + halo.SetLineJoin(gd::LineJoinRound); + halo.SetStartCap(gd::LineCapRound); + halo.SetEndCap(gd::LineCapRound); + for (std::size_t k = 0; k < breaks.size(); ++k) + { + const std::size_t start = breaks[k]; + const std::size_t stop = (k + 1 < breaks.size()) ? breaks[k + 1] : points.size(); + if (stop <= start + 1) + { + continue; + } + g.DrawLines(&halo, points.data() + start, static_cast(stop - start)); + } + } + if (drawLine && !breaks.empty()) { gd::Pen pen(ToGdiColor(StyleColor(*resolved.style)), @@ -1395,13 +1771,63 @@ namespace jdi g.Restore(waveSave); } - // 网格边界:完整矩形描边,使相邻网格之间界限明确(间隔由 m_laneGap 提供)。 - gd::Pen lanePen(ToGdiColor(m_theme->borderStrong), 1.0f); - g.DrawRectangle(&lanePen, lane.rect.X, lane.rect.Y, lane.rect.Width, lane.rect.Height); + // 网格分界线:网格之间不再留间隔,改用较重的横线明确分隔相邻网格。 + const float dividerY = lane.rect.Y; + if (dividerY >= m_plotRect.Y - 1.0f && dividerY <= m_plotRect.Y + m_plotRect.Height) + { + UiDrawLine(g, lane.rect.X, dividerY, lane.rect.X + lane.rect.Width, dividerY, + (laneIndex == 0) ? m_theme->axisLine : m_theme->borderStrong, + (laneIndex == 0) ? 1.6f : kLaneDividerWidth); + } - // 左侧标签列的分隔线,让纵轴读数与波形区有明确分界。 - UiDrawLine(g, m_plotRect.X, lane.rect.Y, m_plotRect.X, lane.rect.Y + lane.rect.Height, - m_theme->border, 1.0f); + // 内侧纵轴标尺作为覆盖层绘制:每个刻度数值带半透明底,保证在波形之上仍清晰可读, + // 同时只在数值背后遮挡波形,不牺牲波形本身的可见范围。 + { + const bool showLabels = (lane.rect.Height >= 34.0f * m_scale); + float lastLabelY = -1.0e9f; + + for (const double tick : yTicks) + { + if (!showLabels) + { + break; + } + + const float y = ValueToY(tick, lane); + if (y < lane.rect.Y - 1.0f || y > lane.rect.Y + lane.rect.Height + 1.0f) + { + continue; + } + if (std::fabs(y - lastLabelY) < 15.0f * m_scale) + { + continue; + } + lastLabelY = y; + + const std::wstring raw = FormatTick(tick, yStep); + const float textWidth = UiMeasureTextWidth(m_fonts->monoSmall, raw); + const float pillHeight = 17.0f * m_scale; + const gd::RectF pill(lane.rect.X + kLaneLabelPad * m_scale, y - pillHeight * 0.5f, + std::min(textWidth + 8.0f * m_scale, + lane.rect.Width - 2.0f * kLaneLabelPad * m_scale), + pillHeight); + if (pill.Width <= 0.0f) + { + continue; + } + // 文本必须完整落在可见区内,否则会越过绘图区顶部(GDI 文本不受 GDI+ 裁剪约束)。 + if (!IsFullyVisible(pill, clipTop, clipBottom)) + { + continue; + } + + UiFillRoundRect(g, pill, 3.0f * m_scale, WithAlpha(m_theme->cardBg, 0xF0)); + const std::wstring text = UiEllipsize(g, raw, m_fonts->monoSmall, pill.Width - 6.0f * m_scale); + UiText(g, text, m_fonts->monoSmall, + gd::RectF(pill.X + 4.0f * m_scale, pill.Y, pill.Width - 8.0f * m_scale, pill.Height), + (tick == 0.0) ? m_theme->text : m_theme->textMuted, TextAlign::LEFT); + } + } if (lane.rect.Height >= 24.0f * m_scale) { @@ -1412,9 +1838,7 @@ namespace jdi } } - g.Restore(saveState); - (void)timeStep; } void PlotView::PaintLaneLegend(gd::Graphics& g, int laneIndex, const LaneVisual& lane, float visibleTop, @@ -1438,7 +1862,8 @@ namespace jdi const float top = lane.rect.Y + 4.0f * m_scale; const float height = 18.0f * m_scale; - float cursorX = lane.rect.X + kLanePadding * m_scale; + // 图例从内侧标尺带的右侧开始,避免与纵轴刻度重叠。 + float cursorX = lane.rect.X + m_labelBandWidth + 6.0f * m_scale; const float maxX = lane.rect.X + lane.rect.Width - kLanePadding * m_scale; bool anyVisible = false; @@ -1514,7 +1939,7 @@ namespace jdi if (!anyVisible) { UiText(g, L"(无可见信号)", m_fonts->uiSmall, - gd::RectF(lane.rect.X + kLanePadding * m_scale, top, lane.rect.Width, height), + gd::RectF(lane.rect.X + m_labelBandWidth + 6.0f * m_scale, top, lane.rect.Width, height), m_theme->textMuted, TextAlign::LEFT); } else if (drawnCount < visibleCount) @@ -1621,30 +2046,11 @@ namespace jdi } void PlotView::PaintTimeAxis(gd::Graphics& g, const std::vector& timeTicks, double timeStep) - { const float axisY = m_plotRect.Y + m_plotRect.Height; + { + const float axisY = m_plotRect.Y + m_plotRect.Height; UiDrawLine(g, m_plotRect.X, axisY, m_plotRect.X + m_plotRect.Width, axisY, m_theme->axisLine, 1.0f); - for (const double tick : timeTicks) - { - const float x = TimeToX(tick); - if (x < m_plotRect.X - 1.0f || x > m_plotRect.X + m_plotRect.Width + 1.0f) - { - continue; - } - - UiDrawLine(g, x, axisY, x, axisY + 4.0f * m_scale, m_theme->axisLine, 1.0f); - - // 刻度标签夹在绘图区横向范围内,避免越界压到左侧的纵轴/时间列名。 - const float labelWidth = 96.0f * m_scale; - float labelX = x - labelWidth * 0.5f; - labelX = std::max(m_plotRect.X, std::min(labelX, m_plotRect.X + m_plotRect.Width - labelWidth)); - const gd::RectF labelRect(labelX, axisY + 4.0f * m_scale, labelWidth, - std::max(0.0f, m_timeAxisRect.Height - 6.0f * m_scale)); - UiText(g, FormatTick(tick, timeStep), m_fonts->monoSmall, labelRect, m_theme->textMuted, - TextAlign::CENTER); - } - - // 时间轴列名(取自首个可见信号所属文件)。 + // 时间轴列名(取自首个可见信号所属文件),贴左侧绘制。 std::wstring axisName = L"时间"; if (m_dataSet != nullptr && m_tab != nullptr) { @@ -1663,10 +2069,37 @@ namespace jdi } } - UiText(g, UiEllipsize(g, axisName, m_fonts->monoSmall, m_yAxisWidth - kAxisLabelGap * m_scale), - m_fonts->monoSmall, - gd::RectF(m_area.X, axisY, m_yAxisWidth - kAxisLabelGap * m_scale, m_timeAxisRect.Height), - m_theme->textMuted, TextAlign::RIGHT); + const float nameWidth = + std::min(UiMeasureTextWidth(m_fonts->monoSmall, axisName) + 8.0f * m_scale, + m_plotRect.Width * 0.4f); + const gd::RectF nameRect(m_plotRect.X + 2.0f * m_scale, axisY + 3.0f * m_scale, nameWidth, + std::max(0.0f, m_timeAxisRect.Height - 5.0f * m_scale)); + UiText(g, UiEllipsize(g, axisName, m_fonts->monoSmall, nameWidth), m_fonts->monoSmall, nameRect, + m_theme->textMuted, TextAlign::LEFT); + + const float labelWidth = 96.0f * m_scale; + for (const double tick : timeTicks) + { + const float x = TimeToX(tick); + if (x < m_plotRect.X - 1.0f || x > m_plotRect.X + m_plotRect.Width + 1.0f) + { + continue; + } + + UiDrawLine(g, x, axisY, x, axisY + 4.0f * m_scale, m_theme->axisLine, 1.0f); + + // 刻度标签夹在绘图区横向范围内,并与左侧列名互不重叠。 + float labelX = x - labelWidth * 0.5f; + labelX = std::max(m_plotRect.X, std::min(labelX, m_plotRect.X + m_plotRect.Width - labelWidth)); + const gd::RectF labelRect(labelX, axisY + 4.0f * m_scale, labelWidth, + std::max(0.0f, m_timeAxisRect.Height - 6.0f * m_scale)); + if (labelRect.X < nameRect.X + nameRect.Width) + { + continue; + } + UiText(g, FormatTick(tick, timeStep), m_fonts->monoSmall, labelRect, m_theme->textMuted, + TextAlign::CENTER); + } } void PlotView::PaintCursor(gd::Graphics& g, const std::vector& lanes) @@ -1737,7 +2170,12 @@ namespace jdi } // 具体时间显示在绘图区顶部(不遮挡时间轴刻度),且仅在鼠标覆盖光标时出现。 - if (!m_cursorHover && !m_cursorEditing) + if (m_editTarget == PlotEditTarget::CURSOR_TIME) + { + PaintEditField(g); + return; + } + if (!m_cursorHover) { return; } @@ -1748,31 +2186,138 @@ namespace jdi return; } - if (m_cursorEditing) - { - // 编辑态:强调色外框 + 整段高亮文本,首次键入即整体替换(与线宽输入一致)。 - UiFillRoundRect(g, labelRect, 3.0f * m_scale, m_theme->cardBg); - UiDrawRoundRect(g, labelRect, 3.0f * m_scale, m_theme->accent, 1.6f); + UiFillRoundRect(g, labelRect, 3.0f * m_scale, m_theme->cursor); + UiText(g, FormatValue(cursorTime), m_fonts->monoSmall, labelRect, m_theme->textOnAccent, + TextAlign::CENTER); + } - const std::wstring text = m_cursorEditText.empty() ? std::wstring(L" ") : m_cursorEditText; - if (m_cursorEditFresh) + void PlotView::PaintEditField(gd::Graphics& g) + { + const gd::RectF fieldRect = EditFieldRect(); + if (fieldRect.Width <= 0.0f || fieldRect.Height <= 0.0f) + { + return; + } + + // 编辑态:强调色外框 + 整段高亮文本,首次键入即整体替换。 + UiFillRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->cardBg); + UiDrawRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->accent, 1.6f); + + const std::wstring text = m_editText.empty() ? std::wstring(L" ") : m_editText; + if (m_editFresh) + { + const float textWidth = UiTextWidth(g, text, m_fonts->monoSmall) + 8.0f * m_scale; + const gd::RectF highlight(fieldRect.X + (fieldRect.Width - textWidth) * 0.5f, + fieldRect.Y + 2.0f * m_scale, textWidth, + fieldRect.Height - 4.0f * m_scale); + UiFillRoundRect(g, highlight, 2.0f * m_scale, m_theme->accent); + UiText(g, text, m_fonts->monoSmall, highlight, m_theme->textOnAccent, TextAlign::CENTER); + } + else + { + UiText(g, text + L"|", m_fonts->monoSmall, fieldRect, m_theme->textStrong, TextAlign::CENTER); + } + } + + void PlotView::PaintSignalInteraction(gd::Graphics& g) + { + if (m_tab == nullptr || m_dataSet == nullptr || m_lanes.empty()) + { + return; + } + + // 拖拽中:在目标网格内以虚线绘制该信号的“虚影”,并高亮目标网格。 + if (m_dragMode == PlotDragMode::MOVE_SIGNAL && m_dragSignal.IsValid()) + { + const int target = (m_dragTargetLane >= 0) ? m_dragTargetLane : m_tab->LaneOfSignal(m_dragSignal); + if (target >= 0 && target < static_cast(m_lanes.size())) { - const float textWidth = UiTextWidth(g, text, m_fonts->monoSmall) + 6.0f * m_scale; - const gd::RectF highlight(labelRect.X + (labelRect.Width - textWidth) * 0.5f, labelRect.Y + 2.0f * m_scale, - textWidth, labelRect.Height - 4.0f * m_scale); - UiFillRoundRect(g, highlight, 2.0f * m_scale, m_theme->accent); - UiText(g, text, m_fonts->monoSmall, highlight, m_theme->textOnAccent, TextAlign::CENTER); - } - else - { - UiText(g, text + L"|", m_fonts->monoSmall, labelRect, m_theme->textStrong, TextAlign::CENTER); + const LaneVisual& lane = m_lanes[static_cast(target)]; + const ResolvedSignal resolved = ResolveSignal(m_dragSignal); + if (resolved.data != nullptr && resolved.style != nullptr) + { + const int saveState = g.Save(); + g.SetClip(gd::RectF(m_plotRect.X, std::max(lane.rect.Y, m_plotRect.Y), m_plotRect.Width, + std::min(lane.rect.Y + lane.rect.Height, + m_plotRect.Y + m_plotRect.Height) - + std::max(lane.rect.Y, m_plotRect.Y)), + gd::CombineModeIntersect); + + // 目标网格边框高亮。 + gd::Pen frame(ToGdiColor(m_theme->accent), 2.0f); + g.DrawRectangle(&frame, lane.rect.X, lane.rect.Y, lane.rect.Width, lane.rect.Height); + + std::vector points; + std::vector breaks; + BuildPolyline(resolved, lane, points, breaks); + + gd::Pen ghost(ToGdiColor(WithAlpha(m_theme->accent, 0xB0)), + std::max(1.6f, resolved.style->lineWidth) * m_scale); + ghost.SetDashStyle(gd::DashStyleDash); + for (std::size_t k = 0; k < breaks.size(); ++k) + { + const std::size_t start = breaks[k]; + const std::size_t stop = (k + 1 < breaks.size()) ? breaks[k + 1] : points.size(); + if (stop <= start + 1) + { + continue; + } + g.DrawLines(&ghost, points.data() + start, static_cast(stop - start)); + } + + g.Restore(saveState); + + // 目标网格名称提示。 + const std::wstring hint = + L"移动到网格 " + std::to_wstring(target + 1) + L":松开左键确认"; + const float textWidth = UiTextWidth(g, hint, m_fonts->uiSmall) + 16.0f * m_scale; + const float height = 20.0f * m_scale; + float left = lane.rect.X + (lane.rect.Width - textWidth) * 0.5f; + left = std::max(m_plotRect.X, std::min(left, m_plotRect.X + m_plotRect.Width - textWidth)); + const gd::RectF pill(left, lane.rect.Y + 4.0f * m_scale, textWidth, height); + UiFillRoundRect(g, pill, 3.0f * m_scale, m_theme->accent); + UiText(g, hint, m_fonts->uiSmall, pill, m_theme->textOnAccent, TextAlign::CENTER); + } } return; } - UiFillRoundRect(g, labelRect, 3.0f * m_scale, m_theme->cursor); - UiText(g, FormatValue(cursorTime), m_fonts->monoSmall, labelRect, m_theme->textOnAccent, - TextAlign::CENTER); + // 悬停提示:光标贴近曲线时给出可拖拽说明(与时间光标的提示方式一致)。 + if (!m_hoverSignal.IsValid()) + { + return; + } + const int laneIndex = m_tab->LaneOfSignal(m_hoverSignal); + if (laneIndex < 0 || laneIndex >= static_cast(m_lanes.size())) + { + return; + } + const ResolvedSignal resolved = ResolveSignal(m_hoverSignal); + if (resolved.data == nullptr) + { + return; + } + + const LaneVisual& lane = m_lanes[static_cast(laneIndex)]; + const std::wstring text = resolved.data->name + L" · 按住拖拽可移入其它网格"; + + const float textWidth = UiTextWidth(g, text, m_fonts->uiSmall) + 16.0f * m_scale; + const float height = 20.0f * m_scale; + float left = m_mouseX + 12.0f * m_scale; + if (left + textWidth > m_plotRect.X + m_plotRect.Width) + { + left = m_plotRect.X + m_plotRect.Width - textWidth; + } + left = std::max(m_plotRect.X, left); + + float top = m_mouseY + 14.0f * m_scale; + top = std::min(top, m_plotRect.Y + m_plotRect.Height - height - 2.0f * m_scale); + top = std::max(top, lane.rect.Y + 2.0f * m_scale); + + const gd::RectF pill(left, top, textWidth, height); + UiFillRoundRect(g, pill, 3.0f * m_scale, WithAlpha(m_theme->readoutBg, 0xF2)); + UiDrawRoundRect(g, pill, 3.0f * m_scale, m_theme->accent, 1.0f); + UiText(g, text, m_fonts->uiSmall, pill, m_theme->textStrong, TextAlign::CENTER); } void PlotView::PaintScrollBar(gd::Graphics& g) diff --git a/Components/Toolbar/Inc/Toolbar.hpp b/Components/Toolbar/Inc/Toolbar.hpp index 438bfc7..763c1fb 100644 --- a/Components/Toolbar/Inc/Toolbar.hpp +++ b/Components/Toolbar/Inc/Toolbar.hpp @@ -58,12 +58,6 @@ namespace jdi */ void SetDocumentLoaded(bool hasDocument); - /** - * @brief 设置工具栏右侧的状态文本。 - * @param [IN] text 状态文本。 - */ - void SetStatusText(const std::wstring& text); - /** * @brief 设置可切换按钮的选中状态。 * @param [IN] action 目标按钮动作。 @@ -156,7 +150,6 @@ namespace jdi const Theme* m_theme = nullptr; /**< 主题资源。 */ float m_scale = 1.0f; /**< DPI 缩放比例。 */ bool m_documentLoaded = false; /**< 是否已加载数据。 */ - std::wstring m_statusText; /**< 右侧状态文本。 */ std::vector