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

+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)))
{