feat: 支持多视图标签页、按文件分组的多文件信号管理与网格随勾选自动收敛
数据层与视图配置解耦:新增 DataSet(多文件只读数据层)与 TabConfig(每标签页独立配置),
以 SignalRef{fileIndex,signalIndex} 标识全局信号,替换原有单文件 SignalSet。
- 标签页:新增 TabBar 组件,支持新建/切换/关闭标签页;每页独立保存信号勾选、
颜色/线宽/显示方式、网格划分、视图范围与时间光标,互不影响。
- 网格收敛:可见性变更统一经 TabConfig 的 Set*Visible 接口并同步重算网格,
取消勾选后对应的空网格立即回收(修复此前留下空轨道的问题)。
- 多文件分组:左侧面板改为「文件分组 + 信号」树形列表,分组可折叠、可整组勾选,
分组复选框支持全选/部分/未选三态;新加载文件的信号默认未勾选,由用户自行选择。
- 来源标识:信号行、样式编辑区标题、波形图例与光标读数在跨文件时标注来源文件名。
- 交互补充:面板滚动条支持点击跳转与拖拽,切换标签页自动滚动到已选信号。
- 工具栏精简为「打开 CSV/适应视图/放大/缩小/合并网格/每信号一格/全选信号/清空勾选」,
按钮宽度改为按实际文本像素测量;打开对话框支持多选文件,拖放支持批量载入。
- 新增 TestData/[2].CSV 作为跨文件对比测试数据。
This commit is contained in:
1 parent
132b305544
commit
9c6c374cce
21 files changed
+4066
-1441
No files matched your search
@@ -6,11 +6,13 @@
|
|||||||
#include <gdiplus.h>
|
#include <gdiplus.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "DataModel.hpp"
|
||||||
#include "PaintUtil.hpp"
|
#include "PaintUtil.hpp"
|
||||||
#include "PlotView.hpp"
|
#include "PlotView.hpp"
|
||||||
#include "SignalPanel.hpp"
|
#include "SignalPanel.hpp"
|
||||||
#include "SignalSet.hpp"
|
#include "TabBar.hpp"
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
#include "Toolbar.hpp"
|
#include "Toolbar.hpp"
|
||||||
|
|
||||||
@@ -18,7 +20,10 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 应用主框架:负责窗口、布局、消息分发、绘制合成与命令处理。
|
* @brief 应用主框架:负责窗口、布局、消息分发、标签页管理与绘制合成。
|
||||||
|
*
|
||||||
|
* 数据层(DataSet)全局共享,标签页仅保存各自的视图配置(TabConfig),
|
||||||
|
* 因此新标签页可以独立勾选信号、独立配色与分组,互不影响。
|
||||||
*/
|
*/
|
||||||
class Application
|
class Application
|
||||||
{
|
{
|
||||||
@@ -54,6 +59,7 @@ namespace jdi
|
|||||||
enum class Target : int
|
enum class Target : int
|
||||||
{
|
{
|
||||||
NONE = 0, /**< 无目标。 */
|
NONE = 0, /**< 无目标。 */
|
||||||
|
TAB_BAR, /**< 标签栏。 */
|
||||||
TOOLBAR, /**< 工具栏。 */
|
TOOLBAR, /**< 工具栏。 */
|
||||||
PANEL, /**< 信号面板。 */
|
PANEL, /**< 信号面板。 */
|
||||||
PLOT, /**< 波形绘图区。 */
|
PLOT, /**< 波形绘图区。 */
|
||||||
@@ -122,6 +128,69 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
void PaintStatusBar(gd::Graphics& g);
|
void PaintStatusBar(gd::Graphics& g);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取当前激活标签页配置(可写)。
|
||||||
|
* @return 激活标签页配置引用。
|
||||||
|
*/
|
||||||
|
TabConfig& ActiveTab();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取当前激活标签页配置(只读)。
|
||||||
|
* @return 激活标签页配置引用。
|
||||||
|
*/
|
||||||
|
const TabConfig& ActiveTab() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 新建标签页并切换为激活页。
|
||||||
|
*/
|
||||||
|
void NewTab();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 关闭指定标签页。
|
||||||
|
* @param [IN] index 标签页下标。
|
||||||
|
*/
|
||||||
|
void CloseTab(int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 切换激活标签页并重新绑定各组件上下文。
|
||||||
|
* @param [IN] index 标签页下标。
|
||||||
|
*/
|
||||||
|
void SetActiveTab(int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将标签栏内容与当前标签页集合同步。
|
||||||
|
*/
|
||||||
|
void RefreshTabBar();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 检测激活标签页的可见性/网格变化,并同步绘图区与布局。
|
||||||
|
* @param [IN] force 是否强制同步(忽略缓存比对)。
|
||||||
|
*/
|
||||||
|
void SynchronizeView(bool force);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将全部标签页配置与数据层对齐(文件增删后调用)。
|
||||||
|
*/
|
||||||
|
void SyncTabsToDataSet();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 从工作区移除指定文件并修正各标签页配置。
|
||||||
|
* @param [IN] fileIndex 文件下标。
|
||||||
|
*/
|
||||||
|
void RemoveFile(int fileIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理标签栏交互结果。
|
||||||
|
* @param [IN] result 交互结果。
|
||||||
|
*/
|
||||||
|
void OnTabBarResult(const TabBarResult& result);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 弹出标签页右键菜单。
|
||||||
|
* @param [IN] result 上下文菜单请求。
|
||||||
|
*/
|
||||||
|
void ShowTabContextMenu(const TabBarResult& result);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 弹出文件选择对话框并加载所选文件。
|
* @brief 弹出文件选择对话框并加载所选文件。
|
||||||
*/
|
*/
|
||||||
@@ -162,7 +231,7 @@ namespace jdi
|
|||||||
/**
|
/**
|
||||||
* @brief 解析命令行中的文件参数并加载。
|
* @brief 解析命令行中的文件参数并加载。
|
||||||
*/
|
*/
|
||||||
void LoadCommandLineFile();
|
void LoadCommandLineFiles();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 处理客户区鼠标移动。
|
* @brief 处理客户区鼠标移动。
|
||||||
@@ -182,18 +251,24 @@ namespace jdi
|
|||||||
|
|
||||||
Theme m_theme; /**< 主题配色。 */
|
Theme m_theme; /**< 主题配色。 */
|
||||||
UiFonts m_fonts; /**< 界面字体。 */
|
UiFonts m_fonts; /**< 界面字体。 */
|
||||||
SignalSet m_document; /**< 已加载的数据集。 */
|
DataSet m_dataSet; /**< 全局共享的数据集合。 */
|
||||||
|
std::vector<TabConfig> m_tabs; /**< 标签页配置集合。 */
|
||||||
|
int m_activeTab = -1; /**< 当前激活标签页下标。 */
|
||||||
|
int m_tabNameCounter = 0; /**< 标签页命名计数器。 */
|
||||||
|
|
||||||
|
TabBar m_tabBar; /**< 顶部标签栏。 */
|
||||||
Toolbar m_toolbar; /**< 顶部工具栏。 */
|
Toolbar m_toolbar; /**< 顶部工具栏。 */
|
||||||
SignalPanel m_panel; /**< 左侧信号面板。 */
|
SignalPanel m_panel; /**< 左侧信号面板。 */
|
||||||
PlotView m_plot; /**< 波形绘图区。 */
|
PlotView m_plot; /**< 波形绘图区。 */
|
||||||
|
|
||||||
|
gd::RectF m_tabBarRect; /**< 标签栏区域。 */
|
||||||
gd::RectF m_toolbarRect; /**< 工具栏区域。 */
|
gd::RectF m_toolbarRect; /**< 工具栏区域。 */
|
||||||
gd::RectF m_panelRect; /**< 信号面板区域。 */
|
gd::RectF m_panelRect; /**< 信号面板区域。 */
|
||||||
gd::RectF m_splitterRect; /**< 面板分隔条区域。 */
|
gd::RectF m_splitterRect; /**< 面板分隔条区域。 */
|
||||||
gd::RectF m_plotRect; /**< 绘图区区域。 */
|
gd::RectF m_plotRect; /**< 绘图区区域。 */
|
||||||
gd::RectF m_statusRect; /**< 状态栏区域。 */
|
gd::RectF m_statusRect; /**< 状态栏区域。 */
|
||||||
|
|
||||||
float m_panelWidth = 292.0f; /**< 面板宽度(逻辑像素)。 */
|
float m_panelWidth = 300.0f; /**< 面板宽度(逻辑像素)。 */
|
||||||
bool m_draggingSplitter = false; /**< 是否正在拖动分隔条。 */
|
bool m_draggingSplitter = false; /**< 是否正在拖动分隔条。 */
|
||||||
int m_splitterGrabOffset = 0; /**< 分隔条拖动的抓取偏移。 */
|
int m_splitterGrabOffset = 0; /**< 分隔条拖动的抓取偏移。 */
|
||||||
|
|
||||||
@@ -205,9 +280,12 @@ namespace jdi
|
|||||||
|
|
||||||
Target m_activeTarget = Target::NONE; /**< 当前鼠标作用目标。 */
|
Target m_activeTarget = Target::NONE; /**< 当前鼠标作用目标。 */
|
||||||
bool m_trackingLeave = false; /**< 是否已注册鼠标离开通知。 */
|
bool m_trackingLeave = false; /**< 是否已注册鼠标离开通知。 */
|
||||||
std::size_t m_lastLaneCount = 0; /**< 上次渲染时的网格数量,用于检测布局失效。 */
|
|
||||||
std::wstring m_statusMessage; /**< 状态栏提示文本。 */
|
std::wstring m_statusMessage; /**< 状态栏提示文本。 */
|
||||||
bool m_statusIsError = false; /**< 状态提示是否为错误。 */
|
bool m_statusIsError = false; /**< 状态提示是否为错误。 */
|
||||||
|
|
||||||
|
int m_cachedVisibleCount = -1; /**< 上次同步时的可见信号数量。 */
|
||||||
|
std::size_t m_cachedLaneCount = 0; /**< 上次同步时的网格数量。 */
|
||||||
|
bool m_cachedAnyVisible = false; /**< 上次同步时是否存在可见信号。 */
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace jdi
|
} // namespace jdi
|
||||||
|
|||||||
@@ -27,12 +27,18 @@ namespace jdi
|
|||||||
const wchar_t* kWindowTitle = L"Just DataInspector"; /**< 默认窗口标题。 */
|
const wchar_t* kWindowTitle = L"Just DataInspector"; /**< 默认窗口标题。 */
|
||||||
const float kStatusBarHeight = 26.0f; /**< 状态栏高度(逻辑像素)。 */
|
const float kStatusBarHeight = 26.0f; /**< 状态栏高度(逻辑像素)。 */
|
||||||
const float kSplitterWidth = 4.0f; /**< 分隔条宽度(逻辑像素)。 */
|
const float kSplitterWidth = 4.0f; /**< 分隔条宽度(逻辑像素)。 */
|
||||||
const float kMinPanelWidth = 210.0f; /**< 面板最小宽度(逻辑像素)。 */
|
const float kMinPanelWidth = 220.0f; /**< 面板最小宽度(逻辑像素)。 */
|
||||||
const float kMaxPanelWidth = 460.0f; /**< 面板最大宽度(逻辑像素)。 */
|
const float kMaxPanelWidth = 480.0f; /**< 面板最大宽度(逻辑像素)。 */
|
||||||
const int kMinClientWidth = 820; /**< 窗口最小客户区宽度。 */
|
const int kMinClientWidth = 860; /**< 窗口最小客户区宽度。 */
|
||||||
const int kMinClientHeight = 520; /**< 窗口最小客户区高度。 */
|
const int kMinClientHeight = 540; /**< 窗口最小客户区高度。 */
|
||||||
|
const UINT kMessageRemoveFile = WM_APP + 1; /**< 面板请求移除文件的自定义消息。 */
|
||||||
const wchar_t* kStatusHint = L"滚轮缩放 · Shift+滚轮平移 · Ctrl+滚轮调幅值 · 右键框选 · 双击自适应";
|
const wchar_t* kStatusHint = L"滚轮缩放 · Shift+滚轮平移 · Ctrl+滚轮调幅值 · 右键框选 · 双击自适应";
|
||||||
|
|
||||||
|
/** @brief 标签页右键菜单命令 ID。 */
|
||||||
|
const int kMenuTabNew = 1;
|
||||||
|
const int kMenuTabClose = 2;
|
||||||
|
const int kMenuTabCloseOthers = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 开启进程级 DPI 感知(优先 Per-Monitor V2)。
|
* @brief 开启进程级 DPI 感知(优先 Per-Monitor V2)。
|
||||||
*/
|
*/
|
||||||
@@ -45,8 +51,7 @@ namespace jdi
|
|||||||
SetProcessDpiAwarenessContextFn setContext =
|
SetProcessDpiAwarenessContextFn setContext =
|
||||||
reinterpret_cast<SetProcessDpiAwarenessContextFn>(
|
reinterpret_cast<SetProcessDpiAwarenessContextFn>(
|
||||||
reinterpret_cast<void*>(GetProcAddress(user32, "SetProcessDpiAwarenessContext")));
|
reinterpret_cast<void*>(GetProcAddress(user32, "SetProcessDpiAwarenessContext")));
|
||||||
if (setContext != nullptr &&
|
if (setContext != nullptr && setContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) != FALSE)
|
||||||
setContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) != FALSE)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -166,14 +171,14 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 依据工作区(排除任务栏)计算默认窗口尺寸并居中,避免窗口超出屏幕。
|
// 依据工作区(排除任务栏)计算默认窗口尺寸并居中,避免窗口超出屏幕。
|
||||||
RECT workArea = {0, 0, static_cast<LONG>(ScaleOf(1340.0f, m_scale)),
|
RECT workArea = {0, 0, static_cast<LONG>(ScaleOf(1380.0f, m_scale)),
|
||||||
static_cast<LONG>(ScaleOf(860.0f, m_scale))};
|
static_cast<LONG>(ScaleOf(880.0f, m_scale))};
|
||||||
SystemParametersInfoW(SPI_GETWORKAREA, 0, &workArea, 0);
|
SystemParametersInfoW(SPI_GETWORKAREA, 0, &workArea, 0);
|
||||||
|
|
||||||
const int workWidth = workArea.right - workArea.left;
|
const int workWidth = workArea.right - workArea.left;
|
||||||
const int workHeight = workArea.bottom - workArea.top;
|
const int workHeight = workArea.bottom - workArea.top;
|
||||||
const int windowWidth = std::min(static_cast<int>(ScaleOf(1340.0f, m_scale)), workWidth - 24);
|
const int windowWidth = std::min(static_cast<int>(ScaleOf(1380.0f, m_scale)), workWidth - 24);
|
||||||
const int windowHeight = std::min(static_cast<int>(ScaleOf(860.0f, m_scale)), workHeight - 24);
|
const int windowHeight = std::min(static_cast<int>(ScaleOf(880.0f, m_scale)), workHeight - 24);
|
||||||
const int windowX = workArea.left + std::max(0, (workWidth - windowWidth) / 2);
|
const int windowX = workArea.left + std::max(0, (workWidth - windowWidth) / 2);
|
||||||
const int windowY = workArea.top + std::max(0, (workHeight - windowHeight) / 2);
|
const int windowY = workArea.top + std::max(0, (workHeight - windowHeight) / 2);
|
||||||
|
|
||||||
@@ -186,6 +191,8 @@ namespace jdi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_tabBar.Setup(&m_fonts, &m_theme);
|
||||||
|
m_tabBar.SetScale(m_scale);
|
||||||
m_toolbar.Setup(&m_fonts, &m_theme);
|
m_toolbar.Setup(&m_fonts, &m_theme);
|
||||||
m_toolbar.SetScale(m_scale);
|
m_toolbar.SetScale(m_scale);
|
||||||
m_panel.Setup(m_hwnd, &m_fonts, &m_theme);
|
m_panel.Setup(m_hwnd, &m_fonts, &m_theme);
|
||||||
@@ -197,10 +204,13 @@ namespace jdi
|
|||||||
SendMessageW(m_hwnd, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(icon));
|
SendMessageW(m_hwnd, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(icon));
|
||||||
SendMessageW(m_hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(icon));
|
SendMessageW(m_hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(icon));
|
||||||
|
|
||||||
|
// 创建首个标签页。
|
||||||
|
NewTab();
|
||||||
|
|
||||||
Relayout();
|
Relayout();
|
||||||
ShowWindow(m_hwnd, showCommand);
|
ShowWindow(m_hwnd, showCommand);
|
||||||
UpdateWindow(m_hwnd);
|
UpdateWindow(m_hwnd);
|
||||||
LoadCommandLineFile();
|
LoadCommandLineFiles();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +237,7 @@ namespace jdi
|
|||||||
m_scale = static_cast<float>(dpi) / 96.0f;
|
m_scale = static_cast<float>(dpi) / 96.0f;
|
||||||
|
|
||||||
m_fonts.Create(dpi);
|
m_fonts.Create(dpi);
|
||||||
|
m_tabBar.SetScale(m_scale);
|
||||||
m_toolbar.SetScale(m_scale);
|
m_toolbar.SetScale(m_scale);
|
||||||
m_panel.SetScale(m_scale);
|
m_panel.SetScale(m_scale);
|
||||||
m_plot.SetScale(m_scale);
|
m_plot.SetScale(m_scale);
|
||||||
@@ -234,6 +245,179 @@ namespace jdi
|
|||||||
Relayout();
|
Relayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TabConfig& Application::ActiveTab()
|
||||||
|
{
|
||||||
|
static TabConfig fallback;
|
||||||
|
if (m_activeTab < 0 || m_activeTab >= static_cast<int>(m_tabs.size()))
|
||||||
|
{
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return m_tabs[static_cast<std::size_t>(m_activeTab)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const TabConfig& Application::ActiveTab() const
|
||||||
|
{
|
||||||
|
static const TabConfig fallback;
|
||||||
|
if (m_activeTab < 0 || m_activeTab >= static_cast<int>(m_tabs.size()))
|
||||||
|
{
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return m_tabs[static_cast<std::size_t>(m_activeTab)];
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::NewTab()
|
||||||
|
{
|
||||||
|
++m_tabNameCounter;
|
||||||
|
|
||||||
|
TabConfig tab;
|
||||||
|
tab.SetName(L"视图 " + std::to_wstring(m_tabNameCounter));
|
||||||
|
tab.SyncToDataSet(m_dataSet);
|
||||||
|
|
||||||
|
m_tabs.push_back(std::move(tab));
|
||||||
|
SetActiveTab(static_cast<int>(m_tabs.size()) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::CloseTab(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= static_cast<int>(m_tabs.size()))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m_tabs.size() <= 1)
|
||||||
|
{
|
||||||
|
SetStatusMessage(L"至少需要保留一个标签页。", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_tabs.erase(m_tabs.begin() + index);
|
||||||
|
|
||||||
|
int next = index;
|
||||||
|
if (next >= static_cast<int>(m_tabs.size()))
|
||||||
|
{
|
||||||
|
next = static_cast<int>(m_tabs.size()) - 1;
|
||||||
|
}
|
||||||
|
SetActiveTab(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::SetActiveTab(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= static_cast<int>(m_tabs.size()))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_activeTab = index;
|
||||||
|
|
||||||
|
// 重新绑定上下文:面板与绘图区均指向同一数据层与当前标签页配置。
|
||||||
|
m_panel.SetContext(&m_dataSet, &m_tabs[static_cast<std::size_t>(m_activeTab)]);
|
||||||
|
m_plot.SetContext(&m_dataSet, &m_tabs[static_cast<std::size_t>(m_activeTab)]);
|
||||||
|
|
||||||
|
RefreshTabBar();
|
||||||
|
Relayout();
|
||||||
|
|
||||||
|
// 切换标签页后把该页已选中的信号滚动到可见位置。
|
||||||
|
m_panel.ScrollToSignal(m_tabs[static_cast<std::size_t>(m_activeTab)].Selected());
|
||||||
|
|
||||||
|
SynchronizeView(true);
|
||||||
|
UpdateWindowTitle();
|
||||||
|
InvalidateRect(m_hwnd, nullptr, TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::RefreshTabBar()
|
||||||
|
{
|
||||||
|
std::vector<std::wstring> names;
|
||||||
|
names.reserve(m_tabs.size());
|
||||||
|
for (const TabConfig& tab : m_tabs)
|
||||||
|
{
|
||||||
|
names.push_back(tab.Name());
|
||||||
|
}
|
||||||
|
m_tabBar.SetTabs(names);
|
||||||
|
m_tabBar.SetActiveIndex(m_activeTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::SynchronizeView(bool force)
|
||||||
|
{
|
||||||
|
if (m_tabs.empty() || m_activeTab < 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TabConfig& tab = m_tabs[static_cast<std::size_t>(m_activeTab)];
|
||||||
|
|
||||||
|
const int visibleCount = tab.VisibleCount();
|
||||||
|
const bool anyVisible = tab.AnyVisible();
|
||||||
|
const std::size_t laneCount = tab.Lanes().size();
|
||||||
|
|
||||||
|
const bool changed = (visibleCount != m_cachedVisibleCount) || (anyVisible != m_cachedAnyVisible) ||
|
||||||
|
(laneCount != m_cachedLaneCount);
|
||||||
|
if (!changed && !force)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t previousLaneCount = m_cachedLaneCount;
|
||||||
|
m_cachedVisibleCount = visibleCount;
|
||||||
|
m_cachedAnyVisible = anyVisible;
|
||||||
|
m_cachedLaneCount = laneCount;
|
||||||
|
|
||||||
|
// 标签页首次出现可见信号时,把时间轴对齐到可见数据的范围。
|
||||||
|
if (anyVisible && !tab.ViewInitialized())
|
||||||
|
{
|
||||||
|
m_plot.FitAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool laneCountChanged = (laneCount != previousLaneCount);
|
||||||
|
if (laneCountChanged || force)
|
||||||
|
{
|
||||||
|
// 网格数量变化会影响滚动条与绘图区宽度,必须重新布局。
|
||||||
|
Relayout();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_plot.RefreshLanes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::SyncTabsToDataSet()
|
||||||
|
{
|
||||||
|
for (TabConfig& tab : m_tabs)
|
||||||
|
{
|
||||||
|
tab.SyncToDataSet(m_dataSet);
|
||||||
|
}
|
||||||
|
m_panel.InvalidateRows();
|
||||||
|
|
||||||
|
// 强制下一次同步重新计算视图状态。
|
||||||
|
m_cachedVisibleCount = -1;
|
||||||
|
m_cachedAnyVisible = false;
|
||||||
|
m_cachedLaneCount = 0;
|
||||||
|
|
||||||
|
if (m_hwnd != nullptr)
|
||||||
|
{
|
||||||
|
InvalidateRect(m_hwnd, nullptr, TRUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::RemoveFile(int fileIndex)
|
||||||
|
{
|
||||||
|
const DataFile* file = m_dataSet.FileAt(fileIndex);
|
||||||
|
if (file == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::wstring name = file->fileName;
|
||||||
|
|
||||||
|
// 先按旧下标修正各标签页配置,再从数据层移除,最后统一对齐。
|
||||||
|
for (TabConfig& tab : m_tabs)
|
||||||
|
{
|
||||||
|
tab.RemapAfterFileRemoval(fileIndex);
|
||||||
|
}
|
||||||
|
m_dataSet.RemoveFile(fileIndex);
|
||||||
|
SyncTabsToDataSet();
|
||||||
|
|
||||||
|
SetStatusMessage(L"已从工作区移除:" + name, false);
|
||||||
|
UpdateWindowTitle();
|
||||||
|
}
|
||||||
|
|
||||||
void Application::Relayout()
|
void Application::Relayout()
|
||||||
{
|
{
|
||||||
RECT client = {};
|
RECT client = {};
|
||||||
@@ -245,25 +429,29 @@ namespace jdi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const float tabBarHeight = m_tabBar.DesiredHeight();
|
||||||
const float toolbarHeight = m_toolbar.DesiredHeight();
|
const float toolbarHeight = m_toolbar.DesiredHeight();
|
||||||
const float statusHeight = ScaleOf(kStatusBarHeight, m_scale);
|
const float statusHeight = ScaleOf(kStatusBarHeight, m_scale);
|
||||||
const float splitterWidth = ScaleOf(kSplitterWidth, m_scale);
|
const float splitterWidth = ScaleOf(kSplitterWidth, m_scale);
|
||||||
|
|
||||||
m_toolbarRect = gd::RectF(0.0f, 0.0f, width, toolbarHeight);
|
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);
|
m_statusRect = gd::RectF(0.0f, height - statusHeight, width, statusHeight);
|
||||||
|
|
||||||
const float contentHeight = std::max(0.0f, height - toolbarHeight - statusHeight);
|
const float contentTop = m_toolbarRect.Y + m_toolbarRect.Height;
|
||||||
|
const float contentHeight = std::max(0.0f, m_statusRect.Y - contentTop);
|
||||||
|
|
||||||
float panelWidth = ScaleOf(m_panelWidth, m_scale);
|
float panelWidth = ScaleOf(m_panelWidth, m_scale);
|
||||||
const float maximum = std::min(ScaleOf(kMaxPanelWidth, m_scale), width * 0.45f);
|
const float maximum = std::min(ScaleOf(kMaxPanelWidth, m_scale), width * 0.45f);
|
||||||
panelWidth = std::max(ScaleOf(kMinPanelWidth, m_scale), std::min(panelWidth, maximum));
|
panelWidth = std::max(ScaleOf(kMinPanelWidth, m_scale), std::min(panelWidth, maximum));
|
||||||
m_panelWidth = panelWidth / m_scale;
|
m_panelWidth = panelWidth / m_scale;
|
||||||
|
|
||||||
m_panelRect = gd::RectF(0.0f, toolbarHeight, panelWidth, contentHeight);
|
m_panelRect = gd::RectF(0.0f, contentTop, panelWidth, contentHeight);
|
||||||
m_splitterRect = gd::RectF(panelWidth, toolbarHeight, splitterWidth, contentHeight);
|
m_splitterRect = gd::RectF(panelWidth, contentTop, splitterWidth, contentHeight);
|
||||||
m_plotRect = gd::RectF(panelWidth + splitterWidth, toolbarHeight,
|
m_plotRect = gd::RectF(panelWidth + splitterWidth, contentTop,
|
||||||
std::max(0.0f, width - panelWidth - splitterWidth), contentHeight);
|
std::max(0.0f, width - panelWidth - splitterWidth), contentHeight);
|
||||||
|
|
||||||
|
m_tabBar.Layout(m_tabBarRect);
|
||||||
m_toolbar.Layout(m_toolbarRect);
|
m_toolbar.Layout(m_toolbarRect);
|
||||||
m_panel.Layout(m_panelRect);
|
m_panel.Layout(m_panelRect);
|
||||||
m_plot.Layout(m_plotRect);
|
m_plot.Layout(m_plotRect);
|
||||||
@@ -340,16 +528,11 @@ namespace jdi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 网格数量变化(如合并/拆分/移动信号)会影响滚动条与绘图区宽度,需要重新布局。
|
// 任何交互(勾选、移动网格、切换标签页)之后都在此统一收敛视图状态。
|
||||||
const std::size_t laneCount = m_document.IsLoaded() ? m_document.Lanes().size() : 0;
|
SynchronizeView(false);
|
||||||
if (laneCount != m_lastLaneCount)
|
|
||||||
{
|
|
||||||
m_lastLaneCount = laneCount;
|
|
||||||
Relayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
m_toolbar.SetDocumentLoaded(m_document.IsLoaded());
|
m_toolbar.SetDocumentLoaded(!m_dataSet.Empty());
|
||||||
if (m_document.IsLoaded())
|
if (!m_dataSet.Empty())
|
||||||
{
|
{
|
||||||
m_toolbar.SetStatusText(L"视图 " + FormatValue(m_plot.ViewMinTime()) + L" ~ " +
|
m_toolbar.SetStatusText(L"视图 " + FormatValue(m_plot.ViewMinTime()) + L" ~ " +
|
||||||
FormatValue(m_plot.ViewMaxTime()));
|
FormatValue(m_plot.ViewMaxTime()));
|
||||||
@@ -368,6 +551,7 @@ namespace jdi
|
|||||||
UiFillRect(g, gd::RectF(0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height)),
|
UiFillRect(g, gd::RectF(0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height)),
|
||||||
m_theme.windowBg);
|
m_theme.windowBg);
|
||||||
|
|
||||||
|
m_tabBar.Paint(g);
|
||||||
m_toolbar.Paint(g);
|
m_toolbar.Paint(g);
|
||||||
m_panel.Paint(g);
|
m_panel.Paint(g);
|
||||||
m_plot.Paint(g);
|
m_plot.Paint(g);
|
||||||
@@ -395,19 +579,20 @@ namespace jdi
|
|||||||
const float halfWidth = m_statusRect.Width * 0.5f;
|
const float halfWidth = m_statusRect.Width * 0.5f;
|
||||||
|
|
||||||
std::wstring leftText;
|
std::wstring leftText;
|
||||||
if (m_document.IsLoaded())
|
if (!m_dataSet.Empty())
|
||||||
{
|
{
|
||||||
leftText = m_document.FileName() + L" · " +
|
const TabConfig& tab = ActiveTab();
|
||||||
FormatCount(static_cast<unsigned long long>(m_document.SampleCount())) + L" 采样点 · " +
|
leftText = std::to_wstring(m_dataSet.FileCount()) + L" 个文件 · " +
|
||||||
std::to_wstring(m_document.Signals().size()) + L" 信号 · " +
|
FormatCount(static_cast<unsigned long long>(m_dataSet.SignalCount())) + L" 条信号 · " +
|
||||||
std::to_wstring(m_document.Lanes().size()) + L" 网格";
|
std::to_wstring(tab.VisibleCount()) + L" 条已选 · " +
|
||||||
|
std::to_wstring(tab.Lanes().size()) + L" 个网格";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
leftText = L"未加载数据 · 请点击「打开 CSV」或拖拽文件到窗口";
|
leftText = L"未加载数据 · 请点击「打开 CSV」或拖拽文件到窗口";
|
||||||
}
|
}
|
||||||
|
|
||||||
UiText(g, leftText, m_fonts.uiSmall,
|
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),
|
gd::RectF(m_statusRect.X + padding, m_statusRect.Y, halfWidth - padding, m_statusRect.Height),
|
||||||
m_theme.text, TextAlign::LEFT);
|
m_theme.text, TextAlign::LEFT);
|
||||||
|
|
||||||
@@ -418,13 +603,13 @@ namespace jdi
|
|||||||
rightText = m_statusMessage;
|
rightText = m_statusMessage;
|
||||||
rightColor = m_statusIsError ? m_theme.danger : m_theme.textStrong;
|
rightColor = m_statusIsError ? m_theme.danger : m_theme.textStrong;
|
||||||
}
|
}
|
||||||
else if (m_plot.HasCursor() && m_document.IsLoaded())
|
else if (m_plot.HasCursor() && !m_dataSet.Empty())
|
||||||
{
|
{
|
||||||
rightText = L"光标 · " + FormatValue(m_plot.CursorTime());
|
rightText = L"光标 · " + FormatValue(m_plot.CursorTime());
|
||||||
rightColor = m_theme.text;
|
rightColor = m_theme.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
UiText(g, rightText, m_fonts.uiSmall,
|
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),
|
gd::RectF(m_statusRect.X + halfWidth, m_statusRect.Y, halfWidth - padding, m_statusRect.Height),
|
||||||
rightColor, TextAlign::RIGHT);
|
rightColor, TextAlign::RIGHT);
|
||||||
}
|
}
|
||||||
@@ -438,75 +623,123 @@ namespace jdi
|
|||||||
void Application::UpdateWindowTitle()
|
void Application::UpdateWindowTitle()
|
||||||
{
|
{
|
||||||
std::wstring title = kWindowTitle;
|
std::wstring title = kWindowTitle;
|
||||||
if (m_document.IsLoaded())
|
if (m_activeTab >= 0 && m_activeTab < static_cast<int>(m_tabs.size()))
|
||||||
{
|
{
|
||||||
title += L" — ";
|
title += L" — ";
|
||||||
title += m_document.FileName();
|
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, title.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::OpenFileDialog()
|
void Application::OpenFileDialog()
|
||||||
{
|
{
|
||||||
wchar_t buffer[MAX_PATH * 4] = {};
|
// 允许一次选择多个文件,便于跨文件对比信号。
|
||||||
|
std::vector<wchar_t> buffer(static_cast<std::size_t>(MAX_PATH) * 16, L'\0');
|
||||||
|
|
||||||
OPENFILENAMEW dialog = {};
|
OPENFILENAMEW dialog = {};
|
||||||
dialog.lStructSize = sizeof(dialog);
|
dialog.lStructSize = sizeof(dialog);
|
||||||
dialog.hwndOwner = m_hwnd;
|
dialog.hwndOwner = m_hwnd;
|
||||||
dialog.lpstrFilter = L"CSV 数据文件 (*.csv)\0*.csv\0文本数据文件 (*.txt)\0*.txt\0所有文件 (*.*)\0*.*\0\0";
|
dialog.lpstrFilter = L"CSV 数据文件 (*.csv)\0*.csv\0文本数据文件 (*.txt)\0*.txt\0所有文件 (*.*)\0*.*\0\0";
|
||||||
dialog.lpstrFile = buffer;
|
dialog.lpstrFile = buffer.data();
|
||||||
dialog.nMaxFile = MAX_PATH * 4;
|
dialog.nMaxFile = static_cast<DWORD>(buffer.size());
|
||||||
dialog.lpstrTitle = L"打开仿真数据文件";
|
dialog.lpstrTitle = L"打开仿真数据文件(可多选)";
|
||||||
dialog.lpstrDefExt = L"csv";
|
dialog.lpstrDefExt = L"csv";
|
||||||
dialog.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_EXPLORER | OFN_NOCHANGEDIR;
|
dialog.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_EXPLORER | OFN_NOCHANGEDIR |
|
||||||
|
OFN_ALLOWMULTISELECT;
|
||||||
|
|
||||||
if (GetOpenFileNameW(&dialog) == FALSE)
|
if (GetOpenFileNameW(&dialog) == FALSE)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LoadFile(buffer);
|
|
||||||
|
// 多选时首段为目录,其余为文件名;单选时整段即为完整路径。
|
||||||
|
const std::wstring first(buffer.data());
|
||||||
|
const wchar_t* cursor = buffer.data() + first.size() + 1;
|
||||||
|
|
||||||
|
int loaded = 0;
|
||||||
|
if (*cursor == L'\0')
|
||||||
|
{
|
||||||
|
if (LoadFile(first))
|
||||||
|
{
|
||||||
|
++loaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
while (*cursor != L'\0')
|
||||||
|
{
|
||||||
|
const std::wstring name(cursor);
|
||||||
|
if (LoadFile(first + L"\\" + name))
|
||||||
|
{
|
||||||
|
++loaded;
|
||||||
|
}
|
||||||
|
cursor += name.size() + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loaded > 0)
|
||||||
|
{
|
||||||
|
SetStatusMessage(L"已加载 " + std::to_wstring(loaded) +
|
||||||
|
L" 个文件,请在左侧勾选需要查看的信号。",
|
||||||
|
false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Application::LoadFile(const std::wstring& path)
|
bool Application::LoadFile(const std::wstring& path)
|
||||||
{
|
{
|
||||||
|
if (path.empty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int existing = m_dataSet.IndexOfFile(path);
|
||||||
|
if (existing >= 0)
|
||||||
|
{
|
||||||
|
const DataFile* file = m_dataSet.FileAt(existing);
|
||||||
|
SetStatusMessage(L"该文件已在工作区中:" + (file != nullptr ? file->fileName : path), true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
CsvTable table;
|
CsvTable table;
|
||||||
std::wstring error;
|
std::wstring error;
|
||||||
if (!LoadCsvTable(path, table, error))
|
if (!LoadCsvTable(path, table, error))
|
||||||
{
|
{
|
||||||
SetStatusMessage(L"解析失败:" + error, true);
|
SetStatusMessage(L"解析失败:" + error, true);
|
||||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
SignalSet loaded;
|
int fileIndex = -1;
|
||||||
if (!loaded.BuildFromTable(table, path, error))
|
if (!m_dataSet.AddFromTable(table, path, error, fileIndex))
|
||||||
{
|
{
|
||||||
SetStatusMessage(L"解析失败:" + error, true);
|
SetStatusMessage(L"解析失败:" + error, true);
|
||||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_document = std::move(loaded);
|
// 新文件的信号在全部标签页中默认均为未勾选状态,由用户自行决定查看内容。
|
||||||
m_panel.SetDocument(&m_document);
|
SyncTabsToDataSet();
|
||||||
m_plot.SetDocument(&m_document);
|
UpdateWindowTitle();
|
||||||
m_plot.FitAll();
|
|
||||||
Relayout();
|
|
||||||
|
|
||||||
std::wstring message = L"已加载 " + m_document.FileName() + L"(" +
|
const DataFile* file = m_dataSet.FileAt(fileIndex);
|
||||||
FormatCount(static_cast<unsigned long long>(m_document.SampleCount())) +
|
if (file != nullptr)
|
||||||
L" 行采样,时间列:" + m_document.TimeColumnName() + L")";
|
{
|
||||||
if (!m_document.TimeMonotonic())
|
std::wstring message = L"已加载 " + file->fileName + L"(" +
|
||||||
|
FormatCount(static_cast<unsigned long long>(file->time.size())) +
|
||||||
|
L" 行采样," + std::to_wstring(file->signals.size()) +
|
||||||
|
L" 条信号,时间列:" + file->timeColumnName + L")";
|
||||||
|
if (!file->timeMonotonic)
|
||||||
{
|
{
|
||||||
message += L" 提示:时间列非单调递增,光标取值使用最近邻策略。";
|
message += L" 提示:时间列非单调递增,光标取值使用最近邻策略。";
|
||||||
}
|
}
|
||||||
SetStatusMessage(message, false);
|
SetStatusMessage(message, false);
|
||||||
|
}
|
||||||
UpdateWindowTitle();
|
|
||||||
InvalidateRect(m_hwnd, nullptr, TRUE);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::LoadCommandLineFile()
|
void Application::LoadCommandLineFiles()
|
||||||
{
|
{
|
||||||
int argumentCount = 0;
|
int argumentCount = 0;
|
||||||
LPWSTR* arguments = CommandLineToArgvW(GetCommandLineW(), &argumentCount);
|
LPWSTR* arguments = CommandLineToArgvW(GetCommandLineW(), &argumentCount);
|
||||||
@@ -515,6 +748,7 @@ namespace jdi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int loaded = 0;
|
||||||
for (int index = 1; index < argumentCount; ++index)
|
for (int index = 1; index < argumentCount; ++index)
|
||||||
{
|
{
|
||||||
const std::wstring argument = arguments[index];
|
const std::wstring argument = arguments[index];
|
||||||
@@ -522,10 +756,19 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
LoadFile(argument);
|
if (LoadFile(argument))
|
||||||
break;
|
{
|
||||||
|
++loaded;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
LocalFree(arguments);
|
LocalFree(arguments);
|
||||||
|
|
||||||
|
if (loaded > 0)
|
||||||
|
{
|
||||||
|
SetStatusMessage(L"已加载 " + std::to_wstring(loaded) +
|
||||||
|
L" 个文件,请在左侧勾选需要查看的信号。",
|
||||||
|
false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::ExecuteToolbarAction(ToolbarAction action)
|
void Application::ExecuteToolbarAction(ToolbarAction action)
|
||||||
@@ -538,9 +781,6 @@ namespace jdi
|
|||||||
case ToolbarAction::FIT_ALL:
|
case ToolbarAction::FIT_ALL:
|
||||||
m_plot.FitAll();
|
m_plot.FitAll();
|
||||||
break;
|
break;
|
||||||
case ToolbarAction::FIT_TIME:
|
|
||||||
m_plot.FitTime();
|
|
||||||
break;
|
|
||||||
case ToolbarAction::ZOOM_IN:
|
case ToolbarAction::ZOOM_IN:
|
||||||
m_plot.ZoomTimeStep(1.4);
|
m_plot.ZoomTimeStep(1.4);
|
||||||
break;
|
break;
|
||||||
@@ -548,34 +788,45 @@ namespace jdi
|
|||||||
m_plot.ZoomTimeStep(1.0 / 1.4);
|
m_plot.ZoomTimeStep(1.0 / 1.4);
|
||||||
break;
|
break;
|
||||||
case ToolbarAction::MERGE_LANES:
|
case ToolbarAction::MERGE_LANES:
|
||||||
if (m_document.IsLoaded())
|
if (!m_dataSet.Empty())
|
||||||
{
|
{
|
||||||
m_document.MergeIntoSingleLane();
|
ActiveTab().SetMode(m_dataSet, LaneMode::MERGED);
|
||||||
m_panel.SetDocument(&m_document);
|
SetStatusMessage(L"已将当前标签页的全部可见信号合并到同一网格。", false);
|
||||||
m_plot.FitAll();
|
|
||||||
Relayout();
|
|
||||||
SetStatusMessage(L"已将全部信号合并到同一网格。", false);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ToolbarAction::SPLIT_LANES:
|
case ToolbarAction::SPLIT_LANES:
|
||||||
if (m_document.IsLoaded())
|
if (!m_dataSet.Empty())
|
||||||
{
|
{
|
||||||
m_document.SplitIntoLanes();
|
ActiveTab().SetMode(m_dataSet, LaneMode::PER_SIGNAL);
|
||||||
m_panel.SetDocument(&m_document);
|
SetStatusMessage(L"当前标签页已切换为「每信号一格」。", false);
|
||||||
m_plot.FitAll();
|
|
||||||
Relayout();
|
|
||||||
SetStatusMessage(L"已按信号拆分为独立网格。", false);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ToolbarAction::RESET_VIEW:
|
case ToolbarAction::SELECT_ALL:
|
||||||
m_plot.FitAll();
|
if (!m_dataSet.Empty())
|
||||||
|
{
|
||||||
|
ActiveTab().SetAllVisible(m_dataSet, true);
|
||||||
|
m_panel.InvalidateRows();
|
||||||
|
SetStatusMessage(L"已勾选当前标签页全部信号。", false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ToolbarAction::CLEAR_ALL:
|
||||||
|
if (!m_dataSet.Empty())
|
||||||
|
{
|
||||||
|
ActiveTab().SetAllVisible(m_dataSet, false);
|
||||||
|
m_panel.InvalidateRows();
|
||||||
|
SetStatusMessage(L"已清空当前标签页的信号勾选。", false);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case ToolbarAction::NONE:
|
case ToolbarAction::NONE:
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_hwnd != nullptr)
|
||||||
|
{
|
||||||
InvalidateRect(m_hwnd, nullptr, TRUE);
|
InvalidateRect(m_hwnd, nullptr, TRUE);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool Application::HandleKeyDown(WPARAM key)
|
bool Application::HandleKeyDown(WPARAM key)
|
||||||
{
|
{
|
||||||
@@ -586,6 +837,26 @@ namespace jdi
|
|||||||
OpenFileDialog();
|
OpenFileDialog();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (control && key == L'T')
|
||||||
|
{
|
||||||
|
NewTab();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (control && key == L'W')
|
||||||
|
{
|
||||||
|
CloseTab(m_activeTab);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (control && key == VK_TAB)
|
||||||
|
{
|
||||||
|
if (m_tabs.size() > 1)
|
||||||
|
{
|
||||||
|
const bool backward = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
|
||||||
|
const int count = static_cast<int>(m_tabs.size());
|
||||||
|
SetActiveTab((m_activeTab + (backward ? count - 1 : 1)) % count);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
switch (key)
|
switch (key)
|
||||||
{
|
{
|
||||||
@@ -621,11 +892,6 @@ namespace jdi
|
|||||||
m_plot.FitAll();
|
m_plot.FitAll();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (key == L'G')
|
|
||||||
{
|
|
||||||
m_plot.FitTime();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -639,8 +905,8 @@ namespace jdi
|
|||||||
if (m_draggingSplitter && m_scale > 0.0f)
|
if (m_draggingSplitter && m_scale > 0.0f)
|
||||||
{
|
{
|
||||||
const float desired = static_cast<float>(x - m_splitterGrabOffset) / m_scale;
|
const float desired = static_cast<float>(x - m_splitterGrabOffset) / m_scale;
|
||||||
const float maximum = std::min(kMaxPanelWidth,
|
const float maximum =
|
||||||
static_cast<float>(kMinClientWidth) / m_scale);
|
std::min(kMaxPanelWidth, static_cast<float>(kMinClientWidth) / m_scale);
|
||||||
m_panelWidth = std::max(kMinPanelWidth, std::min(desired, maximum));
|
m_panelWidth = std::max(kMinPanelWidth, std::min(desired, maximum));
|
||||||
Relayout();
|
Relayout();
|
||||||
return true;
|
return true;
|
||||||
@@ -652,6 +918,8 @@ namespace jdi
|
|||||||
return m_panel.OnMouseMove(x, y);
|
return m_panel.OnMouseMove(x, y);
|
||||||
case Target::TOOLBAR:
|
case Target::TOOLBAR:
|
||||||
return m_toolbar.OnMouseMove(x, y);
|
return m_toolbar.OnMouseMove(x, y);
|
||||||
|
case Target::TAB_BAR:
|
||||||
|
return m_tabBar.OnMouseMove(x, y);
|
||||||
case Target::NONE:
|
case Target::NONE:
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@@ -659,12 +927,84 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
changed = (m_toolbar.HitTest(x, y) ? m_toolbar.OnMouseMove(x, y) : m_toolbar.OnMouseLeave()) || changed;
|
changed =
|
||||||
|
(m_tabBar.HitTest(x, y) ? m_tabBar.OnMouseMove(x, y) : m_tabBar.OnMouseLeave()) || changed;
|
||||||
|
changed =
|
||||||
|
(m_toolbar.HitTest(x, y) ? m_toolbar.OnMouseMove(x, y) : m_toolbar.OnMouseLeave()) || changed;
|
||||||
changed = (m_panel.HitTest(x, y) ? m_panel.OnMouseMove(x, y) : m_panel.OnMouseLeave()) || changed;
|
changed = (m_panel.HitTest(x, y) ? m_panel.OnMouseMove(x, y) : m_panel.OnMouseLeave()) || changed;
|
||||||
changed = (m_plot.HitTest(x, y) ? m_plot.OnMouseMove(x, y) : m_plot.OnMouseLeave()) || changed;
|
changed = (m_plot.HitTest(x, y) ? m_plot.OnMouseMove(x, y) : m_plot.OnMouseLeave()) || changed;
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Application::OnTabBarResult(const TabBarResult& result)
|
||||||
|
{
|
||||||
|
switch (result.type)
|
||||||
|
{
|
||||||
|
case TabBarResult::Type::ADD:
|
||||||
|
NewTab();
|
||||||
|
break;
|
||||||
|
case TabBarResult::Type::SELECT:
|
||||||
|
if (result.index != m_activeTab)
|
||||||
|
{
|
||||||
|
SetActiveTab(result.index);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TabBarResult::Type::CLOSE:
|
||||||
|
CloseTab(result.index);
|
||||||
|
break;
|
||||||
|
case TabBarResult::Type::CONTEXT:
|
||||||
|
ShowTabContextMenu(result);
|
||||||
|
break;
|
||||||
|
case TabBarResult::Type::NONE:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::ShowTabContextMenu(const TabBarResult& result)
|
||||||
|
{
|
||||||
|
if (result.index < 0 || result.index >= static_cast<int>(m_tabs.size()))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HMENU menu = CreatePopupMenu();
|
||||||
|
if (menu == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuTabNew), L"新建标签页");
|
||||||
|
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuTabClose), L"关闭该标签页");
|
||||||
|
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuTabCloseOthers), L"关闭其它标签页");
|
||||||
|
if (m_tabs.size() <= 1)
|
||||||
|
{
|
||||||
|
EnableMenuItem(menu, static_cast<UINT>(kMenuTabClose), MF_BYCOMMAND | MF_GRAYED);
|
||||||
|
EnableMenuItem(menu, static_cast<UINT>(kMenuTabCloseOthers), MF_BYCOMMAND | MF_GRAYED);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetForegroundWindow(m_hwnd);
|
||||||
|
const int command = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_LEFTALIGN | TPM_TOPALIGN,
|
||||||
|
result.screenX, result.screenY, 0, m_hwnd, nullptr);
|
||||||
|
DestroyMenu(menu);
|
||||||
|
|
||||||
|
if (command == kMenuTabNew)
|
||||||
|
{
|
||||||
|
NewTab();
|
||||||
|
}
|
||||||
|
else if (command == kMenuTabClose)
|
||||||
|
{
|
||||||
|
CloseTab(result.index);
|
||||||
|
}
|
||||||
|
else if (command == kMenuTabCloseOthers)
|
||||||
|
{
|
||||||
|
const TabConfig keep = m_tabs[static_cast<std::size_t>(result.index)];
|
||||||
|
m_tabs.clear();
|
||||||
|
m_tabs.push_back(keep);
|
||||||
|
SetActiveTab(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LRESULT Application::HandleMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
LRESULT Application::HandleMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||||
{
|
{
|
||||||
switch (message)
|
switch (message)
|
||||||
@@ -731,7 +1071,8 @@ namespace jdi
|
|||||||
case WM_MOUSELEAVE:
|
case WM_MOUSELEAVE:
|
||||||
{
|
{
|
||||||
m_trackingLeave = false;
|
m_trackingLeave = false;
|
||||||
bool changed = m_toolbar.OnMouseLeave();
|
bool changed = m_tabBar.OnMouseLeave();
|
||||||
|
changed = m_toolbar.OnMouseLeave() || changed;
|
||||||
changed = m_panel.OnMouseLeave() || changed;
|
changed = m_panel.OnMouseLeave() || changed;
|
||||||
changed = m_plot.OnMouseLeave() || changed;
|
changed = m_plot.OnMouseLeave() || changed;
|
||||||
if (changed)
|
if (changed)
|
||||||
@@ -755,6 +1096,15 @@ namespace jdi
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_tabBar.HitTest(x, y))
|
||||||
|
{
|
||||||
|
SetCapture(hwnd);
|
||||||
|
m_activeTarget = Target::TAB_BAR;
|
||||||
|
m_tabBar.OnLButtonDown(x, y);
|
||||||
|
InvalidateRect(hwnd, nullptr, FALSE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_toolbar.HitTest(x, y))
|
if (m_toolbar.HitTest(x, y))
|
||||||
{
|
{
|
||||||
SetCapture(hwnd);
|
SetCapture(hwnd);
|
||||||
@@ -771,7 +1121,7 @@ namespace jdi
|
|||||||
m_activeTarget = Target::PANEL;
|
m_activeTarget = Target::PANEL;
|
||||||
if (m_panel.OnLButtonDown(x, y))
|
if (m_panel.OnLButtonDown(x, y))
|
||||||
{
|
{
|
||||||
InvalidateRect(hwnd, nullptr, FALSE);
|
InvalidateRect(hwnd, nullptr, TRUE);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -796,19 +1146,23 @@ namespace jdi
|
|||||||
const int y = GET_Y_LPARAM(lParam);
|
const int y = GET_Y_LPARAM(lParam);
|
||||||
ReleaseCapture();
|
ReleaseCapture();
|
||||||
|
|
||||||
bool changed = false;
|
|
||||||
switch (m_activeTarget)
|
switch (m_activeTarget)
|
||||||
{
|
{
|
||||||
case Target::SPLITTER:
|
case Target::SPLITTER:
|
||||||
m_draggingSplitter = false;
|
m_draggingSplitter = false;
|
||||||
break;
|
break;
|
||||||
|
case Target::TAB_BAR:
|
||||||
|
{
|
||||||
|
TabBarResult result;
|
||||||
|
m_tabBar.OnLButtonUp(x, y, result);
|
||||||
|
m_activeTarget = Target::NONE;
|
||||||
|
OnTabBarResult(result);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
case Target::TOOLBAR:
|
case Target::TOOLBAR:
|
||||||
{
|
{
|
||||||
ToolbarAction action = ToolbarAction::NONE;
|
ToolbarAction action = ToolbarAction::NONE;
|
||||||
if (m_toolbar.OnLButtonUp(x, y, action))
|
m_toolbar.OnLButtonUp(x, y, action);
|
||||||
{
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
m_activeTarget = Target::NONE;
|
m_activeTarget = Target::NONE;
|
||||||
if (action != ToolbarAction::NONE)
|
if (action != ToolbarAction::NONE)
|
||||||
{
|
{
|
||||||
@@ -818,10 +1172,10 @@ namespace jdi
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Target::PANEL:
|
case Target::PANEL:
|
||||||
changed = m_panel.OnLButtonUp(x, y) || changed;
|
m_panel.OnLButtonUp(x, y);
|
||||||
break;
|
break;
|
||||||
case Target::PLOT:
|
case Target::PLOT:
|
||||||
changed = m_plot.OnLButtonUp(x, y) || changed;
|
m_plot.OnLButtonUp(x, y);
|
||||||
break;
|
break;
|
||||||
case Target::NONE:
|
case Target::NONE:
|
||||||
default:
|
default:
|
||||||
@@ -829,11 +1183,7 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_activeTarget = Target::NONE;
|
m_activeTarget = Target::NONE;
|
||||||
changed = true;
|
InvalidateRect(hwnd, nullptr, TRUE);
|
||||||
if (changed)
|
|
||||||
{
|
|
||||||
InvalidateRect(hwnd, nullptr, FALSE);
|
|
||||||
}
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
case WM_LBUTTONDBLCLK:
|
case WM_LBUTTONDBLCLK:
|
||||||
@@ -851,7 +1201,7 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
if (changed)
|
if (changed)
|
||||||
{
|
{
|
||||||
InvalidateRect(hwnd, nullptr, FALSE);
|
InvalidateRect(hwnd, nullptr, TRUE);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -860,13 +1210,26 @@ namespace jdi
|
|||||||
const int x = GET_X_LPARAM(lParam);
|
const int x = GET_X_LPARAM(lParam);
|
||||||
const int y = GET_Y_LPARAM(lParam);
|
const int y = GET_Y_LPARAM(lParam);
|
||||||
|
|
||||||
|
POINT cursor = {x, y};
|
||||||
|
ClientToScreen(hwnd, &cursor);
|
||||||
|
|
||||||
|
if (m_tabBar.HitTest(x, y))
|
||||||
|
{
|
||||||
|
TabBarResult result;
|
||||||
|
if (m_tabBar.OnRButtonDown(x, y, cursor.x, cursor.y, result))
|
||||||
|
{
|
||||||
|
OnTabBarResult(result);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_panel.HitTest(x, y))
|
if (m_panel.HitTest(x, y))
|
||||||
{
|
{
|
||||||
SetCapture(hwnd);
|
SetCapture(hwnd);
|
||||||
m_activeTarget = Target::PANEL;
|
m_activeTarget = Target::PANEL;
|
||||||
if (m_panel.OnRButtonDown(x, y))
|
if (m_panel.OnRButtonDown(x, y))
|
||||||
{
|
{
|
||||||
InvalidateRect(hwnd, nullptr, FALSE);
|
InvalidateRect(hwnd, nullptr, TRUE);
|
||||||
}
|
}
|
||||||
ReleaseCapture();
|
ReleaseCapture();
|
||||||
m_activeTarget = Target::NONE;
|
m_activeTarget = Target::NONE;
|
||||||
@@ -955,23 +1318,44 @@ namespace jdi
|
|||||||
case WM_KEYDOWN:
|
case WM_KEYDOWN:
|
||||||
if (HandleKeyDown(wParam))
|
if (HandleKeyDown(wParam))
|
||||||
{
|
{
|
||||||
InvalidateRect(hwnd, nullptr, FALSE);
|
InvalidateRect(hwnd, nullptr, TRUE);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case kMessageRemoveFile:
|
||||||
|
{
|
||||||
|
RemoveFile(static_cast<int>(wParam));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
case WM_DROPFILES:
|
case WM_DROPFILES:
|
||||||
{
|
{
|
||||||
HDROP drop = reinterpret_cast<HDROP>(wParam);
|
HDROP drop = reinterpret_cast<HDROP>(wParam);
|
||||||
const UINT length = DragQueryFileW(drop, 0, nullptr, 0);
|
const UINT count = DragQueryFileW(drop, 0xFFFFFFFF, nullptr, 0);
|
||||||
if (length > 0)
|
int loaded = 0;
|
||||||
|
for (UINT index = 0; index < count; ++index)
|
||||||
{
|
{
|
||||||
|
const UINT length = DragQueryFileW(drop, index, nullptr, 0);
|
||||||
|
if (length == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
std::vector<wchar_t> buffer(static_cast<std::size_t>(length) + 1, L'\0');
|
std::vector<wchar_t> buffer(static_cast<std::size_t>(length) + 1, L'\0');
|
||||||
if (DragQueryFileW(drop, 0, buffer.data(), length + 1) > 0)
|
if (DragQueryFileW(drop, index, buffer.data(), length + 1) > 0)
|
||||||
{
|
{
|
||||||
LoadFile(std::wstring(buffer.data()));
|
if (LoadFile(std::wstring(buffer.data())))
|
||||||
|
{
|
||||||
|
++loaded;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DragFinish(drop);
|
DragFinish(drop);
|
||||||
|
|
||||||
|
if (loaded > 0)
|
||||||
|
{
|
||||||
|
SetStatusMessage(L"已加载 " + std::to_wstring(loaded) +
|
||||||
|
L" 个文件,请在左侧勾选需要查看的信号。",
|
||||||
|
false);
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
case WM_DESTROY:
|
case WM_DESTROY:
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "DataModel.hpp"
|
||||||
#include "PaintUtil.hpp"
|
#include "PaintUtil.hpp"
|
||||||
#include "SignalSet.hpp"
|
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
|
|
||||||
namespace jdi
|
namespace jdi
|
||||||
@@ -29,6 +29,9 @@ namespace jdi
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 波形绘图区:网格、时间轴、光标、缩放与自适应。
|
* @brief 波形绘图区:网格、时间轴、光标、缩放与自适应。
|
||||||
|
*
|
||||||
|
* 绘图区同时依赖数据层(DataSet,只读)与当前标签页配置(TabConfig,可写),
|
||||||
|
* 视图范围、网格滚动与时间光标均保存在标签页配置中,因此各标签页互不干扰。
|
||||||
*/
|
*/
|
||||||
class PlotView
|
class PlotView
|
||||||
{
|
{
|
||||||
@@ -43,10 +46,11 @@ namespace jdi
|
|||||||
void Setup(const UiFonts* fonts, const Theme* theme);
|
void Setup(const UiFonts* fonts, const Theme* theme);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 绑定数据集。
|
* @brief 绑定数据层与当前标签页配置。
|
||||||
* @param [IN] document 数据集指针,允许为空。
|
* @param [IN] dataSet 数据集合,允许为空。
|
||||||
|
* @param [IN] tab 当前标签页配置,允许为空。
|
||||||
*/
|
*/
|
||||||
void SetDocument(SignalSet* document);
|
void SetContext(const DataSet* dataSet, TabConfig* tab);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 设置 DPI 缩放比例。
|
* @brief 设置 DPI 缩放比例。
|
||||||
@@ -149,12 +153,7 @@ namespace jdi
|
|||||||
HCURSOR QueryCursor(int x, int y) const;
|
HCURSOR QueryCursor(int x, int y) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 时间轴自适应到全部数据范围。
|
* @brief 时间轴与幅值轴同时自适应到可见信号范围。
|
||||||
*/
|
|
||||||
void FitTime();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 时间轴与幅值轴同时自适应。
|
|
||||||
*/
|
*/
|
||||||
void FitAll();
|
void FitAll();
|
||||||
|
|
||||||
@@ -180,7 +179,7 @@ namespace jdi
|
|||||||
* @brief 获取网格当前内容总高度。
|
* @brief 获取网格当前内容总高度。
|
||||||
* @return 内容高度(像素)。
|
* @return 内容高度(像素)。
|
||||||
*/
|
*/
|
||||||
float LaneContentHeight() const;
|
float LaneContentHeight() const { return m_laneContentHeight; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 清除已固定的时间光标。
|
* @brief 清除已固定的时间光标。
|
||||||
@@ -191,31 +190,30 @@ namespace jdi
|
|||||||
* @brief 判断当前是否存在激活的时间光标。
|
* @brief 判断当前是否存在激活的时间光标。
|
||||||
* @return 存在返回 true。
|
* @return 存在返回 true。
|
||||||
*/
|
*/
|
||||||
bool HasCursor() const { return m_cursorVisible; }
|
bool HasCursor() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 获取当前激活光标对应的时间。
|
* @brief 获取当前激活光标对应的时间。
|
||||||
* @return 光标时间。
|
* @return 光标时间。
|
||||||
*/
|
*/
|
||||||
double CursorTime() const { return m_cursorTime; }
|
double CursorTime() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 获取当前视图左边界时间。
|
* @brief 获取当前视图左边界时间。
|
||||||
* @return 视图左边界时间。
|
* @return 视图左边界时间。
|
||||||
*/
|
*/
|
||||||
double ViewMinTime() const { return m_viewMin; }
|
double ViewMinTime() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 获取当前视图右边界时间。
|
* @brief 获取当前视图右边界时间。
|
||||||
* @return 视图右边界时间。
|
* @return 视图右边界时间。
|
||||||
*/
|
*/
|
||||||
double ViewMaxTime() const { return m_viewMax; }
|
double ViewMaxTime() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 获取绘图区(波形区域)矩形。
|
* @brief 重新生成网格几何(可见性或网格划分变化后调用)。
|
||||||
* @return 波形区域矩形。
|
|
||||||
*/
|
*/
|
||||||
const gd::RectF& WaveformRect() const { return m_plotRect; }
|
void RefreshLanes();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/**
|
/**
|
||||||
@@ -226,9 +224,57 @@ namespace jdi
|
|||||||
gd::RectF rect; /**< 网格波形区域。 */
|
gd::RectF rect; /**< 网格波形区域。 */
|
||||||
double yMin = 0.0; /**< 幅值下限。 */
|
double yMin = 0.0; /**< 幅值下限。 */
|
||||||
double yMax = 1.0; /**< 幅值上限。 */
|
double yMax = 1.0; /**< 幅值上限。 */
|
||||||
bool empty = true; /**< 是否没有可见信号。 */
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 已解析的信号视图:数据与显示配置的组合。
|
||||||
|
*/
|
||||||
|
struct ResolvedSignal
|
||||||
|
{
|
||||||
|
const SignalData* data = nullptr; /**< 信号数据。 */
|
||||||
|
const SignalStyle* style = nullptr; /**< 显示配置。 */
|
||||||
|
const DataFile* file = nullptr; /**< 所属文件。 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取当前生效的标签页配置(未绑定时返回内部占位对象)。
|
||||||
|
* @return 标签页配置引用。
|
||||||
|
*/
|
||||||
|
TabConfig& Tab() { return (m_tab != nullptr) ? *m_tab : m_localTab; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取当前生效的标签页配置(只读)。
|
||||||
|
* @return 标签页配置引用。
|
||||||
|
*/
|
||||||
|
const TabConfig& Tab() const { return (m_tab != nullptr) ? *m_tab : m_localTab; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断是否存在可绘制内容(已加载数据且至少有一条可见信号)。
|
||||||
|
* @return 可绘制返回 true。
|
||||||
|
*/
|
||||||
|
bool HasDrawableContent() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算当前视图应使用的时间范围。
|
||||||
|
* @param [OUT] low 时间下限。
|
||||||
|
* @param [OUT] high 时间上限。
|
||||||
|
*/
|
||||||
|
void TimeBounds(double& low, double& high) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 解析信号引用,得到数据与显示配置。
|
||||||
|
* @param [IN] ref 信号引用。
|
||||||
|
* @return 解析结果,引用无效时各指针为空。
|
||||||
|
*/
|
||||||
|
ResolvedSignal ResolveSignal(const SignalRef& ref) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断某网格内的可见信号是否来自多个文件。
|
||||||
|
* @param [IN] laneIndex 网格下标。
|
||||||
|
* @return 跨越多个文件返回 true。
|
||||||
|
*/
|
||||||
|
bool LaneSpansMultipleFiles(int laneIndex) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 重新计算各网格的布局与幅值范围。
|
* @brief 重新计算各网格的布局与幅值范围。
|
||||||
*/
|
*/
|
||||||
@@ -290,13 +336,13 @@ namespace jdi
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 构建单条信号在当前视图下的折线点序列。
|
* @brief 构建单条信号在当前视图下的折线点序列。
|
||||||
* @param [IN] signal 目标信号。
|
* @param [IN] resolved 已解析的信号视图。
|
||||||
* @param [IN] lane 所属网格绘制信息。
|
* @param [IN] lane 所属网格绘制信息。
|
||||||
* @param [IN] stepMode 是否使用阶梯保持连线。
|
* @param [IN] stepMode 是否使用阶梯保持连线。
|
||||||
* @param [OUT] points 输出点序列。
|
* @param [OUT] points 输出点序列。
|
||||||
* @param [OUT] breaks 输出每段折线的起始下标(用于处理缺失值断线)。
|
* @param [OUT] breaks 输出每段折线的起始下标(用于处理缺失值断线)。
|
||||||
*/
|
*/
|
||||||
void BuildPolyline(const Signal& signal, const LaneVisual& lane, bool stepMode,
|
void BuildPolyline(const ResolvedSignal& resolved, const LaneVisual& lane, bool stepMode,
|
||||||
std::vector<gd::PointF>& points, std::vector<std::size_t>& breaks) const;
|
std::vector<gd::PointF>& points, std::vector<std::size_t>& breaks) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -307,8 +353,8 @@ namespace jdi
|
|||||||
* @param [IN] timeTicks 时间刻度序列。
|
* @param [IN] timeTicks 时间刻度序列。
|
||||||
* @param [IN] timeStep 时间刻度步长。
|
* @param [IN] timeStep 时间刻度步长。
|
||||||
*/
|
*/
|
||||||
void PaintLane(gd::Graphics& g, int laneIndex, const LaneVisual& lane, const std::vector<double>& timeTicks,
|
void PaintLane(gd::Graphics& g, int laneIndex, const LaneVisual& lane,
|
||||||
double timeStep);
|
const std::vector<double>& timeTicks, double timeStep);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 绘制底部时间轴。
|
* @brief 绘制底部时间轴。
|
||||||
@@ -347,9 +393,17 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
void PaintLaneReadout(gd::Graphics& g, int laneIndex, const LaneVisual& lane);
|
void PaintLaneReadout(gd::Graphics& g, int laneIndex, const LaneVisual& lane);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 绘制空状态提示文本。
|
||||||
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
|
*/
|
||||||
|
void PaintEmptyState(gd::Graphics& g);
|
||||||
|
|
||||||
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
|
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
|
||||||
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
||||||
SignalSet* m_document = nullptr; /**< 数据集。 */
|
const DataSet* m_dataSet = nullptr; /**< 数据集合。 */
|
||||||
|
TabConfig* m_tab = nullptr; /**< 当前标签页配置。 */
|
||||||
|
TabConfig m_localTab; /**< 未绑定标签页时使用的占位配置。 */
|
||||||
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
||||||
|
|
||||||
gd::RectF m_area; /**< 绘图区整体区域。 */
|
gd::RectF m_area; /**< 绘图区整体区域。 */
|
||||||
@@ -357,21 +411,13 @@ namespace jdi
|
|||||||
gd::RectF m_timeAxisRect; /**< 时间轴区域。 */
|
gd::RectF m_timeAxisRect; /**< 时间轴区域。 */
|
||||||
gd::RectF m_scrollBarRect; /**< 网格滚动条区域。 */
|
gd::RectF m_scrollBarRect; /**< 网格滚动条区域。 */
|
||||||
|
|
||||||
float m_yAxisWidth = 64.0f; /**< 纵轴标签列宽度(逻辑像素)。 */
|
float m_yAxisWidth = 74.0f; /**< 纵轴标签列宽度(逻辑像素)。 */
|
||||||
float m_timeAxisHeight = 34.0f; /**< 时间轴高度(逻辑像素)。 */
|
float m_timeAxisHeight = 32.0f; /**< 时间轴高度(逻辑像素)。 */
|
||||||
float m_laneGap = 6.0f; /**< 网格间距(逻辑像素)。 */
|
float m_laneGap = 6.0f; /**< 网格间距(逻辑像素)。 */
|
||||||
float m_minLaneHeight = 46.0f; /**< 网格最小高度(逻辑像素)。 */
|
float m_minLaneHeight = 46.0f; /**< 网格最小高度(逻辑像素)。 */
|
||||||
float m_laneScroll = 0.0f; /**< 网格纵向滚动偏移量。 */
|
|
||||||
float m_laneContentHeight = 0.0f; /**< 网格内容总高度。 */
|
float m_laneContentHeight = 0.0f; /**< 网格内容总高度。 */
|
||||||
bool m_scrollBarVisible = false; /**< 是否需要显示滚动条。 */
|
bool m_scrollBarVisible = false; /**< 是否需要显示滚动条。 */
|
||||||
|
|
||||||
double m_viewMin = 0.0; /**< 视图左边界时间。 */
|
|
||||||
double m_viewMax = 1.0; /**< 视图右边界时间。 */
|
|
||||||
|
|
||||||
bool m_cursorVisible = false; /**< 是否存在激活光标。 */
|
|
||||||
bool m_cursorPinned = false; /**< 光标是否已被固定。 */
|
|
||||||
double m_cursorTime = 0.0; /**< 光标时间。 */
|
|
||||||
|
|
||||||
PlotDragMode m_dragMode = PlotDragMode::NONE; /**< 当前拖拽模式。 */
|
PlotDragMode m_dragMode = PlotDragMode::NONE; /**< 当前拖拽模式。 */
|
||||||
int m_dragLane = -1; /**< 拖拽所作用的网格下标。 */
|
int m_dragLane = -1; /**< 拖拽所作用的网格下标。 */
|
||||||
bool m_dragMoved = false; /**< 拖拽过程中是否发生了位移。 */
|
bool m_dragMoved = false; /**< 拖拽过程中是否发生了位移。 */
|
||||||
@@ -379,8 +425,6 @@ namespace jdi
|
|||||||
int m_dragStartY = 0; /**< 拖拽起点纵坐标。 */
|
int m_dragStartY = 0; /**< 拖拽起点纵坐标。 */
|
||||||
int m_dragLastX = 0; /**< 上一次拖拽横坐标。 */
|
int m_dragLastX = 0; /**< 上一次拖拽横坐标。 */
|
||||||
int m_dragLastY = 0; /**< 上一次拖拽纵坐标。 */
|
int m_dragLastY = 0; /**< 上一次拖拽纵坐标。 */
|
||||||
double m_dragStartViewMin = 0.0; /**< 拖拽起始视图左边界。 */
|
|
||||||
double m_dragStartViewMax = 1.0; /**< 拖拽起始视图右边界。 */
|
|
||||||
double m_dragAnchorValue = 0.0; /**< 纵轴缩放锚点幅值。 */
|
double m_dragAnchorValue = 0.0; /**< 纵轴缩放锚点幅值。 */
|
||||||
double m_dragAnchorRatio = 0.5; /**< 纵轴缩放锚点相对位置。 */
|
double m_dragAnchorRatio = 0.5; /**< 纵轴缩放锚点相对位置。 */
|
||||||
double m_dragStartYMin = 0.0; /**< 拖拽起始幅值下限。 */
|
double m_dragStartYMin = 0.0; /**< 拖拽起始幅值下限。 */
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -7,16 +7,20 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "DataModel.hpp"
|
||||||
#include "PaintUtil.hpp"
|
#include "PaintUtil.hpp"
|
||||||
#include "SignalSet.hpp"
|
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
|
|
||||||
namespace jdi
|
namespace jdi
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 左侧信号面板:信号列表、可见性开关与样式编辑。
|
* @brief 左侧信号面板:按文件分组的信号树、可见性勾选与样式编辑。
|
||||||
|
*
|
||||||
|
* 面板只读取数据层(DataSet),全部显示配置读写均落在当前标签页(TabConfig)上,
|
||||||
|
* 因此不同标签页的信号勾选与样式互不影响。
|
||||||
*/
|
*/
|
||||||
class SignalPanel
|
class SignalPanel
|
||||||
{
|
{
|
||||||
@@ -32,10 +36,28 @@ namespace jdi
|
|||||||
void Setup(HWND host, const UiFonts* fonts, const Theme* theme);
|
void Setup(HWND host, const UiFonts* fonts, const Theme* theme);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 绑定数据集。
|
* @brief 绑定数据层与当前标签页配置。
|
||||||
* @param [IN] document 数据集指针,允许为空。
|
* @param [IN] dataSet 数据集合,允许为空。
|
||||||
|
* @param [IN] tab 当前标签页配置,允许为空。
|
||||||
*/
|
*/
|
||||||
void SetDocument(SignalSet* document);
|
void SetContext(const DataSet* dataSet, TabConfig* tab);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 标记列表结构失效(数据或折叠状态变化后调用)。
|
||||||
|
*/
|
||||||
|
void InvalidateRows()
|
||||||
|
{
|
||||||
|
m_rowsDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断列表内容是否需要纵向滚动。
|
||||||
|
* @return 内容超出可视区域返回 true。
|
||||||
|
*/
|
||||||
|
bool NeedsScrollBar() const
|
||||||
|
{
|
||||||
|
return m_contentHeight > m_listRect.Height + 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 设置 DPI 缩放比例。
|
* @brief 设置 DPI 缩放比例。
|
||||||
@@ -88,7 +110,7 @@ namespace jdi
|
|||||||
bool OnDoubleClick(int x, int y);
|
bool OnDoubleClick(int x, int y);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 处理鼠标右键按下(弹出信号上下文菜单)。
|
* @brief 处理鼠标右键按下(弹出上下文菜单)。
|
||||||
* @param [IN] x 客户区横坐标。
|
* @param [IN] x 客户区横坐标。
|
||||||
* @param [IN] y 客户区纵坐标。
|
* @param [IN] y 客户区纵坐标。
|
||||||
* @return 需要重绘返回 true。
|
* @return 需要重绘返回 true。
|
||||||
@@ -116,7 +138,33 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
bool HitTest(int x, int y) const;
|
bool HitTest(int x, int y) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将列表滚动到指定信号所在位置。
|
||||||
|
* @param [IN] ref 目标信号引用。
|
||||||
|
*/
|
||||||
|
void ScrollToSignal(const SignalRef& ref);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
/**
|
||||||
|
* @brief 列表行的类型。
|
||||||
|
*/
|
||||||
|
enum class RowKind : int
|
||||||
|
{
|
||||||
|
FILE = 0, /**< 文件分组标题行。 */
|
||||||
|
SIGNAL = 1 /**< 信号行。 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 行的可交互区域。
|
||||||
|
*/
|
||||||
|
enum class RowPart : int
|
||||||
|
{
|
||||||
|
NONE = 0, /**< 未命中。 */
|
||||||
|
DISCLOSURE, /**< 折叠三角。 */
|
||||||
|
CHECK, /**< 复选框。 */
|
||||||
|
BODY /**< 行主体。 */
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 样式编辑区中的可点击条目类型。
|
* @brief 样式编辑区中的可点击条目类型。
|
||||||
*/
|
*/
|
||||||
@@ -131,8 +179,34 @@ namespace jdi
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 计算列表区域内容高度。
|
* @brief 列表中的一行(文件分组或信号)。
|
||||||
* @return 内容高度。
|
*/
|
||||||
|
struct ListRow
|
||||||
|
{
|
||||||
|
RowKind kind = RowKind::SIGNAL; /**< 行类型。 */
|
||||||
|
int fileIndex = -1; /**< 所属文件下标。 */
|
||||||
|
int signalIndex = -1; /**< 信号下标(仅信号行有效)。 */
|
||||||
|
float height = 0.0f; /**< 行高(像素)。 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重新构建列表行集合。
|
||||||
|
*/
|
||||||
|
void BuildRows();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 在需要时重建列表行集合。
|
||||||
|
*/
|
||||||
|
void EnsureRows();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 依据内容高度重算滚动条可见性与列表可用宽度。
|
||||||
|
*/
|
||||||
|
void RefreshScrollBar();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算列表内容总高度。
|
||||||
|
* @return 内容高度(像素)。
|
||||||
*/
|
*/
|
||||||
float ContentHeight() const;
|
float ContentHeight() const;
|
||||||
|
|
||||||
@@ -142,11 +216,13 @@ namespace jdi
|
|||||||
void ClampScroll();
|
void ClampScroll();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 查询指定坐标对应的信号行下标。
|
* @brief 查询坐标命中的行下标与行内区域。
|
||||||
|
* @param [IN] x 客户区横坐标。
|
||||||
* @param [IN] y 客户区纵坐标。
|
* @param [IN] y 客户区纵坐标。
|
||||||
|
* @param [OUT] part 行内区域类型。
|
||||||
* @return 行下标,未命中返回 -1。
|
* @return 行下标,未命中返回 -1。
|
||||||
*/
|
*/
|
||||||
int RowAtY(int y) const;
|
int HitRow(int x, int y, RowPart& part);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 计算行的纵向矩形。
|
* @brief 计算行的纵向矩形。
|
||||||
@@ -156,11 +232,18 @@ namespace jdi
|
|||||||
gd::RectF RowRect(int row) const;
|
gd::RectF RowRect(int row) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 绘制列表区域的单行。
|
* @brief 绘制文件分组标题行。
|
||||||
* @param [IN,OUT] g 绘图上下文。
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
* @param [IN] row 行下标。
|
* @param [IN] row 行下标。
|
||||||
*/
|
*/
|
||||||
void PaintRow(gd::Graphics& g, int row);
|
void PaintFileRow(gd::Graphics& g, int row);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 绘制信号行。
|
||||||
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
|
* @param [IN] row 行下标。
|
||||||
|
*/
|
||||||
|
void PaintSignalRow(gd::Graphics& g, int row);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 绘制样式编辑区。
|
* @brief 绘制样式编辑区。
|
||||||
@@ -168,13 +251,33 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
void PaintStyleSection(gd::Graphics& g);
|
void PaintStyleSection(gd::Graphics& g);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 绘制列表滚动条。
|
||||||
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
|
*/
|
||||||
|
void PaintScrollBar(gd::Graphics& g);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算滚动条滑块矩形。
|
||||||
|
* @return 滑块矩形,无需滚动时返回空矩形。
|
||||||
|
*/
|
||||||
|
gd::RectF ScrollThumbRect() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 弹出信号右键菜单。
|
* @brief 弹出信号右键菜单。
|
||||||
* @param [IN] signalIndex 目标信号下标。
|
* @param [IN] ref 目标信号引用。
|
||||||
* @param [IN] screenX 屏幕坐标横坐标。
|
* @param [IN] screenX 屏幕坐标横坐标。
|
||||||
* @param [IN] screenY 屏幕坐标纵坐标。
|
* @param [IN] screenY 屏幕坐标纵坐标。
|
||||||
*/
|
*/
|
||||||
void ShowSignalMenu(int signalIndex, int screenX, int screenY);
|
void ShowSignalMenu(const SignalRef& ref, int screenX, int screenY);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 弹出文件分组右键菜单。
|
||||||
|
* @param [IN] fileIndex 目标文件下标。
|
||||||
|
* @param [IN] screenX 屏幕坐标横坐标。
|
||||||
|
* @param [IN] screenY 屏幕坐标纵坐标。
|
||||||
|
*/
|
||||||
|
void ShowFileMenu(int fileIndex, int screenX, int screenY);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 弹出网格分配菜单。
|
* @brief 弹出网格分配菜单。
|
||||||
@@ -182,15 +285,22 @@ namespace jdi
|
|||||||
void ShowLaneMenu();
|
void ShowLaneMenu();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 判断全部信号是否均为可见状态。
|
* @brief 判断当前标签页中全部信号是否均为可见状态。
|
||||||
* @return 全部可见返回 true。
|
* @return 全部可见且至少有一条信号返回 true。
|
||||||
*/
|
*/
|
||||||
bool AreAllSignalsVisible() const;
|
bool AreAllSignalsVisible() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 统计全部信号数量。
|
||||||
|
* @return 信号总数。
|
||||||
|
*/
|
||||||
|
int TotalSignalCount() const;
|
||||||
|
|
||||||
HWND m_host = nullptr; /**< 宿主窗口句柄。 */
|
HWND m_host = nullptr; /**< 宿主窗口句柄。 */
|
||||||
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
|
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
|
||||||
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
||||||
SignalSet* m_document = nullptr; /**< 数据集。 */
|
const DataSet* m_dataSet = nullptr; /**< 数据集合。 */
|
||||||
|
TabConfig* m_tab = nullptr; /**< 当前标签页配置。 */
|
||||||
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
||||||
|
|
||||||
gd::RectF m_area; /**< 面板整体区域。 */
|
gd::RectF m_area; /**< 面板整体区域。 */
|
||||||
@@ -199,16 +309,24 @@ namespace jdi
|
|||||||
gd::RectF m_styleRect; /**< 样式编辑区域。 */
|
gd::RectF m_styleRect; /**< 样式编辑区域。 */
|
||||||
gd::RectF m_toggleAllRect; /**< 全部显示/隐藏按钮。 */
|
gd::RectF m_toggleAllRect; /**< 全部显示/隐藏按钮。 */
|
||||||
gd::RectF m_laneButtonRect; /**< 网格选择按钮。 */
|
gd::RectF m_laneButtonRect; /**< 网格选择按钮。 */
|
||||||
|
gd::RectF m_scrollBarRect; /**< 滚动条区域。 */
|
||||||
|
|
||||||
std::array<gd::RectF, 12> m_colorRects; /**< 颜色色块矩形。 */
|
std::array<gd::RectF, 12> m_colorRects; /**< 颜色色块矩形。 */
|
||||||
std::array<gd::RectF, 4> m_widthRects; /**< 线宽按钮矩形。 */
|
std::array<gd::RectF, 4> m_widthRects; /**< 线宽按钮矩形。 */
|
||||||
std::array<gd::RectF, 4> m_modeRects; /**< 绘制方式按钮矩形。 */
|
std::array<gd::RectF, 4> m_modeRects; /**< 绘制方式按钮矩形。 */
|
||||||
int m_colorCount = 0; /**< 实际可用的颜色数量。 */
|
int m_colorCount = 0; /**< 实际可用的颜色数量。 */
|
||||||
float m_widthRowLabelX = 0.0f; /**< 样式标签列起始横坐标。 */
|
|
||||||
float m_styleRowHeight = 0.0f; /**< 样式行高。 */
|
std::vector<ListRow> m_rows; /**< 列表行集合(惰性缓存)。 */
|
||||||
|
bool m_rowsDirty = true; /**< 列表行集合是否需要重建。 */
|
||||||
|
float m_contentHeight = 0.0f; /**< 列表内容总高度。 */
|
||||||
|
float m_listFullWidth = 0.0f; /**< 列表区不含滚动条的完整宽度。 */
|
||||||
|
bool m_scrollBarVisible = false; /**< 是否需要显示滚动条。 */
|
||||||
|
|
||||||
float m_scrollOffset = 0.0f; /**< 列表滚动偏移量。 */
|
float m_scrollOffset = 0.0f; /**< 列表滚动偏移量。 */
|
||||||
|
bool m_draggingScrollBar = false; /**< 是否正在拖拽滚动条。 */
|
||||||
|
int m_scrollGrabOffset = 0; /**< 拖拽时鼠标相对滑块顶部的偏移。 */
|
||||||
int m_hoverRow = -1; /**< 当前悬停行。 */
|
int m_hoverRow = -1; /**< 当前悬停行。 */
|
||||||
|
RowPart m_hoverPart = RowPart::NONE; /**< 当前悬停的行内区域。 */
|
||||||
StyleHit m_hoverStyle = StyleHit::NONE; /**< 当前悬停的样式条目类型。 */
|
StyleHit m_hoverStyle = StyleHit::NONE; /**< 当前悬停的样式条目类型。 */
|
||||||
int m_hoverStyleIndex = -1; /**< 当前悬停的样式条目下标。 */
|
int m_hoverStyleIndex = -1; /**< 当前悬停的样式条目下标。 */
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,179 @@
|
|||||||
|
#ifndef __TAB_BAR_HPP__
|
||||||
|
#define __TAB_BAR_HPP__
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <objidl.h>
|
||||||
|
#include <gdiplus.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "PaintUtil.hpp"
|
||||||
|
#include "Theme.hpp"
|
||||||
|
|
||||||
|
namespace jdi
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 标签栏交互结果。
|
||||||
|
*/
|
||||||
|
struct TabBarResult
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief 交互动作类型。
|
||||||
|
*/
|
||||||
|
enum class Type : int
|
||||||
|
{
|
||||||
|
NONE = 0, /**< 未触发任何动作。 */
|
||||||
|
SELECT, /**< 切换到指定标签页。 */
|
||||||
|
CLOSE, /**< 关闭指定标签页。 */
|
||||||
|
ADD, /**< 新建标签页。 */
|
||||||
|
CONTEXT /**< 请求弹出标签页上下文菜单。 */
|
||||||
|
};
|
||||||
|
|
||||||
|
Type type = Type::NONE; /**< 动作类型。 */
|
||||||
|
int index = -1; /**< 标签页下标(ADD 时无意义)。 */
|
||||||
|
int screenX = 0; /**< 上下文菜单请求的屏幕横坐标。 */
|
||||||
|
int screenY = 0; /**< 上下文菜单请求的屏幕纵坐标。 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 顶部标签栏:支持多视图切换、新建与关闭。
|
||||||
|
*/
|
||||||
|
class TabBar
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TabBar();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 绑定字体与主题资源。
|
||||||
|
* @param [IN] fonts 字体集合,需在标签栏生命周期内保持有效。
|
||||||
|
* @param [IN] theme 主题配色,需在标签栏生命周期内保持有效。
|
||||||
|
*/
|
||||||
|
void Setup(const UiFonts* fonts, const Theme* theme);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置 DPI 缩放比例。
|
||||||
|
* @param [IN] scale 缩放比例(1.0 对应 96 DPI)。
|
||||||
|
*/
|
||||||
|
void SetScale(float scale);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 更新标签页名称集合。
|
||||||
|
* @param [IN] names 标签页名称列表。
|
||||||
|
*/
|
||||||
|
void SetTabs(const std::vector<std::wstring>& names);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置当前激活的标签页下标。
|
||||||
|
* @param [IN] index 标签页下标,-1 表示无激活页。
|
||||||
|
*/
|
||||||
|
void SetActiveIndex(int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算标签栏期望高度。
|
||||||
|
* @return 期望高度(像素)。
|
||||||
|
*/
|
||||||
|
float DesiredHeight() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按给定区域重新排布标签与按钮。
|
||||||
|
* @param [IN] area 标签栏可用区域。
|
||||||
|
*/
|
||||||
|
void Layout(const gd::RectF& area);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 绘制标签栏。
|
||||||
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
|
*/
|
||||||
|
void Paint(gd::Graphics& g);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理鼠标移动。
|
||||||
|
* @param [IN] x 客户区横坐标。
|
||||||
|
* @param [IN] y 客户区纵坐标。
|
||||||
|
* @return 悬停状态发生变化返回 true。
|
||||||
|
*/
|
||||||
|
bool OnMouseMove(int x, int y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理鼠标左键按下。
|
||||||
|
* @param [IN] x 客户区横坐标。
|
||||||
|
* @param [IN] y 客户区纵坐标。
|
||||||
|
* @return 命中可点击元素返回 true。
|
||||||
|
*/
|
||||||
|
bool OnLButtonDown(int x, int y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理鼠标左键抬起并给出交互结果。
|
||||||
|
* @param [IN] x 客户区横坐标。
|
||||||
|
* @param [IN] y 客户区纵坐标。
|
||||||
|
* @param [OUT] result 交互结果。
|
||||||
|
* @return 视觉状态发生变化返回 true。
|
||||||
|
*/
|
||||||
|
bool OnLButtonUp(int x, int y, TabBarResult& result);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理鼠标右键按下(请求标签页上下文菜单)。
|
||||||
|
* @param [IN] x 客户区横坐标。
|
||||||
|
* @param [IN] y 客户区纵坐标。
|
||||||
|
* @param [IN] screenX 屏幕横坐标。
|
||||||
|
* @param [IN] screenY 屏幕纵坐标。
|
||||||
|
* @param [OUT] result 交互结果。
|
||||||
|
* @return 命中标签页返回 true。
|
||||||
|
*/
|
||||||
|
bool OnRButtonDown(int x, int y, int screenX, int screenY, TabBarResult& result);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理鼠标离开标签栏。
|
||||||
|
* @return 视觉状态发生变化返回 true。
|
||||||
|
*/
|
||||||
|
bool OnMouseLeave();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断坐标是否落在标签栏区域内。
|
||||||
|
* @param [IN] x 客户区横坐标。
|
||||||
|
* @param [IN] y 客户区纵坐标。
|
||||||
|
* @return 落在区域内返回 true。
|
||||||
|
*/
|
||||||
|
bool HitTest(int x, int y) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* @brief 单个标签页的布局信息。
|
||||||
|
*/
|
||||||
|
struct TabItem
|
||||||
|
{
|
||||||
|
std::wstring name; /**< 标签页名称。 */
|
||||||
|
gd::RectF rect; /**< 标签整体矩形。 */
|
||||||
|
gd::RectF closeRect; /**< 关闭按钮矩形。 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断坐标命中的标签页下标。
|
||||||
|
* @param [IN] x 客户区横坐标。
|
||||||
|
* @param [IN] y 客户区纵坐标。
|
||||||
|
* @return 标签页下标,未命中返回 -1。
|
||||||
|
*/
|
||||||
|
int TabAtPoint(int x, int y) const;
|
||||||
|
|
||||||
|
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
|
||||||
|
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
||||||
|
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
||||||
|
|
||||||
|
std::vector<TabItem> m_tabs; /**< 标签页布局集合。 */
|
||||||
|
gd::RectF m_addRect; /**< 新建按钮矩形。 */
|
||||||
|
gd::RectF m_area; /**< 标签栏整体区域。 */
|
||||||
|
|
||||||
|
int m_activeIndex = -1; /**< 当前激活标签页下标。 */
|
||||||
|
int m_hoverIndex = -1; /**< 当前悬停标签页下标。 */
|
||||||
|
bool m_hoverClose = false; /**< 是否悬停在关闭按钮上。 */
|
||||||
|
bool m_hoverAdd = false; /**< 是否悬停在新建按钮上。 */
|
||||||
|
int m_pressedIndex = -1; /**< 当前按下的标签页下标。 */
|
||||||
|
bool m_pressedClose = false; /**< 是否按在关闭按钮上。 */
|
||||||
|
bool m_pressedAdd = false; /**< 是否按在新建按钮上。 */
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace jdi
|
||||||
|
|
||||||
|
#endif // __TAB_BAR_HPP__
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
#include "TabBar.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace jdi
|
||||||
|
{
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const float kTabHeight = 32.0f; /**< 标签高度(逻辑像素)。 */
|
||||||
|
const float kTopMargin = 4.0f; /**< 标签顶部留白。 */
|
||||||
|
const float kLeftPadding = 8.0f; /**< 左侧留白。 */
|
||||||
|
const float kTabPadding = 14.0f; /**< 标签文本左右内边距。 */
|
||||||
|
const float kCloseSize = 16.0f; /**< 关闭按钮尺寸。 */
|
||||||
|
const float kCloseGap = 6.0f; /**< 文本与关闭按钮间距。 */
|
||||||
|
const float kAddSize = 24.0f; /**< 新建按钮尺寸。 */
|
||||||
|
const float kAddGap = 6.0f; /**< 新建按钮与末个标签的间距。 */
|
||||||
|
const float kCorner = 5.0f; /**< 标签圆角半径。 */
|
||||||
|
const float kMaxTabWidth = 200.0f; /**< 单个标签最大宽度(逻辑像素)。 */
|
||||||
|
}
|
||||||
|
|
||||||
|
TabBar::TabBar() = default;
|
||||||
|
|
||||||
|
void TabBar::Setup(const UiFonts* fonts, const Theme* theme)
|
||||||
|
{
|
||||||
|
m_fonts = fonts;
|
||||||
|
m_theme = theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabBar::SetScale(float scale)
|
||||||
|
{
|
||||||
|
m_scale = (scale > 0.1f) ? scale : 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabBar::SetTabs(const std::vector<std::wstring>& names)
|
||||||
|
{
|
||||||
|
m_tabs.clear();
|
||||||
|
m_tabs.resize(names.size());
|
||||||
|
for (std::size_t index = 0; index < names.size(); ++index)
|
||||||
|
{
|
||||||
|
m_tabs[index].name = names[index];
|
||||||
|
}
|
||||||
|
if (m_activeIndex >= static_cast<int>(m_tabs.size()))
|
||||||
|
{
|
||||||
|
m_activeIndex = m_tabs.empty() ? -1 : static_cast<int>(m_tabs.size()) - 1;
|
||||||
|
}
|
||||||
|
m_hoverIndex = -1;
|
||||||
|
m_hoverClose = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabBar::SetActiveIndex(int index)
|
||||||
|
{
|
||||||
|
m_activeIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
float TabBar::DesiredHeight() const
|
||||||
|
{
|
||||||
|
return (kTabHeight + kTopMargin) * m_scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabBar::Layout(const gd::RectF& area)
|
||||||
|
{
|
||||||
|
m_area = area;
|
||||||
|
if (m_fonts == nullptr || m_fonts->ui == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float tabHeight = kTabHeight * m_scale;
|
||||||
|
const float top = area.Y + kTopMargin * m_scale;
|
||||||
|
const float closeSize = kCloseSize * m_scale;
|
||||||
|
const float maxWidth = kMaxTabWidth * m_scale;
|
||||||
|
const float minWidth = 70.0f * m_scale;
|
||||||
|
|
||||||
|
float cursorX = area.X + kLeftPadding * m_scale;
|
||||||
|
|
||||||
|
for (TabItem& tab : m_tabs)
|
||||||
|
{
|
||||||
|
const float textWidth = UiMeasureTextWidth(m_fonts->ui, tab.name);
|
||||||
|
float width = kTabPadding * 2.0f * m_scale + textWidth + kCloseGap * m_scale + closeSize;
|
||||||
|
width = std::max(minWidth, std::min(width, maxWidth));
|
||||||
|
|
||||||
|
tab.rect = gd::RectF(cursorX, top, width, tabHeight);
|
||||||
|
tab.closeRect = gd::RectF(cursorX + width - kTabPadding * m_scale - closeSize,
|
||||||
|
top + (tabHeight - closeSize) * 0.5f, closeSize, closeSize);
|
||||||
|
|
||||||
|
cursorX += width + kAddGap * m_scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float addSize = kAddSize * m_scale;
|
||||||
|
m_addRect = gd::RectF(cursorX, top + (tabHeight - addSize) * 0.5f, addSize, addSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabBar::Paint(gd::Graphics& g)
|
||||||
|
{
|
||||||
|
if (m_theme == nullptr || m_fonts == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UiFillRect(g, m_area, m_theme->panelBg);
|
||||||
|
|
||||||
|
const float closeIconSize = 8.0f * m_scale;
|
||||||
|
|
||||||
|
for (std::size_t index = 0; index < m_tabs.size(); ++index)
|
||||||
|
{
|
||||||
|
const TabItem& tab = m_tabs[index];
|
||||||
|
const bool active = (static_cast<int>(index) == m_activeIndex);
|
||||||
|
const bool hovered = (static_cast<int>(index) == m_hoverIndex);
|
||||||
|
|
||||||
|
std::uint32_t background = m_theme->panelBg;
|
||||||
|
if (active)
|
||||||
|
{
|
||||||
|
background = m_theme->cardBg;
|
||||||
|
}
|
||||||
|
else if (hovered)
|
||||||
|
{
|
||||||
|
background = m_theme->hover;
|
||||||
|
}
|
||||||
|
UiFillRoundRect(g, tab.rect, kCorner * m_scale, background);
|
||||||
|
|
||||||
|
if (!active)
|
||||||
|
{
|
||||||
|
UiDrawRoundRect(g, tab.rect, kCorner * m_scale, m_theme->border, 1.0f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 激活页使用顶部强调条指示。
|
||||||
|
UiFillRoundRect(g, gd::RectF(tab.rect.X + kCorner * m_scale, tab.rect.Y,
|
||||||
|
tab.rect.Width - kCorner * 2.0f * m_scale, 2.5f * m_scale),
|
||||||
|
1.0f * m_scale, m_theme->accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float textLeft = tab.rect.X + kTabPadding * m_scale;
|
||||||
|
const float textWidth = tab.closeRect.X - kCloseGap * m_scale - textLeft;
|
||||||
|
const std::wstring label = UiEllipsize(g, tab.name, m_fonts->ui, std::max(8.0f, textWidth));
|
||||||
|
UiText(g, label, active ? m_fonts->uiBold : m_fonts->ui,
|
||||||
|
gd::RectF(textLeft, tab.rect.Y, std::max(8.0f, textWidth), tab.rect.Height),
|
||||||
|
active ? m_theme->textStrong : m_theme->text, TextAlign::LEFT);
|
||||||
|
|
||||||
|
// 关闭按钮:悬停时显示底色,图标始终可见。
|
||||||
|
const bool closeHot = hovered && m_hoverClose;
|
||||||
|
if (closeHot)
|
||||||
|
{
|
||||||
|
UiFillRoundRect(g, tab.closeRect, 3.0f * m_scale, m_theme->pressed);
|
||||||
|
}
|
||||||
|
const float cx = tab.closeRect.X + tab.closeRect.Width * 0.5f;
|
||||||
|
const float cy = tab.closeRect.Y + tab.closeRect.Height * 0.5f;
|
||||||
|
const std::uint32_t iconColor =
|
||||||
|
closeHot ? m_theme->danger : (active ? m_theme->text : m_theme->textMuted);
|
||||||
|
UiDrawLine(g, cx - closeIconSize * 0.5f, cy - closeIconSize * 0.5f, cx + closeIconSize * 0.5f,
|
||||||
|
cy + closeIconSize * 0.5f, iconColor, 1.4f * m_scale);
|
||||||
|
UiDrawLine(g, cx + closeIconSize * 0.5f, cy - closeIconSize * 0.5f, cx - closeIconSize * 0.5f,
|
||||||
|
cy + closeIconSize * 0.5f, iconColor, 1.4f * m_scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建按钮。
|
||||||
|
const std::uint32_t addBg = m_hoverAdd ? m_theme->hover : m_theme->panelBg;
|
||||||
|
UiFillRoundRect(g, m_addRect, kCorner * m_scale, addBg);
|
||||||
|
UiDrawRoundRect(g, m_addRect, kCorner * m_scale, m_theme->border, 1.0f);
|
||||||
|
|
||||||
|
const float ax = m_addRect.X + m_addRect.Width * 0.5f;
|
||||||
|
const float ay = m_addRect.Y + m_addRect.Height * 0.5f;
|
||||||
|
const float armLength = 4.5f * m_scale;
|
||||||
|
const std::uint32_t addColor = m_hoverAdd ? m_theme->accent : m_theme->text;
|
||||||
|
UiDrawLine(g, ax - armLength, ay, ax + armLength, ay, addColor, 1.6f * m_scale);
|
||||||
|
UiDrawLine(g, ax, ay - armLength, ax, ay + armLength, addColor, 1.6f * m_scale);
|
||||||
|
|
||||||
|
// 底部分隔线。
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TabBar::HitTest(int x, int y) const
|
||||||
|
{
|
||||||
|
return UiPointInRect(m_area, static_cast<float>(x), static_cast<float>(y));
|
||||||
|
}
|
||||||
|
|
||||||
|
int TabBar::TabAtPoint(int x, int y) const
|
||||||
|
{
|
||||||
|
const float fx = static_cast<float>(x);
|
||||||
|
const float fy = static_cast<float>(y);
|
||||||
|
for (std::size_t index = 0; index < m_tabs.size(); ++index)
|
||||||
|
{
|
||||||
|
if (UiPointInRect(m_tabs[index].rect, fx, fy))
|
||||||
|
{
|
||||||
|
return static_cast<int>(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TabBar::OnMouseMove(int x, int y)
|
||||||
|
{
|
||||||
|
const int index = TabAtPoint(x, y);
|
||||||
|
const bool overClose =
|
||||||
|
(index >= 0 && UiPointInRect(m_tabs[static_cast<std::size_t>(index)].closeRect,
|
||||||
|
static_cast<float>(x), static_cast<float>(y)));
|
||||||
|
const bool overAdd = UiPointInRect(m_addRect, static_cast<float>(x), static_cast<float>(y));
|
||||||
|
|
||||||
|
if (index == m_hoverIndex && overClose == m_hoverClose && overAdd == m_hoverAdd)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_hoverIndex = index;
|
||||||
|
m_hoverClose = overClose;
|
||||||
|
m_hoverAdd = overAdd;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TabBar::OnLButtonDown(int x, int y)
|
||||||
|
{
|
||||||
|
m_pressedIndex = -1;
|
||||||
|
m_pressedClose = false;
|
||||||
|
m_pressedAdd = false;
|
||||||
|
|
||||||
|
if (UiPointInRect(m_addRect, static_cast<float>(x), static_cast<float>(y)))
|
||||||
|
{
|
||||||
|
m_pressedAdd = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int index = TabAtPoint(x, y);
|
||||||
|
if (index < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_pressedIndex = index;
|
||||||
|
m_pressedClose = UiPointInRect(m_tabs[static_cast<std::size_t>(index)].closeRect,
|
||||||
|
static_cast<float>(x), static_cast<float>(y));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TabBar::OnLButtonUp(int x, int y, TabBarResult& result)
|
||||||
|
{
|
||||||
|
result = TabBarResult();
|
||||||
|
|
||||||
|
if (m_pressedAdd)
|
||||||
|
{
|
||||||
|
m_pressedAdd = false;
|
||||||
|
if (UiPointInRect(m_addRect, static_cast<float>(x), static_cast<float>(y)))
|
||||||
|
{
|
||||||
|
result.type = TabBarResult::Type::ADD;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_pressedIndex < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int index = m_pressedIndex;
|
||||||
|
const bool wasClose = m_pressedClose;
|
||||||
|
m_pressedIndex = -1;
|
||||||
|
m_pressedClose = false;
|
||||||
|
|
||||||
|
if (TabAtPoint(x, y) != index)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wasClose)
|
||||||
|
{
|
||||||
|
result.type = TabBarResult::Type::CLOSE;
|
||||||
|
result.index = index;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.type = TabBarResult::Type::SELECT;
|
||||||
|
result.index = index;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TabBar::OnRButtonDown(int x, int y, int screenX, int screenY, TabBarResult& result)
|
||||||
|
{
|
||||||
|
result = TabBarResult();
|
||||||
|
|
||||||
|
const int index = TabAtPoint(x, y);
|
||||||
|
if (index < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.type = TabBarResult::Type::CONTEXT;
|
||||||
|
result.index = index;
|
||||||
|
result.screenX = screenX;
|
||||||
|
result.screenY = screenY;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TabBar::OnMouseLeave()
|
||||||
|
{
|
||||||
|
if (m_hoverIndex < 0 && !m_hoverClose && !m_hoverAdd)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_hoverIndex = -1;
|
||||||
|
m_hoverClose = false;
|
||||||
|
m_hoverAdd = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace jdi
|
||||||
@@ -21,13 +21,13 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
NONE = 0, /**< 未触发任何动作。 */
|
NONE = 0, /**< 未触发任何动作。 */
|
||||||
OPEN_FILE, /**< 打开 CSV 文件。 */
|
OPEN_FILE, /**< 打开 CSV 文件。 */
|
||||||
FIT_ALL, /**< 时间与幅值同时自适应。 */
|
FIT_ALL, /**< 视图自适应到可见信号范围。 */
|
||||||
FIT_TIME, /**< 仅时间轴自适应。 */
|
|
||||||
ZOOM_IN, /**< 放大时间轴。 */
|
ZOOM_IN, /**< 放大时间轴。 */
|
||||||
ZOOM_OUT, /**< 缩小时间轴。 */
|
ZOOM_OUT, /**< 缩小时间轴。 */
|
||||||
MERGE_LANES, /**< 合并为单一网格。 */
|
MERGE_LANES, /**< 合并为单一网格。 */
|
||||||
SPLIT_LANES, /**< 按信号拆分为多网格。 */
|
SPLIT_LANES, /**< 按信号拆分为多网格。 */
|
||||||
RESET_VIEW /**< 恢复默认视图范围。 */
|
SELECT_ALL, /**< 勾选当前标签页全部信号。 */
|
||||||
|
CLEAR_ALL /**< 取消当前标签页全部信号勾选。 */
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -71,18 +71,20 @@ namespace jdi
|
|||||||
|
|
||||||
const Spec specs[] = {
|
const Spec specs[] = {
|
||||||
{ToolbarAction::OPEN_FILE, L"打开 CSV", true, false},
|
{ToolbarAction::OPEN_FILE, L"打开 CSV", true, false},
|
||||||
{ToolbarAction::FIT_ALL, L"适应窗口", false, true},
|
{ToolbarAction::FIT_ALL, L"适应视图", false, true},
|
||||||
{ToolbarAction::FIT_TIME, L"适应时间", false, true},
|
|
||||||
{ToolbarAction::ZOOM_IN, L"放大", false, true},
|
{ToolbarAction::ZOOM_IN, L"放大", false, true},
|
||||||
{ToolbarAction::ZOOM_OUT, L"缩小", false, true},
|
{ToolbarAction::ZOOM_OUT, L"缩小", false, true},
|
||||||
{ToolbarAction::MERGE_LANES, L"合并网格", false, true},
|
{ToolbarAction::MERGE_LANES, L"合并网格", false, true},
|
||||||
{ToolbarAction::SPLIT_LANES, L"每信号一格", false, true},
|
{ToolbarAction::SPLIT_LANES, L"每信号一格", false, true},
|
||||||
|
{ToolbarAction::SELECT_ALL, L"全选信号", false, true},
|
||||||
|
{ToolbarAction::CLEAR_ALL, L"清空勾选", false, true},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 按钮宽度先按固定预算计算,避免依赖绘图上下文。
|
// 按钮宽度按实际文本像素宽 + 固定内边距计算,避免中文标签被裁切。
|
||||||
float cursorX = area.X + kSidePadding * m_scale;
|
float cursorX = area.X + kSidePadding * m_scale;
|
||||||
const float top = area.Y + kTopPadding * m_scale;
|
const float top = area.Y + kTopPadding * m_scale;
|
||||||
const float height = kButtonHeight * m_scale;
|
const float height = kButtonHeight * m_scale;
|
||||||
|
const float horizontalPadding = kButtonPadding * 2.0f * m_scale;
|
||||||
|
|
||||||
for (const Spec& spec : specs)
|
for (const Spec& spec : specs)
|
||||||
{
|
{
|
||||||
@@ -91,8 +93,7 @@ namespace jdi
|
|||||||
button.label = spec.label;
|
button.label = spec.label;
|
||||||
button.primary = spec.primary;
|
button.primary = spec.primary;
|
||||||
button.needsDocument = spec.needsDocument;
|
button.needsDocument = spec.needsDocument;
|
||||||
button.width = (kButtonPadding * 2.0f +
|
button.width = horizontalPadding + UiMeasureTextWidth(m_fonts->ui, button.label);
|
||||||
static_cast<float>(button.label.size()) * 13.0f) * m_scale;
|
|
||||||
button.rect = gd::RectF(cursorX, top, button.width, height);
|
button.rect = gd::RectF(cursorX, top, button.width, height);
|
||||||
cursorX += button.width + kButtonGap * m_scale;
|
cursorX += button.width + kButtonGap * m_scale;
|
||||||
|
|
||||||
|
|||||||
@@ -79,8 +79,10 @@ UNICODE \
|
|||||||
# C++ 头文件目录
|
# C++ 头文件目录
|
||||||
CXX_INCLUDES_PATHS = \
|
CXX_INCLUDES_PATHS = \
|
||||||
Utilities/Inc \
|
Utilities/Inc \
|
||||||
|
Resources/Inc \
|
||||||
Components/Main/Inc \
|
Components/Main/Inc \
|
||||||
Components/Application/Inc \
|
Components/Application/Inc \
|
||||||
|
Components/TabBar/Inc \
|
||||||
Components/Toolbar/Inc \
|
Components/Toolbar/Inc \
|
||||||
Components/SignalPanel/Inc \
|
Components/SignalPanel/Inc \
|
||||||
Components/PlotView/Inc \
|
Components/PlotView/Inc \
|
||||||
@@ -90,6 +92,7 @@ CXX_SOURCES_PATHS = \
|
|||||||
Utilities/Src \
|
Utilities/Src \
|
||||||
Components/Main/Src \
|
Components/Main/Src \
|
||||||
Components/Application/Src \
|
Components/Application/Src \
|
||||||
|
Components/TabBar/Src \
|
||||||
Components/Toolbar/Src \
|
Components/Toolbar/Src \
|
||||||
Components/SignalPanel/Src \
|
Components/SignalPanel/Src \
|
||||||
Components/PlotView/Src \
|
Components/PlotView/Src \
|
||||||
@@ -99,9 +102,13 @@ CXX_EXTERA_SOURCES = \
|
|||||||
|
|
||||||
# Windows 资源文件脚本头文件路径
|
# Windows 资源文件脚本头文件路径
|
||||||
WIN_RESOURCE_INCLUDES_PATHS = \
|
WIN_RESOURCE_INCLUDES_PATHS = \
|
||||||
|
Resources \
|
||||||
|
Resources/Inc \
|
||||||
|
# Windows 资源文件脚本输入代码页(对应 INPUT_CHAR_ENCODING = UTF-8)
|
||||||
|
WIN_RESOURCE_CODEPAGE = 65001
|
||||||
# Windows 资源文件脚本列表
|
# Windows 资源文件脚本列表
|
||||||
WIN_RESOURCE_SCRIPTS = \
|
WIN_RESOURCE_SCRIPTS = \
|
||||||
|
Resources/JustDataInspector.rc \
|
||||||
|
|
||||||
##################################################################################
|
##################################################################################
|
||||||
|
|
||||||
@@ -216,7 +223,7 @@ $(BUILD_DIR)/$(SUB_DIR_RELEASE)/%.o: %.cpp Makefile | $(BUILD_DIR)/$(SUB_DIR_REL
|
|||||||
# Release 资源脚本文件编译
|
# Release 资源脚本文件编译
|
||||||
$(BUILD_DIR)/$(SUB_DIR_RELEASE)/%.o: %.rc Makefile | $(BUILD_DIR)/$(SUB_DIR_RELEASE)
|
$(BUILD_DIR)/$(SUB_DIR_RELEASE)/%.o: %.rc Makefile | $(BUILD_DIR)/$(SUB_DIR_RELEASE)
|
||||||
@echo ====== [Release] Windows Resource Script File "$<" Compiling... ======
|
@echo ====== [Release] Windows Resource Script File "$<" Compiling... ======
|
||||||
$(WIN_RES_COMPLIER) $(foreach path,$(WIN_RESOURCE_INCLUDES_PATHS),$(addprefix -I,$(path))) $< -o $@
|
$(WIN_RES_COMPLIER) -c $(WIN_RESOURCE_CODEPAGE) $(foreach path,$(WIN_RESOURCE_INCLUDES_PATHS),$(addprefix -I,$(path))) $< -o $@
|
||||||
|
|
||||||
# C Debug 目标文件编译
|
# C Debug 目标文件编译
|
||||||
$(BUILD_DIR)/$(SUB_DIR_DEBUG)/%.o: %.c Makefile | $(BUILD_DIR)/$(SUB_DIR_DEBUG)
|
$(BUILD_DIR)/$(SUB_DIR_DEBUG)/%.o: %.c Makefile | $(BUILD_DIR)/$(SUB_DIR_DEBUG)
|
||||||
@@ -231,7 +238,7 @@ $(BUILD_DIR)/$(SUB_DIR_DEBUG)/%.o: %.cpp Makefile | $(BUILD_DIR)/$(SUB_DIR_DEBUG
|
|||||||
# Debug 资源脚本文件编译
|
# Debug 资源脚本文件编译
|
||||||
$(BUILD_DIR)/$(SUB_DIR_DEBUG)/%.o: %.rc Makefile | $(BUILD_DIR)/$(SUB_DIR_DEBUG)
|
$(BUILD_DIR)/$(SUB_DIR_DEBUG)/%.o: %.rc Makefile | $(BUILD_DIR)/$(SUB_DIR_DEBUG)
|
||||||
@echo ====== [Debug] Windows Resource Script File "$<" Compiling... ======
|
@echo ====== [Debug] Windows Resource Script File "$<" Compiling... ======
|
||||||
$(WIN_RES_COMPLIER) $(foreach path,$(WIN_RESOURCE_INCLUDES_PATHS),$(addprefix -I,$(path))) $< -o $@
|
$(WIN_RES_COMPLIER) -c $(WIN_RESOURCE_CODEPAGE) $(foreach path,$(WIN_RESOURCE_INCLUDES_PATHS),$(addprefix -I,$(path))) $< -o $@
|
||||||
|
|
||||||
|
|
||||||
# 依赖关系
|
# 依赖关系
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#ifndef __RESOURCE_IDS_H__
|
||||||
|
#define __RESOURCE_IDS_H__
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file ResourceIds.h
|
||||||
|
* @brief Windows 资源脚本使用的资源标识符定义。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @brief 应用程序主图标资源标识符。 */
|
||||||
|
#define IDI_APP_ICON 1
|
||||||
|
|
||||||
|
/** @brief 应用程序清单资源标识符(等价于 CREATEPROCESS_MANIFEST_RESOURCE_ID)。 */
|
||||||
|
#define IDR_APP_MANIFEST 1
|
||||||
|
|
||||||
|
#endif // __RESOURCE_IDS_H__
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 295 KiB |
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||||
|
<assemblyIdentity
|
||||||
|
type="win32"
|
||||||
|
name="JustDataInspector.Application"
|
||||||
|
version="1.0.0.0"
|
||||||
|
processorArchitecture="*"/>
|
||||||
|
<description>Just DataInspector - CSV 仿真数据波形查看器</description>
|
||||||
|
|
||||||
|
<!-- 启用视觉样式(Common Controls v6),使窗口框架与系统控件保持 WinUI 风格。 -->
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity
|
||||||
|
type="win32"
|
||||||
|
name="Microsoft.Windows.Common-Controls"
|
||||||
|
version="6.0.0.0"
|
||||||
|
processorArchitecture="*"
|
||||||
|
publicKeyToken="6595b64144ccf1df"
|
||||||
|
language="*"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 声明 Per-Monitor V2 DPI 感知,配合代码中的 WM_DPICHANGED 处理实现多屏缩放自适应。 -->
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor,system</dpiAwareness>
|
||||||
|
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
<!-- 兼容性声明:声明支持的系统版本范围。 -->
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- Windows 10 / 11 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||||
|
<!-- Windows 8.1 -->
|
||||||
|
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||||
|
<!-- Windows 8 -->
|
||||||
|
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
|
||||||
|
<!-- Windows 7 -->
|
||||||
|
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#include <winresrc.h>
|
||||||
|
|
||||||
|
#include "ResourceIds.h"
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// 图标
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
IDI_APP_ICON ICON "JustDataInspector.ico"
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// 应用程序清单
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
IDR_APP_MANIFEST 24 "JustDataInspector.manifest"
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// 版本信息
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
VS_VERSION_INFO VERSIONINFO
|
||||||
|
FILEVERSION 1,0,0,0
|
||||||
|
PRODUCTVERSION 1,0,0,0
|
||||||
|
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||||
|
FILEFLAGS 0x0L
|
||||||
|
FILEOS VOS_NT_WINDOWS32
|
||||||
|
FILETYPE VFT_APP
|
||||||
|
FILESUBTYPE VFT2_UNKNOWN
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "080404B0"
|
||||||
|
BEGIN
|
||||||
|
VALUE "CompanyName", "Just DataInspector"
|
||||||
|
VALUE "FileDescription", "Just DataInspector - CSV 仿真数据波形查看器"
|
||||||
|
VALUE "FileVersion", "1.0.0.0"
|
||||||
|
VALUE "InternalName", "JustDataInspector"
|
||||||
|
VALUE "LegalCopyright", "Copyright (C) 2026"
|
||||||
|
VALUE "OriginalFilename", "Target.exe"
|
||||||
|
VALUE "ProductName", "Just DataInspector"
|
||||||
|
VALUE "ProductVersion", "1.0.0.0"
|
||||||
|
END
|
||||||
|
END
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x0804, 1200
|
||||||
|
END
|
||||||
|
END
|
||||||
@@ -0,0 +1,494 @@
|
|||||||
|
#ifndef __DATA_MODEL_HPP__
|
||||||
|
#define __DATA_MODEL_HPP__
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "CsvTable.hpp"
|
||||||
|
|
||||||
|
namespace jdi
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 波形绘制方式。
|
||||||
|
*/
|
||||||
|
enum class DrawMode : int
|
||||||
|
{
|
||||||
|
LINE = 0, /**< 相邻采样点之间以插值直线连接。 */
|
||||||
|
SCATTER = 1, /**< 仅绘制采样点散点。 */
|
||||||
|
LINE_SCATTER = 2, /**< 同时绘制插值直线与采样点散点。 */
|
||||||
|
STEP = 3 /**< 阶梯保持(零阶保持)连线。 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将绘制方式转换为中文显示名称。
|
||||||
|
* @param [IN] mode 绘制方式。
|
||||||
|
* @return 显示名称。
|
||||||
|
*/
|
||||||
|
const wchar_t* DrawModeName(DrawMode mode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 网格划分策略:决定信号可见性与网格数量的联动方式。
|
||||||
|
*/
|
||||||
|
enum class LaneMode : int
|
||||||
|
{
|
||||||
|
PER_SIGNAL = 0, /**< 每条可见信号独占一个网格(随可见性自动增删)。 */
|
||||||
|
MERGED = 1, /**< 全部可见信号叠加在同一网格。 */
|
||||||
|
CUSTOM = 2 /**< 由用户手动指定归属;可见性变化时仅清理空网格。 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 全局信号引用:文件下标 + 文件内信号下标。
|
||||||
|
*/
|
||||||
|
struct SignalRef
|
||||||
|
{
|
||||||
|
int fileIndex = -1; /**< 所属文件下标。 */
|
||||||
|
int signalIndex = -1; /**< 文件内信号下标。 */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断引用是否指向有效位置。
|
||||||
|
* @return 两个下标均非负返回 true。
|
||||||
|
*/
|
||||||
|
bool IsValid() const { return fileIndex >= 0 && signalIndex >= 0; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 相等比较。
|
||||||
|
* @param [IN] other 另一引用。
|
||||||
|
* @return 指向同一信号返回 true。
|
||||||
|
*/
|
||||||
|
bool operator==(const SignalRef& other) const
|
||||||
|
{
|
||||||
|
return fileIndex == other.fileIndex && signalIndex == other.signalIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 不等比较。
|
||||||
|
* @param [IN] other 另一引用。
|
||||||
|
* @return 指向不同信号返回 true。
|
||||||
|
*/
|
||||||
|
bool operator!=(const SignalRef& other) const { return !(*this == other); }
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 单条信号的元数据与采样值(纯数据,不含显示配置)。
|
||||||
|
*/
|
||||||
|
struct SignalData
|
||||||
|
{
|
||||||
|
std::wstring name; /**< 显示名称(已剥离 "[n]" 序号前缀)。 */
|
||||||
|
std::wstring rawHeader; /**< 原始列名。 */
|
||||||
|
std::wstring unit; /**< 从列名括号中提取的单位,可能为空。 */
|
||||||
|
std::vector<double> values; /**< 采样值,缺失处为 NaN。 */
|
||||||
|
double minValue = 0.0; /**< 有限采样值最小值。 */
|
||||||
|
double maxValue = 0.0; /**< 有限采样值最大值。 */
|
||||||
|
std::size_t finiteCount = 0; /**< 有限采样点数量。 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 一个已加载的数据文件及其自有时间轴。
|
||||||
|
*/
|
||||||
|
struct DataFile
|
||||||
|
{
|
||||||
|
std::wstring filePath; /**< 文件完整路径。 */
|
||||||
|
std::wstring fileName; /**< 文件名称。 */
|
||||||
|
std::wstring timeColumnName; /**< 时间列列名。 */
|
||||||
|
std::vector<double> time; /**< 时间轴数值。 */
|
||||||
|
std::vector<SignalData> signals; /**< 该文件包含的信号。 */
|
||||||
|
double minTime = 0.0; /**< 时间轴最小值。 */
|
||||||
|
double maxTime = 0.0; /**< 时间轴最大值。 */
|
||||||
|
bool timeMonotonic = true; /**< 时间轴是否单调不减。 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 数据集合:统一管理全部已加载文件,与视图配置无关。
|
||||||
|
*/
|
||||||
|
class DataSet
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief 清空全部已加载文件。
|
||||||
|
*/
|
||||||
|
void Clear();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 依据 CSV 解析结果追加一个数据文件。
|
||||||
|
* @param [IN] table CSV 解析结果。
|
||||||
|
* @param [IN] filePath 源文件路径。
|
||||||
|
* @param [OUT] error 失败原因描述(成功时清空)。
|
||||||
|
* @param [OUT] fileIndex 新文件下标,失败时为 -1。
|
||||||
|
* @return 追加成功返回 true。
|
||||||
|
*/
|
||||||
|
bool AddFromTable(const CsvTable& table, const std::wstring& filePath, std::wstring& error, int& fileIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 移除指定下标的文件。
|
||||||
|
* @param [IN] fileIndex 文件下标。
|
||||||
|
* @return 移除成功返回 true。
|
||||||
|
*/
|
||||||
|
bool RemoveFile(int fileIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断数据集合是否为空。
|
||||||
|
* @return 未加载任何文件返回 true。
|
||||||
|
*/
|
||||||
|
bool Empty() const { return m_files.empty(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取文件数量。
|
||||||
|
* @return 文件数量。
|
||||||
|
*/
|
||||||
|
std::size_t FileCount() const { return m_files.size(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取信号总数。
|
||||||
|
* @return 信号总数。
|
||||||
|
*/
|
||||||
|
std::size_t SignalCount() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取全部文件(只读)。
|
||||||
|
* @return 文件集合。
|
||||||
|
*/
|
||||||
|
const std::vector<DataFile>& Files() const { return m_files; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按下标获取文件。
|
||||||
|
* @param [IN] fileIndex 文件下标。
|
||||||
|
* @return 文件指针,下标非法时返回 nullptr。
|
||||||
|
*/
|
||||||
|
const DataFile* FileAt(int fileIndex) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 解析信号引用。
|
||||||
|
* @param [IN] ref 信号引用。
|
||||||
|
* @return 信号数据指针,引用无效时返回 nullptr。
|
||||||
|
*/
|
||||||
|
const SignalData* Resolve(const SignalRef& ref) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按路径查找已加载文件下标。
|
||||||
|
* @param [IN] filePath 文件完整路径。
|
||||||
|
* @return 文件下标,未找到返回 -1。
|
||||||
|
*/
|
||||||
|
int IndexOfFile(const std::wstring& filePath) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算全部文件时间范围的并集下限。
|
||||||
|
* @return 时间下限。
|
||||||
|
*/
|
||||||
|
double MinTime() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算全部文件时间范围的并集上限。
|
||||||
|
* @return 时间上限。
|
||||||
|
*/
|
||||||
|
double MaxTime() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<DataFile> m_files; /**< 已加载文件集合。 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 单个信号的显示配置(属于某个标签页)。
|
||||||
|
*/
|
||||||
|
struct SignalStyle
|
||||||
|
{
|
||||||
|
bool visible = false; /**< 是否参与绘制。默认关闭,由用户自行勾选。 */
|
||||||
|
std::uint32_t color = 0xFF268BD2; /**< 波形颜色,格式 0xAARRGGBB。 */
|
||||||
|
float lineWidth = 1.6f; /**< 线宽(逻辑像素,未乘 DPI 缩放)。 */
|
||||||
|
DrawMode mode = DrawMode::LINE; /**< 绘制方式。 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 一个波形网格(纵向泳道),可容纳多条信号叠加显示。
|
||||||
|
*/
|
||||||
|
struct Lane
|
||||||
|
{
|
||||||
|
std::wstring name; /**< 网格名称。 */
|
||||||
|
std::vector<SignalRef> signals; /**< 该网格包含的信号(按加入顺序)。 */
|
||||||
|
bool autoRange = true; /**< 是否依据信号取值范围自动计算 Y 轴。 */
|
||||||
|
double yMin = -1.0; /**< 手动 Y 轴下限。 */
|
||||||
|
double yMax = 1.0; /**< 手动 Y 轴上限。 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算某条信号在给定时刻的取值。
|
||||||
|
* @param [IN] file 信号所属文件。
|
||||||
|
* @param [IN] signal 目标信号。
|
||||||
|
* @param [IN] time 采样时刻。
|
||||||
|
* @param [IN] interpolate 是否在相邻采样点之间做线性插值。
|
||||||
|
* @param [OUT] value 输出采样值,缺失时为 NaN。
|
||||||
|
* @return 时间点落在数据范围内且取到有效值返回 true。
|
||||||
|
*/
|
||||||
|
bool SampleFileAt(const DataFile& file, const SignalData& signal, double time, bool interpolate, double& value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 在指定文件的时间轴上查找与给定时刻最接近的采样点。
|
||||||
|
* @param [IN] file 目标文件。
|
||||||
|
* @param [IN] time 采样时刻。
|
||||||
|
* @return 采样点下标,文件为空时返回 -1。
|
||||||
|
*/
|
||||||
|
int FindNearestSample(const DataFile& file, double time);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 单个标签页的独立视图配置(信号可见性、样式、网格划分与视图范围)。
|
||||||
|
*/
|
||||||
|
class TabConfig
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TabConfig();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取标签页名称。
|
||||||
|
* @return 名称。
|
||||||
|
*/
|
||||||
|
const std::wstring& Name() const { return m_name; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置标签页名称。
|
||||||
|
* @param [IN] name 新名称。
|
||||||
|
*/
|
||||||
|
void SetName(const std::wstring& name) { m_name = name; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 依据数据集合调整配置容量(新增文件补默认配置,移除文件清理配置)。
|
||||||
|
* @param [IN] dataSet 数据集合。
|
||||||
|
*/
|
||||||
|
void SyncToDataSet(const DataSet& dataSet);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 在数据层移除某文件之前,按旧下标修正本配置(不重建网格)。
|
||||||
|
* @param [IN] fileIndex 即将被移除的文件下标。
|
||||||
|
* @note 调用顺序必须为:各标签页 RemapAfterFileRemoval → 数据层 RemoveFile → 各标签页 SyncToDataSet。
|
||||||
|
*/
|
||||||
|
void RemapAfterFileRemoval(int fileIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取指定信号的显示配置(只读)。
|
||||||
|
* @param [IN] ref 信号引用。
|
||||||
|
* @return 配置指针,引用无效时返回 nullptr。
|
||||||
|
*/
|
||||||
|
const SignalStyle* Style(const SignalRef& ref) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取指定信号的显示配置(可写)。
|
||||||
|
* @param [IN] ref 信号引用。
|
||||||
|
* @return 配置指针,引用无效时返回 nullptr。
|
||||||
|
*/
|
||||||
|
SignalStyle* Style(const SignalRef& ref);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断指定信号是否可见。
|
||||||
|
* @param [IN] ref 信号引用。
|
||||||
|
* @return 可见返回 true。
|
||||||
|
*/
|
||||||
|
bool IsVisible(const SignalRef& ref) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置指定信号的可见性,并同步网格划分。
|
||||||
|
* @param [IN] dataSet 数据集合。
|
||||||
|
* @param [IN] ref 信号引用。
|
||||||
|
* @param [IN] visible 目标可见状态。
|
||||||
|
*/
|
||||||
|
void SetVisible(const DataSet& dataSet, const SignalRef& ref, bool visible);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 切换指定信号的可见性,并同步网格划分。
|
||||||
|
* @param [IN] dataSet 数据集合。
|
||||||
|
* @param [IN] ref 信号引用。
|
||||||
|
*/
|
||||||
|
void ToggleVisible(const DataSet& dataSet, const SignalRef& ref);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置某个文件全部信号的可见性,并同步网格划分。
|
||||||
|
* @param [IN] dataSet 数据集合。
|
||||||
|
* @param [IN] fileIndex 文件下标。
|
||||||
|
* @param [IN] visible 目标可见状态。
|
||||||
|
*/
|
||||||
|
void SetFileVisible(const DataSet& dataSet, int fileIndex, bool visible);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置全部信号可见性,并同步网格划分。
|
||||||
|
* @param [IN] dataSet 数据集合。
|
||||||
|
* @param [IN] visible 目标可见状态。
|
||||||
|
*/
|
||||||
|
void SetAllVisible(const DataSet& dataSet, bool visible);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 仅显示指定信号,其余全部隐藏。
|
||||||
|
* @param [IN] dataSet 数据集合。
|
||||||
|
* @param [IN] ref 需要保持可见的信号。
|
||||||
|
*/
|
||||||
|
void SoloSignal(const DataSet& dataSet, const SignalRef& ref);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断某文件是否存在可见信号。
|
||||||
|
* @param [IN] dataSet 数据集合。
|
||||||
|
* @param [IN] fileIndex 文件下标。
|
||||||
|
* @return 存在可见信号返回 true。
|
||||||
|
*/
|
||||||
|
bool FileHasVisible(const DataSet& dataSet, int fileIndex) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断某文件是否全部信号可见。
|
||||||
|
* @param [IN] dataSet 数据集合。
|
||||||
|
* @param [IN] fileIndex 文件下标。
|
||||||
|
* @return 全部可见且至少有一条信号返回 true。
|
||||||
|
*/
|
||||||
|
bool FileAllVisible(const DataSet& dataSet, int fileIndex) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断是否存在任何可见信号。
|
||||||
|
* @return 存在返回 true。
|
||||||
|
*/
|
||||||
|
bool AnyVisible() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 统计可见信号数量。
|
||||||
|
* @return 可见信号数量。
|
||||||
|
*/
|
||||||
|
int VisibleCount() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取网格划分策略。
|
||||||
|
* @return 策略枚举。
|
||||||
|
*/
|
||||||
|
LaneMode Mode() const { return m_laneMode; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置网格划分策略并同步网格。
|
||||||
|
* @param [IN] dataSet 数据集合。
|
||||||
|
* @param [IN] mode 目标策略。
|
||||||
|
*/
|
||||||
|
void SetMode(const DataSet& dataSet, LaneMode mode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 依据当前可见性与策略重新生成网格划分。
|
||||||
|
* @param [IN] dataSet 数据集合。
|
||||||
|
*/
|
||||||
|
void SyncLanes(const DataSet& dataSet);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取网格集合(只读)。
|
||||||
|
* @return 网格集合。
|
||||||
|
*/
|
||||||
|
const std::vector<Lane>& Lanes() const { return m_lanes; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取网格集合(可写)。
|
||||||
|
* @return 网格集合。
|
||||||
|
*/
|
||||||
|
std::vector<Lane>& Lanes() { return m_lanes; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 查询某信号所属网格下标。
|
||||||
|
* @param [IN] ref 信号引用。
|
||||||
|
* @return 网格下标,未找到返回 -1。
|
||||||
|
*/
|
||||||
|
int LaneOfSignal(const SignalRef& ref) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将信号移动到指定网格,并切换为自定义划分策略。
|
||||||
|
* @param [IN] dataSet 数据集合。
|
||||||
|
* @param [IN] ref 信号引用。
|
||||||
|
* @param [IN] laneIndex 目标网格下标,传入负值表示新建网格。
|
||||||
|
* @return 移动后该信号所在的网格下标。
|
||||||
|
*/
|
||||||
|
int MoveSignalToLane(const DataSet& dataSet, const SignalRef& ref, int laneIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 查询某文件的分组是否处于折叠状态。
|
||||||
|
* @param [IN] fileIndex 文件下标。
|
||||||
|
* @return 折叠状态。
|
||||||
|
*/
|
||||||
|
bool IsCollapsed(int fileIndex) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 切换某文件分组的折叠状态。
|
||||||
|
* @param [IN] fileIndex 文件下标。
|
||||||
|
*/
|
||||||
|
void ToggleCollapsed(int fileIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取当前选中的信号引用。
|
||||||
|
* @return 信号引用。
|
||||||
|
*/
|
||||||
|
SignalRef Selected() const { return m_selected; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置当前选中的信号。
|
||||||
|
* @param [IN] ref 信号引用,无效值表示取消选择。
|
||||||
|
*/
|
||||||
|
void SetSelected(const SignalRef& ref) { m_selected = ref; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算某网格的 Y 轴显示范围。
|
||||||
|
* @param [IN] dataSet 数据集合。
|
||||||
|
* @param [IN] laneIndex 网格下标。
|
||||||
|
* @param [OUT] yMin 输出下限。
|
||||||
|
* @param [OUT] yMax 输出上限。
|
||||||
|
*/
|
||||||
|
void ComputeLaneRange(const DataSet& dataSet, int laneIndex, double& yMin, double& yMax) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算全部可见信号覆盖的时间范围。
|
||||||
|
* @param [IN] dataSet 数据集合。
|
||||||
|
* @param [OUT] minTime 输出下限。
|
||||||
|
* @param [OUT] maxTime 输出上限。
|
||||||
|
* @return 存在可见信号返回 true。
|
||||||
|
*/
|
||||||
|
bool ComputeVisibleTimeBounds(const DataSet& dataSet, double& minTime, double& maxTime) const;
|
||||||
|
|
||||||
|
/// @brief 视图左边界时间(可读写)。
|
||||||
|
double& ViewMin() { return m_viewMin; }
|
||||||
|
/// @brief 视图左边界时间(只读)。
|
||||||
|
double ViewMin() const { return m_viewMin; }
|
||||||
|
/// @brief 视图右边界时间(可读写)。
|
||||||
|
double& ViewMax() { return m_viewMax; }
|
||||||
|
/// @brief 视图右边界时间(只读)。
|
||||||
|
double ViewMax() const { return m_viewMax; }
|
||||||
|
/// @brief 视图范围是否已初始化(可读写)。
|
||||||
|
bool& ViewInitialized() { return m_viewInitialized; }
|
||||||
|
/// @brief 网格纵向滚动偏移量(可读写)。
|
||||||
|
float& LaneScroll() { return m_laneScroll; }
|
||||||
|
/// @brief 时间光标是否可见(可读写)。
|
||||||
|
bool& CursorVisible() { return m_cursorVisible; }
|
||||||
|
/// @brief 时间光标是否可见(只读)。
|
||||||
|
bool CursorVisible() const { return m_cursorVisible; }
|
||||||
|
/// @brief 时间光标是否已固定(可读写)。
|
||||||
|
bool& CursorPinned() { return m_cursorPinned; }
|
||||||
|
/// @brief 时间光标是否已固定(只读)。
|
||||||
|
bool CursorPinned() const { return m_cursorPinned; }
|
||||||
|
/// @brief 时间光标位置(可读写)。
|
||||||
|
double& CursorTime() { return m_cursorTime; }
|
||||||
|
/// @brief 时间光标位置(只读)。
|
||||||
|
double CursorTime() const { return m_cursorTime; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* @brief 为新增信号补默认样式(含调色板取色)。
|
||||||
|
* @param [IN] dataSet 数据集合。
|
||||||
|
* @param [IN] fileIndex 文件下标。
|
||||||
|
* @param [IN] signalIndex 信号下标。
|
||||||
|
*/
|
||||||
|
void EnsureStyle(const DataSet& dataSet, int fileIndex, int signalIndex);
|
||||||
|
|
||||||
|
std::wstring m_name; /**< 标签页名称。 */
|
||||||
|
std::vector<std::vector<SignalStyle>> m_styles; /**< 显示配置,按 [文件][信号] 索引。 */
|
||||||
|
std::vector<bool> m_collapsed; /**< 文件分组折叠状态,按文件索引。 */
|
||||||
|
std::vector<Lane> m_lanes; /**< 网格划分。 */
|
||||||
|
LaneMode m_laneMode = LaneMode::PER_SIGNAL; /**< 网格划分策略。 */
|
||||||
|
SignalRef m_selected; /**< 当前选中信号。 */
|
||||||
|
double m_viewMin = 0.0; /**< 视图左边界时间。 */
|
||||||
|
double m_viewMax = 1.0; /**< 视图右边界时间。 */
|
||||||
|
bool m_viewInitialized = false; /**< 视图范围是否已初始化。 */
|
||||||
|
float m_laneScroll = 0.0f; /**< 网格纵向滚动偏移量。 */
|
||||||
|
bool m_cursorVisible = false; /**< 时间光标是否可见。 */
|
||||||
|
bool m_cursorPinned = false; /**< 时间光标是否已固定。 */
|
||||||
|
double m_cursorTime = 0.0; /**< 时间光标位置。 */
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace jdi
|
||||||
|
|
||||||
|
#endif // __DATA_MODEL_HPP__
|
||||||
@@ -148,6 +148,14 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
float UiTextWidth(gd::Graphics& g, const std::wstring& text, HFONT font);
|
float UiTextWidth(gd::Graphics& g, const std::wstring& text, HFONT font);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 测量单行文本的像素宽度(无需 GDI+ 上下文,可在布局阶段调用)。
|
||||||
|
* @param [IN] font 字体句柄。
|
||||||
|
* @param [IN] text 文本内容。
|
||||||
|
* @return 文本像素宽度。
|
||||||
|
*/
|
||||||
|
float UiMeasureTextWidth(HFONT font, const std::wstring& text);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 在超出可用宽度时对文本做尾部省略截断。
|
* @brief 在超出可用宽度时对文本做尾部省略截断。
|
||||||
* @param [IN,OUT] g 绘图上下文。
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
|
|||||||
@@ -1,212 +0,0 @@
|
|||||||
#ifndef __SIGNAL_SET_HPP__
|
|
||||||
#define __SIGNAL_SET_HPP__
|
|
||||||
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "CsvTable.hpp"
|
|
||||||
|
|
||||||
namespace jdi
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 波形绘制方式。
|
|
||||||
*/
|
|
||||||
enum class DrawMode : int
|
|
||||||
{
|
|
||||||
LINE = 0, /**< 相邻采样点之间以插值直线连接。 */
|
|
||||||
SCATTER = 1, /**< 仅绘制采样点散点。 */
|
|
||||||
LINE_SCATTER = 2, /**< 同时绘制插值直线与采样点散点。 */
|
|
||||||
STEP = 3 /**< 阶梯保持(零阶保持)连线。 */
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 将绘制方式转换为中文显示名称。
|
|
||||||
* @param [IN] mode 绘制方式。
|
|
||||||
* @return 显示名称。
|
|
||||||
*/
|
|
||||||
const wchar_t* DrawModeName(DrawMode mode);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 单条信号(一列数据及其显示属性)。
|
|
||||||
*/
|
|
||||||
struct Signal
|
|
||||||
{
|
|
||||||
std::wstring name; /**< 显示名称(已剥离 "[n]" 序号前缀)。 */
|
|
||||||
std::wstring rawHeader; /**< CSV 原始列名。 */
|
|
||||||
std::wstring unit; /**< 从列名括号中提取的单位,可能为空。 */
|
|
||||||
std::vector<double> values; /**< 采样值,缺失处为 NaN。 */
|
|
||||||
bool visible = true; /**< 是否参与绘制。 */
|
|
||||||
std::uint32_t color = 0xFF268BD2; /**< 波形颜色,格式 0xAARRGGBB。 */
|
|
||||||
float lineWidth = 1.6f; /**< 线宽(逻辑像素,未乘 DPI 缩放)。 */
|
|
||||||
DrawMode mode = DrawMode::LINE; /**< 绘制方式。 */
|
|
||||||
double minValue = 0.0; /**< 有限采样值最小值。 */
|
|
||||||
double maxValue = 0.0; /**< 有限采样值最大值。 */
|
|
||||||
std::size_t finiteCount = 0; /**< 有限采样点数量。 */
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 一个波形网格(纵向泳道),可容纳多条信号叠加显示。
|
|
||||||
*/
|
|
||||||
struct Lane
|
|
||||||
{
|
|
||||||
std::wstring name; /**< 网格名称。 */
|
|
||||||
std::vector<int> signalIndices; /**< 该网格包含的信号下标(按加入顺序)。 */
|
|
||||||
bool autoRange = true; /**< 是否依据信号取值范围自动计算 Y 轴。 */
|
|
||||||
double yMin = -1.0; /**< 手动 Y 轴下限。 */
|
|
||||||
double yMax = 1.0; /**< 手动 Y 轴上限。 */
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 仿真数据集合:时间轴 + 多条信号 + 网格划分。
|
|
||||||
*/
|
|
||||||
class SignalSet
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
SignalSet();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 清空全部数据与显示状态。
|
|
||||||
*/
|
|
||||||
void Clear();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 依据 CSV 解析结果构建数据集(自动识别时间列与信号列)。
|
|
||||||
* @param [IN] table CSV 解析结果。
|
|
||||||
* @param [IN] filePath 源文件路径,用于界面展示。
|
|
||||||
* @param [OUT] error 失败原因描述。
|
|
||||||
* @return 构建成功返回 true。
|
|
||||||
*/
|
|
||||||
bool BuildFromTable(const CsvTable& table, const std::wstring& filePath, std::wstring& error);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 判断数据集是否已加载数据。
|
|
||||||
* @return 已加载返回 true。
|
|
||||||
*/
|
|
||||||
bool IsLoaded() const { return !m_signals.empty(); }
|
|
||||||
|
|
||||||
/// @brief 信号集合(可读写)。
|
|
||||||
std::vector<Signal>& Signals() { return m_signals; }
|
|
||||||
/// @brief 信号集合(只读)。
|
|
||||||
const std::vector<Signal>& Signals() const { return m_signals; }
|
|
||||||
/// @brief 网格集合(可读写)。
|
|
||||||
std::vector<Lane>& Lanes() { return m_lanes; }
|
|
||||||
/// @brief 网格集合(只读)。
|
|
||||||
const std::vector<Lane>& Lanes() const { return m_lanes; }
|
|
||||||
/// @brief 时间轴数值(只读)。
|
|
||||||
const std::vector<double>& Time() const { return m_time; }
|
|
||||||
/// @brief 时间轴列名(只读)。
|
|
||||||
const std::wstring& TimeColumnName() const { return m_timeColumnName; }
|
|
||||||
/// @brief 源文件路径(只读)。
|
|
||||||
const std::wstring& FilePath() const { return m_filePath; }
|
|
||||||
/// @brief 源文件名称(只读)。
|
|
||||||
const std::wstring& FileName() const { return m_fileName; }
|
|
||||||
/// @brief 采样点数量(只读)。
|
|
||||||
std::size_t SampleCount() const { return m_time.size(); }
|
|
||||||
/// @brief 时间轴最小值(只读)。
|
|
||||||
double MinTime() const { return m_minTime; }
|
|
||||||
/// @brief 时间轴最大值(只读)。
|
|
||||||
double MaxTime() const { return m_maxTime; }
|
|
||||||
/// @brief 时间轴是否单调不减(只读)。
|
|
||||||
bool TimeMonotonic() const { return m_timeMonotonic; }
|
|
||||||
/// @brief 当前选中信号下标,-1 表示未选中(只读)。
|
|
||||||
int SelectedSignal() const { return m_selectedSignal; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 设置当前选中信号。
|
|
||||||
* @param [IN] index 信号下标,传入越界值等价于取消选择。
|
|
||||||
*/
|
|
||||||
void SetSelectedSignal(int index);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 查询某信号所属网格下标。
|
|
||||||
* @param [IN] signalIndex 信号下标。
|
|
||||||
* @return 网格下标,未找到返回 -1。
|
|
||||||
*/
|
|
||||||
int LaneOfSignal(int signalIndex) const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 将信号移动到指定网格,必要时创建新网格并回收空网格。
|
|
||||||
* @param [IN] signalIndex 信号下标。
|
|
||||||
* @param [IN] laneIndex 目标网格下标,传入负值表示新建网格。
|
|
||||||
* @return 移动后该信号所在的网格下标。
|
|
||||||
*/
|
|
||||||
int MoveSignalToLane(int signalIndex, int laneIndex);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 将所有可见信号合并到同一个网格中。
|
|
||||||
*/
|
|
||||||
void MergeIntoSingleLane();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 为每条信号创建独立网格。
|
|
||||||
*/
|
|
||||||
void SplitIntoLanes();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 设置全部信号的可见状态。
|
|
||||||
* @param [IN] visible 目标可见状态。
|
|
||||||
*/
|
|
||||||
void SetAllVisible(bool visible);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 仅显示指定信号,其余全部隐藏。
|
|
||||||
* @param [IN] signalIndex 需要保持可见的信号下标。
|
|
||||||
*/
|
|
||||||
void SoloSignal(int signalIndex);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 计算某网格的 Y 轴显示范围。
|
|
||||||
* @param [IN] laneIndex 网格下标。
|
|
||||||
* @param [OUT] yMin 输出下限。
|
|
||||||
* @param [OUT] yMax 输出上限。
|
|
||||||
*/
|
|
||||||
void ComputeLaneRange(int laneIndex, double& yMin, double& yMax) const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 在当前时间轴上采样指定信号的值。
|
|
||||||
* @param [IN] signalIndex 信号下标。
|
|
||||||
* @param [IN] time 采样时刻。
|
|
||||||
* @param [IN] interpolate 是否在相邻采样点之间做线性插值。
|
|
||||||
* @param [OUT] value 输出采样值,缺失时为 NaN。
|
|
||||||
* @return 时间点落在数据范围内且取到有效值返回 true。
|
|
||||||
*/
|
|
||||||
bool SampleAt(int signalIndex, double time, bool interpolate, double& value) const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 在当前数据集中查找时间最为接近的采样点下标。
|
|
||||||
* @param [IN] time 采样时刻。
|
|
||||||
* @return 采样点下标,数据集为空时返回 -1。
|
|
||||||
*/
|
|
||||||
int FindNearestSample(double time) const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
/**
|
|
||||||
* @brief 移除没有包含任何信号的网格(至少保留一个)。
|
|
||||||
*/
|
|
||||||
void PruneEmptyLanes();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 为新增信号分配默认颜色。
|
|
||||||
* @param [IN] index 信号序号。
|
|
||||||
* @return 0xAARRGGBB 颜色值。
|
|
||||||
*/
|
|
||||||
std::uint32_t PickDefaultColor(std::size_t index) const;
|
|
||||||
|
|
||||||
std::vector<double> m_time; /**< 时间轴数值。 */
|
|
||||||
std::vector<Signal> m_signals; /**< 信号集合。 */
|
|
||||||
std::vector<Lane> m_lanes; /**< 网格集合。 */
|
|
||||||
std::wstring m_timeColumnName; /**< 时间列列名。 */
|
|
||||||
std::wstring m_filePath; /**< 源文件完整路径。 */
|
|
||||||
std::wstring m_fileName; /**< 源文件名称。 */
|
|
||||||
double m_minTime = 0.0; /**< 时间轴最小值。 */
|
|
||||||
double m_maxTime = 0.0; /**< 时间轴最大值。 */
|
|
||||||
bool m_timeMonotonic = true; /**< 时间轴是否单调不减。 */
|
|
||||||
int m_selectedSignal = -1; /**< 当前选中信号下标。 */
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace jdi
|
|
||||||
|
|
||||||
#endif // __SIGNAL_SET_HPP__
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -236,6 +236,28 @@ namespace jdi
|
|||||||
return static_cast<float>(size.cx);
|
return static_cast<float>(size.cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float UiMeasureTextWidth(HFONT font, const std::wstring& text)
|
||||||
|
{
|
||||||
|
if (text.empty() || font == nullptr)
|
||||||
|
{
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
HDC hdc = GetDC(nullptr);
|
||||||
|
if (hdc == nullptr)
|
||||||
|
{
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
HGDIOBJ oldFont = SelectObject(hdc, font);
|
||||||
|
SIZE size = {};
|
||||||
|
GetTextExtentPoint32W(hdc, text.c_str(), static_cast<int>(text.size()), &size);
|
||||||
|
SelectObject(hdc, oldFont);
|
||||||
|
ReleaseDC(nullptr, hdc);
|
||||||
|
|
||||||
|
return static_cast<float>(size.cx);
|
||||||
|
}
|
||||||
|
|
||||||
std::wstring UiEllipsize(gd::Graphics& g, const std::wstring& text, HFONT font, float maxWidth)
|
std::wstring UiEllipsize(gd::Graphics& g, const std::wstring& text, HFONT font, float maxWidth)
|
||||||
{
|
{
|
||||||
if (text.empty() || font == nullptr || maxWidth <= 0.0f)
|
if (text.empty() || font == nullptr || maxWidth <= 0.0f)
|
||||||
|
|||||||
@@ -1,612 +0,0 @@
|
|||||||
#include "SignalSet.hpp"
|
|
||||||
|
|
||||||
#include "Theme.hpp"
|
|
||||||
#include "Utf8Util.hpp"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cwctype>
|
|
||||||
#include <limits>
|
|
||||||
|
|
||||||
namespace jdi
|
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
const double kMissing = std::numeric_limits<double>::quiet_NaN(); /**< 缺失值占位。 */
|
|
||||||
const double kDefaultLineWidth = 1.6; /**< 默认线宽。 */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 判断列名是否为时间轴候选列。
|
|
||||||
* @param [IN] header 原始列名。
|
|
||||||
* @return 可能为时间轴返回 true。
|
|
||||||
*/
|
|
||||||
bool LooksLikeTimeHeader(const std::wstring& header)
|
|
||||||
{
|
|
||||||
const std::wstring text = ToLowerAscii(Trim(header));
|
|
||||||
if (text.empty())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (text == L"t" || text == L"time" || text == L"timestamp" || text == L"simulation time" ||
|
|
||||||
text == L"时间" || text == L"时间轴")
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return ContainsInsensitive(text, L"time") || ContainsInsensitive(text, L"时间");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 剥离列名中的 "[n]" 序号前缀,得到显示名称。
|
|
||||||
* @param [IN] header 原始列名。
|
|
||||||
* @return 显示名称。
|
|
||||||
*/
|
|
||||||
std::wstring MakeDisplayName(const std::wstring& header)
|
|
||||||
{
|
|
||||||
const std::wstring text = Trim(header);
|
|
||||||
if (text.size() >= 3 && text[0] == L'[')
|
|
||||||
{
|
|
||||||
const std::size_t close = text.find(L']');
|
|
||||||
if (close != std::wstring::npos && close > 1 && (close + 1) < text.size())
|
|
||||||
{
|
|
||||||
bool numeric = true;
|
|
||||||
for (std::size_t i = 1; i < close; ++i)
|
|
||||||
{
|
|
||||||
if (!iswdigit(text[i]))
|
|
||||||
{
|
|
||||||
numeric = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (numeric)
|
|
||||||
{
|
|
||||||
const std::wstring rest = Trim(text.substr(close + 1));
|
|
||||||
if (!rest.empty())
|
|
||||||
{
|
|
||||||
return rest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return text.empty() ? header : text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 从列名末尾的括号中提取单位(仅接受形如 mA / V / deg/s 的短标记)。
|
|
||||||
* @param [IN] header 原始列名。
|
|
||||||
* @return 单位文本,无法识别时返回空字符串。
|
|
||||||
*/
|
|
||||||
std::wstring ExtractUnit(const std::wstring& header)
|
|
||||||
{
|
|
||||||
const std::wstring text = Trim(header);
|
|
||||||
if (text.size() < 3 || text.back() != L')')
|
|
||||||
{
|
|
||||||
return std::wstring();
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::size_t open = text.rfind(L'(');
|
|
||||||
if (open == std::wstring::npos || open + 2 >= text.size())
|
|
||||||
{
|
|
||||||
return std::wstring();
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::wstring inner = Trim(text.substr(open + 1, text.size() - open - 2));
|
|
||||||
if (inner.empty() || inner.size() > 10)
|
|
||||||
{
|
|
||||||
return std::wstring();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const wchar_t c : inner)
|
|
||||||
{
|
|
||||||
const bool allowed = iswalpha(c) || iswdigit(c) || c == L'%' || c == L'/' || c == L'^' ||
|
|
||||||
c == L'-' || c == L'.' || c == L'\u00B0' || c == L'\u00B5' ||
|
|
||||||
c == L'\u03A9' || c == L'\u00B2' || c == L'\u00B3' || c == L'\u00B7';
|
|
||||||
if (!allowed)
|
|
||||||
{
|
|
||||||
return std::wstring();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return inner;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 为网格范围补充留白,避免波形紧贴边界。
|
|
||||||
* @param [IN, OUT] yMin 范围下限。
|
|
||||||
* @param [IN, OUT] yMax 范围上限。
|
|
||||||
*/
|
|
||||||
void PadRange(double& yMin, double& yMax)
|
|
||||||
{
|
|
||||||
if (!std::isfinite(yMin) || !std::isfinite(yMax))
|
|
||||||
{
|
|
||||||
yMin = 0.0;
|
|
||||||
yMax = 1.0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const double span = yMax - yMin;
|
|
||||||
if (span <= 0.0)
|
|
||||||
{
|
|
||||||
const double magnitude = std::max(1.0, std::fabs(yMin) * 0.1);
|
|
||||||
yMin -= magnitude;
|
|
||||||
yMax += magnitude;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const double pad = span * 0.06;
|
|
||||||
yMin -= pad;
|
|
||||||
yMax += pad;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const wchar_t* DrawModeName(DrawMode mode)
|
|
||||||
{
|
|
||||||
switch (mode)
|
|
||||||
{
|
|
||||||
case DrawMode::LINE:
|
|
||||||
return L"线段";
|
|
||||||
case DrawMode::SCATTER:
|
|
||||||
return L"散点";
|
|
||||||
case DrawMode::LINE_SCATTER:
|
|
||||||
return L"点线";
|
|
||||||
case DrawMode::STEP:
|
|
||||||
return L"阶梯";
|
|
||||||
default:
|
|
||||||
return L"线段";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SignalSet::SignalSet() = default;
|
|
||||||
|
|
||||||
void SignalSet::Clear()
|
|
||||||
{
|
|
||||||
m_time.clear();
|
|
||||||
m_signals.clear();
|
|
||||||
m_lanes.clear();
|
|
||||||
m_timeColumnName.clear();
|
|
||||||
m_filePath.clear();
|
|
||||||
m_fileName.clear();
|
|
||||||
m_minTime = 0.0;
|
|
||||||
m_maxTime = 0.0;
|
|
||||||
m_timeMonotonic = true;
|
|
||||||
m_selectedSignal = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SignalSet::BuildFromTable(const CsvTable& table, const std::wstring& filePath, std::wstring& error)
|
|
||||||
{
|
|
||||||
Clear();
|
|
||||||
error.clear();
|
|
||||||
|
|
||||||
if (!table.HasData())
|
|
||||||
{
|
|
||||||
error = L"数据表为空。";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::size_t columnCount = table.headers.size();
|
|
||||||
const std::size_t rowCount = table.rows.size();
|
|
||||||
|
|
||||||
// 1. 识别时间轴列:优先取列名匹配的列,否则回退到第一列。
|
|
||||||
int timeColumn = -1;
|
|
||||||
for (std::size_t column = 0; column < columnCount; ++column)
|
|
||||||
{
|
|
||||||
if (LooksLikeTimeHeader(table.headers[column]))
|
|
||||||
{
|
|
||||||
timeColumn = static_cast<int>(column);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (timeColumn < 0)
|
|
||||||
{
|
|
||||||
timeColumn = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_time.resize(rowCount);
|
|
||||||
std::size_t finiteTimeCount = 0;
|
|
||||||
bool monotonic = true;
|
|
||||||
double previousTime = 0.0;
|
|
||||||
bool hasPrevious = false;
|
|
||||||
|
|
||||||
for (std::size_t row = 0; row < rowCount; ++row)
|
|
||||||
{
|
|
||||||
const double value = table.rows[row][static_cast<std::size_t>(timeColumn)];
|
|
||||||
m_time[row] = value;
|
|
||||||
if (std::isfinite(value))
|
|
||||||
{
|
|
||||||
++finiteTimeCount;
|
|
||||||
if (hasPrevious && value < previousTime)
|
|
||||||
{
|
|
||||||
monotonic = false;
|
|
||||||
}
|
|
||||||
previousTime = value;
|
|
||||||
hasPrevious = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (finiteTimeCount < 2)
|
|
||||||
{
|
|
||||||
error = L"时间轴列(" + table.headers[static_cast<std::size_t>(timeColumn)] + L")有效数值不足。";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_timeColumnName = table.headers[static_cast<std::size_t>(timeColumn)];
|
|
||||||
m_timeMonotonic = monotonic;
|
|
||||||
m_filePath = filePath;
|
|
||||||
|
|
||||||
const std::size_t slashPos = filePath.find_last_of(L"\\/");
|
|
||||||
m_fileName = (slashPos == std::wstring::npos) ? filePath : filePath.substr(slashPos + 1);
|
|
||||||
|
|
||||||
// 2. 构建信号列。
|
|
||||||
m_signals.reserve(columnCount);
|
|
||||||
for (std::size_t column = 0; column < columnCount; ++column)
|
|
||||||
{
|
|
||||||
if (static_cast<int>(column) == timeColumn)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Signal signal;
|
|
||||||
signal.rawHeader = table.headers[column];
|
|
||||||
signal.name = MakeDisplayName(signal.rawHeader);
|
|
||||||
signal.unit = ExtractUnit(signal.rawHeader);
|
|
||||||
signal.color = PickDefaultColor(m_signals.size());
|
|
||||||
signal.lineWidth = static_cast<float>(kDefaultLineWidth);
|
|
||||||
signal.mode = DrawMode::LINE;
|
|
||||||
signal.visible = true;
|
|
||||||
|
|
||||||
signal.values.resize(rowCount);
|
|
||||||
double minValue = std::numeric_limits<double>::infinity();
|
|
||||||
double maxValue = -std::numeric_limits<double>::infinity();
|
|
||||||
std::size_t finiteCount = 0;
|
|
||||||
|
|
||||||
for (std::size_t row = 0; row < rowCount; ++row)
|
|
||||||
{
|
|
||||||
const double value = table.rows[row][column];
|
|
||||||
signal.values[row] = value;
|
|
||||||
if (std::isfinite(value))
|
|
||||||
{
|
|
||||||
++finiteCount;
|
|
||||||
minValue = std::min(minValue, value);
|
|
||||||
maxValue = std::max(maxValue, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (finiteCount == 0)
|
|
||||||
{
|
|
||||||
signal.minValue = 0.0;
|
|
||||||
signal.maxValue = 0.0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
signal.minValue = minValue;
|
|
||||||
signal.maxValue = maxValue;
|
|
||||||
}
|
|
||||||
signal.finiteCount = finiteCount;
|
|
||||||
|
|
||||||
m_signals.push_back(std::move(signal));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_signals.empty())
|
|
||||||
{
|
|
||||||
error = L"除时间轴外未发现任何数据列。";
|
|
||||||
Clear();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 统计时间范围。
|
|
||||||
double tMin = std::numeric_limits<double>::infinity();
|
|
||||||
double tMax = -std::numeric_limits<double>::infinity();
|
|
||||||
for (const double value : m_time)
|
|
||||||
{
|
|
||||||
if (std::isfinite(value))
|
|
||||||
{
|
|
||||||
tMin = std::min(tMin, value);
|
|
||||||
tMax = std::max(tMax, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_minTime = tMin;
|
|
||||||
m_maxTime = tMax;
|
|
||||||
|
|
||||||
// 4. 默认每个信号独占一个网格,便于区分量纲差异较大的数据。
|
|
||||||
m_lanes.clear();
|
|
||||||
m_lanes.resize(m_signals.size());
|
|
||||||
for (std::size_t index = 0; index < m_signals.size(); ++index)
|
|
||||||
{
|
|
||||||
m_lanes[index].name = L"网格 " + std::to_wstring(index + 1);
|
|
||||||
m_lanes[index].signalIndices.push_back(static_cast<int>(index));
|
|
||||||
m_lanes[index].autoRange = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_selectedSignal = m_signals.empty() ? -1 : 0;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalSet::SetSelectedSignal(int index)
|
|
||||||
{
|
|
||||||
if (index < 0 || index >= static_cast<int>(m_signals.size()))
|
|
||||||
{
|
|
||||||
m_selectedSignal = -1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_selectedSignal = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
int SignalSet::LaneOfSignal(int signalIndex) const
|
|
||||||
{
|
|
||||||
for (std::size_t lane = 0; lane < m_lanes.size(); ++lane)
|
|
||||||
{
|
|
||||||
const std::vector<int>& indices = m_lanes[lane].signalIndices;
|
|
||||||
if (std::find(indices.begin(), indices.end(), signalIndex) != indices.end())
|
|
||||||
{
|
|
||||||
return static_cast<int>(lane);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int SignalSet::MoveSignalToLane(int signalIndex, int laneIndex)
|
|
||||||
{
|
|
||||||
if (signalIndex < 0 || signalIndex >= static_cast<int>(m_signals.size()))
|
|
||||||
{
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Lane& lane : m_lanes)
|
|
||||||
{
|
|
||||||
std::vector<int>& indices = lane.signalIndices;
|
|
||||||
indices.erase(std::remove(indices.begin(), indices.end(), signalIndex), indices.end());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
|
|
||||||
{
|
|
||||||
Lane lane;
|
|
||||||
lane.name = L"网格 " + std::to_wstring(m_lanes.size() + 1);
|
|
||||||
lane.autoRange = true;
|
|
||||||
lane.signalIndices.push_back(signalIndex);
|
|
||||||
m_lanes.push_back(std::move(lane));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
m_lanes[static_cast<std::size_t>(laneIndex)].signalIndices.push_back(signalIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
PruneEmptyLanes();
|
|
||||||
return LaneOfSignal(signalIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalSet::MergeIntoSingleLane()
|
|
||||||
{
|
|
||||||
m_lanes.clear();
|
|
||||||
|
|
||||||
Lane lane;
|
|
||||||
lane.name = L"网格 1";
|
|
||||||
lane.autoRange = true;
|
|
||||||
lane.signalIndices.reserve(m_signals.size());
|
|
||||||
for (std::size_t index = 0; index < m_signals.size(); ++index)
|
|
||||||
{
|
|
||||||
lane.signalIndices.push_back(static_cast<int>(index));
|
|
||||||
}
|
|
||||||
m_lanes.push_back(std::move(lane));
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalSet::SplitIntoLanes()
|
|
||||||
{
|
|
||||||
m_lanes.clear();
|
|
||||||
m_lanes.resize(m_signals.size());
|
|
||||||
for (std::size_t index = 0; index < m_signals.size(); ++index)
|
|
||||||
{
|
|
||||||
Lane& lane = m_lanes[index];
|
|
||||||
lane.name = L"网格 " + std::to_wstring(index + 1);
|
|
||||||
lane.autoRange = true;
|
|
||||||
lane.signalIndices.push_back(static_cast<int>(index));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalSet::SetAllVisible(bool visible)
|
|
||||||
{
|
|
||||||
for (Signal& signal : m_signals)
|
|
||||||
{
|
|
||||||
signal.visible = visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalSet::SoloSignal(int signalIndex)
|
|
||||||
{
|
|
||||||
for (std::size_t index = 0; index < m_signals.size(); ++index)
|
|
||||||
{
|
|
||||||
m_signals[index].visible = (static_cast<int>(index) == signalIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalSet::ComputeLaneRange(int laneIndex, double& yMin, double& yMax) const
|
|
||||||
{
|
|
||||||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
|
|
||||||
{
|
|
||||||
yMin = 0.0;
|
|
||||||
yMax = 1.0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Lane& lane = m_lanes[static_cast<std::size_t>(laneIndex)];
|
|
||||||
if (!lane.autoRange)
|
|
||||||
{
|
|
||||||
yMin = lane.yMin;
|
|
||||||
yMax = lane.yMax;
|
|
||||||
if (!(yMax > yMin))
|
|
||||||
{
|
|
||||||
yMin = 0.0;
|
|
||||||
yMax = 1.0;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
double low = std::numeric_limits<double>::infinity();
|
|
||||||
double high = -std::numeric_limits<double>::infinity();
|
|
||||||
for (const int signalIndex : lane.signalIndices)
|
|
||||||
{
|
|
||||||
if (signalIndex < 0 || signalIndex >= static_cast<int>(m_signals.size()))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const Signal& signal = m_signals[static_cast<std::size_t>(signalIndex)];
|
|
||||||
if (!signal.visible || signal.finiteCount == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
low = std::min(low, signal.minValue);
|
|
||||||
high = std::max(high, signal.maxValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!std::isfinite(low) || !std::isfinite(high))
|
|
||||||
{
|
|
||||||
low = 0.0;
|
|
||||||
high = 1.0;
|
|
||||||
}
|
|
||||||
PadRange(low, high);
|
|
||||||
yMin = low;
|
|
||||||
yMax = high;
|
|
||||||
}
|
|
||||||
|
|
||||||
int SignalSet::FindNearestSample(double time) const
|
|
||||||
{
|
|
||||||
const std::size_t count = m_time.size();
|
|
||||||
if (count == 0)
|
|
||||||
{
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (m_timeMonotonic)
|
|
||||||
{
|
|
||||||
const std::vector<double>::const_iterator lower =
|
|
||||||
std::lower_bound(m_time.begin(), m_time.end(), time);
|
|
||||||
if (lower == m_time.begin())
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (lower == m_time.end())
|
|
||||||
{
|
|
||||||
return static_cast<int>(count - 1);
|
|
||||||
}
|
|
||||||
const std::size_t rightIndex = static_cast<std::size_t>(lower - m_time.begin());
|
|
||||||
const std::size_t leftIndex = rightIndex - 1;
|
|
||||||
const double leftDistance = std::fabs(time - m_time[leftIndex]);
|
|
||||||
const double rightDistance = std::fabs(m_time[rightIndex] - time);
|
|
||||||
return (leftDistance <= rightDistance) ? static_cast<int>(leftIndex) : static_cast<int>(rightIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
int best = 0;
|
|
||||||
double bestDistance = std::numeric_limits<double>::infinity();
|
|
||||||
for (std::size_t index = 0; index < count; ++index)
|
|
||||||
{
|
|
||||||
if (!std::isfinite(m_time[index]))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const double distance = std::fabs(m_time[index] - time);
|
|
||||||
if (distance < bestDistance)
|
|
||||||
{
|
|
||||||
bestDistance = distance;
|
|
||||||
best = static_cast<int>(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SignalSet::SampleAt(int signalIndex, double time, bool interpolate, double& value) const
|
|
||||||
{
|
|
||||||
value = kMissing;
|
|
||||||
if (signalIndex < 0 || signalIndex >= static_cast<int>(m_signals.size()))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (m_time.empty() || time < m_minTime || time > m_maxTime)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::vector<double>& samples = m_signals[static_cast<std::size_t>(signalIndex)].values;
|
|
||||||
|
|
||||||
if (!m_timeMonotonic)
|
|
||||||
{
|
|
||||||
const int nearest = FindNearestSample(time);
|
|
||||||
if (nearest < 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
value = samples[static_cast<std::size_t>(nearest)];
|
|
||||||
return std::isfinite(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::vector<double>::const_iterator lower =
|
|
||||||
std::lower_bound(m_time.begin(), m_time.end(), time);
|
|
||||||
const std::size_t rightIndex = static_cast<std::size_t>(lower - m_time.begin());
|
|
||||||
if (rightIndex == 0)
|
|
||||||
{
|
|
||||||
value = samples[0];
|
|
||||||
return std::isfinite(value);
|
|
||||||
}
|
|
||||||
if (rightIndex >= m_time.size())
|
|
||||||
{
|
|
||||||
value = samples.back();
|
|
||||||
return std::isfinite(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::size_t leftIndex = rightIndex - 1;
|
|
||||||
const double t0 = m_time[leftIndex];
|
|
||||||
const double t1 = m_time[rightIndex];
|
|
||||||
const double v0 = samples[leftIndex];
|
|
||||||
const double v1 = samples[rightIndex];
|
|
||||||
|
|
||||||
if (!interpolate || !std::isfinite(v0) || !std::isfinite(v1) || !(t1 > t0))
|
|
||||||
{
|
|
||||||
const double leftDistance = std::fabs(time - t0);
|
|
||||||
const double rightDistance = std::fabs(t1 - time);
|
|
||||||
value = (leftDistance <= rightDistance) ? v0 : v1;
|
|
||||||
return std::isfinite(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const double ratio = (time - t0) / (t1 - t0);
|
|
||||||
value = v0 + (v1 - v0) * ratio;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalSet::PruneEmptyLanes()
|
|
||||||
{
|
|
||||||
if (m_lanes.size() <= 1)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Lane> kept;
|
|
||||||
kept.reserve(m_lanes.size());
|
|
||||||
for (Lane& lane : m_lanes)
|
|
||||||
{
|
|
||||||
if (!lane.signalIndices.empty())
|
|
||||||
{
|
|
||||||
kept.push_back(std::move(lane));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (kept.empty())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_lanes = std::move(kept);
|
|
||||||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
|
||||||
{
|
|
||||||
m_lanes[index].name = L"网格 " + std::to_wstring(index + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::uint32_t SignalSet::PickDefaultColor(std::size_t index) const
|
|
||||||
{
|
|
||||||
int paletteCount = 0;
|
|
||||||
const std::uint32_t* palette = SignalPalette(paletteCount);
|
|
||||||
if (palette == nullptr || paletteCount <= 0)
|
|
||||||
{
|
|
||||||
return 0xFF268BD2;
|
|
||||||
}
|
|
||||||
return palette[index % static_cast<std::size_t>(paletteCount)];
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace jdi
|
|
||||||
Reference in new issue
Block a user