feat: 重构绘图区网格布局与信号拖拽,精简界面信息并新增 Y 轴精确控制
网格与标尺: - 消除网格之间无意义的间隔,改用较重的横向分界线明确区分相邻网格 - 纵轴标尺从绘图区左侧外侧移入网格内侧,并改为带半透明衬底的覆盖层绘制, 使其在波形之上依然清晰可读;绘图区左侧空间因此完全还给波形 - 继续保证底部时间轴预留高度,并把时间列名贴左显示、避开刻度标签 常规模式拖拽: - 常规模式下水平拖动平移时间轴,纵向拖动偏移当前网格的幅值显示位置 - 绘图区右键菜单新增「恢复 Y 轴自适应」「设置 Y 轴最大值…」「设置 Y 轴最小值…」 「全部自适应」,上下限通过内联输入框直接键入(与线宽输入一致的交互) 信号拖拽: - 鼠标贴近信号曲线时给出光晕高亮与提示气泡(与时间光标的提示方式一致) - 仅在出现提示时按左键才会进入拖拽;拖拽过程中在目标网格内绘制曲线虚影、 高亮目标网格边框并提示「移动到网格 N」,松开左键确认归属变更 - 命中检测基于折线到光标的精确点到线段距离,散点模式按采样点距离判定 界面精简: - 移除底部状态栏(左下角统计与右下角提示均不再占用空间),错误改由对话框提示 - 标题栏只显示应用名称 - 移除工具栏右上角的视图时间范围文本;按钮除「导入文件」外整体靠右排列 其他: - 新增 PlotView::PaintEditField 统一绘制时间光标与网格 Y 轴上下限的输入框 - 移除已无引用的 UiDrawDashedLine 与绘图区标尺占位宽度
This commit is contained in:
1 parent
17eb03a1b3
commit
542aa51e40
9 files changed
+981
-335
No files matched your search
@@ -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; /**< 上次同步时的网格数量。 */
|
||||
|
||||
@@ -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)))
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user