Files
Just-DataInspector/Components/Application/Src/Application.cpp
T
DeepSeek V4.1 Flash 04e98ee041 Esc 退出缩放模式、修复拖拽时波形变模糊、加速超大文件的模式切换
Esc 行为(用户要求):
- 网格内按 Esc 在取消拖拽选区的同时,退出全部缩放模式(时间轴缩放 / Y 轴缩放),
  工具栏两个缩放按钮随之回到未选中态;时间光标按钮不受影响。
- 新增 PlotView::ExitZoomModes(),由 Application 的 VK_ESCAPE 分支与 CancelDrag 一起调用。

修复拖拽时波形变模糊(用户报告):
- 上一轮为提升拖拽帧率引入了"交互降级":拖拽期间关闭抗锯齿 + 顶点预算减半 +
  每列只扫 32 个采样点。实测这会让包络丢掉峰值、线条失去抗锯齿,
  在选区放大 / 拖拽信号 / 拖拽光标时明显变"糊"。现已**全部移除**,
  任何交互期间都按全质量绘制。
- 用"粗粒度包络层"替代降级来保证交互帧率:
  SignalData 新增 coarseMin/coarseMax/coarseStride(每 256 个采样一组存 min/max,
  额外内存约为采样数据的 0.8%),在 BuildDataFile 统计 min/max 的同一个循环里顺带构建
  (不额外扫数据)。折线包络改为读取"每列覆盖到的分组",
  数据读取量从 O(可见采样数) 降到 O(像素列数 × 少量分组),**绘制结果与全量扫描完全一致**。
- 实测(VERY_BIG_DATA.CSV = 1.77GB、2580 万行 × 5 信号,全选显示):
  视图变化时的包络构建由 176~196ms 降到 ≈0ms;平移拖拽整帧由约 336ms 降到 90~120ms。
- 注意:包络路径按分组判断无效值(整组无有效值才断线),
  因此密集视图中孤立的 NaN 不再把波形切成两段(原先会),视觉上更干净。

加速超大文件的显示模式切换(用户报告):
- 散点绘制由逐点 FillEllipse 改为把全部散点合并进一条 GraphicsPath 后一次 FillPath:
  实测 2500 点由 63ms 降到 7ms(基准),程序内单条密集信号由约 51ms 降到约 22ms,
  仍是抗锯齿圆形,观感与逐点绘制一致。
- 线段/散点/点线切换时不再有卡顿感;空闲 CPU 仍为 0.00%。

验证:
- Esc:分别启用时间轴缩放 / Y 轴缩放后按 Esc,两个按钮均回到未选中(截图对比),
  时间光标按钮保持选中;框选拖拽中按 Esc 时间轴保持 0.2~1.2 不变。
- 清晰度:光标拖拽中与静止时对同一区域做像素哈希,结果完全一致;
  框选拖拽同样一致;信号拖拽中间态截图中源波形与虚影均为抗锯齿全质量。
- 超大文件:线段/散点/点线切换与拖拽的正常渲染(截图),1.77GB 文件加载过渡动画正常。
- 回归:菜单内联编辑 Y 轴上下限、选区 Y 轴自适应、图例点击选中、面板改名均正常。
- 内存:BIG_DATA 全选 122MB(粗粒度层约 +1MB);VERY_BIG 全选 1224MB(理论下限 1.25GB)。
- make clean && make all 零警告零错误。
2026-09-30 19:34:52 +08:00

