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:
DeepSeek V4.1 Flash committed 2026-09-29 15:05:29 +08:00
commit 132b305544
22 files changed
+7146

No files matched your search

+987
View File
@@ -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