网格菜单重构、大文件异步加载与内存/解析优化、修复应用图标
网格右键菜单: - 菜单顺序改为: 网格 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
+353
-3
@@ -21,6 +21,9 @@ namespace jdi
|
||||
const float kCorner = 6.0f; /**< 菜单圆角半径。 */
|
||||
const float kSwatchSize = 22.0f; /**< 色块尺寸。 */
|
||||
const float kSwatchGap = 5.0f; /**< 色块间距。 */
|
||||
const float kEditFieldWidth = 88.0f; /**< EDIT 项输入框宽度。 */
|
||||
const float kEditFieldGap = 6.0f; /**< 说明文字与输入框之间的最小间隔。 */
|
||||
const float kEditFieldPadV = 5.0f; /**< 输入框上下内缩。 */
|
||||
}
|
||||
|
||||
PopupMenu::PopupMenu() = default;
|
||||
@@ -46,6 +49,9 @@ namespace jdi
|
||||
return kHeaderHeight * m_scale;
|
||||
case MenuItemKind::SWATCHES:
|
||||
return kSwatchRowHeight * m_scale;
|
||||
case MenuItemKind::EDIT:
|
||||
// 编辑项比普通项略高,给输入框留出上下呼吸空间。
|
||||
return (kRowHeight + 4.0f) * m_scale;
|
||||
case MenuItemKind::NORMAL:
|
||||
default:
|
||||
return kRowHeight * m_scale;
|
||||
@@ -90,6 +96,17 @@ namespace jdi
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, item.label) + kRightPad * m_scale;
|
||||
contentWidth = std::max(contentWidth, width);
|
||||
}
|
||||
else if (item.kind == MenuItemKind::EDIT)
|
||||
{
|
||||
// 说明文字 + 输入框 + 右侧留白;输入框宽度按数值文本自适应(取一个上限)。
|
||||
const float fieldWidth =
|
||||
std::max(kEditFieldWidth * m_scale,
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, item.value) + 20.0f * m_scale);
|
||||
const float width = (kPadH + 8.0f) * m_scale +
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, item.label) + kEditFieldGap * m_scale +
|
||||
fieldWidth + kRightPad * m_scale;
|
||||
contentWidth = std::max(contentWidth, width);
|
||||
}
|
||||
}
|
||||
|
||||
contentWidth = std::min(contentWidth, kMaxWidth * m_scale);
|
||||
@@ -140,6 +157,211 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
gd::RectF PopupMenu::EditFieldRect(int index) const
|
||||
{
|
||||
if (index < 0 || index >= static_cast<int>(m_items.size()))
|
||||
{
|
||||
return gd::RectF();
|
||||
}
|
||||
if (m_items[static_cast<std::size_t>(index)].kind != MenuItemKind::EDIT)
|
||||
{
|
||||
return gd::RectF();
|
||||
}
|
||||
|
||||
// 输入框靠右对齐:宽度取固定下限与当前文本宽度中的较大者,但不得超过菜单可用宽度的一半。
|
||||
const gd::RectF& rect = m_itemRects[static_cast<std::size_t>(index)];
|
||||
const float maxWidth = std::max(40.0f * m_scale, rect.Width * 0.5f);
|
||||
float width = std::max(kEditFieldWidth * m_scale,
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, m_items[static_cast<std::size_t>(index)].value) +
|
||||
20.0f * m_scale);
|
||||
width = std::min(width, maxWidth);
|
||||
|
||||
const float height = rect.Height - kEditFieldPadV * 2.0f * m_scale;
|
||||
return gd::RectF(rect.X + rect.Width - width - 4.0f * m_scale,
|
||||
rect.Y + (rect.Height - height) * 0.5f, width, height);
|
||||
}
|
||||
|
||||
int PopupMenu::EditingItemId() const
|
||||
{
|
||||
if (m_editIndex < 0 || m_editIndex >= static_cast<int>(m_items.size()))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return m_items[static_cast<std::size_t>(m_editIndex)].id;
|
||||
}
|
||||
|
||||
bool PopupMenu::IsOnEditField(int x, int y) const
|
||||
{
|
||||
if (m_editIndex < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const gd::RectF rect = EditFieldRect(m_editIndex);
|
||||
return UiPointInRect(rect, static_cast<float>(x), static_cast<float>(y));
|
||||
}
|
||||
|
||||
bool PopupMenu::BeginItemEdit(int index)
|
||||
{
|
||||
if (!m_open || index < 0 || index >= static_cast<int>(m_items.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const MenuItem& item = m_items[static_cast<std::size_t>(index)];
|
||||
if (item.kind != MenuItemKind::EDIT || !item.enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 输入框初值取当前显示值,整段选中,便于直接键入新数值。
|
||||
m_edit.Begin(item.value, true, item.filter, 0u);
|
||||
m_editIndex = index;
|
||||
m_hoverIndex = index;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PopupMenu::EndItemEdit(bool commit, MenuResult& result)
|
||||
{
|
||||
if (m_editIndex < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int index = m_editIndex;
|
||||
const std::wstring text = m_edit.Text();
|
||||
m_editIndex = -1;
|
||||
m_edit.End();
|
||||
|
||||
if (!commit || index >= static_cast<int>(m_items.size()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
MenuItem& item = m_items[static_cast<std::size_t>(index)];
|
||||
// 立即回写显示值,使菜单上的数值与数据层一致(宿主会在派发后再纠正为夹取后的真实值)。
|
||||
item.value = text;
|
||||
|
||||
result.valid = true;
|
||||
result.scope = m_scope;
|
||||
result.id = item.id;
|
||||
result.editCommit = true;
|
||||
result.editText = text;
|
||||
return true;
|
||||
}
|
||||
|
||||
void PopupMenu::RebuildLayout()
|
||||
{
|
||||
if (!m_open)
|
||||
{
|
||||
return;
|
||||
}
|
||||
BuildLayout(m_buildBounds, m_buildX, m_buildY);
|
||||
}
|
||||
|
||||
bool PopupMenu::UpdateItemValue(int id, const std::wstring& value)
|
||||
{
|
||||
for (MenuItem& item : m_items)
|
||||
{
|
||||
if (item.id == id && item.kind == MenuItemKind::EDIT)
|
||||
{
|
||||
if (item.value == value)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
item.value = value;
|
||||
RebuildLayout();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PopupMenu::UpdateItemLabel(int id, const std::wstring& label, bool checked, bool enabled)
|
||||
{
|
||||
for (MenuItem& item : m_items)
|
||||
{
|
||||
if (item.id == id && item.kind == MenuItemKind::NORMAL)
|
||||
{
|
||||
if (item.label == label && item.checked == checked && item.enabled == enabled)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
item.label = label;
|
||||
item.checked = checked;
|
||||
item.enabled = enabled;
|
||||
RebuildLayout();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PopupMenu::OnEditMouseDown(int x, int y)
|
||||
{
|
||||
if (m_editIndex < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return UiEditFieldMouseDown(m_fonts->uiSmall, EditFieldRect(m_editIndex), m_edit, m_scale, x, y);
|
||||
}
|
||||
|
||||
bool PopupMenu::OnEditMouseMove(int x)
|
||||
{
|
||||
if (m_editIndex < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return UiEditFieldMouseMove(m_fonts->uiSmall, EditFieldRect(m_editIndex), m_edit, m_scale, x);
|
||||
}
|
||||
|
||||
bool PopupMenu::OnEditMouseUp()
|
||||
{
|
||||
if (!m_edit.Dragging())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_edit.EndDrag();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PopupMenu::OnChar(wchar_t character, MenuResult& result)
|
||||
{
|
||||
result = MenuResult();
|
||||
if (m_editIndex < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 剪贴板与全选快捷键经 WM_CHAR 送达(Ctrl+C/X/V/A 分别对应 3/24/22/1)。
|
||||
switch (character)
|
||||
{
|
||||
case 0x03: // Ctrl+C
|
||||
m_edit.CopyToClipboard(m_host);
|
||||
return false;
|
||||
case 0x18: // Ctrl+X
|
||||
return m_edit.CutToClipboard(m_host);
|
||||
case 0x16: // Ctrl+V
|
||||
return m_edit.PasteFromClipboard(m_host);
|
||||
case 0x01: // Ctrl+A
|
||||
m_edit.SelectAll();
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (character == L'\r' || character == L'\n')
|
||||
{
|
||||
// 回车提交:菜单保持打开,仅把新数值回传宿主。
|
||||
EndItemEdit(true, result);
|
||||
return true;
|
||||
}
|
||||
// 退格/删除已由 WM_KEYDOWN 处理,此处忽略控制字符避免重复删除。
|
||||
if (character < 0x20 || character == 0x7F)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return m_edit.Insert(character);
|
||||
}
|
||||
|
||||
void PopupMenu::Open(MenuScope scope, const gd::RectF& bounds, int x, int y,
|
||||
const std::vector<MenuItem>& items)
|
||||
{
|
||||
@@ -156,7 +378,11 @@ namespace jdi
|
||||
m_hoverSwatch = -1;
|
||||
m_pressedIndex = -1;
|
||||
m_pressedSwatch = -1;
|
||||
m_buildBounds = bounds;
|
||||
m_buildX = x;
|
||||
m_buildY = y;
|
||||
|
||||
CloseEditState();
|
||||
BuildLayout(bounds, x, y);
|
||||
}
|
||||
|
||||
@@ -171,6 +397,16 @@ namespace jdi
|
||||
m_hoverSwatch = -1;
|
||||
m_pressedIndex = -1;
|
||||
m_pressedSwatch = -1;
|
||||
CloseEditState();
|
||||
}
|
||||
|
||||
void PopupMenu::CloseEditState()
|
||||
{
|
||||
if (m_editIndex >= 0)
|
||||
{
|
||||
m_edit.End();
|
||||
}
|
||||
m_editIndex = -1;
|
||||
}
|
||||
|
||||
bool PopupMenu::ContainsPoint(int x, int y) const
|
||||
@@ -251,7 +487,7 @@ namespace jdi
|
||||
}
|
||||
|
||||
const MenuItem& item = m_items[static_cast<std::size_t>(index)];
|
||||
if (item.kind == MenuItemKind::NORMAL && item.enabled)
|
||||
if ((item.kind == MenuItemKind::NORMAL || item.kind == MenuItemKind::EDIT) && item.enabled)
|
||||
{
|
||||
m_hoverIndex = index;
|
||||
m_hoverSwatch = -1;
|
||||
@@ -268,7 +504,18 @@ namespace jdi
|
||||
}
|
||||
|
||||
const MenuItem& item = m_items[static_cast<std::size_t>(m_hoverIndex)];
|
||||
if (item.kind != MenuItemKind::NORMAL || !item.enabled)
|
||||
if (!item.enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 编辑项:回车等价于单击进入编辑,菜单不关闭。
|
||||
if (item.kind == MenuItemKind::EDIT)
|
||||
{
|
||||
BeginItemEdit(m_hoverIndex);
|
||||
return false;
|
||||
}
|
||||
if (item.kind != MenuItemKind::NORMAL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -310,11 +557,26 @@ namespace jdi
|
||||
|
||||
if (!ContainsPoint(x, y))
|
||||
{
|
||||
// 点击菜单外部:关闭菜单,不产生任何命令。
|
||||
// 点击菜单外部:先提交正在编辑的数值,再关闭菜单。
|
||||
EndItemEdit(true, result);
|
||||
Close();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 已经处于编辑状态时:点击当前输入框 → 重新定位插入符;
|
||||
// 点击其它位置 → 先提交本次编辑(菜单不关闭),再按新位置继续处理。
|
||||
if (m_editIndex >= 0)
|
||||
{
|
||||
if (IsOnEditField(x, y))
|
||||
{
|
||||
OnEditMouseDown(x, y);
|
||||
m_pressedIndex = m_editIndex;
|
||||
m_pressedSwatch = -1;
|
||||
return true;
|
||||
}
|
||||
EndItemEdit(true, result);
|
||||
}
|
||||
|
||||
int swatchIndex = -1;
|
||||
m_pressedIndex = ItemAtPoint(x, y, swatchIndex);
|
||||
m_pressedSwatch = swatchIndex;
|
||||
@@ -323,12 +585,23 @@ namespace jdi
|
||||
|
||||
bool PopupMenu::OnLButtonUp(int x, int y, MenuResult& result)
|
||||
{
|
||||
const MenuResult editResult = result;
|
||||
result = MenuResult();
|
||||
if (!m_open)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 编辑态下松开:先结束拖拽框选,不当作普通项点击。
|
||||
if (m_editIndex >= 0)
|
||||
{
|
||||
OnEditMouseUp();
|
||||
m_pressedIndex = -1;
|
||||
m_pressedSwatch = -1;
|
||||
result = editResult;
|
||||
return true;
|
||||
}
|
||||
|
||||
int swatchIndex = -1;
|
||||
const int index = ItemAtPoint(x, y, swatchIndex);
|
||||
const int pressedIndex = m_pressedIndex;
|
||||
@@ -350,6 +623,13 @@ namespace jdi
|
||||
|
||||
const MenuItem& item = m_items[static_cast<std::size_t>(index)];
|
||||
|
||||
// 编辑项:单击进入菜单内联编辑,菜单保持打开。
|
||||
if (item.kind == MenuItemKind::EDIT)
|
||||
{
|
||||
BeginItemEdit(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
result.valid = true;
|
||||
result.scope = m_scope;
|
||||
result.id = item.id;
|
||||
@@ -378,6 +658,42 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
// 编辑态下按键全部交给输入框(回车提交、Esc 取消本次编辑但菜单保持打开)。
|
||||
if (m_editIndex >= 0)
|
||||
{
|
||||
const bool shift = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
|
||||
switch (key)
|
||||
{
|
||||
case VK_ESCAPE:
|
||||
EndItemEdit(false, result);
|
||||
return true;
|
||||
case VK_RETURN:
|
||||
EndItemEdit(true, result);
|
||||
return true;
|
||||
case VK_BACK:
|
||||
m_edit.Backspace();
|
||||
return true;
|
||||
case VK_DELETE:
|
||||
m_edit.DeleteForward();
|
||||
return true;
|
||||
case VK_LEFT:
|
||||
m_edit.MoveCaret(-1, shift);
|
||||
return true;
|
||||
case VK_RIGHT:
|
||||
m_edit.MoveCaret(1, shift);
|
||||
return true;
|
||||
case VK_HOME:
|
||||
m_edit.MoveHome(shift);
|
||||
return true;
|
||||
case VK_END:
|
||||
m_edit.MoveEnd(shift);
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case VK_ESCAPE:
|
||||
@@ -445,6 +761,40 @@ namespace jdi
|
||||
const bool hovered = (static_cast<int>(index) == m_hoverIndex) &&
|
||||
(item.kind != MenuItemKind::SWATCHES || m_hoverSwatch < 0);
|
||||
|
||||
if (item.kind == MenuItemKind::EDIT)
|
||||
{
|
||||
// 编辑项:左侧说明文字 + 右侧内联输入框(与拾色器的 RGB 编辑同一种处理方式,
|
||||
// 菜单始终不关闭,数值就地修改)。
|
||||
if (hovered && item.enabled && static_cast<int>(index) != m_editIndex)
|
||||
{
|
||||
UiFillRoundRect(g, rect, 4.0f * m_scale, WithAlpha(m_theme->accent, 0x22));
|
||||
}
|
||||
|
||||
const std::uint32_t labelColor = item.enabled ? m_theme->text : m_theme->textMuted;
|
||||
const float labelLeft = rect.X + 8.0f * m_scale;
|
||||
const gd::RectF fieldRect = EditFieldRect(static_cast<int>(index));
|
||||
const float labelWidth = std::max(8.0f, fieldRect.X - labelLeft - kEditFieldGap * m_scale);
|
||||
UiText(g, UiEllipsize(g, item.label, m_fonts->uiSmall, labelWidth), m_fonts->uiSmall,
|
||||
gd::RectF(labelLeft, rect.Y, labelWidth, rect.Height), labelColor, TextAlign::LEFT);
|
||||
|
||||
if (static_cast<int>(index) == m_editIndex)
|
||||
{
|
||||
UiDrawEditField(g, fieldRect, m_edit, m_fonts->uiSmall, m_scale);
|
||||
}
|
||||
else
|
||||
{
|
||||
UiFillRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->pressed);
|
||||
UiDrawRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->border, 1.0f);
|
||||
const std::wstring text =
|
||||
UiEllipsize(g, item.value, m_fonts->uiSmall, fieldRect.Width - 12.0f * m_scale);
|
||||
UiText(g, text, m_fonts->uiSmall,
|
||||
gd::RectF(fieldRect.X + 6.0f * m_scale, fieldRect.Y,
|
||||
std::max(8.0f, fieldRect.Width - 12.0f * m_scale), fieldRect.Height),
|
||||
item.enabled ? m_theme->textStrong : m_theme->textMuted, TextAlign::LEFT);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.kind == MenuItemKind::SWATCHES)
|
||||
{
|
||||
const std::vector<gd::RectF>& rects = m_swatchRects[index];
|
||||
|
||||
Reference in new issue
Block a user