feat: 搭建 Just DataInspector 波形数据查看器基础功能
基于 Win32 API + GDI+ 从零实现仿真数据检查器,用于替代 Simulink.sdi.view 查看带时间轴的 CSV 数据。 - 新增 Utilities 层:UTF-8/宽字符与数值格式化工具、CSV 解析器(自动识别分隔符与引号)、 信号数据集模型(时间轴识别、信号名解析、取值采样)、Solarised Light 主题色板与绘图基础工具 - 新增 Toolbar 组件:打开 CSV、适应窗口、适应时间、放大、缩小、合并网格、每信号一格 - 新增 SignalPanel 组件:信号列表(可见性开关、颜色标识、所属网格标记)、样式编辑区 (颜色、线宽、线段/散点/点线/阶梯显示方式、所属网格),支持右键上下文菜单与滚轮滚动 - 新增 PlotView 组件:多网格波形绘制、自适应时间轴与幅值轴、网格线、信号图例、 时间光标与实时数值读数、纵向滚动条 - 新增 Application 组件:DPI 感知(Per-Monitor V2)、双缓冲绘制、区域布局与分隔条拖拽、 鼠标与键盘消息分发、文件拖放、状态栏提示 - 交互能力:滚轮缩放、Shift+滚轮平移、Ctrl+滚轮缩放幅值、拖拽平移、幅值轴纵向缩放、 右键框选缩放、单击固定/取消时间光标、双击自适应、方向键平移与滚动 - 补齐 Makefile 的源文件与头文件索引,并新增 gdiplus、comdlg32 链接库
This commit is contained in:
commit
132b305544
22 files changed
+7146
No files matched your search
@@ -0,0 +1,215 @@
|
||||
#ifndef __APPLICATION_HPP__
|
||||
#define __APPLICATION_HPP__
|
||||
|
||||
#include <windows.h>
|
||||
#include <objidl.h>
|
||||
#include <gdiplus.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "PaintUtil.hpp"
|
||||
#include "PlotView.hpp"
|
||||
#include "SignalPanel.hpp"
|
||||
#include "SignalSet.hpp"
|
||||
#include "Theme.hpp"
|
||||
#include "Toolbar.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief 应用主框架:负责窗口、布局、消息分发、绘制合成与命令处理。
|
||||
*/
|
||||
class Application
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief 获取全局唯一应用实例。
|
||||
* @return 应用实例引用。
|
||||
*/
|
||||
static Application& Instance();
|
||||
|
||||
/**
|
||||
* @brief 初始化环境、创建窗口并进入消息循环。
|
||||
* @param [IN] instance 模块实例句柄。
|
||||
* @param [IN] showCommand 窗口显示方式。
|
||||
* @return 进程退出码。
|
||||
*/
|
||||
int Run(HINSTANCE instance, int showCommand);
|
||||
|
||||
/**
|
||||
* @brief 窗口过程中转处理函数。
|
||||
* @param [IN] hwnd 窗口句柄。
|
||||
* @param [IN] message 消息标识。
|
||||
* @param [IN] wParam 消息参数。
|
||||
* @param [IN] lParam 消息参数。
|
||||
* @return 消息处理结果。
|
||||
*/
|
||||
LRESULT HandleMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 鼠标交互的作用目标。
|
||||
*/
|
||||
enum class Target : int
|
||||
{
|
||||
NONE = 0, /**< 无目标。 */
|
||||
TOOLBAR, /**< 工具栏。 */
|
||||
PANEL, /**< 信号面板。 */
|
||||
PLOT, /**< 波形绘图区。 */
|
||||
SPLITTER /**< 面板分隔条。 */
|
||||
};
|
||||
|
||||
Application() = default;
|
||||
~Application() = default;
|
||||
Application(const Application&) = delete;
|
||||
Application& operator=(const Application&) = delete;
|
||||
|
||||
/**
|
||||
* @brief 窗口过程静态回调。
|
||||
* @param [IN] hwnd 窗口句柄。
|
||||
* @param [IN] message 消息标识。
|
||||
* @param [IN] wParam 消息参数。
|
||||
* @param [IN] lParam 消息参数。
|
||||
* @return 消息处理结果。
|
||||
*/
|
||||
static LRESULT CALLBACK WindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
|
||||
|
||||
/**
|
||||
* @brief 创建主窗口与相关资源。
|
||||
* @param [IN] instance 模块实例句柄。
|
||||
* @param [IN] showCommand 窗口显示方式。
|
||||
* @return 创建成功返回 true。
|
||||
*/
|
||||
bool Initialize(HINSTANCE instance, int showCommand);
|
||||
|
||||
/**
|
||||
* @brief 释放窗口与绘图资源。
|
||||
*/
|
||||
void Shutdown();
|
||||
|
||||
/**
|
||||
* @brief 依据 DPI 重建字体与布局。
|
||||
* @param [IN] dpi 目标 DPI。
|
||||
*/
|
||||
void ApplyDpi(UINT dpi);
|
||||
|
||||
/**
|
||||
* @brief 重新计算各区域的布局。
|
||||
*/
|
||||
void Relayout();
|
||||
|
||||
/**
|
||||
* @brief 创建或调整后备缓冲区尺寸。
|
||||
* @param [IN] width 客户区宽度。
|
||||
* @param [IN] height 客户区高度。
|
||||
*/
|
||||
void EnsureBackBuffer(int width, int height);
|
||||
|
||||
/**
|
||||
* @brief 释放后备缓冲区。
|
||||
*/
|
||||
void ReleaseBackBuffer();
|
||||
|
||||
/**
|
||||
* @brief 完整重绘客户区并呈现到窗口。
|
||||
*/
|
||||
void Render();
|
||||
|
||||
/**
|
||||
* @brief 绘制底部状态栏。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
*/
|
||||
void PaintStatusBar(gd::Graphics& g);
|
||||
|
||||
/**
|
||||
* @brief 弹出文件选择对话框并加载所选文件。
|
||||
*/
|
||||
void OpenFileDialog();
|
||||
|
||||
/**
|
||||
* @brief 加载指定 CSV 文件并刷新界面。
|
||||
* @param [IN] path 文件完整路径。
|
||||
* @return 加载成功返回 true。
|
||||
*/
|
||||
bool LoadFile(const std::wstring& path);
|
||||
|
||||
/**
|
||||
* @brief 执行工具栏动作。
|
||||
* @param [IN] action 动作标识。
|
||||
*/
|
||||
void ExecuteToolbarAction(ToolbarAction action);
|
||||
|
||||
/**
|
||||
* @brief 处理键盘按键。
|
||||
* @param [IN] key 虚拟键码。
|
||||
* @return 已处理返回 true。
|
||||
*/
|
||||
bool HandleKeyDown(WPARAM key);
|
||||
|
||||
/**
|
||||
* @brief 设置状态栏提示信息。
|
||||
* @param [IN] text 提示文本。
|
||||
* @param [IN] isError 是否为错误提示。
|
||||
*/
|
||||
void SetStatusMessage(const std::wstring& text, bool isError);
|
||||
|
||||
/**
|
||||
* @brief 刷新窗口标题。
|
||||
*/
|
||||
void UpdateWindowTitle();
|
||||
|
||||
/**
|
||||
* @brief 解析命令行中的文件参数并加载。
|
||||
*/
|
||||
void LoadCommandLineFile();
|
||||
|
||||
/**
|
||||
* @brief 处理客户区鼠标移动。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @param [IN] buttonDown 是否有按键处于按下状态。
|
||||
* @return 需要重绘返回 true。
|
||||
*/
|
||||
bool HandleMouseMove(int x, int y, bool buttonDown);
|
||||
|
||||
HINSTANCE m_instance = nullptr; /**< 模块实例句柄。 */
|
||||
HWND m_hwnd = nullptr; /**< 主窗口句柄。 */
|
||||
UINT m_dpi = 96; /**< 当前 DPI。 */
|
||||
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
||||
ULONG_PTR m_gdiplusToken = 0; /**< GDI+ 运行时令牌。 */
|
||||
bool m_gdiplusReady = false; /**< GDI+ 是否初始化成功。 */
|
||||
|
||||
Theme m_theme; /**< 主题配色。 */
|
||||
UiFonts m_fonts; /**< 界面字体。 */
|
||||
SignalSet m_document; /**< 已加载的数据集。 */
|
||||
Toolbar m_toolbar; /**< 顶部工具栏。 */
|
||||
SignalPanel m_panel; /**< 左侧信号面板。 */
|
||||
PlotView m_plot; /**< 波形绘图区。 */
|
||||
|
||||
gd::RectF m_toolbarRect; /**< 工具栏区域。 */
|
||||
gd::RectF m_panelRect; /**< 信号面板区域。 */
|
||||
gd::RectF m_splitterRect; /**< 面板分隔条区域。 */
|
||||
gd::RectF m_plotRect; /**< 绘图区区域。 */
|
||||
gd::RectF m_statusRect; /**< 状态栏区域。 */
|
||||
|
||||
float m_panelWidth = 292.0f; /**< 面板宽度(逻辑像素)。 */
|
||||
bool m_draggingSplitter = false; /**< 是否正在拖动分隔条。 */
|
||||
int m_splitterGrabOffset = 0; /**< 分隔条拖动的抓取偏移。 */
|
||||
|
||||
HDC m_backDc = nullptr; /**< 后备缓冲设备上下文。 */
|
||||
HBITMAP m_backBitmap = nullptr; /**< 后备缓冲位图。 */
|
||||
HGDIOBJ m_backOldBitmap = nullptr; /**< 后备缓冲原始位图。 */
|
||||
int m_backWidth = 0; /**< 后备缓冲宽度。 */
|
||||
int m_backHeight = 0; /**< 后备缓冲高度。 */
|
||||
|
||||
Target m_activeTarget = Target::NONE; /**< 当前鼠标作用目标。 */
|
||||
bool m_trackingLeave = false; /**< 是否已注册鼠标离开通知。 */
|
||||
std::size_t m_lastLaneCount = 0; /**< 上次渲染时的网格数量,用于检测布局失效。 */
|
||||
std::wstring m_statusMessage; /**< 状态栏提示文本。 */
|
||||
bool m_statusIsError = false; /**< 状态提示是否为错误。 */
|
||||
};
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __APPLICATION_HPP__
|
||||
@@ -0,0 +1,987 @@
|
||||
#include "Application.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include <commdlg.h>
|
||||
#include <shellapi.h>
|
||||
#include <windowsx.h>
|
||||
|
||||
#include "CsvTable.hpp"
|
||||
#include "Utf8Util.hpp"
|
||||
|
||||
#ifndef WM_DPICHANGED
|
||||
#define WM_DPICHANGED 0x02E0
|
||||
#endif
|
||||
|
||||
#ifndef DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
|
||||
#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 (reinterpret_cast<HANDLE>(-4))
|
||||
#endif
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
const wchar_t* kWindowClassName = L"JustDataInspectorMainWindow"; /**< 窗口类名。 */
|
||||
const wchar_t* kWindowTitle = L"Just DataInspector"; /**< 默认窗口标题。 */
|
||||
const float kStatusBarHeight = 26.0f; /**< 状态栏高度(逻辑像素)。 */
|
||||
const float kSplitterWidth = 4.0f; /**< 分隔条宽度(逻辑像素)。 */
|
||||
const float kMinPanelWidth = 210.0f; /**< 面板最小宽度(逻辑像素)。 */
|
||||
const float kMaxPanelWidth = 460.0f; /**< 面板最大宽度(逻辑像素)。 */
|
||||
const int kMinClientWidth = 820; /**< 窗口最小客户区宽度。 */
|
||||
const int kMinClientHeight = 520; /**< 窗口最小客户区高度。 */
|
||||
const wchar_t* kStatusHint = L"滚轮缩放 · Shift+滚轮平移 · Ctrl+滚轮调幅值 · 右键框选 · 双击自适应";
|
||||
|
||||
/**
|
||||
* @brief 开启进程级 DPI 感知(优先 Per-Monitor V2)。
|
||||
*/
|
||||
void EnableDpiAwareness()
|
||||
{
|
||||
HMODULE user32 = GetModuleHandleW(L"user32.dll");
|
||||
if (user32 != nullptr)
|
||||
{
|
||||
typedef BOOL(WINAPI * SetProcessDpiAwarenessContextFn)(HANDLE);
|
||||
SetProcessDpiAwarenessContextFn setContext =
|
||||
reinterpret_cast<SetProcessDpiAwarenessContextFn>(
|
||||
reinterpret_cast<void*>(GetProcAddress(user32, "SetProcessDpiAwarenessContext")));
|
||||
if (setContext != nullptr &&
|
||||
setContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) != FALSE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
SetProcessDPIAware();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 查询屏幕 DPI。
|
||||
* @return DPI 数值,失败时返回 96。
|
||||
*/
|
||||
UINT QuerySystemDpi()
|
||||
{
|
||||
HDC hdc = GetDC(nullptr);
|
||||
if (hdc == nullptr)
|
||||
{
|
||||
return 96u;
|
||||
}
|
||||
const int dpi = GetDeviceCaps(hdc, LOGPIXELSX);
|
||||
ReleaseDC(nullptr, hdc);
|
||||
return (dpi > 0) ? static_cast<UINT>(dpi) : 96u;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 依据 DPI 缩放逻辑像素。
|
||||
* @param [IN] value 逻辑像素值。
|
||||
* @param [IN] scale 缩放比例。
|
||||
* @return 物理像素值。
|
||||
*/
|
||||
float ScaleOf(float value, float scale)
|
||||
{
|
||||
return value * scale;
|
||||
}
|
||||
}
|
||||
|
||||
Application& Application::Instance()
|
||||
{
|
||||
static Application instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK Application::WindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
Application* self = reinterpret_cast<Application*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||
if (message == WM_NCCREATE)
|
||||
{
|
||||
CREATESTRUCTW* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
|
||||
self = reinterpret_cast<Application*>(createStruct->lpCreateParams);
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(self));
|
||||
}
|
||||
|
||||
if (self != nullptr)
|
||||
{
|
||||
return self->HandleMessage(hwnd, message, wParam, lParam);
|
||||
}
|
||||
return DefWindowProcW(hwnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
int Application::Run(HINSTANCE instance, int showCommand)
|
||||
{
|
||||
if (!Initialize(instance, showCommand))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
MSG message = {};
|
||||
while (GetMessageW(&message, nullptr, 0, 0) > 0)
|
||||
{
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
}
|
||||
|
||||
Shutdown();
|
||||
return static_cast<int>(message.wParam);
|
||||
}
|
||||
|
||||
bool Application::Initialize(HINSTANCE instance, int showCommand)
|
||||
{
|
||||
m_instance = instance;
|
||||
|
||||
EnableDpiAwareness();
|
||||
m_dpi = QuerySystemDpi();
|
||||
m_scale = static_cast<float>(m_dpi) / 96.0f;
|
||||
|
||||
gd::GdiplusStartupInput startupInput;
|
||||
if (gd::GdiplusStartup(&m_gdiplusToken, &startupInput, nullptr) != gd::Ok)
|
||||
{
|
||||
MessageBoxW(nullptr, L"GDI+ 初始化失败,程序无法启动。", kWindowTitle, MB_ICONERROR | MB_OK);
|
||||
return false;
|
||||
}
|
||||
m_gdiplusReady = true;
|
||||
|
||||
m_theme = SolarizedLight();
|
||||
if (!m_fonts.Create(m_dpi))
|
||||
{
|
||||
m_gdiplusReady = false;
|
||||
gd::GdiplusShutdown(m_gdiplusToken);
|
||||
MessageBoxW(nullptr, L"界面字体创建失败,程序无法启动。", kWindowTitle, MB_ICONERROR | MB_OK);
|
||||
return false;
|
||||
}
|
||||
|
||||
WNDCLASSEXW windowClass = {};
|
||||
windowClass.cbSize = sizeof(windowClass);
|
||||
windowClass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
|
||||
windowClass.lpfnWndProc = &Application::WindowProc;
|
||||
windowClass.hInstance = instance;
|
||||
windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||
windowClass.hbrBackground = nullptr;
|
||||
windowClass.lpszClassName = kWindowClassName;
|
||||
windowClass.hIcon = LoadIconW(nullptr, IDI_APPLICATION);
|
||||
windowClass.hIconSm = windowClass.hIcon;
|
||||
|
||||
if (RegisterClassExW(&windowClass) == 0)
|
||||
{
|
||||
MessageBoxW(nullptr, L"窗口类注册失败。", kWindowTitle, MB_ICONERROR | MB_OK);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 依据工作区(排除任务栏)计算默认窗口尺寸并居中,避免窗口超出屏幕。
|
||||
RECT workArea = {0, 0, static_cast<LONG>(ScaleOf(1340.0f, m_scale)),
|
||||
static_cast<LONG>(ScaleOf(860.0f, m_scale))};
|
||||
SystemParametersInfoW(SPI_GETWORKAREA, 0, &workArea, 0);
|
||||
|
||||
const int workWidth = workArea.right - workArea.left;
|
||||
const int workHeight = workArea.bottom - workArea.top;
|
||||
const int windowWidth = std::min(static_cast<int>(ScaleOf(1340.0f, m_scale)), workWidth - 24);
|
||||
const int windowHeight = std::min(static_cast<int>(ScaleOf(860.0f, m_scale)), workHeight - 24);
|
||||
const int windowX = workArea.left + std::max(0, (workWidth - windowWidth) / 2);
|
||||
const int windowY = workArea.top + std::max(0, (workHeight - windowHeight) / 2);
|
||||
|
||||
m_hwnd = CreateWindowExW(WS_EX_ACCEPTFILES, kWindowClassName, kWindowTitle, WS_OVERLAPPEDWINDOW,
|
||||
windowX, windowY, windowWidth, windowHeight, nullptr, nullptr, instance,
|
||||
this);
|
||||
if (m_hwnd == nullptr)
|
||||
{
|
||||
MessageBoxW(nullptr, L"窗口创建失败。", kWindowTitle, MB_ICONERROR | MB_OK);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_toolbar.Setup(&m_fonts, &m_theme);
|
||||
m_toolbar.SetScale(m_scale);
|
||||
m_panel.Setup(m_hwnd, &m_fonts, &m_theme);
|
||||
m_panel.SetScale(m_scale);
|
||||
m_plot.Setup(&m_fonts, &m_theme);
|
||||
m_plot.SetScale(m_scale);
|
||||
|
||||
HICON icon = LoadIconW(nullptr, IDI_APPLICATION);
|
||||
SendMessageW(m_hwnd, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(icon));
|
||||
SendMessageW(m_hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(icon));
|
||||
|
||||
Relayout();
|
||||
ShowWindow(m_hwnd, showCommand);
|
||||
UpdateWindow(m_hwnd);
|
||||
LoadCommandLineFile();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Application::Shutdown()
|
||||
{
|
||||
ReleaseBackBuffer();
|
||||
m_fonts.Destroy();
|
||||
|
||||
if (m_gdiplusReady)
|
||||
{
|
||||
gd::GdiplusShutdown(m_gdiplusToken);
|
||||
m_gdiplusReady = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Application::ApplyDpi(UINT dpi)
|
||||
{
|
||||
if (dpi == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_dpi = dpi;
|
||||
m_scale = static_cast<float>(dpi) / 96.0f;
|
||||
|
||||
m_fonts.Create(dpi);
|
||||
m_toolbar.SetScale(m_scale);
|
||||
m_panel.SetScale(m_scale);
|
||||
m_plot.SetScale(m_scale);
|
||||
ReleaseBackBuffer();
|
||||
Relayout();
|
||||
}
|
||||
|
||||
void Application::Relayout()
|
||||
{
|
||||
RECT client = {};
|
||||
GetClientRect(m_hwnd, &client);
|
||||
const float width = static_cast<float>(client.right - client.left);
|
||||
const float height = static_cast<float>(client.bottom - client.top);
|
||||
if (width <= 1.0f || height <= 1.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const float toolbarHeight = m_toolbar.DesiredHeight();
|
||||
const float statusHeight = ScaleOf(kStatusBarHeight, m_scale);
|
||||
const float splitterWidth = ScaleOf(kSplitterWidth, m_scale);
|
||||
|
||||
m_toolbarRect = gd::RectF(0.0f, 0.0f, width, toolbarHeight);
|
||||
m_statusRect = gd::RectF(0.0f, height - statusHeight, width, statusHeight);
|
||||
|
||||
const float contentHeight = std::max(0.0f, height - toolbarHeight - statusHeight);
|
||||
|
||||
float panelWidth = ScaleOf(m_panelWidth, m_scale);
|
||||
const float maximum = std::min(ScaleOf(kMaxPanelWidth, m_scale), width * 0.45f);
|
||||
panelWidth = std::max(ScaleOf(kMinPanelWidth, m_scale), std::min(panelWidth, maximum));
|
||||
m_panelWidth = panelWidth / m_scale;
|
||||
|
||||
m_panelRect = gd::RectF(0.0f, toolbarHeight, panelWidth, contentHeight);
|
||||
m_splitterRect = gd::RectF(panelWidth, toolbarHeight, splitterWidth, contentHeight);
|
||||
m_plotRect = gd::RectF(panelWidth + splitterWidth, toolbarHeight,
|
||||
std::max(0.0f, width - panelWidth - splitterWidth), contentHeight);
|
||||
|
||||
m_toolbar.Layout(m_toolbarRect);
|
||||
m_panel.Layout(m_panelRect);
|
||||
m_plot.Layout(m_plotRect);
|
||||
}
|
||||
|
||||
void Application::EnsureBackBuffer(int width, int height)
|
||||
{
|
||||
if (m_backDc != nullptr && width == m_backWidth && height == m_backHeight)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ReleaseBackBuffer();
|
||||
|
||||
HDC screenDc = GetDC(m_hwnd);
|
||||
if (screenDc == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_backDc = CreateCompatibleDC(screenDc);
|
||||
if (m_backDc != nullptr)
|
||||
{
|
||||
m_backBitmap = CreateCompatibleBitmap(screenDc, width, height);
|
||||
if (m_backBitmap != nullptr)
|
||||
{
|
||||
m_backOldBitmap = SelectObject(m_backDc, m_backBitmap);
|
||||
m_backWidth = width;
|
||||
m_backHeight = height;
|
||||
}
|
||||
else
|
||||
{
|
||||
DeleteDC(m_backDc);
|
||||
m_backDc = nullptr;
|
||||
}
|
||||
}
|
||||
ReleaseDC(m_hwnd, screenDc);
|
||||
}
|
||||
|
||||
void Application::ReleaseBackBuffer()
|
||||
{
|
||||
if (m_backDc != nullptr)
|
||||
{
|
||||
if (m_backOldBitmap != nullptr)
|
||||
{
|
||||
SelectObject(m_backDc, m_backOldBitmap);
|
||||
m_backOldBitmap = nullptr;
|
||||
}
|
||||
DeleteDC(m_backDc);
|
||||
m_backDc = nullptr;
|
||||
}
|
||||
if (m_backBitmap != nullptr)
|
||||
{
|
||||
DeleteObject(m_backBitmap);
|
||||
m_backBitmap = nullptr;
|
||||
}
|
||||
m_backWidth = 0;
|
||||
m_backHeight = 0;
|
||||
}
|
||||
|
||||
void Application::Render()
|
||||
{
|
||||
RECT client = {};
|
||||
GetClientRect(m_hwnd, &client);
|
||||
const int width = client.right - client.left;
|
||||
const int height = client.bottom - client.top;
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureBackBuffer(width, height);
|
||||
if (m_backDc == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 网格数量变化(如合并/拆分/移动信号)会影响滚动条与绘图区宽度,需要重新布局。
|
||||
const std::size_t laneCount = m_document.IsLoaded() ? m_document.Lanes().size() : 0;
|
||||
if (laneCount != m_lastLaneCount)
|
||||
{
|
||||
m_lastLaneCount = laneCount;
|
||||
Relayout();
|
||||
}
|
||||
|
||||
m_toolbar.SetDocumentLoaded(m_document.IsLoaded());
|
||||
if (m_document.IsLoaded())
|
||||
{
|
||||
m_toolbar.SetStatusText(L"视图 " + FormatValue(m_plot.ViewMinTime()) + L" ~ " +
|
||||
FormatValue(m_plot.ViewMaxTime()));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_toolbar.SetStatusText(std::wstring());
|
||||
}
|
||||
|
||||
{
|
||||
gd::Graphics g(m_backDc);
|
||||
g.SetSmoothingMode(gd::SmoothingModeAntiAlias);
|
||||
g.SetPixelOffsetMode(gd::PixelOffsetModeHalf);
|
||||
g.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit);
|
||||
|
||||
UiFillRect(g, gd::RectF(0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height)),
|
||||
m_theme.windowBg);
|
||||
|
||||
m_toolbar.Paint(g);
|
||||
m_panel.Paint(g);
|
||||
m_plot.Paint(g);
|
||||
|
||||
UiFillRect(g, m_splitterRect, m_theme.border);
|
||||
|
||||
PaintStatusBar(g);
|
||||
}
|
||||
|
||||
HDC windowDc = GetDC(m_hwnd);
|
||||
if (windowDc != nullptr)
|
||||
{
|
||||
BitBlt(windowDc, 0, 0, width, height, m_backDc, 0, 0, SRCCOPY);
|
||||
ReleaseDC(m_hwnd, windowDc);
|
||||
}
|
||||
}
|
||||
|
||||
void Application::PaintStatusBar(gd::Graphics& g)
|
||||
{
|
||||
UiFillRect(g, m_statusRect, m_theme.statusBg);
|
||||
UiDrawLine(g, m_statusRect.X, m_statusRect.Y, m_statusRect.X + m_statusRect.Width, m_statusRect.Y,
|
||||
m_theme.border, 1.0f);
|
||||
|
||||
const float padding = ScaleOf(12.0f, m_scale);
|
||||
const float halfWidth = m_statusRect.Width * 0.5f;
|
||||
|
||||
std::wstring leftText;
|
||||
if (m_document.IsLoaded())
|
||||
{
|
||||
leftText = m_document.FileName() + L" · " +
|
||||
FormatCount(static_cast<unsigned long long>(m_document.SampleCount())) + L" 采样点 · " +
|
||||
std::to_wstring(m_document.Signals().size()) + L" 信号 · " +
|
||||
std::to_wstring(m_document.Lanes().size()) + L" 网格";
|
||||
}
|
||||
else
|
||||
{
|
||||
leftText = L"未加载数据 · 请点击「打开 CSV」或拖拽文件到窗口";
|
||||
}
|
||||
|
||||
UiText(g, leftText, m_fonts.uiSmall,
|
||||
gd::RectF(m_statusRect.X + padding, m_statusRect.Y, halfWidth - padding, m_statusRect.Height),
|
||||
m_theme.text, TextAlign::LEFT);
|
||||
|
||||
std::wstring rightText = kStatusHint;
|
||||
std::uint32_t rightColor = m_theme.textMuted;
|
||||
if (!m_statusMessage.empty())
|
||||
{
|
||||
rightText = m_statusMessage;
|
||||
rightColor = m_statusIsError ? m_theme.danger : m_theme.textStrong;
|
||||
}
|
||||
else if (m_plot.HasCursor() && m_document.IsLoaded())
|
||||
{
|
||||
rightText = L"光标 · " + FormatValue(m_plot.CursorTime());
|
||||
rightColor = m_theme.text;
|
||||
}
|
||||
|
||||
UiText(g, rightText, m_fonts.uiSmall,
|
||||
gd::RectF(m_statusRect.X + halfWidth, m_statusRect.Y, halfWidth - padding, m_statusRect.Height),
|
||||
rightColor, TextAlign::RIGHT);
|
||||
}
|
||||
|
||||
void Application::SetStatusMessage(const std::wstring& text, bool isError)
|
||||
{
|
||||
m_statusMessage = text;
|
||||
m_statusIsError = isError;
|
||||
}
|
||||
|
||||
void Application::UpdateWindowTitle()
|
||||
{
|
||||
std::wstring title = kWindowTitle;
|
||||
if (m_document.IsLoaded())
|
||||
{
|
||||
title += L" — ";
|
||||
title += m_document.FileName();
|
||||
}
|
||||
SetWindowTextW(m_hwnd, title.c_str());
|
||||
}
|
||||
|
||||
void Application::OpenFileDialog()
|
||||
{
|
||||
wchar_t buffer[MAX_PATH * 4] = {};
|
||||
|
||||
OPENFILENAMEW dialog = {};
|
||||
dialog.lStructSize = sizeof(dialog);
|
||||
dialog.hwndOwner = m_hwnd;
|
||||
dialog.lpstrFilter = L"CSV 数据文件 (*.csv)\0*.csv\0文本数据文件 (*.txt)\0*.txt\0所有文件 (*.*)\0*.*\0\0";
|
||||
dialog.lpstrFile = buffer;
|
||||
dialog.nMaxFile = MAX_PATH * 4;
|
||||
dialog.lpstrTitle = L"打开仿真数据文件";
|
||||
dialog.lpstrDefExt = L"csv";
|
||||
dialog.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_EXPLORER | OFN_NOCHANGEDIR;
|
||||
|
||||
if (GetOpenFileNameW(&dialog) == FALSE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
LoadFile(buffer);
|
||||
}
|
||||
|
||||
bool Application::LoadFile(const std::wstring& path)
|
||||
{
|
||||
CsvTable table;
|
||||
std::wstring error;
|
||||
if (!LoadCsvTable(path, table, error))
|
||||
{
|
||||
SetStatusMessage(L"解析失败:" + error, true);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
return false;
|
||||
}
|
||||
|
||||
SignalSet loaded;
|
||||
if (!loaded.BuildFromTable(table, path, error))
|
||||
{
|
||||
SetStatusMessage(L"解析失败:" + error, true);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_document = std::move(loaded);
|
||||
m_panel.SetDocument(&m_document);
|
||||
m_plot.SetDocument(&m_document);
|
||||
m_plot.FitAll();
|
||||
Relayout();
|
||||
|
||||
std::wstring message = L"已加载 " + m_document.FileName() + L"(" +
|
||||
FormatCount(static_cast<unsigned long long>(m_document.SampleCount())) +
|
||||
L" 行采样,时间列:" + m_document.TimeColumnName() + L")";
|
||||
if (!m_document.TimeMonotonic())
|
||||
{
|
||||
message += L" 提示:时间列非单调递增,光标取值使用最近邻策略。";
|
||||
}
|
||||
SetStatusMessage(message, false);
|
||||
|
||||
UpdateWindowTitle();
|
||||
InvalidateRect(m_hwnd, nullptr, TRUE);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Application::LoadCommandLineFile()
|
||||
{
|
||||
int argumentCount = 0;
|
||||
LPWSTR* arguments = CommandLineToArgvW(GetCommandLineW(), &argumentCount);
|
||||
if (arguments == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int index = 1; index < argumentCount; ++index)
|
||||
{
|
||||
const std::wstring argument = arguments[index];
|
||||
if (argument.empty() || argument[0] == L'-' || argument[0] == L'/')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
LoadFile(argument);
|
||||
break;
|
||||
}
|
||||
LocalFree(arguments);
|
||||
}
|
||||
|
||||
void Application::ExecuteToolbarAction(ToolbarAction action)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case ToolbarAction::OPEN_FILE:
|
||||
OpenFileDialog();
|
||||
break;
|
||||
case ToolbarAction::FIT_ALL:
|
||||
m_plot.FitAll();
|
||||
break;
|
||||
case ToolbarAction::FIT_TIME:
|
||||
m_plot.FitTime();
|
||||
break;
|
||||
case ToolbarAction::ZOOM_IN:
|
||||
m_plot.ZoomTimeStep(1.4);
|
||||
break;
|
||||
case ToolbarAction::ZOOM_OUT:
|
||||
m_plot.ZoomTimeStep(1.0 / 1.4);
|
||||
break;
|
||||
case ToolbarAction::MERGE_LANES:
|
||||
if (m_document.IsLoaded())
|
||||
{
|
||||
m_document.MergeIntoSingleLane();
|
||||
m_panel.SetDocument(&m_document);
|
||||
m_plot.FitAll();
|
||||
Relayout();
|
||||
SetStatusMessage(L"已将全部信号合并到同一网格。", false);
|
||||
}
|
||||
break;
|
||||
case ToolbarAction::SPLIT_LANES:
|
||||
if (m_document.IsLoaded())
|
||||
{
|
||||
m_document.SplitIntoLanes();
|
||||
m_panel.SetDocument(&m_document);
|
||||
m_plot.FitAll();
|
||||
Relayout();
|
||||
SetStatusMessage(L"已按信号拆分为独立网格。", false);
|
||||
}
|
||||
break;
|
||||
case ToolbarAction::RESET_VIEW:
|
||||
m_plot.FitAll();
|
||||
break;
|
||||
case ToolbarAction::NONE:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
InvalidateRect(m_hwnd, nullptr, TRUE);
|
||||
}
|
||||
|
||||
bool Application::HandleKeyDown(WPARAM key)
|
||||
{
|
||||
const bool control = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
|
||||
|
||||
if (control && key == L'O')
|
||||
{
|
||||
OpenFileDialog();
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case VK_ESCAPE:
|
||||
m_plot.ClearCursor();
|
||||
return true;
|
||||
case VK_ADD:
|
||||
case VK_OEM_PLUS:
|
||||
m_plot.ZoomTimeStep(1.4);
|
||||
return true;
|
||||
case VK_SUBTRACT:
|
||||
case VK_OEM_MINUS:
|
||||
m_plot.ZoomTimeStep(1.0 / 1.4);
|
||||
return true;
|
||||
case VK_LEFT:
|
||||
m_plot.PanTimeByFraction(-0.12);
|
||||
return true;
|
||||
case VK_RIGHT:
|
||||
m_plot.PanTimeByFraction(0.12);
|
||||
return true;
|
||||
case VK_UP:
|
||||
m_plot.ScrollLanesByFraction(-0.12);
|
||||
return true;
|
||||
case VK_DOWN:
|
||||
m_plot.ScrollLanesByFraction(0.12);
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (key == L'F')
|
||||
{
|
||||
m_plot.FitAll();
|
||||
return true;
|
||||
}
|
||||
if (key == L'G')
|
||||
{
|
||||
m_plot.FitTime();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Application::HandleMouseMove(int x, int y, bool buttonDown)
|
||||
{
|
||||
if (buttonDown && m_activeTarget != Target::NONE)
|
||||
{
|
||||
switch (m_activeTarget)
|
||||
{
|
||||
case Target::SPLITTER:
|
||||
if (m_draggingSplitter && m_scale > 0.0f)
|
||||
{
|
||||
const float desired = static_cast<float>(x - m_splitterGrabOffset) / m_scale;
|
||||
const float maximum = std::min(kMaxPanelWidth,
|
||||
static_cast<float>(kMinClientWidth) / m_scale);
|
||||
m_panelWidth = std::max(kMinPanelWidth, std::min(desired, maximum));
|
||||
Relayout();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case Target::PLOT:
|
||||
return m_plot.OnMouseMove(x, y);
|
||||
case Target::PANEL:
|
||||
return m_panel.OnMouseMove(x, y);
|
||||
case Target::TOOLBAR:
|
||||
return m_toolbar.OnMouseMove(x, y);
|
||||
case Target::NONE:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
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_plot.HitTest(x, y) ? m_plot.OnMouseMove(x, y) : m_plot.OnMouseLeave()) || changed;
|
||||
return changed;
|
||||
}
|
||||
|
||||
LRESULT Application::HandleMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case WM_SIZE:
|
||||
{
|
||||
ReleaseBackBuffer();
|
||||
Relayout();
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
return 0;
|
||||
}
|
||||
case WM_GETMINMAXINFO:
|
||||
{
|
||||
MINMAXINFO* info = reinterpret_cast<MINMAXINFO*>(lParam);
|
||||
info->ptMinTrackSize.x = static_cast<LONG>(ScaleOf(static_cast<float>(kMinClientWidth), m_scale));
|
||||
info->ptMinTrackSize.y = static_cast<LONG>(ScaleOf(static_cast<float>(kMinClientHeight), m_scale));
|
||||
return 0;
|
||||
}
|
||||
case WM_DPICHANGED:
|
||||
{
|
||||
const UINT dpi = HIWORD(wParam);
|
||||
const RECT* suggested = reinterpret_cast<const RECT*>(lParam);
|
||||
if (suggested != nullptr)
|
||||
{
|
||||
SetWindowPos(hwnd, nullptr, suggested->left, suggested->top,
|
||||
suggested->right - suggested->left, suggested->bottom - suggested->top,
|
||||
SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
ApplyDpi(dpi);
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
return 0;
|
||||
}
|
||||
case WM_ERASEBKGND:
|
||||
return 1;
|
||||
case WM_PAINT:
|
||||
{
|
||||
PAINTSTRUCT paintStruct = {};
|
||||
BeginPaint(hwnd, &paintStruct);
|
||||
Render();
|
||||
EndPaint(hwnd, &paintStruct);
|
||||
return 0;
|
||||
}
|
||||
case WM_MOUSEMOVE:
|
||||
{
|
||||
if (!m_trackingLeave)
|
||||
{
|
||||
TRACKMOUSEEVENT track = {};
|
||||
track.cbSize = sizeof(track);
|
||||
track.dwFlags = TME_LEAVE;
|
||||
track.hwndTrack = hwnd;
|
||||
TrackMouseEvent(&track);
|
||||
m_trackingLeave = true;
|
||||
}
|
||||
|
||||
const int x = GET_X_LPARAM(lParam);
|
||||
const int y = GET_Y_LPARAM(lParam);
|
||||
const bool buttonDown = (wParam & (MK_LBUTTON | MK_RBUTTON)) != 0;
|
||||
if (HandleMouseMove(x, y, buttonDown))
|
||||
{
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_MOUSELEAVE:
|
||||
{
|
||||
m_trackingLeave = false;
|
||||
bool changed = m_toolbar.OnMouseLeave();
|
||||
changed = m_panel.OnMouseLeave() || changed;
|
||||
changed = m_plot.OnMouseLeave() || changed;
|
||||
if (changed)
|
||||
{
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_LBUTTONDOWN:
|
||||
{
|
||||
SetFocus(hwnd);
|
||||
const int x = GET_X_LPARAM(lParam);
|
||||
const int y = GET_Y_LPARAM(lParam);
|
||||
|
||||
if (UiPointInRect(m_splitterRect, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
SetCapture(hwnd);
|
||||
m_draggingSplitter = true;
|
||||
m_activeTarget = Target::SPLITTER;
|
||||
m_splitterGrabOffset = x - static_cast<int>(m_splitterRect.X);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (m_toolbar.HitTest(x, y))
|
||||
{
|
||||
SetCapture(hwnd);
|
||||
m_activeTarget = Target::TOOLBAR;
|
||||
ToolbarAction action = ToolbarAction::NONE;
|
||||
m_toolbar.OnLButtonDown(x, y, action);
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (m_panel.HitTest(x, y))
|
||||
{
|
||||
SetCapture(hwnd);
|
||||
m_activeTarget = Target::PANEL;
|
||||
if (m_panel.OnLButtonDown(x, y))
|
||||
{
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (m_plot.HitTest(x, y))
|
||||
{
|
||||
SetCapture(hwnd);
|
||||
m_activeTarget = Target::PLOT;
|
||||
if (m_plot.OnLButtonDown(x, y))
|
||||
{
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
m_activeTarget = Target::NONE;
|
||||
return 0;
|
||||
}
|
||||
case WM_LBUTTONUP:
|
||||
{
|
||||
const int x = GET_X_LPARAM(lParam);
|
||||
const int y = GET_Y_LPARAM(lParam);
|
||||
ReleaseCapture();
|
||||
|
||||
bool changed = false;
|
||||
switch (m_activeTarget)
|
||||
{
|
||||
case Target::SPLITTER:
|
||||
m_draggingSplitter = false;
|
||||
break;
|
||||
case Target::TOOLBAR:
|
||||
{
|
||||
ToolbarAction action = ToolbarAction::NONE;
|
||||
if (m_toolbar.OnLButtonUp(x, y, action))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
m_activeTarget = Target::NONE;
|
||||
if (action != ToolbarAction::NONE)
|
||||
{
|
||||
ExecuteToolbarAction(action);
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Target::PANEL:
|
||||
changed = m_panel.OnLButtonUp(x, y) || changed;
|
||||
break;
|
||||
case Target::PLOT:
|
||||
changed = m_plot.OnLButtonUp(x, y) || changed;
|
||||
break;
|
||||
case Target::NONE:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
m_activeTarget = Target::NONE;
|
||||
changed = true;
|
||||
if (changed)
|
||||
{
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_LBUTTONDBLCLK:
|
||||
{
|
||||
const int x = GET_X_LPARAM(lParam);
|
||||
const int y = GET_Y_LPARAM(lParam);
|
||||
bool changed = false;
|
||||
if (m_panel.HitTest(x, y))
|
||||
{
|
||||
changed = m_panel.OnDoubleClick(x, y);
|
||||
}
|
||||
else if (m_plot.HitTest(x, y))
|
||||
{
|
||||
changed = m_plot.OnDoubleClick(x, y);
|
||||
}
|
||||
if (changed)
|
||||
{
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_RBUTTONDOWN:
|
||||
{
|
||||
const int x = GET_X_LPARAM(lParam);
|
||||
const int y = GET_Y_LPARAM(lParam);
|
||||
|
||||
if (m_panel.HitTest(x, y))
|
||||
{
|
||||
SetCapture(hwnd);
|
||||
m_activeTarget = Target::PANEL;
|
||||
if (m_panel.OnRButtonDown(x, y))
|
||||
{
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
}
|
||||
ReleaseCapture();
|
||||
m_activeTarget = Target::NONE;
|
||||
return 0;
|
||||
}
|
||||
if (m_plot.HitTest(x, y))
|
||||
{
|
||||
SetCapture(hwnd);
|
||||
m_activeTarget = Target::PLOT;
|
||||
if (m_plot.OnRButtonDown(x, y))
|
||||
{
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_RBUTTONUP:
|
||||
{
|
||||
const int x = GET_X_LPARAM(lParam);
|
||||
const int y = GET_Y_LPARAM(lParam);
|
||||
bool changed = false;
|
||||
if (m_activeTarget == Target::PLOT)
|
||||
{
|
||||
changed = m_plot.OnRButtonUp(x, y);
|
||||
}
|
||||
ReleaseCapture();
|
||||
m_activeTarget = Target::NONE;
|
||||
if (changed)
|
||||
{
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_MOUSEWHEEL:
|
||||
{
|
||||
POINT point = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
|
||||
ScreenToClient(hwnd, &point);
|
||||
|
||||
const int delta = GET_WHEEL_DELTA_WPARAM(wParam);
|
||||
const bool control = (GET_KEYSTATE_WPARAM(wParam) & MK_CONTROL) != 0;
|
||||
const bool shift = (GET_KEYSTATE_WPARAM(wParam) & MK_SHIFT) != 0;
|
||||
const bool alt = (GetKeyState(VK_MENU) & 0x8000) != 0;
|
||||
|
||||
bool changed = false;
|
||||
if (m_panel.HitTest(point.x, point.y))
|
||||
{
|
||||
changed = m_panel.OnMouseWheel(delta);
|
||||
}
|
||||
else if (m_plot.HitTest(point.x, point.y))
|
||||
{
|
||||
changed = m_plot.OnMouseWheel(point.x, point.y, delta, control, shift, alt);
|
||||
}
|
||||
if (changed)
|
||||
{
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_SETCURSOR:
|
||||
{
|
||||
if (LOWORD(lParam) == HTCLIENT)
|
||||
{
|
||||
POINT point = {};
|
||||
GetCursorPos(&point);
|
||||
ScreenToClient(hwnd, &point);
|
||||
|
||||
HCURSOR cursor = nullptr;
|
||||
if (UiPointInRect(m_splitterRect, static_cast<float>(point.x), static_cast<float>(point.y)))
|
||||
{
|
||||
cursor = LoadCursorW(nullptr, IDC_SIZEWE);
|
||||
}
|
||||
else if (m_plot.HitTest(point.x, point.y))
|
||||
{
|
||||
cursor = m_plot.QueryCursor(point.x, point.y);
|
||||
}
|
||||
if (cursor == nullptr)
|
||||
{
|
||||
cursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||
}
|
||||
SetCursor(cursor);
|
||||
return TRUE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WM_KEYDOWN:
|
||||
if (HandleKeyDown(wParam))
|
||||
{
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_DROPFILES:
|
||||
{
|
||||
HDROP drop = reinterpret_cast<HDROP>(wParam);
|
||||
const UINT length = DragQueryFileW(drop, 0, nullptr, 0);
|
||||
if (length > 0)
|
||||
{
|
||||
std::vector<wchar_t> buffer(static_cast<std::size_t>(length) + 1, L'\0');
|
||||
if (DragQueryFileW(drop, 0, buffer.data(), length + 1) > 0)
|
||||
{
|
||||
LoadFile(std::wstring(buffer.data()));
|
||||
}
|
||||
}
|
||||
DragFinish(drop);
|
||||
return 0;
|
||||
}
|
||||
case WM_DESTROY:
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return DefWindowProcW(hwnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
@@ -0,0 +1,21 @@
|
||||
#ifndef __MAIN_HPP__
|
||||
#define __MAIN_HPP__
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief 应用程序入口逻辑(GUI 子系统)。
|
||||
* @param [IN] instance 当前模块实例句柄。
|
||||
* @param [IN] prevInstance 保留参数,Win32 下恒为空。
|
||||
* @param [IN] commandLine 命令行参数字符串(ANSI 编码)。
|
||||
* @param [IN] showCommand 主窗口显示方式。
|
||||
* @return 进程退出码。
|
||||
*/
|
||||
int RunApplication(HINSTANCE instance, HINSTANCE prevInstance, LPSTR commandLine, int showCommand);
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __MAIN_HPP__
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "Main.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
int RunApplication(HINSTANCE instance, HINSTANCE /*prevInstance*/, LPSTR /*commandLine*/, int showCommand)
|
||||
{
|
||||
return Application::Instance().Run(instance, showCommand);
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
/**
|
||||
* @brief Windows GUI 程序入口。
|
||||
* @param [IN] instance 当前模块实例句柄。
|
||||
* @param [IN] prevInstance 保留参数,Win32 下恒为空。
|
||||
* @param [IN] commandLine 命令行参数字符串(ANSI 编码)。
|
||||
* @param [IN] showCommand 主窗口显示方式。
|
||||
* @return 进程退出码。
|
||||
*/
|
||||
int WINAPI WinMain(HINSTANCE instance, HINSTANCE prevInstance, LPSTR commandLine, int showCommand)
|
||||
{
|
||||
return jdi::RunApplication(instance, prevInstance, commandLine, showCommand);
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
#ifndef __PLOT_VIEW_HPP__
|
||||
#define __PLOT_VIEW_HPP__
|
||||
|
||||
#include <windows.h>
|
||||
#include <objidl.h>
|
||||
#include <gdiplus.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "PaintUtil.hpp"
|
||||
#include "SignalSet.hpp"
|
||||
#include "Theme.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief 绘图区拖拽交互模式。
|
||||
*/
|
||||
enum class PlotDragMode : int
|
||||
{
|
||||
NONE = 0, /**< 未拖拽。 */
|
||||
PAN_TIME, /**< 水平平移时间轴。 */
|
||||
ZOOM_Y, /**< 纵向缩放某网格的幅值轴。 */
|
||||
BOX_ZOOM, /**< 右键框选缩放。 */
|
||||
SCROLL_LANES /**< 拖动网格滚动条。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 波形绘图区:网格、时间轴、光标、缩放与自适应。
|
||||
*/
|
||||
class PlotView
|
||||
{
|
||||
public:
|
||||
PlotView();
|
||||
|
||||
/**
|
||||
* @brief 绑定字体与主题资源。
|
||||
* @param [IN] fonts 字体集合,需在视图生命周期内保持有效。
|
||||
* @param [IN] theme 主题配色,需在视图生命周期内保持有效。
|
||||
*/
|
||||
void Setup(const UiFonts* fonts, const Theme* theme);
|
||||
|
||||
/**
|
||||
* @brief 绑定数据集。
|
||||
* @param [IN] document 数据集指针,允许为空。
|
||||
*/
|
||||
void SetDocument(SignalSet* document);
|
||||
|
||||
/**
|
||||
* @brief 设置 DPI 缩放比例。
|
||||
* @param [IN] scale 缩放比例(1.0 对应 96 DPI)。
|
||||
*/
|
||||
void SetScale(float scale);
|
||||
|
||||
/**
|
||||
* @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 客户区纵坐标。
|
||||
* @return 需要重绘返回 true。
|
||||
*/
|
||||
bool OnLButtonUp(int x, int y);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标右键按下(开始框选缩放)。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 需要重绘返回 true。
|
||||
*/
|
||||
bool OnRButtonDown(int x, int y);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标右键抬起(结束框选缩放或清除光标)。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 需要重绘返回 true。
|
||||
*/
|
||||
bool OnRButtonUp(int x, int y);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标滚轮。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @param [IN] delta 滚轮增量。
|
||||
* @param [IN] ctrl 是否按下 Ctrl(缩放当前网格幅值)。
|
||||
* @param [IN] shift 是否按下 Shift(平移时间轴)。
|
||||
* @param [IN] alt 是否按下 Alt(纵向滚动网格)。
|
||||
* @return 需要重绘返回 true。
|
||||
*/
|
||||
bool OnMouseWheel(int x, int y, int delta, bool ctrl, bool shift, bool alt);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标双击(恢复自适应视图)。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 需要重绘返回 true。
|
||||
*/
|
||||
bool OnDoubleClick(int x, int y);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标离开绘图区。
|
||||
* @return 需要重绘返回 true。
|
||||
*/
|
||||
bool OnMouseLeave();
|
||||
|
||||
/**
|
||||
* @brief 判断坐标是否落在绘图区范围内。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 落在范围内返回 true。
|
||||
*/
|
||||
bool HitTest(int x, int y) const;
|
||||
|
||||
/**
|
||||
* @brief 查询当前鼠标位置应使用的系统光标。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 系统光标句柄,未命中特殊区域返回 nullptr。
|
||||
*/
|
||||
HCURSOR QueryCursor(int x, int y) const;
|
||||
|
||||
/**
|
||||
* @brief 时间轴自适应到全部数据范围。
|
||||
*/
|
||||
void FitTime();
|
||||
|
||||
/**
|
||||
* @brief 时间轴与幅值轴同时自适应。
|
||||
*/
|
||||
void FitAll();
|
||||
|
||||
/**
|
||||
* @brief 以给定倍率缩放时间轴(以视图中心为锚点)。
|
||||
* @param [IN] factor 缩放倍率,大于 1 表示放大。
|
||||
*/
|
||||
void ZoomTimeStep(double factor);
|
||||
|
||||
/**
|
||||
* @brief 按当前视图跨度的比例平移时间轴。
|
||||
* @param [IN] fraction 平移比例,正值向右(时间增大)平移。
|
||||
*/
|
||||
void PanTimeByFraction(double fraction);
|
||||
|
||||
/**
|
||||
* @brief 按网格内容高度的比例纵向滚动网格。
|
||||
* @param [IN] fraction 滚动比例,正值向下滚动。
|
||||
*/
|
||||
void ScrollLanesByFraction(double fraction);
|
||||
|
||||
/**
|
||||
* @brief 获取网格当前内容总高度。
|
||||
* @return 内容高度(像素)。
|
||||
*/
|
||||
float LaneContentHeight() const;
|
||||
|
||||
/**
|
||||
* @brief 清除已固定的时间光标。
|
||||
*/
|
||||
void ClearCursor();
|
||||
|
||||
/**
|
||||
* @brief 判断当前是否存在激活的时间光标。
|
||||
* @return 存在返回 true。
|
||||
*/
|
||||
bool HasCursor() const { return m_cursorVisible; }
|
||||
|
||||
/**
|
||||
* @brief 获取当前激活光标对应的时间。
|
||||
* @return 光标时间。
|
||||
*/
|
||||
double CursorTime() const { return m_cursorTime; }
|
||||
|
||||
/**
|
||||
* @brief 获取当前视图左边界时间。
|
||||
* @return 视图左边界时间。
|
||||
*/
|
||||
double ViewMinTime() const { return m_viewMin; }
|
||||
|
||||
/**
|
||||
* @brief 获取当前视图右边界时间。
|
||||
* @return 视图右边界时间。
|
||||
*/
|
||||
double ViewMaxTime() const { return m_viewMax; }
|
||||
|
||||
/**
|
||||
* @brief 获取绘图区(波形区域)矩形。
|
||||
* @return 波形区域矩形。
|
||||
*/
|
||||
const gd::RectF& WaveformRect() const { return m_plotRect; }
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 单个网格的绘制信息。
|
||||
*/
|
||||
struct LaneVisual
|
||||
{
|
||||
gd::RectF rect; /**< 网格波形区域。 */
|
||||
double yMin = 0.0; /**< 幅值下限。 */
|
||||
double yMax = 1.0; /**< 幅值上限。 */
|
||||
bool empty = true; /**< 是否没有可见信号。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 重新计算各网格的布局与幅值范围。
|
||||
*/
|
||||
void BuildLaneVisuals();
|
||||
|
||||
/**
|
||||
* @brief 限制网格滚动偏移量。
|
||||
*/
|
||||
void ClampLaneScroll();
|
||||
|
||||
/**
|
||||
* @brief 将时间映射为绘图区横坐标。
|
||||
* @param [IN] time 时间值。
|
||||
* @return 横坐标。
|
||||
*/
|
||||
float TimeToX(double time) const;
|
||||
|
||||
/**
|
||||
* @brief 将绘图区横坐标还原为时间。
|
||||
* @param [IN] x 横坐标。
|
||||
* @return 时间值。
|
||||
*/
|
||||
double XToTime(float x) const;
|
||||
|
||||
/**
|
||||
* @brief 将幅值映射为纵坐标。
|
||||
* @param [IN] value 幅值。
|
||||
* @param [IN] lane 网格绘制信息。
|
||||
* @return 纵坐标。
|
||||
*/
|
||||
float ValueToY(double value, const LaneVisual& lane) const;
|
||||
|
||||
/**
|
||||
* @brief 将纵坐标还原为幅值。
|
||||
* @param [IN] y 纵坐标。
|
||||
* @param [IN] lane 网格绘制信息。
|
||||
* @return 幅值。
|
||||
*/
|
||||
double YToValue(float y, const LaneVisual& lane) const;
|
||||
|
||||
/**
|
||||
* @brief 查询指定纵坐标落在哪个网格内。
|
||||
* @param [IN] y 纵坐标。
|
||||
* @return 网格下标,未命中返回 -1。
|
||||
*/
|
||||
int LaneAtY(int y) const;
|
||||
|
||||
/**
|
||||
* @brief 限制时间视图范围,避免过度平移到数据之外。
|
||||
*/
|
||||
void ClampTimeView();
|
||||
|
||||
/**
|
||||
* @brief 以指定锚点缩放时间轴。
|
||||
* @param [IN] factor 缩放倍率,大于 1 表示放大。
|
||||
* @param [IN] anchorTime 锚点时间。
|
||||
*/
|
||||
void ZoomTimeAt(double factor, double anchorTime);
|
||||
|
||||
/**
|
||||
* @brief 构建单条信号在当前视图下的折线点序列。
|
||||
* @param [IN] signal 目标信号。
|
||||
* @param [IN] lane 所属网格绘制信息。
|
||||
* @param [IN] stepMode 是否使用阶梯保持连线。
|
||||
* @param [OUT] points 输出点序列。
|
||||
* @param [OUT] breaks 输出每段折线的起始下标(用于处理缺失值断线)。
|
||||
*/
|
||||
void BuildPolyline(const Signal& signal, const LaneVisual& lane, bool stepMode,
|
||||
std::vector<gd::PointF>& points, std::vector<std::size_t>& breaks) const;
|
||||
|
||||
/**
|
||||
* @brief 绘制单个网格。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] laneIndex 网格下标。
|
||||
* @param [IN] lane 网格绘制信息。
|
||||
* @param [IN] timeTicks 时间刻度序列。
|
||||
* @param [IN] timeStep 时间刻度步长。
|
||||
*/
|
||||
void PaintLane(gd::Graphics& g, int laneIndex, const LaneVisual& lane, const std::vector<double>& timeTicks,
|
||||
double timeStep);
|
||||
|
||||
/**
|
||||
* @brief 绘制底部时间轴。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] timeTicks 时间刻度序列。
|
||||
* @param [IN] timeStep 时间刻度步长。
|
||||
*/
|
||||
void PaintTimeAxis(gd::Graphics& g, const std::vector<double>& timeTicks, double timeStep);
|
||||
|
||||
/**
|
||||
* @brief 绘制时间光标及其读数。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] lanes 网格绘制信息集合。
|
||||
*/
|
||||
void PaintCursor(gd::Graphics& g, const std::vector<LaneVisual>& lanes);
|
||||
|
||||
/**
|
||||
* @brief 绘制网格纵向滚动条。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
*/
|
||||
void PaintScrollBar(gd::Graphics& g);
|
||||
|
||||
/**
|
||||
* @brief 绘制网格标题(信号图例)。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] laneIndex 网格下标。
|
||||
* @param [IN] lane 网格绘制信息。
|
||||
*/
|
||||
void PaintLaneLegend(gd::Graphics& g, int laneIndex, const LaneVisual& lane);
|
||||
|
||||
/**
|
||||
* @brief 在网格内绘制各信号的幅值读数。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] laneIndex 网格下标。
|
||||
* @param [IN] lane 网格绘制信息。
|
||||
*/
|
||||
void PaintLaneReadout(gd::Graphics& g, int laneIndex, const LaneVisual& lane);
|
||||
|
||||
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
|
||||
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
||||
SignalSet* m_document = nullptr; /**< 数据集。 */
|
||||
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
||||
|
||||
gd::RectF m_area; /**< 绘图区整体区域。 */
|
||||
gd::RectF m_plotRect; /**< 波形绘制区域。 */
|
||||
gd::RectF m_timeAxisRect; /**< 时间轴区域。 */
|
||||
gd::RectF m_scrollBarRect; /**< 网格滚动条区域。 */
|
||||
|
||||
float m_yAxisWidth = 64.0f; /**< 纵轴标签列宽度(逻辑像素)。 */
|
||||
float m_timeAxisHeight = 34.0f; /**< 时间轴高度(逻辑像素)。 */
|
||||
float m_laneGap = 6.0f; /**< 网格间距(逻辑像素)。 */
|
||||
float m_minLaneHeight = 46.0f; /**< 网格最小高度(逻辑像素)。 */
|
||||
float m_laneScroll = 0.0f; /**< 网格纵向滚动偏移量。 */
|
||||
float m_laneContentHeight = 0.0f; /**< 网格内容总高度。 */
|
||||
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; /**< 当前拖拽模式。 */
|
||||
int m_dragLane = -1; /**< 拖拽所作用的网格下标。 */
|
||||
bool m_dragMoved = false; /**< 拖拽过程中是否发生了位移。 */
|
||||
int m_dragStartX = 0; /**< 拖拽起点横坐标。 */
|
||||
int m_dragStartY = 0; /**< 拖拽起点纵坐标。 */
|
||||
int m_dragLastX = 0; /**< 上一次拖拽横坐标。 */
|
||||
int m_dragLastY = 0; /**< 上一次拖拽纵坐标。 */
|
||||
double m_dragStartViewMin = 0.0; /**< 拖拽起始视图左边界。 */
|
||||
double m_dragStartViewMax = 1.0; /**< 拖拽起始视图右边界。 */
|
||||
double m_dragAnchorValue = 0.0; /**< 纵轴缩放锚点幅值。 */
|
||||
double m_dragAnchorRatio = 0.5; /**< 纵轴缩放锚点相对位置。 */
|
||||
double m_dragStartYMin = 0.0; /**< 拖拽起始幅值下限。 */
|
||||
double m_dragStartYMax = 1.0; /**< 拖拽起始幅值上限。 */
|
||||
gd::RectF m_boxRect; /**< 框选缩放矩形。 */
|
||||
|
||||
int m_hoverLane = -1; /**< 当前悬停网格下标。 */
|
||||
std::vector<LaneVisual> m_lanes; /**< 当前网格绘制信息集合。 */
|
||||
};
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __PLOT_VIEW_HPP__
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,218 @@
|
||||
#ifndef __SIGNAL_PANEL_HPP__
|
||||
#define __SIGNAL_PANEL_HPP__
|
||||
|
||||
#include <windows.h>
|
||||
#include <objidl.h>
|
||||
#include <gdiplus.h>
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
#include "PaintUtil.hpp"
|
||||
#include "SignalSet.hpp"
|
||||
#include "Theme.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief 左侧信号面板:信号列表、可见性开关与样式编辑。
|
||||
*/
|
||||
class SignalPanel
|
||||
{
|
||||
public:
|
||||
SignalPanel();
|
||||
|
||||
/**
|
||||
* @brief 绑定宿主窗口、字体与主题资源。
|
||||
* @param [IN] host 宿主窗口句柄,用于弹出上下文菜单。
|
||||
* @param [IN] fonts 字体集合,需在面板生命周期内保持有效。
|
||||
* @param [IN] theme 主题配色,需在面板生命周期内保持有效。
|
||||
*/
|
||||
void Setup(HWND host, const UiFonts* fonts, const Theme* theme);
|
||||
|
||||
/**
|
||||
* @brief 绑定数据集。
|
||||
* @param [IN] document 数据集指针,允许为空。
|
||||
*/
|
||||
void SetDocument(SignalSet* document);
|
||||
|
||||
/**
|
||||
* @brief 设置 DPI 缩放比例。
|
||||
* @param [IN] scale 缩放比例(1.0 对应 96 DPI)。
|
||||
*/
|
||||
void SetScale(float scale);
|
||||
|
||||
/**
|
||||
* @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 客户区纵坐标。
|
||||
* @return 视觉状态发生变化返回 true。
|
||||
*/
|
||||
bool OnLButtonUp(int x, int y);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标左键双击(切换信号可见性)。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 需要重绘返回 true。
|
||||
*/
|
||||
bool OnDoubleClick(int x, int y);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标右键按下(弹出信号上下文菜单)。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 需要重绘返回 true。
|
||||
*/
|
||||
bool OnRButtonDown(int x, int y);
|
||||
|
||||
/**
|
||||
* @brief 处理滚轮滚动(列表纵向滚动)。
|
||||
* @param [IN] delta 滚轮增量。
|
||||
* @return 需要重绘返回 true。
|
||||
*/
|
||||
bool OnMouseWheel(int delta);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标离开面板。
|
||||
* @return 视觉状态发生变化返回 true。
|
||||
*/
|
||||
bool OnMouseLeave();
|
||||
|
||||
/**
|
||||
* @brief 判断坐标是否落在面板区域内。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 落在区域内返回 true。
|
||||
*/
|
||||
bool HitTest(int x, int y) const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 样式编辑区中的可点击条目类型。
|
||||
*/
|
||||
enum class StyleHit : int
|
||||
{
|
||||
NONE = 0, /**< 未命中。 */
|
||||
COLOR, /**< 颜色色块。 */
|
||||
WIDTH, /**< 线宽按钮。 */
|
||||
MODE, /**< 绘制方式按钮。 */
|
||||
LANE, /**< 网格选择按钮。 */
|
||||
TOGGLE_ALL /**< 全部显示/隐藏按钮。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 计算列表区域内容高度。
|
||||
* @return 内容高度。
|
||||
*/
|
||||
float ContentHeight() const;
|
||||
|
||||
/**
|
||||
* @brief 限制滚动偏移量到合法范围。
|
||||
*/
|
||||
void ClampScroll();
|
||||
|
||||
/**
|
||||
* @brief 查询指定坐标对应的信号行下标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 行下标,未命中返回 -1。
|
||||
*/
|
||||
int RowAtY(int y) const;
|
||||
|
||||
/**
|
||||
* @brief 计算行的纵向矩形。
|
||||
* @param [IN] row 行下标。
|
||||
* @return 行矩形。
|
||||
*/
|
||||
gd::RectF RowRect(int row) const;
|
||||
|
||||
/**
|
||||
* @brief 绘制列表区域的单行。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] row 行下标。
|
||||
*/
|
||||
void PaintRow(gd::Graphics& g, int row);
|
||||
|
||||
/**
|
||||
* @brief 绘制样式编辑区。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
*/
|
||||
void PaintStyleSection(gd::Graphics& g);
|
||||
|
||||
/**
|
||||
* @brief 弹出信号右键菜单。
|
||||
* @param [IN] signalIndex 目标信号下标。
|
||||
* @param [IN] screenX 屏幕坐标横坐标。
|
||||
* @param [IN] screenY 屏幕坐标纵坐标。
|
||||
*/
|
||||
void ShowSignalMenu(int signalIndex, int screenX, int screenY);
|
||||
|
||||
/**
|
||||
* @brief 弹出网格分配菜单。
|
||||
*/
|
||||
void ShowLaneMenu();
|
||||
|
||||
/**
|
||||
* @brief 判断全部信号是否均为可见状态。
|
||||
* @return 全部可见返回 true。
|
||||
*/
|
||||
bool AreAllSignalsVisible() const;
|
||||
|
||||
HWND m_host = nullptr; /**< 宿主窗口句柄。 */
|
||||
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
|
||||
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
||||
SignalSet* m_document = nullptr; /**< 数据集。 */
|
||||
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
||||
|
||||
gd::RectF m_area; /**< 面板整体区域。 */
|
||||
gd::RectF m_headerRect; /**< 标题区域。 */
|
||||
gd::RectF m_listRect; /**< 列表区域。 */
|
||||
gd::RectF m_styleRect; /**< 样式编辑区域。 */
|
||||
gd::RectF m_toggleAllRect; /**< 全部显示/隐藏按钮。 */
|
||||
gd::RectF m_laneButtonRect; /**< 网格选择按钮。 */
|
||||
|
||||
std::array<gd::RectF, 12> m_colorRects; /**< 颜色色块矩形。 */
|
||||
std::array<gd::RectF, 4> m_widthRects; /**< 线宽按钮矩形。 */
|
||||
std::array<gd::RectF, 4> m_modeRects; /**< 绘制方式按钮矩形。 */
|
||||
int m_colorCount = 0; /**< 实际可用的颜色数量。 */
|
||||
float m_widthRowLabelX = 0.0f; /**< 样式标签列起始横坐标。 */
|
||||
float m_styleRowHeight = 0.0f; /**< 样式行高。 */
|
||||
|
||||
float m_scrollOffset = 0.0f; /**< 列表滚动偏移量。 */
|
||||
int m_hoverRow = -1; /**< 当前悬停行。 */
|
||||
StyleHit m_hoverStyle = StyleHit::NONE; /**< 当前悬停的样式条目类型。 */
|
||||
int m_hoverStyleIndex = -1; /**< 当前悬停的样式条目下标。 */
|
||||
};
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __SIGNAL_PANEL_HPP__
|
||||
@@ -0,0 +1,889 @@
|
||||
#include "SignalPanel.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "Utf8Util.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
const float kHeaderHeight = 36.0f; /**< 标题区高度。 */
|
||||
const float kRowHeight = 30.0f; /**< 列表行高。 */
|
||||
const float kStyleHeight = 182.0f; /**< 样式编辑区高度。 */
|
||||
const float kPadding = 12.0f; /**< 通用内边距。 */
|
||||
const float kCheckSize = 15.0f; /**< 复选框尺寸。 */
|
||||
const float kSwatchSize = 12.0f; /**< 颜色小方块尺寸。 */
|
||||
const float kBadgeWidth = 30.0f; /**< 网格标签宽度。 */
|
||||
const float kBadgeHeight = 17.0f; /**< 网格标签高度。 */
|
||||
const float kCorner = 4.0f; /**< 通用圆角半径。 */
|
||||
const float kLineLabelWidth = 40.0f; /**< 样式行标签列宽度。 */
|
||||
|
||||
/** @brief 上下文菜单命令 ID 基准值。 */
|
||||
const int kMenuToggleVisible = 1;
|
||||
const int kMenuSolo = 2;
|
||||
const int kMenuShowAll = 3;
|
||||
const int kMenuModeBase = 100;
|
||||
const int kMenuWidthBase = 200;
|
||||
const int kMenuColorBase = 300;
|
||||
const int kMenuLaneBase = 400;
|
||||
const int kMenuLaneNew = 500;
|
||||
|
||||
/** @brief 可选线宽档位。 */
|
||||
const float kWidthOptions[] = {1.0f, 1.5f, 2.0f, 3.0f};
|
||||
|
||||
/** @brief 可选绘制方式顺序。 */
|
||||
const DrawMode kModeOptions[] = {DrawMode::LINE, DrawMode::SCATTER, DrawMode::LINE_SCATTER, DrawMode::STEP};
|
||||
}
|
||||
|
||||
SignalPanel::SignalPanel()
|
||||
{
|
||||
m_colorRects.fill(gd::RectF());
|
||||
m_widthRects.fill(gd::RectF());
|
||||
m_modeRects.fill(gd::RectF());
|
||||
}
|
||||
|
||||
void SignalPanel::Setup(HWND host, const UiFonts* fonts, const Theme* theme)
|
||||
{
|
||||
m_host = host;
|
||||
m_fonts = fonts;
|
||||
m_theme = theme;
|
||||
}
|
||||
|
||||
void SignalPanel::SetDocument(SignalSet* document)
|
||||
{
|
||||
m_document = document;
|
||||
m_scrollOffset = 0.0f;
|
||||
}
|
||||
|
||||
void SignalPanel::SetScale(float scale)
|
||||
{
|
||||
m_scale = (scale > 0.1f) ? scale : 1.0f;
|
||||
}
|
||||
|
||||
bool SignalPanel::HitTest(int x, int y) const
|
||||
{
|
||||
return UiPointInRect(m_area, static_cast<float>(x), static_cast<float>(y));
|
||||
}
|
||||
|
||||
float SignalPanel::ContentHeight() const
|
||||
{
|
||||
if (m_document == nullptr)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
return static_cast<float>(m_document->Signals().size()) * kRowHeight * m_scale;
|
||||
}
|
||||
|
||||
void SignalPanel::ClampScroll()
|
||||
{
|
||||
const float visibleHeight = m_listRect.Height;
|
||||
const float contentHeight = ContentHeight();
|
||||
const float maxOffset = std::max(0.0f, contentHeight - visibleHeight);
|
||||
m_scrollOffset = std::max(0.0f, std::min(m_scrollOffset, maxOffset));
|
||||
}
|
||||
|
||||
int SignalPanel::RowAtY(int y) const
|
||||
{
|
||||
if (m_document == nullptr || m_listRect.Height <= 0.0f)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
const float fy = static_cast<float>(y);
|
||||
if (fy < m_listRect.Y || fy > m_listRect.Y + m_listRect.Height)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
const float local = fy - m_listRect.Y + m_scrollOffset;
|
||||
if (local < 0.0f)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
const int row = static_cast<int>(local / (kRowHeight * m_scale));
|
||||
if (row < 0 || row >= static_cast<int>(m_document->Signals().size()))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
gd::RectF SignalPanel::RowRect(int row) const
|
||||
{
|
||||
const float rowHeight = kRowHeight * m_scale;
|
||||
const float top = m_listRect.Y + static_cast<float>(row) * rowHeight - m_scrollOffset;
|
||||
return gd::RectF(m_listRect.X, top, m_listRect.Width, rowHeight);
|
||||
}
|
||||
|
||||
void SignalPanel::Layout(const gd::RectF& area)
|
||||
{
|
||||
m_area = area;
|
||||
|
||||
m_headerRect = gd::RectF(area.X, area.Y, area.Width, kHeaderHeight * m_scale);
|
||||
|
||||
const float styleHeight = kStyleHeight * m_scale;
|
||||
m_styleRect = gd::RectF(area.X, area.Y + area.Height - styleHeight, area.Width, styleHeight);
|
||||
m_listRect = gd::RectF(area.X, m_headerRect.Y + m_headerRect.Height,
|
||||
area.Width, std::max(0.0f, m_styleRect.Y - m_headerRect.Y - m_headerRect.Height));
|
||||
|
||||
// 标题区右侧的“全部显示/隐藏”按钮。
|
||||
const float toggleWidth = 68.0f * m_scale;
|
||||
const float toggleHeight = 22.0f * m_scale;
|
||||
m_toggleAllRect = gd::RectF(area.X + area.Width - kPadding * m_scale - toggleWidth,
|
||||
m_headerRect.Y + (m_headerRect.Height - toggleHeight) * 0.5f,
|
||||
toggleWidth, toggleHeight);
|
||||
|
||||
// 样式编辑区布局。
|
||||
const float labelWidth = kLineLabelWidth * m_scale;
|
||||
m_widthRowLabelX = m_styleRect.X + kPadding * m_scale + labelWidth;
|
||||
m_styleRowHeight = 30.0f * m_scale;
|
||||
|
||||
const float contentWidth = m_styleRect.Width - kPadding * 2.0f * m_scale - labelWidth;
|
||||
const float contentLeft = m_widthRowLabelX;
|
||||
|
||||
int paletteCount = 0;
|
||||
const std::uint32_t* palette = SignalPalette(paletteCount);
|
||||
(void)palette;
|
||||
|
||||
m_colorCount = std::min(paletteCount, static_cast<int>(m_colorRects.size()));
|
||||
|
||||
// 色块尺寸按可用宽度自适应,避免超出面板。
|
||||
const float colorGap = 4.0f * m_scale;
|
||||
float swatchSize = 20.0f * m_scale;
|
||||
if (m_colorCount > 1)
|
||||
{
|
||||
const float fitted = (contentWidth - colorGap * static_cast<float>(m_colorCount - 1)) /
|
||||
static_cast<float>(m_colorCount);
|
||||
if (fitted < swatchSize)
|
||||
{
|
||||
swatchSize = std::max(10.0f * m_scale, fitted);
|
||||
}
|
||||
}
|
||||
const float colorRowTop = m_styleRect.Y + 30.0f * m_scale;
|
||||
for (int index = 0; index < m_colorCount; ++index)
|
||||
{
|
||||
const float x = contentLeft + static_cast<float>(index) * (swatchSize + colorGap);
|
||||
m_colorRects[static_cast<std::size_t>(index)] =
|
||||
gd::RectF(x, colorRowTop, swatchSize, swatchSize);
|
||||
}
|
||||
|
||||
const float widthRowTop = colorRowTop + swatchSize + 12.0f * m_scale;
|
||||
const float widthGap = 6.0f * m_scale;
|
||||
const float widthButtonWidth = std::min(56.0f * m_scale,
|
||||
(contentWidth - widthGap * 3.0f) / 4.0f);
|
||||
for (int index = 0; index < 4; ++index)
|
||||
{
|
||||
const float x = contentLeft + static_cast<float>(index) * (widthButtonWidth + widthGap);
|
||||
m_widthRects[static_cast<std::size_t>(index)] =
|
||||
gd::RectF(x, widthRowTop, widthButtonWidth, 26.0f * m_scale);
|
||||
}
|
||||
|
||||
const float modeRowTop = widthRowTop + 26.0f * m_scale + 10.0f * m_scale - 2.0f * m_scale;
|
||||
const float modeGap = 6.0f * m_scale;
|
||||
const float modeButtonWidth = (contentWidth - modeGap * 3.0f) / 4.0f;
|
||||
for (int index = 0; index < 4; ++index)
|
||||
{
|
||||
const float x = contentLeft + static_cast<float>(index) * (modeButtonWidth + modeGap);
|
||||
m_modeRects[static_cast<std::size_t>(index)] =
|
||||
gd::RectF(x, modeRowTop, modeButtonWidth, 26.0f * m_scale);
|
||||
}
|
||||
|
||||
const float laneRowTop = modeRowTop + 26.0f * m_scale + 14.0f * m_scale;
|
||||
m_laneButtonRect = gd::RectF(contentLeft, laneRowTop,
|
||||
std::min(contentWidth, 150.0f * m_scale), 26.0f * m_scale);
|
||||
|
||||
ClampScroll();
|
||||
}
|
||||
|
||||
void SignalPanel::Paint(gd::Graphics& g)
|
||||
{
|
||||
if (m_theme == nullptr || m_fonts == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UiFillRect(g, m_area, m_theme->panelBg);
|
||||
|
||||
// 标题区。
|
||||
{
|
||||
const std::wstring title =
|
||||
(m_document == nullptr)
|
||||
? std::wstring(L"信号")
|
||||
: (L"信号 · " + FormatCount(static_cast<unsigned long long>(m_document->Signals().size())));
|
||||
|
||||
gd::RectF titleRect(m_headerRect.X + kPadding * m_scale, m_headerRect.Y,
|
||||
m_headerRect.Width - kPadding * 2.0f * m_scale, m_headerRect.Height);
|
||||
UiText(g, title, m_fonts->uiBold, titleRect, m_theme->textStrong, TextAlign::LEFT);
|
||||
|
||||
const bool allVisible = AreAllSignalsVisible();
|
||||
const std::wstring toggleLabel = allVisible ? L"全部隐藏" : L"全部显示";
|
||||
const bool toggleHover = (m_hoverStyle == StyleHit::TOGGLE_ALL);
|
||||
if (toggleHover)
|
||||
{
|
||||
UiFillRoundRect(g, m_toggleAllRect, kCorner * m_scale, m_theme->hover);
|
||||
}
|
||||
UiDrawRoundRect(g, m_toggleAllRect, kCorner * m_scale, m_theme->border, 1.0f);
|
||||
UiText(g, toggleLabel, m_fonts->uiSmall, m_toggleAllRect,
|
||||
(m_document == nullptr) ? m_theme->textMuted : m_theme->text, TextAlign::CENTER);
|
||||
}
|
||||
|
||||
// 列表区。
|
||||
{
|
||||
const int saveState = g.Save();
|
||||
g.SetClip(gd::RectF(m_listRect.X, m_listRect.Y, m_listRect.Width, m_listRect.Height));
|
||||
|
||||
if (m_document == nullptr || m_document->Signals().empty())
|
||||
{
|
||||
UiText(g, L"尚未加载数据,请打开 CSV 文件", m_fonts->uiSmall, m_listRect, m_theme->textMuted,
|
||||
TextAlign::CENTER);
|
||||
}
|
||||
else
|
||||
{
|
||||
const int rowCount = static_cast<int>(m_document->Signals().size());
|
||||
for (int row = 0; row < rowCount; ++row)
|
||||
{
|
||||
const gd::RectF rect = RowRect(row);
|
||||
if (rect.Y + rect.Height < m_listRect.Y || rect.Y > m_listRect.Y + m_listRect.Height)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
PaintRow(g, row);
|
||||
}
|
||||
}
|
||||
|
||||
g.Restore(saveState);
|
||||
}
|
||||
|
||||
PaintStyleSection(g);
|
||||
|
||||
// 分隔线。
|
||||
UiDrawLine(g, m_area.X + m_area.Width - 1.0f, m_area.Y, m_area.X + m_area.Width - 1.0f,
|
||||
m_area.Y + m_area.Height, m_theme->border, 1.0f);
|
||||
UiDrawLine(g, m_headerRect.X, m_headerRect.Y + m_headerRect.Height - 1.0f, m_headerRect.X + m_headerRect.Width,
|
||||
m_headerRect.Y + m_headerRect.Height - 1.0f, m_theme->border, 1.0f);
|
||||
if (m_styleRect.Height > 0.0f)
|
||||
{
|
||||
UiDrawLine(g, m_styleRect.X, m_styleRect.Y, m_styleRect.X + m_styleRect.Width, m_styleRect.Y,
|
||||
m_theme->border, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void SignalPanel::PaintRow(gd::Graphics& g, int row)
|
||||
{
|
||||
if (m_document == nullptr || row < 0 || row >= static_cast<int>(m_document->Signals().size()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const Signal& signal = m_document->Signals()[static_cast<std::size_t>(row)];
|
||||
const gd::RectF rect = RowRect(row);
|
||||
const bool selected = (m_document->SelectedSignal() == row);
|
||||
|
||||
if (selected)
|
||||
{
|
||||
UiFillRect(g, rect, m_theme->selection);
|
||||
UiFillRect(g, gd::RectF(rect.X, rect.Y, 2.0f * m_scale, rect.Height), m_theme->accent);
|
||||
}
|
||||
else if (m_hoverRow == row)
|
||||
{
|
||||
UiFillRect(g, rect, m_theme->hover);
|
||||
}
|
||||
|
||||
const float centerY = rect.Y + rect.Height * 0.5f;
|
||||
|
||||
// 复选框。
|
||||
const gd::RectF checkRect(rect.X + kPadding * m_scale, centerY - kCheckSize * m_scale * 0.5f,
|
||||
kCheckSize * m_scale, kCheckSize * m_scale);
|
||||
if (signal.visible)
|
||||
{
|
||||
UiFillRoundRect(g, checkRect, 3.0f * m_scale, m_theme->accent);
|
||||
gd::Pen pen(ToGdiColor(m_theme->textOnAccent), 1.6f * m_scale);
|
||||
pen.SetStartCap(gd::LineCapRound);
|
||||
pen.SetEndCap(gd::LineCapRound);
|
||||
g.DrawLine(&pen, checkRect.X + 3.0f * m_scale, centerY,
|
||||
checkRect.X + 6.0f * m_scale, checkRect.Y + checkRect.Height - 4.0f * m_scale);
|
||||
g.DrawLine(&pen, checkRect.X + 6.0f * m_scale, checkRect.Y + checkRect.Height - 4.0f * m_scale,
|
||||
checkRect.X + checkRect.Width - 3.0f * m_scale, checkRect.Y + 4.0f * m_scale);
|
||||
}
|
||||
else
|
||||
{
|
||||
UiFillRoundRect(g, checkRect, 3.0f * m_scale, m_theme->cardBg);
|
||||
UiDrawRoundRect(g, checkRect, 3.0f * m_scale, m_theme->borderStrong, 1.0f);
|
||||
}
|
||||
|
||||
// 颜色块。
|
||||
const gd::RectF swatchRect(checkRect.X + checkRect.Width + 10.0f * m_scale,
|
||||
centerY - kSwatchSize * m_scale * 0.5f, kSwatchSize * m_scale,
|
||||
kSwatchSize * m_scale);
|
||||
UiFillRoundRect(g, swatchRect, 2.5f * m_scale,
|
||||
signal.visible ? signal.color : WithAlpha(signal.color, 0x55));
|
||||
|
||||
// 名称。
|
||||
const float badgeOffset = kBadgeWidth * m_scale + 8.0f * m_scale;
|
||||
const float nameLeft = swatchRect.X + swatchRect.Width + 10.0f * m_scale;
|
||||
const float nameWidth = rect.X + rect.Width - kPadding * m_scale - badgeOffset - nameLeft;
|
||||
const std::wstring nameText = UiEllipsize(g, signal.name, m_fonts->ui, nameWidth);
|
||||
UiText(g, nameText, m_fonts->ui, gd::RectF(nameLeft, rect.Y, nameWidth, rect.Height),
|
||||
signal.visible ? m_theme->text : m_theme->textMuted, TextAlign::LEFT);
|
||||
|
||||
// 网格标记。
|
||||
const int laneIndex = m_document->LaneOfSignal(row);
|
||||
if (laneIndex >= 0)
|
||||
{
|
||||
const gd::RectF badgeRect(rect.X + rect.Width - kPadding * m_scale - kBadgeWidth * m_scale,
|
||||
centerY - kBadgeHeight * m_scale * 0.5f, kBadgeWidth * m_scale,
|
||||
kBadgeHeight * m_scale);
|
||||
const bool laneActive = (selected && m_hoverStyle == StyleHit::LANE);
|
||||
UiFillRoundRect(g, badgeRect, kBadgeHeight * m_scale * 0.5f,
|
||||
laneActive ? m_theme->accentSoft : m_theme->cardBg);
|
||||
UiDrawRoundRect(g, badgeRect, kBadgeHeight * m_scale * 0.5f, m_theme->border, 1.0f);
|
||||
UiText(g, L"G" + std::to_wstring(laneIndex + 1), m_fonts->monoSmall, badgeRect, m_theme->text,
|
||||
TextAlign::CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
void SignalPanel::PaintStyleSection(gd::Graphics& g)
|
||||
{
|
||||
if (m_styleRect.Height <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UiFillRect(g, m_styleRect, m_theme->panelBg);
|
||||
|
||||
const int selectedIndex = (m_document == nullptr) ? -1 : m_document->SelectedSignal();
|
||||
if (m_document == nullptr || selectedIndex < 0 ||
|
||||
selectedIndex >= static_cast<int>(m_document->Signals().size()))
|
||||
{
|
||||
UiText(g, L"请选择信号以编辑样式", m_fonts->uiSmall, m_styleRect, m_theme->textMuted, TextAlign::CENTER);
|
||||
return;
|
||||
}
|
||||
|
||||
const Signal& signal = m_document->Signals()[static_cast<std::size_t>(selectedIndex)];
|
||||
|
||||
// 标题行。
|
||||
const gd::RectF titleRect(m_styleRect.X + kPadding * m_scale, m_styleRect.Y,
|
||||
m_styleRect.Width - kPadding * 2.0f * m_scale, 28.0f * m_scale);
|
||||
UiText(g, L"样式 · " + signal.name, m_fonts->uiBold, titleRect, m_theme->textStrong, TextAlign::LEFT);
|
||||
|
||||
const float labelX = m_styleRect.X + kPadding * m_scale;
|
||||
const float labelWidth = kLineLabelWidth * m_scale;
|
||||
|
||||
// 颜色。
|
||||
{
|
||||
const float rowTop = m_styleRect.Y + 30.0f * m_scale;
|
||||
UiText(g, L"颜色", m_fonts->uiSmall,
|
||||
gd::RectF(labelX, rowTop, labelWidth, 20.0f * m_scale), m_theme->textMuted, TextAlign::LEFT);
|
||||
|
||||
int paletteCount = 0;
|
||||
const std::uint32_t* palette = SignalPalette(paletteCount);
|
||||
for (int index = 0; index < m_colorCount; ++index)
|
||||
{
|
||||
const gd::RectF& rect = m_colorRects[static_cast<std::size_t>(index)];
|
||||
const std::uint32_t color = palette[index % paletteCount];
|
||||
const bool hovered = (m_hoverStyle == StyleHit::COLOR && m_hoverStyleIndex == index);
|
||||
const bool active = (signal.color == color);
|
||||
|
||||
UiFillRoundRect(g, rect, 4.0f * m_scale, color);
|
||||
if (active || hovered)
|
||||
{
|
||||
const gd::RectF ring(rect.X - 2.0f * m_scale, rect.Y - 2.0f * m_scale,
|
||||
rect.Width + 4.0f * m_scale, rect.Height + 4.0f * m_scale);
|
||||
UiDrawRoundRect(g, ring, 5.0f * m_scale, active ? m_theme->textStrong : m_theme->borderStrong,
|
||||
active ? 1.6f : 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 线宽。
|
||||
{
|
||||
const gd::RectF& first = m_widthRects[0];
|
||||
UiText(g, L"线宽", m_fonts->uiSmall, gd::RectF(labelX, first.Y, labelWidth, first.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
|
||||
for (int index = 0; index < 4; ++index)
|
||||
{
|
||||
const gd::RectF& rect = m_widthRects[static_cast<std::size_t>(index)];
|
||||
const bool active = std::fabs(signal.lineWidth - kWidthOptions[index]) < 0.01f;
|
||||
const bool hovered = (m_hoverStyle == StyleHit::WIDTH && m_hoverStyleIndex == index);
|
||||
|
||||
UiFillRoundRect(g, rect, kCorner * m_scale,
|
||||
active ? m_theme->accentSoft : (hovered ? m_theme->hover : m_theme->cardBg));
|
||||
UiDrawRoundRect(g, rect, kCorner * m_scale, active ? m_theme->accent : m_theme->border, 1.0f);
|
||||
|
||||
// 以实际线宽绘制示意线段。
|
||||
const float centerY = rect.Y + rect.Height * 0.5f;
|
||||
const float lineWidth = kWidthOptions[index] * m_scale;
|
||||
UiDrawLine(g, rect.X + 8.0f * m_scale, centerY, rect.X + rect.Width - 8.0f * m_scale, centerY,
|
||||
active ? m_theme->accent : m_theme->text, lineWidth);
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制方式。
|
||||
{
|
||||
const gd::RectF& first = m_modeRects[0];
|
||||
UiText(g, L"方式", m_fonts->uiSmall, gd::RectF(labelX, first.Y, labelWidth, first.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
|
||||
for (int index = 0; index < 4; ++index)
|
||||
{
|
||||
const gd::RectF& rect = m_modeRects[static_cast<std::size_t>(index)];
|
||||
const bool active = (signal.mode == kModeOptions[index]);
|
||||
const bool hovered = (m_hoverStyle == StyleHit::MODE && m_hoverStyleIndex == index);
|
||||
|
||||
UiFillRoundRect(g, rect, kCorner * m_scale,
|
||||
active ? m_theme->accentSoft : (hovered ? m_theme->hover : m_theme->cardBg));
|
||||
UiDrawRoundRect(g, rect, kCorner * m_scale, active ? m_theme->accent : m_theme->border, 1.0f);
|
||||
UiText(g, DrawModeName(kModeOptions[index]), m_fonts->uiSmall, rect,
|
||||
active ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
// 所属网格。
|
||||
{
|
||||
UiText(g, L"网格", m_fonts->uiSmall,
|
||||
gd::RectF(labelX, m_laneButtonRect.Y, labelWidth, m_laneButtonRect.Height), m_theme->textMuted,
|
||||
TextAlign::LEFT);
|
||||
|
||||
const bool hovered = (m_hoverStyle == StyleHit::LANE);
|
||||
UiFillRoundRect(g, m_laneButtonRect, kCorner * m_scale, hovered ? m_theme->hover : m_theme->cardBg);
|
||||
UiDrawRoundRect(g, m_laneButtonRect, kCorner * m_scale, m_theme->border, 1.0f);
|
||||
|
||||
const int laneIndex = m_document->LaneOfSignal(selectedIndex);
|
||||
std::wstring label = (laneIndex >= 0) ? (L"网格 " + std::to_wstring(laneIndex + 1)) : L"未分配";
|
||||
|
||||
const gd::RectF textRect(m_laneButtonRect.X + 8.0f * m_scale, m_laneButtonRect.Y,
|
||||
m_laneButtonRect.Width - 26.0f * m_scale, m_laneButtonRect.Height);
|
||||
UiText(g, label, m_fonts->uiSmall, textRect, m_theme->text, TextAlign::LEFT);
|
||||
|
||||
// 下拉箭头。
|
||||
const float arrowX = m_laneButtonRect.X + m_laneButtonRect.Width - 16.0f * m_scale;
|
||||
const float arrowY = m_laneButtonRect.Y + m_laneButtonRect.Height * 0.5f;
|
||||
gd::PointF points[3];
|
||||
points[0] = gd::PointF(arrowX - 4.0f * m_scale, arrowY - 2.0f * m_scale);
|
||||
points[1] = gd::PointF(arrowX + 4.0f * m_scale, arrowY - 2.0f * m_scale);
|
||||
points[2] = gd::PointF(arrowX, arrowY + 3.0f * m_scale);
|
||||
gd::SolidBrush brush(ToGdiColor(m_theme->textMuted));
|
||||
g.FillPolygon(&brush, points, 3);
|
||||
}
|
||||
}
|
||||
|
||||
bool SignalPanel::AreAllSignalsVisible() const
|
||||
{
|
||||
if (m_document == nullptr || m_document->Signals().empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (const Signal& signal : m_document->Signals())
|
||||
{
|
||||
if (!signal.visible)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SignalPanel::OnMouseMove(int x, int y)
|
||||
{
|
||||
const int hoverRow = RowAtY(y);
|
||||
StyleHit hoverStyle = StyleHit::NONE;
|
||||
int hoverStyleIndex = -1;
|
||||
|
||||
const float fx = static_cast<float>(x);
|
||||
const float fy = static_cast<float>(y);
|
||||
|
||||
if (UiPointInRect(m_toggleAllRect, fx, fy))
|
||||
{
|
||||
hoverStyle = StyleHit::TOGGLE_ALL;
|
||||
}
|
||||
else if (m_document != nullptr && m_document->SelectedSignal() >= 0)
|
||||
{
|
||||
for (int index = 0; index < m_colorCount; ++index)
|
||||
{
|
||||
if (UiPointInRect(m_colorRects[static_cast<std::size_t>(index)], fx, fy))
|
||||
{
|
||||
hoverStyle = StyleHit::COLOR;
|
||||
hoverStyleIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hoverStyle == StyleHit::NONE)
|
||||
{
|
||||
for (int index = 0; index < 4; ++index)
|
||||
{
|
||||
if (UiPointInRect(m_widthRects[static_cast<std::size_t>(index)], fx, fy))
|
||||
{
|
||||
hoverStyle = StyleHit::WIDTH;
|
||||
hoverStyleIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hoverStyle == StyleHit::NONE)
|
||||
{
|
||||
for (int index = 0; index < 4; ++index)
|
||||
{
|
||||
if (UiPointInRect(m_modeRects[static_cast<std::size_t>(index)], fx, fy))
|
||||
{
|
||||
hoverStyle = StyleHit::MODE;
|
||||
hoverStyleIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_laneButtonRect, fx, fy))
|
||||
{
|
||||
hoverStyle = StyleHit::LANE;
|
||||
}
|
||||
}
|
||||
|
||||
if (hoverRow == m_hoverRow && hoverStyle == m_hoverStyle && hoverStyleIndex == m_hoverStyleIndex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
m_hoverRow = hoverRow;
|
||||
m_hoverStyle = hoverStyle;
|
||||
m_hoverStyleIndex = hoverStyleIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SignalPanel::OnLButtonDown(int x, int y)
|
||||
{
|
||||
if (m_document == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const float fx = static_cast<float>(x);
|
||||
const float fy = static_cast<float>(y);
|
||||
|
||||
// 全部显示/隐藏。
|
||||
if (UiPointInRect(m_toggleAllRect, fx, fy))
|
||||
{
|
||||
m_document->SetAllVisible(!AreAllSignalsVisible());
|
||||
return true;
|
||||
}
|
||||
|
||||
// 样式编辑区。
|
||||
const int selectedIndex = m_document->SelectedSignal();
|
||||
if (selectedIndex >= 0 && selectedIndex < static_cast<int>(m_document->Signals().size()))
|
||||
{
|
||||
Signal& signal = m_document->Signals()[static_cast<std::size_t>(selectedIndex)];
|
||||
|
||||
int paletteCount = 0;
|
||||
const std::uint32_t* palette = SignalPalette(paletteCount);
|
||||
for (int index = 0; index < m_colorCount; ++index)
|
||||
{
|
||||
if (UiPointInRect(m_colorRects[static_cast<std::size_t>(index)], fx, fy))
|
||||
{
|
||||
signal.color = palette[index % paletteCount];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < 4; ++index)
|
||||
{
|
||||
if (UiPointInRect(m_widthRects[static_cast<std::size_t>(index)], fx, fy))
|
||||
{
|
||||
signal.lineWidth = kWidthOptions[index];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < 4; ++index)
|
||||
{
|
||||
if (UiPointInRect(m_modeRects[static_cast<std::size_t>(index)], fx, fy))
|
||||
{
|
||||
signal.mode = kModeOptions[index];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (UiPointInRect(m_laneButtonRect, fx, fy))
|
||||
{
|
||||
ShowLaneMenu();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表行。
|
||||
const int row = RowAtY(y);
|
||||
if (row >= 0)
|
||||
{
|
||||
const gd::RectF rect = RowRect(row);
|
||||
const gd::RectF checkRect(rect.X + kPadding * m_scale, rect.Y + (rect.Height - kCheckSize * m_scale) * 0.5f,
|
||||
kCheckSize * m_scale, kCheckSize * m_scale);
|
||||
|
||||
if (UiPointInRect(checkRect, fx, fy))
|
||||
{
|
||||
Signal& signal = m_document->Signals()[static_cast<std::size_t>(row)];
|
||||
signal.visible = !signal.visible;
|
||||
m_document->SetSelectedSignal(row);
|
||||
return true;
|
||||
}
|
||||
|
||||
m_document->SetSelectedSignal(row);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SignalPanel::OnLButtonUp(int /*x*/, int /*y*/)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SignalPanel::OnDoubleClick(int x, int y)
|
||||
{
|
||||
if (m_document == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int row = RowAtY(y);
|
||||
if (row < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Signal& signal = m_document->Signals()[static_cast<std::size_t>(row)];
|
||||
signal.visible = !signal.visible;
|
||||
m_document->SetSelectedSignal(row);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SignalPanel::OnRButtonDown(int x, int y)
|
||||
{
|
||||
if (m_document == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int row = RowAtY(y);
|
||||
if (row < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
m_document->SetSelectedSignal(row);
|
||||
|
||||
POINT cursor = {x, y};
|
||||
if (m_host != nullptr)
|
||||
{
|
||||
ClientToScreen(m_host, &cursor);
|
||||
}
|
||||
ShowSignalMenu(row, cursor.x, cursor.y);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SignalPanel::OnMouseWheel(int delta)
|
||||
{
|
||||
if (m_document == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const float previous = m_scrollOffset;
|
||||
m_scrollOffset -= static_cast<float>(delta) / 120.0f * 54.0f * m_scale;
|
||||
ClampScroll();
|
||||
return std::fabs(previous - m_scrollOffset) > 0.01f;
|
||||
}
|
||||
|
||||
bool SignalPanel::OnMouseLeave()
|
||||
{
|
||||
if (m_hoverRow < 0 && m_hoverStyle == StyleHit::NONE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_hoverRow = -1;
|
||||
m_hoverStyle = StyleHit::NONE;
|
||||
m_hoverStyleIndex = -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SignalPanel::ShowLaneMenu()
|
||||
{
|
||||
if (m_document == nullptr || m_host == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int selectedIndex = m_document->SelectedSignal();
|
||||
if (selectedIndex < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HMENU menu = CreatePopupMenu();
|
||||
if (menu == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int currentLane = m_document->LaneOfSignal(selectedIndex);
|
||||
const std::vector<Lane>& lanes = m_document->Lanes();
|
||||
for (std::size_t index = 0; index < lanes.size(); ++index)
|
||||
{
|
||||
std::wstring label = L"网格 " + std::to_wstring(index + 1) + L"(" +
|
||||
std::to_wstring(lanes[index].signalIndices.size()) + L" 条信号)";
|
||||
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuLaneBase + index), label.c_str());
|
||||
}
|
||||
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
|
||||
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuLaneNew), L"新建网格");
|
||||
|
||||
if (currentLane >= 0 && currentLane < static_cast<int>(lanes.size()))
|
||||
{
|
||||
CheckMenuRadioItem(menu, static_cast<UINT>(kMenuLaneBase),
|
||||
static_cast<UINT>(kMenuLaneBase + lanes.size() - 1),
|
||||
static_cast<UINT>(kMenuLaneBase + currentLane), MF_BYCOMMAND);
|
||||
}
|
||||
|
||||
POINT anchor = {static_cast<LONG>(m_laneButtonRect.X), static_cast<LONG>(m_laneButtonRect.Y)};
|
||||
ClientToScreen(m_host, &anchor);
|
||||
|
||||
SetForegroundWindow(m_host);
|
||||
const int command = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_LEFTALIGN | TPM_TOPALIGN, anchor.x,
|
||||
anchor.y + static_cast<int>(m_laneButtonRect.Height), 0, m_host, nullptr);
|
||||
DestroyMenu(menu);
|
||||
|
||||
if (command >= kMenuLaneBase && command < kMenuLaneNew)
|
||||
{
|
||||
m_document->MoveSignalToLane(selectedIndex, command - kMenuLaneBase);
|
||||
}
|
||||
else if (command == kMenuLaneNew)
|
||||
{
|
||||
m_document->MoveSignalToLane(selectedIndex, -1);
|
||||
}
|
||||
}
|
||||
|
||||
void SignalPanel::ShowSignalMenu(int signalIndex, int screenX, int screenY)
|
||||
{
|
||||
if (m_document == nullptr || m_host == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (signalIndex < 0 || signalIndex >= static_cast<int>(m_document->Signals().size()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Signal& signal = m_document->Signals()[static_cast<std::size_t>(signalIndex)];
|
||||
|
||||
HMENU menu = CreatePopupMenu();
|
||||
if (menu == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuToggleVisible),
|
||||
signal.visible ? L"隐藏该信号" : L"显示该信号");
|
||||
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuSolo), L"仅显示该信号");
|
||||
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(kMenuShowAll), L"显示全部信号");
|
||||
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
|
||||
|
||||
HMENU colorMenu = CreatePopupMenu();
|
||||
int paletteCount = 0;
|
||||
const std::uint32_t* palette = SignalPalette(paletteCount);
|
||||
const wchar_t* colorNames[] = {L"蓝", L"绿", L"红", L"紫", L"黄", L"青", L"品红", L"橙", L"深灰", L"灰"};
|
||||
for (int index = 0; index < SignalColorCount(); ++index)
|
||||
{
|
||||
const std::uint32_t color = palette[index % paletteCount];
|
||||
std::wstring label = (index < 10) ? std::wstring(colorNames[index]) : (L"颜色 " + std::to_wstring(index + 1));
|
||||
AppendMenuW(colorMenu, MF_STRING, static_cast<UINT_PTR>(kMenuColorBase + index), label.c_str());
|
||||
if (signal.color == color)
|
||||
{
|
||||
CheckMenuRadioItem(colorMenu, static_cast<UINT>(kMenuColorBase),
|
||||
static_cast<UINT>(kMenuColorBase + SignalColorCount() - 1),
|
||||
static_cast<UINT>(kMenuColorBase + index), MF_BYCOMMAND);
|
||||
}
|
||||
}
|
||||
AppendMenuW(menu, MF_POPUP, reinterpret_cast<UINT_PTR>(colorMenu), L"颜色");
|
||||
|
||||
HMENU widthMenu = CreatePopupMenu();
|
||||
for (int index = 0; index < 4; ++index)
|
||||
{
|
||||
const std::wstring label = FormatNumber(kWidthOptions[index], 1) + L" px";
|
||||
AppendMenuW(widthMenu, MF_STRING, static_cast<UINT_PTR>(kMenuWidthBase + index), label.c_str());
|
||||
if (std::fabs(signal.lineWidth - kWidthOptions[index]) < 0.01f)
|
||||
{
|
||||
CheckMenuRadioItem(widthMenu, static_cast<UINT>(kMenuWidthBase),
|
||||
static_cast<UINT>(kMenuWidthBase + 3),
|
||||
static_cast<UINT>(kMenuWidthBase + index), MF_BYCOMMAND);
|
||||
}
|
||||
}
|
||||
AppendMenuW(menu, MF_POPUP, reinterpret_cast<UINT_PTR>(widthMenu), L"线宽");
|
||||
|
||||
HMENU modeMenu = CreatePopupMenu();
|
||||
for (int index = 0; index < 4; ++index)
|
||||
{
|
||||
AppendMenuW(modeMenu, MF_STRING, static_cast<UINT_PTR>(kMenuModeBase + index),
|
||||
DrawModeName(kModeOptions[index]));
|
||||
if (signal.mode == kModeOptions[index])
|
||||
{
|
||||
CheckMenuRadioItem(modeMenu, static_cast<UINT>(kMenuModeBase), static_cast<UINT>(kMenuModeBase + 3),
|
||||
static_cast<UINT>(kMenuModeBase + index), MF_BYCOMMAND);
|
||||
}
|
||||
}
|
||||
AppendMenuW(menu, MF_POPUP, reinterpret_cast<UINT_PTR>(modeMenu), L"显示方式");
|
||||
|
||||
HMENU laneMenu = CreatePopupMenu();
|
||||
const std::vector<Lane>& lanes = m_document->Lanes();
|
||||
const int currentLane = m_document->LaneOfSignal(signalIndex);
|
||||
for (std::size_t index = 0; index < lanes.size(); ++index)
|
||||
{
|
||||
const std::wstring laneLabel = L"网格 " + std::to_wstring(index + 1);
|
||||
AppendMenuW(laneMenu, MF_STRING, static_cast<UINT_PTR>(kMenuLaneBase + index), laneLabel.c_str());
|
||||
}
|
||||
AppendMenuW(laneMenu, MF_SEPARATOR, 0, nullptr);
|
||||
AppendMenuW(laneMenu, MF_STRING, static_cast<UINT_PTR>(kMenuLaneNew), L"新建网格");
|
||||
if (currentLane >= 0 && !lanes.empty())
|
||||
{
|
||||
CheckMenuRadioItem(laneMenu, static_cast<UINT>(kMenuLaneBase),
|
||||
static_cast<UINT>(kMenuLaneBase + lanes.size() - 1),
|
||||
static_cast<UINT>(kMenuLaneBase + currentLane), MF_BYCOMMAND);
|
||||
}
|
||||
AppendMenuW(menu, MF_POPUP, reinterpret_cast<UINT_PTR>(laneMenu), L"所属网格");
|
||||
|
||||
SetForegroundWindow(m_host);
|
||||
const int command = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_LEFTALIGN | TPM_TOPALIGN, screenX, screenY, 0,
|
||||
m_host, nullptr);
|
||||
DestroyMenu(menu);
|
||||
|
||||
if (command == kMenuToggleVisible)
|
||||
{
|
||||
signal.visible = !signal.visible;
|
||||
}
|
||||
else if (command == kMenuSolo)
|
||||
{
|
||||
m_document->SoloSignal(signalIndex);
|
||||
}
|
||||
else if (command == kMenuShowAll)
|
||||
{
|
||||
m_document->SetAllVisible(true);
|
||||
}
|
||||
else if (command >= kMenuModeBase && command < kMenuModeBase + 4)
|
||||
{
|
||||
signal.mode = kModeOptions[command - kMenuModeBase];
|
||||
}
|
||||
else if (command >= kMenuWidthBase && command < kMenuWidthBase + 4)
|
||||
{
|
||||
signal.lineWidth = kWidthOptions[command - kMenuWidthBase];
|
||||
}
|
||||
else if (command >= kMenuColorBase && command < kMenuColorBase + SignalColorCount())
|
||||
{
|
||||
signal.color = palette[(command - kMenuColorBase) % paletteCount];
|
||||
}
|
||||
else if (command >= kMenuLaneBase && command < kMenuLaneNew)
|
||||
{
|
||||
m_document->MoveSignalToLane(signalIndex, command - kMenuLaneBase);
|
||||
}
|
||||
else if (command == kMenuLaneNew)
|
||||
{
|
||||
m_document->MoveSignalToLane(signalIndex, -1);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
@@ -0,0 +1,160 @@
|
||||
#ifndef __TOOLBAR_HPP__
|
||||
#define __TOOLBAR_HPP__
|
||||
|
||||
#include <windows.h>
|
||||
#include <objidl.h>
|
||||
#include <gdiplus.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "PaintUtil.hpp"
|
||||
#include "Theme.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief 工具栏按钮触发的动作。
|
||||
*/
|
||||
enum class ToolbarAction : int
|
||||
{
|
||||
NONE = 0, /**< 未触发任何动作。 */
|
||||
OPEN_FILE, /**< 打开 CSV 文件。 */
|
||||
FIT_ALL, /**< 时间与幅值同时自适应。 */
|
||||
FIT_TIME, /**< 仅时间轴自适应。 */
|
||||
ZOOM_IN, /**< 放大时间轴。 */
|
||||
ZOOM_OUT, /**< 缩小时间轴。 */
|
||||
MERGE_LANES, /**< 合并为单一网格。 */
|
||||
SPLIT_LANES, /**< 按信号拆分为多网格。 */
|
||||
RESET_VIEW /**< 恢复默认视图范围。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 顶部工具栏:文件操作与视图控制的入口。
|
||||
*/
|
||||
class Toolbar
|
||||
{
|
||||
public:
|
||||
Toolbar();
|
||||
|
||||
/**
|
||||
* @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] hasDocument 是否已加载数据。
|
||||
*/
|
||||
void SetDocumentLoaded(bool hasDocument);
|
||||
|
||||
/**
|
||||
* @brief 设置工具栏右侧的状态文本。
|
||||
* @param [IN] text 状态文本。
|
||||
*/
|
||||
void SetStatusText(const std::wstring& text);
|
||||
|
||||
/**
|
||||
* @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 客户区纵坐标。
|
||||
* @param [OUT] action 命中的按钮动作。
|
||||
* @return 命中按钮返回 true。
|
||||
*/
|
||||
bool OnLButtonDown(int x, int y, ToolbarAction& action);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标左键抬起。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @param [OUT] action 抬起时命中的按钮动作(仅在按钮上抬起时有效)。
|
||||
* @return 视觉状态发生变化返回 true。
|
||||
*/
|
||||
bool OnLButtonUp(int x, int y, ToolbarAction& action);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标离开工具栏。
|
||||
* @return 视觉状态发生变化返回 true。
|
||||
*/
|
||||
bool OnMouseLeave();
|
||||
|
||||
/**
|
||||
* @brief 判断坐标是否落在工具栏区域内。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 落在区域内返回 true。
|
||||
*/
|
||||
bool HitTest(int x, int y) const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 单个工具栏按钮的运行时状态。
|
||||
*/
|
||||
struct Button
|
||||
{
|
||||
ToolbarAction action = ToolbarAction::NONE; /**< 按钮动作。 */
|
||||
std::wstring label; /**< 按钮文本。 */
|
||||
gd::RectF rect; /**< 按钮矩形。 */
|
||||
float width = 0.0f; /**< 按钮宽度。 */
|
||||
bool primary = false; /**< 是否为主按钮(强调色填充)。 */
|
||||
bool needsDocument = true; /**< 是否依赖已加载数据。 */
|
||||
bool hover = false; /**< 悬停状态。 */
|
||||
bool pressed = false; /**< 按下状态。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 判断按钮当前是否可用。
|
||||
* @param [IN] button 目标按钮。
|
||||
* @return 可用返回 true。
|
||||
*/
|
||||
bool IsEnabled(const Button& button) const;
|
||||
|
||||
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
|
||||
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
||||
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
||||
bool m_documentLoaded = false; /**< 是否已加载数据。 */
|
||||
std::wstring m_statusText; /**< 右侧状态文本。 */
|
||||
std::vector<Button> m_buttons; /**< 按钮集合。 */
|
||||
gd::RectF m_area; /**< 工具栏整体区域。 */
|
||||
int m_hoverIndex = -1; /**< 当前悬停按钮下标。 */
|
||||
int m_pressedIndex = -1; /**< 当前按下按钮下标。 */
|
||||
};
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __TOOLBAR_HPP__
|
||||
@@ -0,0 +1,275 @@
|
||||
#include "Toolbar.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "Utf8Util.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
const float kButtonHeight = 30.0f; /**< 按钮高度(逻辑像素)。 */
|
||||
const float kButtonGap = 6.0f; /**< 按钮间距。 */
|
||||
const float kButtonPadding = 14.0f; /**< 按钮左右内边距。 */
|
||||
const float kGroupGap = 18.0f; /**< 分组间距。 */
|
||||
const float kButtonRadius = 5.0f; /**< 按钮圆角半径。 */
|
||||
const float kSidePadding = 12.0f; /**< 工具栏左右留白。 */
|
||||
const float kTopPadding = 8.0f; /**< 工具栏上下留白。 */
|
||||
}
|
||||
|
||||
Toolbar::Toolbar() = default;
|
||||
|
||||
void Toolbar::Setup(const UiFonts* fonts, const Theme* theme)
|
||||
{
|
||||
m_fonts = fonts;
|
||||
m_theme = theme;
|
||||
}
|
||||
|
||||
void Toolbar::SetScale(float scale)
|
||||
{
|
||||
m_scale = (scale > 0.1f) ? scale : 1.0f;
|
||||
}
|
||||
|
||||
void Toolbar::SetDocumentLoaded(bool hasDocument)
|
||||
{
|
||||
m_documentLoaded = hasDocument;
|
||||
}
|
||||
|
||||
void Toolbar::SetStatusText(const std::wstring& text)
|
||||
{
|
||||
m_statusText = text;
|
||||
}
|
||||
|
||||
float Toolbar::DesiredHeight() const
|
||||
{
|
||||
return (kButtonHeight + kTopPadding * 2.0f) * m_scale;
|
||||
}
|
||||
|
||||
bool Toolbar::IsEnabled(const Button& button) const
|
||||
{
|
||||
return button.needsDocument ? m_documentLoaded : true;
|
||||
}
|
||||
|
||||
void Toolbar::Layout(const gd::RectF& area)
|
||||
{
|
||||
m_area = area;
|
||||
m_buttons.clear();
|
||||
|
||||
if (m_fonts == nullptr || m_fonts->ui == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
struct Spec
|
||||
{
|
||||
ToolbarAction action;
|
||||
const wchar_t* label;
|
||||
bool primary;
|
||||
bool needsDocument;
|
||||
};
|
||||
|
||||
const Spec specs[] = {
|
||||
{ToolbarAction::OPEN_FILE, L"打开 CSV", true, false},
|
||||
{ToolbarAction::FIT_ALL, L"适应窗口", false, true},
|
||||
{ToolbarAction::FIT_TIME, L"适应时间", false, true},
|
||||
{ToolbarAction::ZOOM_IN, L"放大", false, true},
|
||||
{ToolbarAction::ZOOM_OUT, L"缩小", false, true},
|
||||
{ToolbarAction::MERGE_LANES, L"合并网格", false, true},
|
||||
{ToolbarAction::SPLIT_LANES, L"每信号一格", false, true},
|
||||
};
|
||||
|
||||
// 按钮宽度先按固定预算计算,避免依赖绘图上下文。
|
||||
float cursorX = area.X + kSidePadding * m_scale;
|
||||
const float top = area.Y + kTopPadding * m_scale;
|
||||
const float height = kButtonHeight * m_scale;
|
||||
|
||||
for (const Spec& spec : specs)
|
||||
{
|
||||
Button button;
|
||||
button.action = spec.action;
|
||||
button.label = spec.label;
|
||||
button.primary = spec.primary;
|
||||
button.needsDocument = spec.needsDocument;
|
||||
button.width = (kButtonPadding * 2.0f +
|
||||
static_cast<float>(button.label.size()) * 13.0f) * m_scale;
|
||||
button.rect = gd::RectF(cursorX, top, button.width, height);
|
||||
cursorX += button.width + kButtonGap * m_scale;
|
||||
|
||||
if (spec.action == ToolbarAction::OPEN_FILE || spec.action == ToolbarAction::ZOOM_OUT ||
|
||||
spec.action == ToolbarAction::SPLIT_LANES)
|
||||
{
|
||||
cursorX += kGroupGap * m_scale;
|
||||
}
|
||||
|
||||
m_buttons.push_back(std::move(button));
|
||||
}
|
||||
}
|
||||
|
||||
void Toolbar::Paint(gd::Graphics& g)
|
||||
{
|
||||
if (m_theme == nullptr || m_fonts == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UiFillRect(g, m_area, m_theme->toolbarBg);
|
||||
|
||||
for (const Button& button : m_buttons)
|
||||
{
|
||||
const bool enabled = IsEnabled(button);
|
||||
std::uint32_t background = 0;
|
||||
std::uint32_t border = 0;
|
||||
std::uint32_t textColor = 0;
|
||||
|
||||
if (button.primary)
|
||||
{
|
||||
background = enabled ? m_theme->accent : MixColor(m_theme->accent, m_theme->toolbarBg, 0.6);
|
||||
if (button.pressed)
|
||||
{
|
||||
background = MixColor(m_theme->accent, 0xFF000000u, 0.12);
|
||||
}
|
||||
else if (enabled && button.hover)
|
||||
{
|
||||
background = MixColor(m_theme->accent, 0xFFFFFFFFu, 0.12);
|
||||
}
|
||||
border = background;
|
||||
textColor = m_theme->textOnAccent;
|
||||
}
|
||||
else
|
||||
{
|
||||
background = m_theme->toolbarBg;
|
||||
if (button.pressed)
|
||||
{
|
||||
background = m_theme->pressed;
|
||||
}
|
||||
else if (enabled && button.hover)
|
||||
{
|
||||
background = m_theme->hover;
|
||||
}
|
||||
border = MixColor(m_theme->border, m_theme->toolbarBg, enabled ? 0.0 : 0.5);
|
||||
textColor = enabled ? m_theme->text : m_theme->textMuted;
|
||||
}
|
||||
|
||||
if (background != m_theme->toolbarBg)
|
||||
{
|
||||
UiFillRoundRect(g, button.rect, kButtonRadius * m_scale, background);
|
||||
}
|
||||
if (!button.primary)
|
||||
{
|
||||
UiDrawRoundRect(g, button.rect, kButtonRadius * m_scale, border, 1.0f);
|
||||
}
|
||||
|
||||
UiText(g, button.label, m_fonts->ui, button.rect, textColor, TextAlign::CENTER);
|
||||
}
|
||||
|
||||
// 右侧状态文本。
|
||||
if (!m_statusText.empty())
|
||||
{
|
||||
const float rightPadding = kSidePadding * m_scale;
|
||||
gd::RectF textRect(m_area.X, m_area.Y, m_area.Width - rightPadding, m_area.Height);
|
||||
UiText(g, m_statusText, m_fonts->uiSmall, textRect, m_theme->textMuted, TextAlign::RIGHT);
|
||||
}
|
||||
|
||||
// 底部分隔线。
|
||||
UiDrawLine(g, m_area.X, m_area.Y + m_area.Height - 1.0f, m_area.X + m_area.Width,
|
||||
m_area.Y + m_area.Height - 1.0f, m_theme->border, 1.0f);
|
||||
}
|
||||
|
||||
bool Toolbar::HitTest(int x, int y) const
|
||||
{
|
||||
return UiPointInRect(m_area, static_cast<float>(x), static_cast<float>(y));
|
||||
}
|
||||
|
||||
bool Toolbar::OnMouseMove(int x, int y)
|
||||
{
|
||||
int hoverIndex = -1;
|
||||
for (std::size_t index = 0; index < m_buttons.size(); ++index)
|
||||
{
|
||||
if (UiPointInRect(m_buttons[index].rect, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
hoverIndex = static_cast<int>(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hoverIndex == m_hoverIndex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_hoverIndex = hoverIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Toolbar::OnLButtonDown(int x, int y, ToolbarAction& action)
|
||||
{
|
||||
action = ToolbarAction::NONE;
|
||||
|
||||
for (std::size_t index = 0; index < m_buttons.size(); ++index)
|
||||
{
|
||||
Button& button = m_buttons[index];
|
||||
if (!UiPointInRect(button.rect, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!IsEnabled(button))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
m_pressedIndex = static_cast<int>(index);
|
||||
button.pressed = true;
|
||||
action = button.action;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Toolbar::OnLButtonUp(int x, int y, ToolbarAction& action)
|
||||
{
|
||||
action = ToolbarAction::NONE;
|
||||
if (m_pressedIndex < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int index = m_pressedIndex;
|
||||
m_pressedIndex = -1;
|
||||
if (index >= static_cast<int>(m_buttons.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Button& button = m_buttons[static_cast<std::size_t>(index)];
|
||||
button.pressed = false;
|
||||
|
||||
const bool inside = UiPointInRect(button.rect, static_cast<float>(x), static_cast<float>(y));
|
||||
m_hoverIndex = inside ? index : -1;
|
||||
if (inside && IsEnabled(button))
|
||||
{
|
||||
action = button.action;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Toolbar::OnMouseLeave()
|
||||
{
|
||||
bool changed = false;
|
||||
if (m_hoverIndex >= 0)
|
||||
{
|
||||
m_hoverIndex = -1;
|
||||
changed = true;
|
||||
}
|
||||
if (m_pressedIndex >= 0)
|
||||
{
|
||||
if (m_pressedIndex < static_cast<int>(m_buttons.size()))
|
||||
{
|
||||
m_buttons[static_cast<std::size_t>(m_pressedIndex)].pressed = false;
|
||||
}
|
||||
m_pressedIndex = -1;
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
Reference in new issue
Block a user