feat: 重构绘图区网格布局与信号拖拽,精简界面信息并新增 Y 轴精确控制

网格与标尺:
- 消除网格之间无意义的间隔,改用较重的横向分界线明确区分相邻网格
- 纵轴标尺从绘图区左侧外侧移入网格内侧,并改为带半透明衬底的覆盖层绘制,
  使其在波形之上依然清晰可读;绘图区左侧空间因此完全还给波形
- 继续保证底部时间轴预留高度,并把时间列名贴左显示、避开刻度标签

常规模式拖拽:
- 常规模式下水平拖动平移时间轴,纵向拖动偏移当前网格的幅值显示位置
- 绘图区右键菜单新增「恢复 Y 轴自适应」「设置 Y 轴最大值…」「设置 Y 轴最小值…」
  「全部自适应」,上下限通过内联输入框直接键入(与线宽输入一致的交互)

信号拖拽:
- 鼠标贴近信号曲线时给出光晕高亮与提示气泡(与时间光标的提示方式一致)
- 仅在出现提示时按左键才会进入拖拽;拖拽过程中在目标网格内绘制曲线虚影、
  高亮目标网格边框并提示「移动到网格 N」,松开左键确认归属变更
- 命中检测基于折线到光标的精确点到线段距离,散点模式按采样点距离判定

界面精简:
- 移除底部状态栏(左下角统计与右下角提示均不再占用空间),错误改由对话框提示
- 标题栏只显示应用名称
- 移除工具栏右上角的视图时间范围文本;按钮除「导入文件」外整体靠右排列

其他:
- 新增 PlotView::PaintEditField 统一绘制时间光标与网格 Y 轴上下限的输入框
- 移除已无引用的 UiDrawDashedLine 与绘图区标尺占位宽度
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-09-29 20:49:21 +08:00
1 parent 17eb03a1b3
commit 542aa51e40
9 files changed
+981 -335

No files matched your search

