#include "Application.hpp" #include #include #include #include #include #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(-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( reinterpret_cast(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(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(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); if (message == WM_NCCREATE) { CREATESTRUCTW* createStruct = reinterpret_cast(lParam); self = reinterpret_cast(createStruct->lpCreateParams); SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(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(message.wParam); } bool Application::Initialize(HINSTANCE instance, int showCommand) { m_instance = instance; EnableDpiAwareness(); m_dpi = QuerySystemDpi(); m_scale = static_cast(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(ScaleOf(1340.0f, m_scale)), static_cast(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(ScaleOf(1340.0f, m_scale)), workWidth - 24); const int windowHeight = std::min(static_cast(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(icon)); SendMessageW(m_hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast(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(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(client.right - client.left); const float height = static_cast(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(width), static_cast(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(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(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(x - m_splitterGrabOffset) / m_scale; const float maximum = std::min(kMaxPanelWidth, static_cast(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(lParam); info->ptMinTrackSize.x = static_cast(ScaleOf(static_cast(kMinClientWidth), m_scale)); info->ptMinTrackSize.y = static_cast(ScaleOf(static_cast(kMinClientHeight), m_scale)); return 0; } case WM_DPICHANGED: { const UINT dpi = HIWORD(wParam); const RECT* suggested = reinterpret_cast(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(x), static_cast(y))) { SetCapture(hwnd); m_draggingSplitter = true; m_activeTarget = Target::SPLITTER; m_splitterGrabOffset = x - static_cast(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(point.x), static_cast(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(wParam); const UINT length = DragQueryFileW(drop, 0, nullptr, 0); if (length > 0) { std::vector buffer(static_cast(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