网格菜单重构、大文件异步加载与内存/解析优化、修复应用图标
网格右键菜单: - 菜单顺序改为: 网格 N / Y 轴最大值 / Y 轴最小值 / 选区 Y 轴自适应 / --分隔线-- / Y 轴自适应 / 时间轴自适应 / 全部自适应 (恢复自适应移到分隔线以下、时间轴自适应上方) - Y 轴最大值/最小值改为菜单内联输入框:点击就地编辑、回车提交,菜单不关闭, 处理方式与拾色器的 RGB 内联编辑一致;提交后按真实夹取结果回写菜单显示值, 并同步刷新自适应项的文案/对勾/可用状态 - 新增"选区 Y 轴自适应":只按当前显示时间段内的数据做幅值自适应 (时间单调时二分定位区间,非单调时全扫并按区间过滤) 大文件加载: - 加载改为工作线程执行(解析 + 按列重组),主线程抽送消息并播放过渡动画 (旋转弧线 + 进度条 + 文件名 + 百分比 + 按 Esc 取消),界面不再整块卡死 - 加载完成后主线程只做一次 O(1) 的移动入队,不会产生可见卡顿 - 新增 DataModel::BuildDataFile / DataSet::AddPreparedFile:不依赖 DataSet, 因此可以安全地在线程上运行 内存与解析效率: - CsvTable 由"行优先二维数组"改为"列优先":百万行文件原先会产生上百万次独立堆分配 (每行还有 vector 控制块与容量对齐开销),现在每列一整块连续内存 - 解析改为流式分块读取(4MB),不再把整个文件读入内存,也没有体积上限 (原先超过 512MB 直接拒绝);实测 51MB 文件峰值内存约 126MB(数据本身 113MB) - BuildDataFile 直接把整列移动进 SignalData(零拷贝),不再逐元素复制 - TryParseDouble 增加十进制快速路径:ucrt strtod 实测约 335ns/次,手写路径约 13ns (快 25 倍);非十进制写法(十六进制浮点、inf/nan、带尾部杂字符)原样交回 strtod, 输入接受语义完全不变(实测与 strtod 的差异不超过 1 ULP) → 640MB 测试文件加载时间由约 40s 降到 2s 以内 交互流畅性: - 拖拽(平移/框选/拖动信号)期间关闭波形抗锯齿并压缩顶点预算(2 像素一列包络), 松开后立即恢复全质量绘制;实测 BIG_DATA 10 信号全显示的帧耗时由 105~168ms 降到 75~90ms(其中折线绘制 55ms→23ms) - 折线包络抽稀改为"列边界单调推进"的二分,避免每列在全区间二分造成缓存缺失 - 未对波形构建引入多线程:构建已由几何键缓存消除,且拖拽降级后仅约 1~2ms/帧, 并行化收益不足以抵消线程与同步的复杂度(加载路径的多线程收益明确,故只在该处引入) 图标: - 窗口类与 WM_SETICON 原先使用系统默认图标 IDI_APPLICATION,导致标题栏、 任务栏与 Alt+Tab 缩略图都显示成通用应用图标;改为按 SM_CXICON/SM_CXSMICON 从应用的 IDI_APP_ICON 资源加载(多尺寸 ICO 会挑选最接近的一帧) 其他: - PopupMenu 新增 EDIT 项类型(菜单内联输入框)与 UpdateItemValue/UpdateItemLabel
This commit is contained in:
1 parent
d6a81839bb
commit
45b44eae57
11 files changed
+1637
-222
No files matched your search
@@ -202,6 +202,12 @@ namespace jdi
|
||||
*/
|
||||
void ShowPlotContextMenu(int laneIndex, int x, int y);
|
||||
|
||||
/**
|
||||
* @brief 把网格菜单上的 Y 轴上下限显示值刷新为数据层的真实值(菜单保持打开)。
|
||||
* @param [IN] laneIndex 网格下标。
|
||||
*/
|
||||
void RefreshPlotMenuValues(int laneIndex);
|
||||
|
||||
/**
|
||||
* @brief 派发弹出菜单的选择结果。
|
||||
* @param [IN] result 菜单选择结果。
|
||||
@@ -264,6 +270,28 @@ namespace jdi
|
||||
*/
|
||||
void LoadCommandLineFiles();
|
||||
|
||||
/**
|
||||
* @brief 在后台线程解析并构建数据文件,同时播放加载过渡动画(界面不卡死)。
|
||||
*
|
||||
* 大文件的瓶颈在“解析 CSV”与“按列重组采样数据”,这两步都不依赖 DataSet,
|
||||
* 因此放在工作线程执行;主线程循环抽送消息并重绘加载遮罩层,
|
||||
* 直到后台完成后再把结果移动进 DataSet(O(1),不会引起卡顿)。
|
||||
*
|
||||
* @param [IN] path 文件路径。
|
||||
* @param [OUT] file 构建好的数据文件。
|
||||
* @param [OUT] error 失败原因描述。
|
||||
* @return 加载成功返回 true;用户取消或失败返回 false。
|
||||
*/
|
||||
bool LoadFileAsync(const std::wstring& path, DataFile& file, std::wstring& error);
|
||||
|
||||
/**
|
||||
* @brief 绘制加载过渡动画遮罩(在 Render 的最后一步调用,覆盖整个客户区)。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] width 客户区宽度。
|
||||
* @param [IN] height 客户区高度。
|
||||
*/
|
||||
void PaintLoadingOverlay(gd::Graphics& g, int width, int height);
|
||||
|
||||
/**
|
||||
* @brief 处理客户区鼠标移动。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
@@ -317,6 +345,12 @@ namespace jdi
|
||||
int m_cachedVisibleCount = -1; /**< 上次同步时的可见信号数量。 */
|
||||
std::size_t m_cachedLaneCount = 0; /**< 上次同步时的网格数量。 */
|
||||
bool m_cachedAnyVisible = false; /**< 上次同步时是否存在可见信号。 */
|
||||
|
||||
bool m_loading = false; /**< 是否正在显示加载过渡动画。 */
|
||||
std::wstring m_loadingText; /**< 加载遮罩显示的文件名。 */
|
||||
float m_loadingProgress = 0.0f; /**< 加载进度(0-1)。 */
|
||||
float m_loadingSpin = 0.0f; /**< 加载动画旋转相位(度)。 */
|
||||
unsigned long long m_loadingBytes = 0; /**< 当前文件字节数(用于展示体积)。 */
|
||||
};
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
#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
|
||||
@@ -37,6 +40,87 @@ namespace jdi
|
||||
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)。
|
||||
*/
|
||||
@@ -159,8 +243,10 @@ namespace jdi
|
||||
windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||
windowClass.hbrBackground = nullptr;
|
||||
windowClass.lpszClassName = kWindowClassName;
|
||||
windowClass.hIcon = LoadIconW(nullptr, IDI_APPLICATION);
|
||||
windowClass.hIconSm = windowClass.hIcon;
|
||||
// 使用应用自己的图标资源(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)
|
||||
{
|
||||
@@ -204,10 +290,19 @@ namespace jdi
|
||||
m_plot.Setup(&m_fonts, &m_theme);
|
||||
m_plot.SetScale(m_scale);
|
||||
m_plot.SetHostWindow(m_hwnd);
|
||||
m_menu.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));
|
||||
// 标题栏小图标与任务栏/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();
|
||||
@@ -607,6 +702,9 @@ namespace jdi
|
||||
// 弹出菜单与浮动拾色器始终绘制在最上层,保证与主界面风格一致。
|
||||
m_menu.Paint(g);
|
||||
m_colorPicker.Paint(g);
|
||||
|
||||
// 加载遮罩层在一切内容之上(包括菜单与拾色器),形成模态观感。
|
||||
PaintLoadingOverlay(g, width, height);
|
||||
}
|
||||
|
||||
HDC windowDc = GetDC(m_hwnd);
|
||||
@@ -694,21 +792,24 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
// 同一文件允许多次加载:每次都会新建一个独立分组,各自持有独立的采样数据副本,
|
||||
// 因此后续对原始文件的修改或删除都不会影响已加载内容。
|
||||
CsvTable table;
|
||||
// 大文件在工作线程解析并构建,主线程播放加载过渡动画,界面不再整块卡住。
|
||||
DataFile file;
|
||||
std::wstring error;
|
||||
if (!LoadCsvTable(path, table, error))
|
||||
if (!LoadFileAsync(path, file, error))
|
||||
{
|
||||
SetStatusMessage(L"解析失败:" + error, true);
|
||||
if (!error.empty())
|
||||
{
|
||||
SetStatusMessage(L"解析失败:" + error, true);
|
||||
}
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
return false;
|
||||
}
|
||||
|
||||
int fileIndex = -1;
|
||||
if (!m_dataSet.AddFromTable(table, path, error, fileIndex))
|
||||
// 此处只做一次指针/缓冲移动,不拷贝任何采样数据,因此不会造成可见卡顿。
|
||||
if (!m_dataSet.AddPreparedFile(std::move(file), fileIndex))
|
||||
{
|
||||
SetStatusMessage(L"解析失败:" + error, true);
|
||||
SetStatusMessage(L"解析失败:数据为空。", true);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
return false;
|
||||
}
|
||||
@@ -717,14 +818,14 @@ namespace jdi
|
||||
SyncTabsToDataSet();
|
||||
UpdateWindowTitle();
|
||||
|
||||
const DataFile* file = m_dataSet.FileAt(fileIndex);
|
||||
if (file != nullptr)
|
||||
const DataFile* loaded = m_dataSet.FileAt(fileIndex);
|
||||
if (loaded != 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)
|
||||
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" 提示:时间列非单调递增,光标取值使用最近邻策略。";
|
||||
}
|
||||
@@ -733,6 +834,148 @@ namespace jdi
|
||||
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;
|
||||
@@ -1127,21 +1370,38 @@ namespace jdi
|
||||
|
||||
const bool isAuto = m_plot.LaneRangeIsAuto(laneIndex);
|
||||
|
||||
// 当前网格的 Y 轴上下限值直接在菜单项中显示,单击即可直接改写。
|
||||
// 当前网格的 Y 轴上下限值直接在菜单内的输入框里显示,单击即可就地编辑(菜单不关闭)。
|
||||
double laneYMin = 0.0;
|
||||
double laneYMax = 1.0;
|
||||
m_plot.LaneRange(laneIndex, laneYMin, laneYMax);
|
||||
|
||||
MenuItem maxItem;
|
||||
maxItem.label = L"Y 轴最大值:" + FormatValue(laneYMax);
|
||||
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.label = L"Y 轴最小值:" + FormatValue(laneYMin);
|
||||
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;
|
||||
@@ -1149,10 +1409,6 @@ namespace jdi
|
||||
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;
|
||||
@@ -1171,6 +1427,25 @@ namespace jdi
|
||||
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)
|
||||
@@ -1192,6 +1467,21 @@ namespace jdi
|
||||
}
|
||||
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)
|
||||
@@ -1199,11 +1489,8 @@ namespace jdi
|
||||
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);
|
||||
case PLOT_CMD_LANE_AUTO_VIEW:
|
||||
m_plot.AutoRangeLaneToView(laneIndex);
|
||||
break;
|
||||
case PLOT_CMD_LANE_FIT_TIME:
|
||||
m_plot.FitTimeToLane(laneIndex);
|
||||
@@ -1267,6 +1554,15 @@ namespace jdi
|
||||
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);
|
||||
@@ -1274,19 +1570,60 @@ namespace jdi
|
||||
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(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), 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:
|
||||
|
||||
Reference in new issue
Block a user