性能(BIG_DATA.csv 128 万采样 × 10 信号全显示,实测): - 折线抽稀改为按像素列取上下包络(min/max),顶点数由固定 20000 降到像素量级; 既保住尖峰(等间隔抽样会整段漏掉),又不再把大量顶点堆在同一像素列上。 - 新增折线顶点缓存:以“构建折线实际读取的全部输入”(信号、时间轴范围、绘图区横向 几何、网格纵向几何)为键,视图未变化时直接复用,重复绘制不再重算全量采样。 - 悬停命中检测先按 x 二分裁剪到容差范围内的一小段(点到线段距离必不小于横向距离), 鼠标移动不再遍历上万个顶点。 - 结果:整帧渲染 1309ms -> 103~127ms(10 网格);折线构建 25ms -> 0ms(缓存命中); 单条折线绘制 1255ms -> 45~53ms;悬停命中 ~0ms。两文件 6 网格场景约 52ms。 - 未引入多线程:耗时集中在 GDI+ 单线程绘制调用与一次性 O(N) 包络扫描, 并行化的收益上限仅约 25ms,不值得引入线程与同步复杂度。 - 空闲(无交互)CPU 占用实测 0.00%,消息循环阻塞在 GetMessage, 无需重绘时不再产生任何计算。 Esc 行为: - 缩放拖拽(时间轴/幅值轴框选)按住左键时按 Esc:立即放弃选区并清空拖拽状态, 随后松开左键也不会把选区应用到视图(已验证:时间轴保持原范围,对照组正常缩放)。 - Esc 不再关闭时间光标(原先 Esc 会调用 DisableCursor);关闭光标只走工具栏「光标」按钮, 避免顺手按 Esc 时意外关掉指针。 - 数据层结构变化(加载/移除文件、切换上下文)后清空折线缓存, 防止旧几何键与新信号重合而取到过期顶点。
1915 lines
50 KiB
C++
1915 lines
50 KiB
C++
#include "Application.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
|
||
#include <commdlg.h>
|
||
#include <shellapi.h>
|
||
#include <windowsx.h>
|
||
|
||
#include "CsvTable.hpp"
|
||
#include "Utf8Util.hpp"
|
||
|
||
#ifndef WM_DPICHANGED
|
||
#define WM_DPICHANGED 0x02E0
|
||
#endif
|
||
|
||
#ifndef DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
|
||
#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 (reinterpret_cast<HANDLE>(-4))
|
||
#endif
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
namespace
|
||
{
|
||
const wchar_t* kWindowClassName = L"JustDataInspectorMainWindow"; /**< 窗口类名。 */
|
||
const wchar_t* kWindowTitle = L"Just DataInspector"; /**< 窗口标题(仅显示应用名称)。 */
|
||
const float 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 开启进程级 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(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);
|
||
|
||
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));
|
||
|
||
// 创建首个标签页。
|
||
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);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// 同一文件允许多次加载:每次都会新建一个独立分组,各自持有独立的采样数据副本,
|
||
// 因此后续对原始文件的修改或删除都不会影响已加载内容。
|
||
CsvTable table;
|
||
std::wstring error;
|
||
if (!LoadCsvTable(path, table, error))
|
||
{
|
||
SetStatusMessage(L"解析失败:" + error, true);
|
||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||
return false;
|
||
}
|
||
|
||
int fileIndex = -1;
|
||
if (!m_dataSet.AddFromTable(table, path, error, fileIndex))
|
||
{
|
||
SetStatusMessage(L"解析失败:" + error, true);
|
||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||
return false;
|
||
}
|
||
|
||
// 新文件的信号在全部标签页中默认均为未勾选状态,由用户自行决定查看内容。
|
||
SyncTabsToDataSet();
|
||
UpdateWindowTitle();
|
||
|
||
const DataFile* file = m_dataSet.FileAt(fileIndex);
|
||
if (file != nullptr)
|
||
{
|
||
std::wstring message = L"已加载 组" + std::to_wstring(fileIndex + 1) + L"|" + file->Title() +
|
||
L"(" + FormatCount(static_cast<unsigned long long>(file->time.size())) +
|
||
L" 行采样," + std::to_wstring(file->signals.size()) +
|
||
L" 条信号,时间列:" + file->timeColumnName + L")";
|
||
if (!file->timeMonotonic)
|
||
{
|
||
message += L" 提示:时间列非单调递增,光标取值使用最近邻策略。";
|
||
}
|
||
SetStatusMessage(message, false);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
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 只用于放弃当前正在进行的拖拽(典型场景:缩放的框选选区还没松开左键)。
|
||
// 取消后不应用选区、视图范围保持不变;松开左键也不会再生效(拖拽模式已复位)。
|
||
// Esc 不再与时间光标关联:关闭光标请用工具栏「光标」按钮或再次点击该按钮,
|
||
// 避免用户按 Esc 时意外把指针关掉。
|
||
return m_plot.CancelDrag();
|
||
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.label = L"Y 轴最大值:" + FormatValue(laneYMax);
|
||
maxItem.id = PLOT_CMD_LANE_Y_MAX;
|
||
items.push_back(maxItem);
|
||
|
||
MenuItem minItem;
|
||
minItem.label = L"Y 轴最小值:" + FormatValue(laneYMin);
|
||
minItem.id = PLOT_CMD_LANE_Y_MIN;
|
||
items.push_back(minItem);
|
||
|
||
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 separator;
|
||
separator.kind = MenuItemKind::SEPARATOR;
|
||
items.push_back(separator);
|
||
|
||
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::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:
|
||
{
|
||
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_Y_MAX:
|
||
m_plot.BeginLaneValueEdit(laneIndex, true, result.itemRect);
|
||
break;
|
||
case PLOT_CMD_LANE_Y_MIN:
|
||
m_plot.BeginLaneValueEdit(laneIndex, false, result.itemRect);
|
||
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.OnMouseMove(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)))
|
||
{
|
||
InvalidateRect(hwnd, nullptr, FALSE);
|
||
}
|
||
return 0;
|
||
case WM_LBUTTONDOWN:
|
||
{
|
||
MenuResult result;
|
||
m_menu.OnLButtonDown(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), result);
|
||
InvalidateRect(hwnd, nullptr, FALSE);
|
||
return 0;
|
||
}
|
||
case WM_LBUTTONUP:
|
||
{
|
||
MenuResult result;
|
||
m_menu.OnLButtonUp(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), result);
|
||
InvalidateRect(hwnd, nullptr, FALSE);
|
||
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
|