+11 -10
View File
@@ -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; /**< 上次同步时的网格数量。 */
+104 -80
View File
@@ -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<unsigned long long>(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<int>(m_tabs.size()))
{
title += L" — ";
title += m_tabs[static_cast<std::size_t>(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<MenuItem> 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<float>(client.right - client.left),
static_cast<float>(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<wchar_t>(wParam)))
{
+121 -20
View File
@@ -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<double>& timeTicks, double timeStep);
const std::vector<double>& 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<LaneVisual> m_lanes; /**< 当前网格绘制信息集合。 */
};
File diff suppressed because it is too large. Load diff
-7
View File
@@ -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<Button> m_buttons; /**< 按钮集合。 */
gd::RectF m_area; /**< 工具栏整体区域。 */
int m_hoverIndex = -1; /**< 当前悬停按钮下标。 */
+29 -24
View File
@@ -36,11 +36,6 @@ namespace jdi
m_documentLoaded = hasDocument;
}
void Toolbar::SetStatusText(const std::wstring& text)
{
m_statusText = text;
}
float Toolbar::DesiredHeight() const
{
return (kButtonHeight + kTopPadding * 2.0f) * m_scale;
@@ -71,7 +66,7 @@ namespace jdi
};
const Spec specs[] = {
{ToolbarAction::OPEN_FILE, L"打开 CSV", true, false, false},
{ToolbarAction::OPEN_FILE, L"导入文件", true, false, false},
{ToolbarAction::FIT_ALL, L"适应视图", false, true, false},
{ToolbarAction::ZOOM_TIME, L"时间轴缩放", false, true, true},
{ToolbarAction::ZOOM_VALUE, L"Y 轴缩放", false, true, true},
@@ -81,13 +76,17 @@ namespace jdi
{ToolbarAction::SELECT_ALL, L"全选信号", false, true, false},
{ToolbarAction::CLEAR_ALL, L"清空勾选", false, true, false},
};
const std::size_t specCount = sizeof(specs) / sizeof(specs[0]);
// 按钮宽度按实际文本像素宽 + 固定内边距计算,避免中文标签被裁切。
float cursorX = area.X + kSidePadding * m_scale;
const float top = area.Y + kTopPadding * m_scale;
const float height = kButtonHeight * m_scale;
const float horizontalPadding = kButtonPadding * 2.0f * m_scale;
// 先建立全部按钮并测量宽度,再决定“导入文件”固定在左、其余按钮整体靠右。
m_buttons.clear();
m_buttons.reserve(specCount);
for (const Spec& spec : specs)
{
Button button;
@@ -97,17 +96,31 @@ namespace jdi
button.needsDocument = spec.needsDocument;
button.toggle = spec.toggle;
button.width = horizontalPadding + UiMeasureTextWidth(m_fonts->ui, button.label);
button.rect = gd::RectF(cursorX, top, button.width, height);
cursorX += button.width + kButtonGap * m_scale;
if (spec.action == ToolbarAction::OPEN_FILE || spec.action == ToolbarAction::CURSOR ||
spec.action == ToolbarAction::SPLIT_LANES)
{
cursorX += kGroupGap * m_scale;
}
m_buttons.push_back(std::move(button));
}
// 首按钮(导入文件)靠左;其余按钮作为整体靠右,并留出一组额外间距。
const float leftX = area.X + kSidePadding * m_scale;
float groupWidth = 0.0f;
for (std::size_t index = 1; index < m_buttons.size(); ++index)
{
groupWidth += m_buttons[index].width;
if (index + 1 < m_buttons.size())
{
groupWidth += kButtonGap * m_scale;
}
}
float cursorX = leftX;
m_buttons[0].rect = gd::RectF(cursorX, top, m_buttons[0].width, height);
const float rightEdge = area.X + area.Width - kSidePadding * m_scale;
cursorX = std::max(leftX + m_buttons[0].width + kGroupGap * m_scale, rightEdge - groupWidth);
for (std::size_t index = 1; index < m_buttons.size(); ++index)
{
m_buttons[index].rect = gd::RectF(cursorX, top, m_buttons[index].width, height);
cursorX += m_buttons[index].width + kButtonGap * m_scale;
}
}
void Toolbar::SetToggleState(ToolbarAction action, bool checked)
@@ -195,14 +208,6 @@ namespace jdi
UiText(g, button.label, m_fonts->ui, button.rect, textColor, TextAlign::CENTER);
}
// 右侧状态文本。
if (!m_statusText.empty())
{
const float rightPadding = kSidePadding * m_scale;
gd::RectF textRect(m_area.X, m_area.Y, m_area.Width - rightPadding, m_area.Height);
UiText(g, m_statusText, m_fonts->uiSmall, textRect, m_theme->textMuted, TextAlign::RIGHT);
}
// 底部分隔线。
UiDrawLine(g, m_area.X, m_area.Y + m_area.Height - 1.0f, m_area.X + m_area.Width,
m_area.Y + m_area.Height - 1.0f, m_theme->border, 1.0f);
-14
View File
@@ -124,20 +124,6 @@ namespace jdi
*/
void UiDrawLine(gd::Graphics& g, float x1, float y1, float x2, float y2, std::uint32_t argb, float width);
/**
* @brief 绘制一条虚线。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] x1 起点横坐标。
* @param [IN] y1 起点纵坐标。
* @param [IN] x2 终点横坐标。
* @param [IN] y2 终点纵坐标。
* @param [IN] argb 线条颜色。
* @param [IN] width 线条宽度。
* @param [IN] dashPattern 虚线样式。
*/
void UiDrawDashedLine(gd::Graphics& g, float x1, float y1, float x2, float y2, std::uint32_t argb,
float width, gd::DashStyle dashPattern = gd::DashStyleDash);
/**
* @brief 在指定矩形内绘制单行文本(垂直居中,超长自动省略)。
* @param [IN,OUT] g 绘图上下文。
+2 -1
View File
@@ -22,7 +22,8 @@ namespace jdi
{
NONE = 0, /**< 无来源。 */
SIGNAL_PANEL = 1, /**< 信号面板。 */
TAB_BAR = 2 /**< 标签栏。 */
TAB_BAR = 2, /**< 标签栏。 */
PLOT_VIEW = 3 /**< 波形绘图区。 */
};
/**
-10
View File
@@ -166,16 +166,6 @@ namespace jdi
g.DrawLine(&pen, x1, y1, x2, y2);
}
void UiDrawDashedLine(gd::Graphics& g, float x1, float y1, float x2, float y2, std::uint32_t argb,
float width, gd::DashStyle dashPattern)
{
gd::Pen pen(ToGdiColor(argb), width);
pen.SetDashStyle(dashPattern);
pen.SetStartCap(gd::LineCapFlat);
pen.SetEndCap(gd::LineCapFlat);
g.DrawLine(&pen, x1, y1, x2, y2);
}
void UiText(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect, std::uint32_t argb,
TextAlign align)
{