基于 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 链接库
988 lines
25 KiB
C++
988 lines
25 KiB
C++
#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
|