2257 lines
62 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "Application.hpp"
#include <algorithm>
#include <atomic>
#include <cmath>
#include <thread>
#include <commdlg.h>
#include <shellapi.h>
#include <windowsx.h>
#include "CsvTable.hpp"
#include "ResourceIds.h"
#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 kSplitterWidth = 4.0f; /**< 分隔条宽度(逻辑像素)。 */
const float kMinPanelWidth = 260.0f; /**< 面板最小宽度(逻辑像素)。 */
const float kMaxPanelWidth = 400.0f; /**< 面板最大宽度(逻辑像素)。 */
const int kMinClientWidth = 860; /**< 窗口最小客户区宽度。 */
const int kMinClientHeight = 540; /**< 窗口最小客户区高度。 */
const UINT kMessageRemoveFile = WM_APP + 1; /**< 面板请求移除文件的自定义消息。 */
/** @brief 标签页右键菜单命令 ID。 */
const int kMenuTabNew = 1;
const int kMenuTabClose = 2;
const int kMenuTabCloseOthers = 3;
/**
* @brief 从应用资源中按指定尺寸加载图标。
* @param [IN] cx 期望宽度(像素)。
* @param [IN] cy 期望高度(像素)。
* @return 图标句柄,加载失败返回 nullptr。
*/
HICON LoadAppIcon(int cx, int cy)
{
HINSTANCE module = GetModuleHandleW(nullptr);
// LR_DEFAULTSIZE 之外的显式尺寸请求会挑选 ICO 中最接近的一帧并由系统缩放,
// 因此 256/64/48/32/16 的多尺寸资源在任意 DPI 下都能取到合适的一帧。
HICON icon = static_cast<HICON>(LoadImageW(module, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON,
cx, cy, LR_DEFAULTCOLOR));
if (icon == nullptr)
{
icon = static_cast<HICON>(LoadImageW(module, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON, 0, 0,
LR_DEFAULTCOLOR | LR_DEFAULTSIZE));
}
return icon;
}
/**
* @brief 后台加载任务的共享状态。
*
* `done` 是唯一的同步点:工作线程写完其它字段后以 release 语义置位,
* 主线程以 acquire 语义读取,因此无需额外锁。
*/
struct AsyncLoadState
{
std::atomic<bool> done{false}; /**< 工作线程是否已结束。 */
std::atomic<bool> cancelled{false}; /**< 是否请求取消。 */
std::atomic<float> progress{0.0f}; /**< 0-1 进度。 */
bool ok = false; /**< 加载是否成功。 */
std::wstring error; /**< 失败原因。 */
DataFile file; /**< 构建结果。 */
};
/**
* @brief 工作线程主体:解析 CSV 并构建数据文件(不接触 DataSet)。
* @param [IN,OUT] state 共享状态。
* @param [IN] path 文件路径。
* @param [IN] paletteBase 调色板起始序号。
*/
void RunAsyncLoad(AsyncLoadState* state, std::wstring path, std::size_t paletteBase)
{
// 解析阶段占总进度的 0-75%,列重组与统计占 75-100%。
CsvProgress parseProgress = [state](double ratio)
{
state->progress.store(static_cast<float>(ratio * 0.75), std::memory_order_relaxed);
return !state->cancelled.load(std::memory_order_relaxed);
};
CsvTable table;
std::wstring error;
if (!LoadCsvTable(path, table, error, parseProgress))
{
state->error = error;
state->done.store(true, std::memory_order_release);
return;
}
CsvProgress buildProgress = [state](double ratio)
{
state->progress.store(static_cast<float>(0.75 + ratio * 0.25), std::memory_order_relaxed);
return !state->cancelled.load(std::memory_order_relaxed);
};
DataFile file;
if (!BuildDataFile(std::move(table), path, paletteBase, file, error, buildProgress))
{
state->error = error;
state->done.store(true, std::memory_order_release);
return;
}
state->file = std::move(file);
state->ok = true;
state->progress.store(1.0f, std::memory_order_relaxed);
state->done.store(true, std::memory_order_release);
}
/**
* @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;
// 使用应用自己的图标资源(IDI_APP_ICON);
// 原先取 IDI_APPLICATION 是系统默认图标,导致标题栏与任务栏缩略图显示成"通用应用"图标。
windowClass.hIcon = LoadAppIcon(GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON));
windowClass.hIconSm = LoadAppIcon(GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON));
if (RegisterClassExW(&windowClass) == 0)
{
MessageBoxW(nullptr, L"窗口类注册失败。", kWindowTitle, MB_ICONERROR | MB_OK);
return false;
}
// 依据工作区(排除任务栏)计算默认窗口尺寸并居中,避免窗口超出屏幕。
RECT workArea = {0, 0, static_cast<LONG>(ScaleOf(1380.0f, m_scale)),
static_cast<LONG>(ScaleOf(880.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(1380.0f, m_scale)), workWidth - 24);
const int windowHeight = std::min(static_cast<int>(ScaleOf(880.0f, m_scale)), workHeight - 24);
const int windowX = workArea.left + std::max(0, (workWidth - windowWidth) / 2);
const int 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_tabBar.Setup(&m_fonts, &m_theme);
m_tabBar.SetScale(m_scale);
m_tabBar.SetHostWindow(m_hwnd);
m_toolbar.Setup(&m_fonts, &m_theme);
m_toolbar.SetScale(m_scale);
m_menu.Setup(&m_fonts, &m_theme);
m_menu.SetScale(m_scale);
m_colorPicker.Setup(&m_fonts, &m_theme);
m_colorPicker.SetScale(m_scale);
m_colorPicker.SetOwnerWindow(m_hwnd);
m_panel.Setup(m_hwnd, &m_fonts, &m_theme, &m_menu, &m_colorPicker);
m_panel.SetScale(m_scale);
m_plot.Setup(&m_fonts, &m_theme);
m_plot.SetScale(m_scale);
m_plot.SetHostWindow(m_hwnd);
m_menu.SetHostWindow(m_hwnd);
// 标题栏小图标与任务栏/Alt+Tab 缩略图大图标分开设置(同一个多尺寸 ICO 资源的不同尺寸)。
HICON bigIcon = LoadAppIcon(GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON));
HICON smallIcon = LoadAppIcon(GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON));
if (bigIcon != nullptr)
{
SendMessageW(m_hwnd, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(bigIcon));
}
if (smallIcon != nullptr)
{
SendMessageW(m_hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(smallIcon));
}
// 创建首个标签页。
NewTab();
Relayout();
ShowWindow(m_hwnd, showCommand);
UpdateWindow(m_hwnd);
LoadCommandLineFiles();
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_tabBar.SetScale(m_scale);
m_toolbar.SetScale(m_scale);
m_menu.SetScale(m_scale);
m_colorPicker.SetScale(m_scale);
m_panel.SetScale(m_scale);
m_plot.SetScale(m_scale);
ReleaseBackBuffer();
Relayout();
}
TabConfig& Application::ActiveTab()
{
static TabConfig fallback;
if (m_activeTab < 0 || m_activeTab >= static_cast<int>(m_tabs.size()))
{
return fallback;
}
return m_tabs[static_cast<std::size_t>(m_activeTab)];
}
const TabConfig& Application::ActiveTab() const
{
static const TabConfig fallback;
if (m_activeTab < 0 || m_activeTab >= static_cast<int>(m_tabs.size()))
{
return fallback;
}
return m_tabs[static_cast<std::size_t>(m_activeTab)];
}
void Application::NewTab()
{
TabConfig tab;
tab.SetName(NextTabName());
tab.SyncToDataSet(m_dataSet);
m_tabs.push_back(std::move(tab));
SetActiveTab(static_cast<int>(m_tabs.size()) - 1);
}
std::wstring Application::NextTabName() const
{
// 取当前未被占用的最小编号,使关闭标签页后编号可以被复用(3 开 2 关再新建回到「视图 2」)。
for (int number = 1; number <= static_cast<int>(m_tabs.size()) + 1; ++number)
{
const std::wstring candidate = L"视图 " + std::to_wstring(number);
bool used = false;
for (const TabConfig& tab : m_tabs)
{
if (tab.Name() == candidate)
{
used = true;
break;
}
}
if (!used)
{
return candidate;
}
}
return L"视图 " + std::to_wstring(m_tabs.size() + 1);
}
void Application::CloseTab(int index)
{
if (index < 0 || index >= static_cast<int>(m_tabs.size()))
{
return;
}
if (m_tabs.size() <= 1)
{
SetStatusMessage(L"至少需要保留一个标签页。", true);
return;
}
m_tabs.erase(m_tabs.begin() + index);
int next = index;
if (next >= static_cast<int>(m_tabs.size()))
{
next = static_cast<int>(m_tabs.size()) - 1;
}
SetActiveTab(next);
}
void Application::SetActiveTab(int index)
{
if (index < 0 || index >= static_cast<int>(m_tabs.size()))
{
return;
}
m_activeTab = index;
// 重新绑定上下文:面板与绘图区均指向同一数据层与当前标签页配置。
m_panel.SetContext(&m_dataSet, &m_tabs[static_cast<std::size_t>(m_activeTab)]);
m_plot.SetContext(&m_dataSet, &m_tabs[static_cast<std::size_t>(m_activeTab)]);
RefreshTabBar();
Relayout();
// 切换标签页后把该页已选中的信号滚动到可见位置。
// 切换标签页后把共享的选中信号滚动到可见位置(选中状态跨页保持一致)。
m_panel.ScrollToSignal(m_dataSet.Selected());
SynchronizeView(true);
UpdateWindowTitle();
InvalidateRect(m_hwnd, nullptr, TRUE);
}
void Application::RefreshTabBar()
{
std::vector<std::wstring> names;
names.reserve(m_tabs.size());
for (const TabConfig& tab : m_tabs)
{
names.push_back(tab.Name());
}
m_tabBar.SetTabs(names);
m_tabBar.SetActiveIndex(m_activeTab);
}
void Application::SynchronizeView(bool force)
{
if (m_tabs.empty() || m_activeTab < 0)
{
return;
}
TabConfig& tab = m_tabs[static_cast<std::size_t>(m_activeTab)];
const int visibleCount = tab.VisibleCount();
const bool anyVisible = tab.AnyVisible();
const std::size_t laneCount = tab.Lanes().size();
const bool changed = (visibleCount != m_cachedVisibleCount) || (anyVisible != m_cachedAnyVisible) ||
(laneCount != m_cachedLaneCount);
if (!changed && !force)
{
return;
}
const std::size_t previousLaneCount = m_cachedLaneCount;
m_cachedVisibleCount = visibleCount;
m_cachedAnyVisible = anyVisible;
m_cachedLaneCount = laneCount;
// 标签页首次出现可见信号时,把时间轴对齐到可见数据的范围。
// 没有任何可见信号时(未勾选 / 全部取消勾选 / 清空网格)主动复位自适应标记,
// 这样用户下一次勾选信号时会重新做一次自适应,而不是停留在上次的视图范围。
if (!anyVisible)
{
tab.ViewInitialized() = false;
}
else if (!tab.ViewInitialized())
{
m_plot.FitAll();
}
const bool laneCountChanged = (laneCount != previousLaneCount);
if (laneCountChanged || force)
{
// 网格数量变化会影响滚动条与绘图区宽度,必须重新布局。
Relayout();
}
else
{
m_plot.RefreshLanes();
}
}
void Application::SyncTabsToDataSet()
{
for (TabConfig& tab : m_tabs)
{
tab.SyncToDataSet(m_dataSet);
}
// 选中状态由数据层共享维护,数据变动后需校验其是否仍指向存在的信号。
m_dataSet.ValidateSelection();
m_panel.InvalidateRows();
// 文件/信号结构变化会让信号引用与文件地址改变,折线顶点缓存必须作废。
m_plot.InvalidatePolylineCache();
// 强制下一次同步重新计算视图状态。
m_cachedVisibleCount = -1;
m_cachedAnyVisible = false;
m_cachedLaneCount = 0;
if (m_hwnd != nullptr)
{
InvalidateRect(m_hwnd, nullptr, TRUE);
}
}
void Application::RemoveFile(int fileIndex)
{
const DataFile* file = m_dataSet.FileAt(fileIndex);
if (file == nullptr)
{
return;
}
const std::wstring name = file->fileName;
// 先按旧下标修正各标签页配置与共享选中项,再从数据层移除,最后统一对齐。
for (TabConfig& tab : m_tabs)
{
tab.RemapAfterFileRemoval(fileIndex);
}
m_dataSet.RemapSelectionAfterFileRemoval(fileIndex);
m_dataSet.RemoveFile(fileIndex);
SyncTabsToDataSet();
SetStatusMessage(L"已从工作区移除:" + name, false);
UpdateWindowTitle();
}
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 tabBarHeight = m_tabBar.DesiredHeight();
const float toolbarHeight = m_toolbar.DesiredHeight();
const float splitterWidth = ScaleOf(kSplitterWidth, m_scale);
m_tabBarRect = gd::RectF(0.0f, 0.0f, width, tabBarHeight);
m_toolbarRect = gd::RectF(0.0f, m_tabBarRect.Y + m_tabBarRect.Height, width, toolbarHeight);
// 不再保留底部状态栏,内容区一直延伸到窗口底部。
const float contentTop = m_toolbarRect.Y + m_toolbarRect.Height;
const float contentHeight = std::max(0.0f, height - contentTop);
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, contentTop, panelWidth, contentHeight);
m_splitterRect = gd::RectF(panelWidth, contentTop, splitterWidth, contentHeight);
m_plotRect = gd::RectF(panelWidth + splitterWidth, contentTop,
std::max(0.0f, width - panelWidth - splitterWidth), contentHeight);
m_tabBar.Layout(m_tabBarRect);
m_toolbar.Layout(m_toolbarRect);
m_panel.SetClientBounds(gd::RectF(0.0f, 0.0f, width, height));
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;
}
// 面板内展开/收起自定义颜色面板等操作会改变面板所需高度。
if (m_panel.TakeLayoutDirty())
{
Relayout();
}
// 任何交互(勾选、移动网格、切换标签页)之后都在此统一收敛视图状态。
SynchronizeView(false);
m_toolbar.SetDocumentLoaded(!m_dataSet.Empty());
// 工具栏可切换按钮的选中状态始终与当前标签页配置保持一致。
m_toolbar.SetToggleState(ToolbarAction::ZOOM_TIME,
m_plot.CurrentZoomMode() == PlotZoomMode::TIME);
m_toolbar.SetToggleState(ToolbarAction::ZOOM_VALUE,
m_plot.CurrentZoomMode() == PlotZoomMode::VALUE);
m_toolbar.SetToggleState(ToolbarAction::CURSOR, m_plot.CursorEnabled());
{
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_tabBar.Paint(g);
m_toolbar.Paint(g);
m_panel.Paint(g);
m_plot.Paint(g);
UiFillRect(g, m_splitterRect, m_theme.border);
// 弹出菜单与浮动拾色器始终绘制在最上层,保证与主界面风格一致。
m_menu.Paint(g);
m_colorPicker.Paint(g);
// 加载遮罩层在一切内容之上(包括菜单与拾色器),形成模态观感。
PaintLoadingOverlay(g, width, height);
}
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::SetStatusMessage(const std::wstring& text, bool isError)
{
// 界面不再占用底部空间显示常驻提示;仅把真实错误以对话框形式反馈给用户。
if (!isError || text.empty())
{
return;
}
MessageBoxW(m_hwnd, text.c_str(), kWindowTitle, MB_ICONWARNING | MB_OK);
}
void Application::UpdateWindowTitle()
{
// 标题栏只显示应用名称,不附带标签页、文件数量等无关信息。
SetWindowTextW(m_hwnd, kWindowTitle);
}
void Application::OpenFileDialog()
{
// 允许一次选择多个文件,便于跨文件对比信号。
std::vector<wchar_t> buffer(static_cast<std::size_t>(MAX_PATH) * 16, L'\0');
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.data();
dialog.nMaxFile = static_cast<DWORD>(buffer.size());
dialog.lpstrTitle = L"打开仿真数据文件(可多选)";
dialog.lpstrDefExt = L"csv";
dialog.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_EXPLORER | OFN_NOCHANGEDIR |
OFN_ALLOWMULTISELECT;
if (GetOpenFileNameW(&dialog) == FALSE)
{
return;
}
// 多选时首段为目录,其余为文件名;单选时整段即为完整路径。
const std::wstring first(buffer.data());
const wchar_t* cursor = buffer.data() + first.size() + 1;
int loaded = 0;
if (*cursor == L'\0')
{
if (LoadFile(first))
{
++loaded;
}
}
else
{
while (*cursor != L'\0')
{
const std::wstring name(cursor);
if (LoadFile(first + L"\\" + name))
{
++loaded;
}
cursor += name.size() + 1;
}
}
if (loaded > 0)
{
SetStatusMessage(L"已加载 " + std::to_wstring(loaded) +
L" 个文件,请在左侧勾选需要查看的信号。",
false);
}
}
bool Application::LoadFile(const std::wstring& path)
{
if (path.empty())
{
return false;
}
// 大文件在工作线程解析并构建,主线程播放加载过渡动画,界面不再整块卡住。
DataFile file;
std::wstring error;
if (!LoadFileAsync(path, file, error))
{
if (!error.empty())
{
SetStatusMessage(L"解析失败:" + error, true);
}
InvalidateRect(m_hwnd, nullptr, FALSE);
return false;
}
int fileIndex = -1;
// 此处只做一次指针/缓冲移动,不拷贝任何采样数据,因此不会造成可见卡顿。
if (!m_dataSet.AddPreparedFile(std::move(file), fileIndex))
{
SetStatusMessage(L"解析失败:数据为空。", true);
InvalidateRect(m_hwnd, nullptr, FALSE);
return false;
}
// 新文件的信号在全部标签页中默认均为未勾选状态,由用户自行决定查看内容。
SyncTabsToDataSet();
UpdateWindowTitle();
const DataFile* loaded = m_dataSet.FileAt(fileIndex);
if (loaded != nullptr)
{
std::wstring message = L"已加载 组" + std::to_wstring(fileIndex + 1) + L"|" + loaded->Title() +
L"(" + FormatCount(static_cast<unsigned long long>(loaded->time.size())) +
L" 行采样," + std::to_wstring(loaded->signals.size()) +
L" 条信号,时间列:" + loaded->timeColumnName + L")";
if (!loaded->timeMonotonic)
{
message += L" 提示:时间列非单调递增,光标取值使用最近邻策略。";
}
SetStatusMessage(message, false);
}
return true;
}
bool Application::LoadFileAsync(const std::wstring& path, DataFile& file, std::wstring& error)
{
error.clear();
AsyncLoadState state;
const std::size_t paletteBase = m_dataSet.SignalCount();
m_loading = true;
m_loadingProgress = 0.0f;
const std::size_t slashPos = path.find_last_of(L"\\/");
m_loadingText = (slashPos == std::wstring::npos) ? path : path.substr(slashPos + 1);
std::thread worker(RunAsyncLoad, &state, path, paletteBase);
// 主线程循环:抽送消息 + 刷新动画,直到后台完成或用户取消。
while (!state.done.load(std::memory_order_acquire))
{
MSG message = {};
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE))
{
if (message.message == WM_QUIT)
{
// 退出请求:让后台停下并重新投递 WM_QUIT,由主循环正常收尾。
state.cancelled.store(true, std::memory_order_relaxed);
PostQuitMessage(static_cast<int>(message.wParam));
break;
}
// 加载期间屏蔽鼠标与其它按键,避免重入加载或误改数据;
// 保留 Esc 作为取消入口。
if (message.message >= WM_MOUSEFIRST && message.message <= WM_MOUSELAST)
{
continue;
}
if (message.message == WM_KEYDOWN && message.wParam == VK_ESCAPE)
{
state.cancelled.store(true, std::memory_order_relaxed);
continue;
}
if (message.message >= WM_KEYFIRST && message.message <= WM_KEYLAST)
{
continue;
}
TranslateMessage(&message);
DispatchMessageW(&message);
}
m_loadingProgress = state.progress.load(std::memory_order_relaxed);
InvalidateRect(m_hwnd, nullptr, FALSE);
UpdateWindow(m_hwnd);
Sleep(15);
}
m_loadingProgress = 1.0f;
worker.join();
m_loading = false;
if (state.cancelled.load(std::memory_order_relaxed))
{
// 用户主动取消:不弹错误框(与 Esc 取消缩放选区的静默语义一致)。
InvalidateRect(m_hwnd, nullptr, TRUE);
return false;
}
if (!state.ok)
{
error = state.error;
return false;
}
file = std::move(state.file);
return true;
}
void Application::PaintLoadingOverlay(gd::Graphics& g, int width, int height)
{
if (!m_loading)
{
return;
}
// 半透明遮罩压暗整个界面,形成明确的"正在加载"状态。
UiFillRect(g, gd::RectF(0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height)),
0x66000000u);
const float cardWidth = std::min(420.0f * m_scale, static_cast<float>(width) - 40.0f * m_scale);
const float cardHeight = 132.0f * m_scale;
const gd::RectF card((static_cast<float>(width) - cardWidth) * 0.5f,
(static_cast<float>(height) - cardHeight) * 0.5f, cardWidth, cardHeight);
UiFillRoundRect(g, card, 8.0f * m_scale, m_theme.cardBg);
UiDrawRoundRect(g, card, 8.0f * m_scale, m_theme.borderStrong, 1.0f);
// 旋转弧线:用时间驱动的起始角度形成持续的加载动画。
const float spinnerRadius = 11.0f * m_scale;
const gd::RectF spinner(card.X + 26.0f * m_scale, card.Y + 30.0f * m_scale - spinnerRadius,
spinnerRadius * 2.0f, spinnerRadius * 2.0f);
m_loadingSpin += 12.0f;
if (m_loadingSpin >= 360.0f)
{
m_loadingSpin -= 360.0f;
}
gd::Pen trackPen(ToGdiColor(m_theme.border), 3.0f * m_scale);
gd::Pen spinPen(ToGdiColor(m_theme.accent), 3.0f * m_scale);
g.DrawArc(&trackPen, spinner, 0.0f, 359.0f);
g.DrawArc(&spinPen, spinner, m_loadingSpin, 110.0f);
const float textLeft = card.X + 52.0f * m_scale;
const float textWidth = cardWidth - 52.0f * m_scale - 20.0f * m_scale;
UiText(g, L"正在加载数据…", m_fonts.uiBold,
gd::RectF(textLeft, card.Y + 18.0f * m_scale, textWidth, 22.0f * m_scale), m_theme.text,
TextAlign::LEFT);
// 超长文件名用省略号截断,避免撑破卡片。
const std::wstring nameText =
UiEllipsize(g, m_loadingText, m_fonts.uiSmall, textWidth);
UiText(g, nameText, m_fonts.uiSmall,
gd::RectF(textLeft, card.Y + 42.0f * m_scale, textWidth, 20.0f * m_scale),
m_theme.textMuted, TextAlign::LEFT);
// 进度条(复用样式区滑块以外的简单圆角矩形,填充宽度严格按比例)。
const gd::RectF track(card.X + 26.0f * m_scale, card.Y + 76.0f * m_scale,
cardWidth - 52.0f * m_scale, 8.0f * m_scale);
UiFillRoundRect(g, track, track.Height * 0.5f, m_theme.pressed);
float ratio = m_loadingProgress;
ratio = std::max(0.0f, std::min(ratio, 1.0f));
const float fillWidth = track.Width * ratio;
if (fillWidth > 1.0f)
{
UiFillRoundRect(g, gd::RectF(track.X, track.Y, fillWidth, track.Height), track.Height * 0.5f,
m_theme.accent);
}
wchar_t buffer[64];
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%d%%", static_cast<int>(ratio * 100.0f + 0.5f));
UiText(g, buffer, m_fonts.monoSmall,
gd::RectF(track.X, card.Y + 92.0f * m_scale, track.Width, 20.0f * m_scale), m_theme.text,
TextAlign::LEFT);
UiText(g, L"按 Esc 取消", m_fonts.uiSmall,
gd::RectF(track.X, card.Y + 92.0f * m_scale, track.Width, 20.0f * m_scale),
m_theme.textMuted, TextAlign::RIGHT);
}
void Application::LoadCommandLineFiles()
{
int argumentCount = 0;
LPWSTR* arguments = CommandLineToArgvW(GetCommandLineW(), &argumentCount);
if (arguments == nullptr)
{
return;
}
int loaded = 0;
for (int index = 1; index < argumentCount; ++index)
{
const std::wstring argument = arguments[index];
if (argument.empty() || argument[0] == L'-' || argument[0] == L'/')
{
continue;
}
if (LoadFile(argument))
{
++loaded;
}
}
LocalFree(arguments);
if (loaded > 0)
{
SetStatusMessage(L"已加载 " + std::to_wstring(loaded) +
L" 个文件,请在左侧勾选需要查看的信号。",
false);
}
}
void Application::ExecuteToolbarAction(ToolbarAction action)
{
switch (action)
{
case ToolbarAction::OPEN_FILE:
OpenFileDialog();
break;
case ToolbarAction::FIT_ALL:
// 自适应同时回到初始状态:退出缩放模式并显示全部信号范围。
m_plot.FitAll();
SetStatusMessage(L"已恢复自适应视图。", false);
break;
case ToolbarAction::ZOOM_TIME:
if (m_plot.ToggleZoomMode(PlotZoomMode::TIME))
{
SetStatusMessage((m_plot.CurrentZoomMode() == PlotZoomMode::TIME)
? L"时间轴缩放:在绘图区横向拖拽选择区间即可放大。"
: L"已退出时间轴缩放模式。",
false);
}
break;
case ToolbarAction::ZOOM_VALUE:
if (m_plot.ToggleZoomMode(PlotZoomMode::VALUE))
{
SetStatusMessage((m_plot.CurrentZoomMode() == PlotZoomMode::VALUE)
? L"Y 轴缩放:在某个网格内纵向拖拽选择区间即可放大。"
: L"已退出 Y 轴缩放模式。",
false);
}
break;
case ToolbarAction::CURSOR:
if (m_plot.ToggleCursor())
{
SetStatusMessage(m_plot.CursorEnabled()
? L"时间光标已启用:拖动竖线移动,双击竖线可输入精确时间。"
: L"时间光标已关闭。",
false);
}
break;
case ToolbarAction::MERGE_LANES:
if (!m_dataSet.Empty())
{
ActiveTab().SetMode(m_dataSet, LaneMode::MERGED);
SetStatusMessage(L"已将当前标签页的全部可见信号合并到同一网格。", false);
}
break;
case ToolbarAction::SPLIT_LANES:
if (!m_dataSet.Empty())
{
ActiveTab().SetMode(m_dataSet, LaneMode::PER_SIGNAL);
SetStatusMessage(L"当前标签页已切换为「每信号一格」。", false);
}
break;
case ToolbarAction::CLEAR_ALL:
if (!m_dataSet.Empty())
{
ActiveTab().SetAllVisible(m_dataSet, false);
m_panel.InvalidateRows();
SetStatusMessage(L"已清空当前标签页的全部网格。", false);
}
break;
case ToolbarAction::NONE:
default:
break;
}
if (m_hwnd != nullptr)
{
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;
}
if (control && key == L'T')
{
NewTab();
return true;
}
if (control && key == L'W')
{
CloseTab(m_activeTab);
return true;
}
if (control && key == VK_TAB)
{
if (m_tabs.size() > 1)
{
const bool backward = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
const int count = static_cast<int>(m_tabs.size());
SetActiveTab((m_activeTab + (backward ? count - 1 : 1)) % count);
}
return true;
}
switch (key)
{
case VK_ESCAPE:
{
// Esc 的语义:
// 1) 放弃当前正在进行的拖拽(典型场景:缩放的框选选区还没松开左键);
// 取消后不应用选区、视图范围保持不变;
// 2) 同时退出全部缩放模式,使工具栏「时间轴缩放」「Y 轴缩放」回到未选中态。
// Esc 不与时间光标关联:关闭光标请用工具栏「光标」按钮,避免误关指针。
bool changed = m_plot.CancelDrag();
changed = m_plot.ExitZoomModes() || changed;
return changed;
}
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;
}
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::TAB_BAR:
return m_tabBar.OnMouseMove(x, y);
case Target::EDIT_FIELD:
{
// 输入框拖拽框选:按当前正在编辑的组件分发。
bool changed = m_tabBar.OnEditMouseMove(x);
changed = m_panel.OnEditMouseMove(x) || changed;
changed = m_plot.OnEditMouseMove(x) || changed;
changed = m_colorPicker.OnEditMouseMove(x) || changed;
return changed;
}
case Target::NONE:
default:
return false;
}
}
bool changed = false;
changed =
(m_tabBar.HitTest(x, y) ? m_tabBar.OnMouseMove(x, y) : m_tabBar.OnMouseLeave()) || changed;
changed =
(m_toolbar.HitTest(x, y) ? m_toolbar.OnMouseMove(x, y) : m_toolbar.OnMouseLeave()) || changed;
changed = (m_panel.HitTest(x, y) ? m_panel.OnMouseMove(x, y) : m_panel.OnMouseLeave()) || changed;
changed = (m_plot.HitTest(x, y) ? m_plot.OnMouseMove(x, y) : m_plot.OnMouseLeave()) || changed;
return changed;
}
void Application::OnTabBarResult(const TabBarResult& result)
{
switch (result.type)
{
case TabBarResult::Type::ADD:
NewTab();
break;
case TabBarResult::Type::SELECT:
if (result.index != m_activeTab)
{
SetActiveTab(result.index);
}
break;
case TabBarResult::Type::CLOSE:
CloseTab(result.index);
break;
case TabBarResult::Type::CONTEXT:
ShowTabContextMenu(result);
break;
case TabBarResult::Type::REORDER:
MoveTab(result.index, result.targetIndex);
break;
case TabBarResult::Type::RENAME:
if (result.index >= 0 && result.index < static_cast<int>(m_tabs.size()) && !result.text.empty())
{
m_tabs[static_cast<std::size_t>(result.index)].SetName(result.text);
RefreshTabBar();
// SetTabs 会重置各标签的矩形,必须重新布局才能把它们算回来。
Relayout();
InvalidateRect(m_hwnd, nullptr, TRUE);
}
break;
case TabBarResult::Type::NONE:
default:
break;
}
}
void Application::MoveTab(int from, int target)
{
if (from < 0 || from >= static_cast<int>(m_tabs.size()))
{
return;
}
if (target < 0 || target > static_cast<int>(m_tabs.size()))
{
return;
}
// 拖拽语义:target 表示「插入到该下标之前」;向后拖时需扣除自身占位。
int destination = target;
if (destination > from)
{
--destination;
}
destination = std::max(0, std::min(destination, static_cast<int>(m_tabs.size()) - 1));
if (destination == from)
{
return;
}
// 保持当前激活的标签页在重排后仍处于激活状态。
TabConfig moved = m_tabs[static_cast<std::size_t>(from)];
const int activeIndex = m_activeTab;
m_tabs.erase(m_tabs.begin() + from);
m_tabs.insert(m_tabs.begin() + destination, std::move(moved));
int newActive = activeIndex;
if (activeIndex == from)
{
newActive = destination;
}
else if (from < activeIndex && destination >= activeIndex)
{
--newActive;
}
else if (from > activeIndex && destination <= activeIndex)
{
++newActive;
}
SetActiveTab(newActive);
InvalidateRect(m_hwnd, nullptr, TRUE);
}
bool Application::ApplyPendingRename()
{
SignalRef ref;
std::wstring name;
if (!m_plot.TakePendingRename(ref, name))
{
return false;
}
if (!m_dataSet.SetSignalDisplayName(ref.fileIndex, ref.signalIndex, name))
{
return false;
}
// 改名只影响显示,但需要刷新左侧列表与绘图区图例。
m_panel.InvalidateRows();
SetStatusMessage(L"信号已重命名为:" + name, false);
InvalidateRect(m_hwnd, nullptr, TRUE);
return true;
}
void Application::ShowTabContextMenu(const TabBarResult& result)
{
if (result.index < 0 || result.index >= static_cast<int>(m_tabs.size()))
{
return;
}
m_tabMenuIndex = result.index;
std::vector<MenuItem> items;
MenuItem header;
header.kind = MenuItemKind::HEADER;
header.label = m_tabs[static_cast<std::size_t>(result.index)].Name();
items.push_back(header);
MenuItem create;
create.label = L"新建标签页";
create.id = kMenuTabNew;
items.push_back(create);
MenuItem close;
close.label = L"关闭该标签页";
close.id = kMenuTabClose;
close.enabled = (m_tabs.size() > 1);
items.push_back(close);
MenuItem closeOthers;
closeOthers.label = L"关闭其它标签页";
closeOthers.id = kMenuTabCloseOthers;
closeOthers.enabled = (m_tabs.size() > 1);
items.push_back(closeOthers);
// 菜单绘制在客户区内,因此把屏幕坐标换算为客户区坐标。
POINT anchor = {result.screenX, result.screenY};
ScreenToClient(m_hwnd, &anchor);
RECT client = {};
GetClientRect(m_hwnd, &client);
const gd::RectF bounds(0.0f, 0.0f, static_cast<float>(client.right - client.left),
static_cast<float>(client.bottom - client.top));
m_menu.Open(MenuScope::TAB_BAR, bounds, anchor.x, anchor.y, items);
}
void Application::ShowPlotContextMenu(int laneIndex, int x, int y)
{
if (laneIndex < 0)
{
return;
}
m_plotMenuLane = laneIndex;
std::vector<MenuItem> items;
MenuItem header;
header.kind = MenuItemKind::HEADER;
header.label = L"网格 " + std::to_wstring(laneIndex + 1);
items.push_back(header);
const bool isAuto = m_plot.LaneRangeIsAuto(laneIndex);
// 当前网格的 Y 轴上下限值直接在菜单内的输入框里显示,单击即可就地编辑(菜单不关闭)。
double laneYMin = 0.0;
double laneYMax = 1.0;
m_plot.LaneRange(laneIndex, laneYMin, laneYMax);
MenuItem maxItem;
maxItem.kind = MenuItemKind::EDIT;
maxItem.label = L"Y 轴最大值";
maxItem.id = PLOT_CMD_LANE_Y_MAX;
maxItem.value = FormatValue(laneYMax);
maxItem.filter = EditFilter::NUMBER;
items.push_back(maxItem);
MenuItem minItem;
minItem.kind = MenuItemKind::EDIT;
minItem.label = L"Y 轴最小值";
minItem.id = PLOT_CMD_LANE_Y_MIN;
minItem.value = FormatValue(laneYMin);
minItem.filter = EditFilter::NUMBER;
items.push_back(minItem);
// 选区自适应:只按当前显示时间段内的数据做幅值自适应(放大局部后铺满网格)。
MenuItem autoViewItem;
autoViewItem.label = L"选区 Y 轴自适应";
autoViewItem.id = PLOT_CMD_LANE_AUTO_VIEW;
items.push_back(autoViewItem);
MenuItem separator;
separator.kind = MenuItemKind::SEPARATOR;
items.push_back(separator);
// 恢复自适应放在分隔线下方、时间轴自适应上方。
MenuItem autoItem;
autoItem.label = isAuto ? L"Y 轴自适应(当前)" : L"恢复 Y 轴自适应";
autoItem.id = PLOT_CMD_LANE_AUTO;
autoItem.checked = isAuto;
autoItem.enabled = !isAuto;
items.push_back(autoItem);
MenuItem fitLaneTime;
fitLaneTime.label = L"时间轴自适应(匹配本网格)";
fitLaneTime.id = PLOT_CMD_LANE_FIT_TIME;
items.push_back(fitLaneTime);
MenuItem fitItem;
fitItem.label = L"全部自适应";
fitItem.id = PLOT_CMD_FIT_ALL;
items.push_back(fitItem);
RECT client = {};
GetClientRect(m_hwnd, &client);
const gd::RectF bounds(0.0f, 0.0f, static_cast<float>(client.right - client.left),
static_cast<float>(client.bottom - client.top));
m_menu.Open(MenuScope::PLOT_VIEW, bounds, x, y, items);
}
void Application::RefreshPlotMenuValues(int laneIndex)
{
if (laneIndex < 0)
{
return;
}
double laneYMin = 0.0;
double laneYMax = 1.0;
m_plot.LaneRange(laneIndex, laneYMin, laneYMax);
m_menu.UpdateItemValue(PLOT_CMD_LANE_Y_MAX, FormatValue(laneYMax));
m_menu.UpdateItemValue(PLOT_CMD_LANE_Y_MIN, FormatValue(laneYMin));
// 手工输入上下限后已不再是自动自适应,同步刷新该项的文字/选中标记/可用状态。
const bool isAuto = m_plot.LaneRangeIsAuto(laneIndex);
m_menu.UpdateItemLabel(PLOT_CMD_LANE_AUTO, isAuto ? L"Y 轴自适应(当前)" : L"恢复 Y 轴自适应",
isAuto, !isAuto);
}
void Application::DispatchMenuResult(const MenuResult& result)
{
if (!result.valid)
{
return;
}
switch (result.scope)
{
case MenuScope::SIGNAL_PANEL:
{
const bool needRelayout = m_panel.OnMenuCommand(result);
if (needRelayout)
{
Relayout();
}
InvalidateRect(m_hwnd, nullptr, FALSE);
break;
}
case MenuScope::PLOT_VIEW:
{
// 菜单内联输入框的提交:就地更新网格 Y 轴上下限,菜单保持打开,
// 并把夹取后的真实值回写到菜单上(输入值可能被合法区间修正)。
if (result.editCommit)
{
const int laneIndex = m_plotMenuLane;
if (laneIndex >= 0)
{
m_plot.ApplyLaneValueText(laneIndex, result.id == PLOT_CMD_LANE_Y_MAX, result.editText);
RefreshPlotMenuValues(laneIndex);
}
SetFocus(m_hwnd);
InvalidateRect(m_hwnd, nullptr, FALSE);
break;
}
const int laneIndex = m_plotMenuLane;
m_plotMenuLane = -1;
switch (result.id)
{
case PLOT_CMD_LANE_AUTO:
m_plot.ResetLaneRange(laneIndex);
break;
case PLOT_CMD_LANE_AUTO_VIEW:
m_plot.AutoRangeLaneToView(laneIndex);
break;
case PLOT_CMD_LANE_FIT_TIME:
m_plot.FitTimeToLane(laneIndex);
break;
case PLOT_CMD_FIT_ALL:
m_plot.FitAll();
break;
default:
break;
}
SetFocus(m_hwnd);
InvalidateRect(m_hwnd, nullptr, TRUE);
break;
}
case MenuScope::TAB_BAR:
{
const int index = m_tabMenuIndex;
if (result.id == kMenuTabNew)
{
NewTab();
}
else if (result.id == kMenuTabClose)
{
CloseTab(index);
}
else if (result.id == kMenuTabCloseOthers)
{
if (index >= 0 && index < static_cast<int>(m_tabs.size()) && m_tabs.size() > 1)
{
const TabConfig keep = m_tabs[static_cast<std::size_t>(index)];
m_tabs.clear();
m_tabs.push_back(keep);
SetActiveTab(0);
}
}
m_tabMenuIndex = -1;
InvalidateRect(m_hwnd, nullptr, TRUE);
break;
}
case MenuScope::NONE:
default:
break;
}
}
LRESULT Application::HandleMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
// 菜单打开时优先消费鼠标与键盘事件,形成模态交互。
if (m_menu.IsOpen())
{
// 菜单接管输入后,取消底层组件可能仍在进行的拖拽。
if (m_activeTarget != Target::NONE || m_draggingSplitter)
{
ReleaseCapture();
m_activeTarget = Target::NONE;
m_draggingSplitter = false;
m_panel.OnLButtonUp(0, 0);
m_plot.OnLButtonUp(0, 0);
}
switch (message)
{
case WM_MOUSEMOVE:
// 菜单内联输入框拖拽框选优先(不改变悬停项,避免选区被中途清掉)。
if (m_menu.IsEditing() && m_activeTarget == Target::EDIT_FIELD)
{
if (m_menu.OnEditMouseMove(GET_X_LPARAM(lParam)))
{
InvalidateRect(hwnd, nullptr, FALSE);
}
return 0;
}
if (m_menu.OnMouseMove(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)))
{
InvalidateRect(hwnd, nullptr, FALSE);
}
return 0;
case WM_LBUTTONDOWN:
{
const int downX = GET_X_LPARAM(lParam);
const int downY = GET_Y_LPARAM(lParam);
// 点击菜单内联输入框:定位插入符并开始拖拽框选(与常规输入框一致)。
if (m_menu.IsOnEditField(downX, downY))
{
m_menu.OnEditMouseDown(downX, downY);
SetCapture(hwnd);
m_activeTarget = Target::EDIT_FIELD;
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
MenuResult result;
m_menu.OnLButtonDown(downX, downY, result);
InvalidateRect(hwnd, nullptr, FALSE);
// 点击其它位置会先提交正在编辑的数值,需要立即落到数据层并刷新菜单显示。
if (result.valid)
{
DispatchMenuResult(result);
}
return 0;
}
case WM_LBUTTONUP:
{
if (m_menu.IsEditing() && m_activeTarget == Target::EDIT_FIELD)
{
ReleaseCapture();
m_activeTarget = Target::NONE;
m_menu.OnEditMouseUp();
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
MenuResult result;
m_menu.OnLButtonUp(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), result);
InvalidateRect(hwnd, nullptr, FALSE);
DispatchMenuResult(result);
return 0;
}
case WM_CHAR:
{
// 菜单内联输入框的字符输入(数值/名称),回车在此提交。
MenuResult result;
if (m_menu.OnChar(static_cast<wchar_t>(wParam), result))
{
InvalidateRect(hwnd, nullptr, FALSE);
}
if (result.valid)
{
DispatchMenuResult(result);
}
return 0;
}
case WM_RBUTTONDOWN:
case WM_MBUTTONDOWN:
case WM_MOUSEWHEEL:
m_menu.Close();
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
case WM_KEYDOWN:
{
MenuResult result;
m_menu.OnKeyDown(wParam, result);
InvalidateRect(hwnd, nullptr, FALSE);
if (result.valid)
{
DispatchMenuResult(result);
}
return 0;
}
case WM_KILLFOCUS:
case WM_CANCELMODE:
m_menu.Close();
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
case WM_SETCURSOR:
SetCursor(LoadCursorW(nullptr, IDC_ARROW));
return TRUE;
default:
break;
}
}
// 拾色器打开时接管鼠标与键盘(优先级低于弹出菜单、高于常规组件)。
if (m_colorPicker.IsOpen())
{
bool repaint = false;
bool consume = false;
switch (message)
{
case WM_MOUSEMOVE:
repaint = m_colorPicker.OnMouseMove(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
consume = true;
break;
case WM_LBUTTONDOWN:
{
const int pickX = GET_X_LPARAM(lParam);
const int pickY = GET_Y_LPARAM(lParam);
// 拾色器接管输入后,取消底层组件可能仍在进行的拖拽。
m_panel.OnLButtonUp(0, 0);
m_plot.OnLButtonUp(0, 0);
m_activeTarget = Target::NONE;
m_draggingSplitter = false;
if (m_colorPicker.ContainsPoint(pickX, pickY))
{
// 面板内部:开始拖动滑块,并吃掉本次点击,避免穿透到下方图层。
m_colorPicker.OnLButtonDown(pickX, pickY);
SetCapture(hwnd);
consume = true;
}
else
{
// 面板外部:收起面板;若落点在信号面板内则放行本次点击,
// 便于用户在展开状态下直接改选预设色、样式或拖动滑块。
m_colorPicker.Close();
if (!m_panel.HitTest(pickX, pickY))
{
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
}
repaint = true;
break;
}
case WM_SETCURSOR:
{
if (LOWORD(lParam) == HTCLIENT)
{
POINT point = {};
GetCursorPos(&point);
ScreenToClient(hwnd, &point);
HCURSOR cursor = m_colorPicker.QueryCursor(point.x, point.y);
SetCursor(cursor != nullptr ? cursor : LoadCursorW(nullptr, IDC_ARROW));
return TRUE;
}
break;
}
case WM_LBUTTONUP:
m_colorPicker.OnLButtonUp();
ReleaseCapture();
repaint = true;
consume = true;
break;
case WM_KEYDOWN:
m_colorPicker.OnKeyDown(wParam);
repaint = true;
consume = true;
break;
case WM_CHAR:
// 字符输入转发给拾色器,用于 RGB 分量与十六进制色号的内联编辑。
repaint = m_colorPicker.OnChar(static_cast<wchar_t>(wParam));
consume = true;
break;
case WM_RBUTTONDOWN:
case WM_MBUTTONDOWN:
case WM_MOUSEWHEEL:
m_colorPicker.Close();
repaint = true;
consume = true;
break;
default:
break;
}
// 取色变化实时应用到当前选中信号。
std::uint32_t pickedColor = 0;
if (m_colorPicker.TakeChanged(pickedColor))
{
m_panel.ApplyColor(pickedColor);
repaint = true;
}
if (repaint)
{
InvalidateRect(hwnd, nullptr, FALSE);
}
if (consume)
{
return 0;
}
}
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_tabBar.OnMouseLeave();
changed = m_toolbar.OnMouseLeave() || changed;
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 (m_tabBar.OnEditMouseDown(x, y) || m_panel.OnEditMouseDown(x, y) ||
m_plot.OnEditMouseDown(x, y) || m_colorPicker.OnEditMouseDown(x, y))
{
SetCapture(hwnd);
m_activeTarget = Target::EDIT_FIELD;
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
// 内联数值输入处于活动态时,点击绘图区之外视为提交(与线宽输入一致)。
if (m_plot.IsEditing() && !m_plot.HitTest(x, y))
{
m_plot.OnEditKey(VK_RETURN);
ApplyPendingRename();
}
// 点击面板之外的位置时收起面板内展开的自定义颜色面板。
if (!m_panel.HitTest(x, y))
{
if (m_panel.CloseTransientUi())
{
InvalidateRect(hwnd, nullptr, FALSE);
}
}
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_tabBar.HitTest(x, y))
{
SetCapture(hwnd);
m_activeTarget = Target::TAB_BAR;
m_tabBar.OnLButtonDown(x, y);
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
if (m_toolbar.HitTest(x, y))
{
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, TRUE);
}
return 0;
}
if (m_plot.HitTest(x, y))
{
SetCapture(hwnd);
m_activeTarget = Target::PLOT;
if (m_plot.OnLButtonDown(x, y))
{
// 绘图区内的点击也可能提交图例改名,需要立刻落到数据层。
ApplyPendingRename();
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();
switch (m_activeTarget)
{
case Target::SPLITTER:
m_draggingSplitter = false;
break;
case Target::EDIT_FIELD:
{
// 结束输入框内的拖拽框选(选区保留)。
bool changed = m_tabBar.OnEditMouseUp();
changed = m_panel.OnEditMouseUp() || changed;
changed = m_plot.OnEditMouseUp() || changed;
changed = m_colorPicker.OnEditMouseUp() || changed;
m_activeTarget = Target::NONE;
if (changed)
{
InvalidateRect(hwnd, nullptr, FALSE);
}
return 0;
}
case Target::TAB_BAR:
{
TabBarResult result;
m_tabBar.OnLButtonUp(x, y, result);
m_activeTarget = Target::NONE;
OnTabBarResult(result);
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
case Target::TOOLBAR:
{
ToolbarAction action = ToolbarAction::NONE;
m_toolbar.OnLButtonUp(x, y, action);
m_activeTarget = Target::NONE;
if (action != ToolbarAction::NONE)
{
ExecuteToolbarAction(action);
return 0;
}
break;
}
case Target::PANEL:
m_panel.OnLButtonUp(x, y);
break;
case Target::PLOT:
m_plot.OnLButtonUp(x, y);
break;
case Target::NONE:
default:
break;
}
m_activeTarget = Target::NONE;
InvalidateRect(hwnd, nullptr, TRUE);
return 0;
}
case WM_LBUTTONDBLCLK:
{
const int x = GET_X_LPARAM(lParam);
const int y = GET_Y_LPARAM(lParam);
bool changed = false;
if (m_tabBar.HitTest(x, y))
{
SetFocus(hwnd);
changed = m_tabBar.OnDoubleClick(x, y);
if (m_tabBar.IsEditing())
{
// 进入重命名后该标签会加宽,需要重新排布标签栏。
Relayout();
}
}
else 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, TRUE);
}
return 0;
}
case WM_RBUTTONDOWN:
{
const int x = GET_X_LPARAM(lParam);
const int y = GET_Y_LPARAM(lParam);
// 编辑态下右键 = 取消选区并把插入符移到文本末尾(常规输入框行为),
// 其它操作(弹菜单、拖拽)一并被吞掉。
if (m_tabBar.IsEditing() || m_panel.IsEditing() || m_plot.IsEditing() || m_colorPicker.IsEditing())
{
bool changed = m_tabBar.CollapseEditSelectionToEnd();
changed = m_panel.CollapseEditSelectionToEnd() || changed;
changed = m_plot.CollapseEditSelectionToEnd() || changed;
changed = m_colorPicker.CollapseEditSelectionToEnd() || changed;
if (changed)
{
InvalidateRect(hwnd, nullptr, FALSE);
}
return 0;
}
POINT cursor = {x, y};
ClientToScreen(hwnd, &cursor);
if (m_tabBar.HitTest(x, y))
{
TabBarResult result;
if (m_tabBar.OnRButtonDown(x, y, cursor.x, cursor.y, result))
{
OnTabBarResult(result);
// 自绘菜单会覆盖在客户区之上,必须请求重绘才能显示。
InvalidateRect(hwnd, nullptr, FALSE);
}
return 0;
}
if (m_panel.HitTest(x, y))
{
SetCapture(hwnd);
m_activeTarget = Target::PANEL;
if (m_panel.OnRButtonDown(x, y))
{
InvalidateRect(hwnd, nullptr, TRUE);
}
ReleaseCapture();
m_activeTarget = Target::NONE;
return 0;
}
// 绘图区右键:弹出该网格的 Y 轴设置菜单(含上下限输入)。
if (m_plot.HitTest(x, y))
{
const int laneIndex = m_plot.LaneIndexAt(x, y);
if (laneIndex >= 0)
{
ShowPlotContextMenu(laneIndex, x, y);
InvalidateRect(hwnd, nullptr, FALSE);
}
return 0;
}
return 0;
}
case WM_RBUTTONUP:
{
ReleaseCapture();
m_activeTarget = Target::NONE;
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 shift = (GET_KEYSTATE_WPARAM(wParam) & MK_SHIFT) != 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, shift);
}
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:
// 标签栏重命名与数值输入优先接管按键(回车提交、Esc 取消、退格删除)。
if (m_tabBar.IsEditing())
{
TabBarResult tabResult;
m_tabBar.OnEditKey(wParam, tabResult);
OnTabBarResult(tabResult);
if (!m_tabBar.IsEditing())
{
// 退出重命名后标签宽度恢复,需要重新排布。
Relayout();
}
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
if (m_panel.IsEditing())
{
if (m_panel.OnEditKey(wParam))
{
InvalidateRect(hwnd, nullptr, FALSE);
}
return 0;
}
if (m_plot.IsEditing())
{
m_plot.OnEditKey(wParam);
ApplyPendingRename();
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
if (HandleKeyDown(wParam))
{
InvalidateRect(hwnd, nullptr, TRUE);
return 0;
}
break;
case WM_CHAR:
{
// 输入状态由对应控件自行校验可接受的字符(数值仅收数字与小数点,名称可收任意可打印字符)。
if (m_tabBar.IsEditing())
{
if (m_tabBar.OnEditChar(static_cast<wchar_t>(wParam)))
{
// 标签宽度随输入内容增长(重命名时上限更宽),需重新排布才能看到完整名称。
if (m_tabBar.IsEditing())
{
Relayout();
}
InvalidateRect(hwnd, nullptr, FALSE);
}
return 0;
}
if (m_panel.IsEditing())
{
if (m_panel.OnEditChar(static_cast<wchar_t>(wParam)))
{
InvalidateRect(hwnd, nullptr, FALSE);
}
return 0;
}
if (m_plot.IsEditing())
{
if (m_plot.OnEditChar(static_cast<wchar_t>(wParam)))
{
ApplyPendingRename();
InvalidateRect(hwnd, nullptr, FALSE);
}
return 0;
}
break;
}
case WM_KILLFOCUS:
// 失焦时提交输入并收起临时界面,避免失去鼠标前景后控件仍处于编辑态。
m_panel.CloseTransientUi();
m_colorPicker.Close();
m_menu.Close();
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
case kMessageRemoveFile:
{
RemoveFile(static_cast<int>(wParam));
return 0;
}
case WM_DROPFILES:
{
HDROP drop = reinterpret_cast<HDROP>(wParam);
const UINT count = DragQueryFileW(drop, 0xFFFFFFFF, nullptr, 0);
int loaded = 0;
for (UINT index = 0; index < count; ++index)
{
const UINT length = DragQueryFileW(drop, index, nullptr, 0);
if (length == 0)
{
continue;
}
std::vector<wchar_t> buffer(static_cast<std::size_t>(length) + 1, L'\0');
if (DragQueryFileW(drop, index, buffer.data(), length + 1) > 0)
{
if (LoadFile(std::wstring(buffer.data())))
{
++loaded;
}
}
}
DragFinish(drop);
if (loaded > 0)
{
SetStatusMessage(L"已加载 " + std::to_wstring(loaded) +
L" 个文件,请在左侧勾选需要查看的信号。",
false);
}
return 0;
}
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
} // namespace jdi