Files
Just-DataInspector/Components/Application/Src/ApplicationMenus.cpp
T
DeepSeek V4.1 Flash 34365b3c1c perf(size): 用自写整数格式化替换全部 std::to_wstring / std::to_string
背景:Code Review 报告 §10.3 / §11.5 建议「替换 std::to_wstring / std::to_string,
可去掉 gdtoa 链(≈33 KB)」。本轮把它做完,并给出这条建议的**真实边界**。

一、新增 `Utf8Util::FormatInteger*`(手写十进制展开,不经 printf)
* `FormatInteger(long long)` / `FormatIntegerUnsigned(unsigned long long)` /
  `FormatIntegerNarrow(long long)`(后者供 MatLoader 拼 HDF5 成员路径)。
* 语义与 `std::to_wstring` / `std::to_string` 完全一致:`0` → `"0"`、负数带 `-`、
  `LLONG_MIN` 用模 2^64 语义取绝对值(不会溢出)。
* 这样做的直接理由:预编译 libstdc++ 的 `to_string` 内部转调 `__mingw_swprintf`,
  工程里只要留一处,MinGW 的 printf 引擎(`mingw_wpformat.o`)+ gdtoa 就必然被链入。

二、全仓替换(76 处 `to_wstring` + 2 处 `to_string`,20 个文件)
* `MatLoader.cpp` 的 `TrimNumber` 整数分支也改为 `FormatInteger`(它曾是产物里
  **最后一个**引用 MinGW printf 的调用点)。
* 需要的地方补 `#include "Utf8Util.hpp"`(仅 `MatLoader.cpp` 一个新 include,其余由
  `PlotViewInternal` / `SignalPanelInternal` / `ApplicationInternal` / `DataModelInternal`
  已经包含)。

三、⚠️ 实测结论:gdtoa **去不掉**(把这件事彻底查清并写进 Makefile 注释)
* 只替换 `to_wstring`/`to_string`:**−1,024 B**(787,968 → 786,944)。
* 再叠加 `-D__USE_MINGW_ANSI_STDIO=0`(把 `swprintf` 让给系统 UCRT):**0 字节**。
* 根因(定位到文件与行):libstdc++ 的
  `lib/gcc/.../include/c++/x86_64-w64-mingw32/bits/os_defines.h:48-51` **无条件**执行
  `#undef __USE_MINGW_ANSI_STDIO` + `#define __USE_MINGW_ANSI_STDIO 1`
  (注释:libstdc++ depends on POSIX-definitions of those functions)。
  `g++ -E -dM` 实测最终值恒为 1 ⇒ **任何 C++ TU 的 `swprintf` / `strtod` 都是 MinGW 版本**,
  命令行无法覆盖。
* 要真省这 33 KB 只能自己实现「浮点 → 十进制(正确舍入)」与「浮点解析的
  十六进制/inf/nan 分支」,等价于重写 gdtoa。**判定:不做**(风险 >> 收益)。
* 该开关已回退,Makefile 里保留一段说明,避免后续接手者重复这两次实验。

四、验证
* 新增单元测试 `integer_replacement_matches_std`:12 个有符号样本(含 LLONG_MIN/MAX)
  + 5 个无符号样本(含 ULLONG_MAX),与 `std::to_wstring` **逐字符对照**,
  并校验窄/宽版本内容一致。
* `UnitTests`:`checks=260 failed=0 cases=19`(新增 41 条断言)。
* `OffscreenChecks -all`:6 个检查全 PASS,`checks=87 failed=0`。
* `panels -mode compare`:7 个面板像素哈希与几何**逐项不变**
  (整数文本从 `to_wstring` 换到手写实现后,渲染结果零差异 —— 这是最强的语义证据)。
* release 全量零 error / 零 warning;产物 787,968 → **786,944 B**。
2026-11-04 14:31:02 +08:00

410 lines
15 KiB
C++

// Application 实现分片 —— **菜单**:绘图区右键菜单(含「移动到…」子菜单)、
// 菜单数值回写、主题切换、设置菜单,以及菜单结果的统一派发。
//
// 共用的常量与工具函数在 ApplicationInternal.hpp(命名空间 `appdetail`,全部 inline)。
// 本文件由 2026-11-04 代码审查从 `ApplicationCommands.cpp`(原 2,165 行)按职责拆出,
// **纯搬移、零逻辑改动**。分片划分见 `Documents/Code_Review_Plan.md` 的 P11。
#include "ApplicationInternal.hpp"
namespace jdi
{
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"网格 " + FormatInteger(laneIndex + 1);
items.push_back(header);
// 当前网格的 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);
// 选区自适应(切换项):勾选后每次本网格的时间轴选区放大都会自动匹配 Y 轴幅值。
MenuItem autoViewItem;
autoViewItem.label = L"选区 Y 轴自适应";
autoViewItem.id = PLOT_CMD_LANE_AUTO_VIEW;
autoViewItem.checked = m_plot.LaneAutoFitToSelection(laneIndex);
items.push_back(autoViewItem);
// 时间轴自适应到本网格信号范围(原先位于分隔线下方,现移到分隔线上方)。
MenuItem fitLaneTime;
fitLaneTime.label = L"按网格时间轴展开";
fitLaneTime.id = PLOT_CMD_LANE_FIT_TIME;
items.push_back(fitLaneTime);
// 「拆分当前网格」:把该网格内的信号按个拆成独立网格,插到其下方依次排列。
// 可点击时在后方追加「(N个信号)」说明将拆出的网格数;只有 1 条信号时置灰且不加尾缀。
MenuItem splitLane;
const std::size_t laneSignalCount = m_plot.LaneSignalCount(laneIndex);
splitLane.label = (laneSignalCount > 1)
? (L"拆分当前网格(" + FormatInteger(laneSignalCount) + L"个信号)")
: L"拆分当前网格";
splitLane.id = PLOT_CMD_LANE_SPLIT;
splitLane.enabled = laneSignalCount > 1;
items.push_back(splitLane);
// 实验性功能(二级菜单):横线上方,鼠标悬停即向右展开。
MenuItem experimental; experimental.kind = MenuItemKind::SUBMENU;
experimental.label = L"实验性功能";
{
MenuItem edgeCount;
edgeCount.label = L"边沿计数";
edgeCount.id = PLOT_CMD_LANE_EDGE_COUNT;
edgeCount.checked = m_plot.LaneEdgeCount(laneIndex);
experimental.submenu.push_back(edgeCount);
MenuItem edgeCenter;
edgeCenter.label = L"边沿计数(中心轴)";
edgeCenter.id = PLOT_CMD_LANE_EDGE_CENTER;
edgeCenter.checked = m_plot.LaneEdgeCenterCount(laneIndex);
experimental.submenu.push_back(edgeCenter);
MenuItem meanFilter;
meanFilter.label = L"均值滤波";
meanFilter.id = PLOT_CMD_LANE_MEAN_FILTER;
meanFilter.checked = m_plot.LaneMeanFilter(laneIndex);
experimental.submenu.push_back(meanFilter);
MenuItem weakPeak;
weakPeak.label = L"弱峰调整";
weakPeak.id = PLOT_CMD_LANE_WEAK_PEAK;
weakPeak.checked = m_plot.LaneWeakPeak(laneIndex);
experimental.submenu.push_back(weakPeak);
MenuItem freeMarker;
freeMarker.label = L"自由标记";
freeMarker.id = PLOT_CMD_LANE_FREE_MARKER;
freeMarker.checked = m_plot.LaneFreeMarker(laneIndex);
// 「重采样」位于「自由标记」**上方**(用户 2026-11-02 要求)。
MenuItem resample;
resample.label = L"重采样";
resample.id = PLOT_CMD_LANE_RESAMPLE;
resample.checked = m_plot.LaneResample(laneIndex);
experimental.submenu.push_back(resample);
// 「选区截取」位于「重采样」下方、「自由标记」上方(用户 2026-11-03 要求)。
MenuItem clip;
clip.label = L"选区截取";
clip.id = PLOT_CMD_LANE_CLIP;
clip.checked = m_plot.LaneClip(laneIndex);
experimental.submenu.push_back(clip);
experimental.submenu.push_back(freeMarker);
}
items.push_back(experimental);
MenuItem separator;
separator.kind = MenuItemKind::SEPARATOR;
items.push_back(separator);
// 全局开关:一次性打开/关闭全部网格各自的「选区 Y 轴自适应」。
MenuItem autoFitAllItem;
autoFitAllItem.label = L"选区 Y 轴自适应(全局)";
autoFitAllItem.id = PLOT_CMD_AUTO_FIT_ALL;
autoFitAllItem.checked = m_plot.AllLanesAutoFitToSelection();
items.push_back(autoFitAllItem);
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));
// 「选区 Y 轴自适应」(本网格)与「(全局)」都是切换项,实时反映各自的勾选状态。
m_menu.UpdateItemLabel(PLOT_CMD_LANE_AUTO_VIEW, L"选区 Y 轴自适应",
m_plot.LaneAutoFitToSelection(laneIndex), true);
m_menu.UpdateItemLabel(PLOT_CMD_AUTO_FIT_ALL, L"选区 Y 轴自适应(全局)",
m_plot.AllLanesAutoFitToSelection(), true);
}
void Application::ApplyTheme(ThemeKind kind)
{
if (kind == m_themeKind && m_theme.windowBg == ThemeFor(kind).windowBg)
{
return; // 幂等:同一个主题重复点击不必作废缓存
}
m_themeKind = kind;
m_theme = ThemeFor(kind);
SetCurrentTheme(&m_theme);
// 主题色烘进了多处位图缓存,必须整体作废(只作废场景缓存是不够的:
// 宽基底图层、装饰层、浮动面板层、信号列表行位图里都存着旧配色的像素)。
m_plot.OnThemeChanged();
m_panel.OnThemeChanged();
// 装饰区(标签栏 / 工具栏 / 面板)全部重画,并整窗回送。
m_chromeDirty = true;
m_panelDirty = true;
InvalidateRect(m_hwnd, nullptr, TRUE);
}
void Application::OpenSettingsMenu()
{
// 锚点取齿轮按钮的**实际矩形**:按钮宽度由控件实测,宿主不应自己推算几何。
gd::RectF anchor;
if (!m_toolbar.ButtonRect(ToolbarAction::SETTINGS, anchor))
{
// 尚未布局(极端情形):退化为从菜单快捷键位置展开,而不是什么都不发生。
anchor = m_toolbarRect;
}
std::vector<MenuItem> items;
// ── 调试 ──────────────────────────────────────────────
MenuItem debugHeader;
debugHeader.kind = MenuItemKind::HEADER;
debugHeader.label = L"调试";
items.push_back(debugHeader);
MenuItem boundsItem;
boundsItem.label = L"显示布局边界";
boundsItem.id = kAppCmdShowLayoutBounds;
boundsItem.checked = m_showLayoutBounds;
// 切换项:点击后菜单保持打开,勾选态由宿主回写(与既有「镜像」等切换项一致)。
boundsItem.keepOpen = true;
items.push_back(boundsItem);
// ── 主题配色(二级菜单:鼠标悬停展开,带勾选态)──────────────
// 顺序固定为 Light / Solarised Light / Dark / Solarised Dark(用户 2026-10-29 指定)。
// `keepOpen` 让点击主题后菜单不关闭,勾选态由宿主回写 —— 便于连续预览几种配色。
MenuItem themeItem;
themeItem.kind = MenuItemKind::SUBMENU;
themeItem.label = L"主题配色";
for (int index = 0; index <= static_cast<int>(ThemeKind::SOLARIZED_DARK); ++index)
{
const ThemeKind kind = static_cast<ThemeKind>(index);
MenuItem option;
option.kind = MenuItemKind::NORMAL;
option.label = ThemeName(kind);
option.id = kAppCmdThemeBase + index;
option.checked = (m_themeKind == kind);
option.keepOpen = true;
themeItem.submenu.push_back(option);
}
items.push_back(themeItem);
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));
// 右下展开,锚在按钮**下沿**(留 4 逻辑像素,视觉上不贴着按钮边框)。
const int x = static_cast<int>(anchor.X);
const int y = static_cast<int>(anchor.Y + anchor.Height + 4.0f * m_scale);
m_menu.Open(MenuScope::APP_SETTINGS, bounds, x, y, items);
}
void Application::DispatchMenuResult(const MenuResult& result)
{
if (!result.valid)
{
return;
}
switch (result.scope)
{
case MenuScope::APP_SETTINGS:
{
// 应用设置菜单:全部条目都是**带勾选态的切换项**,菜单保持打开,
// 勾选变化通过 `UpdateItemLabel` 立即回写(连续切换多项时不用反复打开菜单)。
if (result.id == kAppCmdShowLayoutBounds)
{
m_showLayoutBounds = !m_showLayoutBounds;
m_menu.UpdateItemLabel(kAppCmdShowLayoutBounds, L"显示布局边界", m_showLayoutBounds);
// 边界比组件矩形外扩 1px 且跨越所有装饰区 ⇒ 必须整帧重绘。
m_chromeDirty = true;
}
else if (result.id >= kAppCmdThemeBase &&
result.id <= kAppCmdThemeBase + static_cast<int>(ThemeKind::SOLARIZED_DARK))
{
const ThemeKind kind = static_cast<ThemeKind>(result.id - kAppCmdThemeBase);
ApplyTheme(kind);
// 回写勾选态:四项里只有当前主题打勾(子菜单项要用专用接口才会重建布局)。
for (int index = 0; index <= static_cast<int>(ThemeKind::SOLARIZED_DARK); ++index)
{
const ThemeKind option = static_cast<ThemeKind>(index);
m_menu.UpdateSubmenuItemLabel(kAppCmdThemeBase + index, ThemeName(option),
m_themeKind == option);
}
}
InvalidateRect(m_hwnd, nullptr, TRUE);
break;
}
case MenuScope::SIGNAL_PANEL:
{
const bool needRelayout = m_panel.OnMenuCommand(result);
// 「公式映射」等就地输入可能产生**非模态**警告(不使用 Windows 消息框),
// 由绘图区底部的自绘提示浮层显示(与加载失败提示同一套)。
{
std::wstring hint;
if (m_panel.TakePendingHint(hint))
{
m_plot.ShowPersistentHint(hint);
}
}
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_VIEW:
// 切换本网格的「选区 Y 轴自适应」。
m_plot.ToggleLaneAutoFitToSelection(laneIndex);
break;
case PLOT_CMD_AUTO_FIT_ALL:
{
// 全局切换:一次打开/关闭全部网格各自的开关;打开时立即生效一次。
const bool enable = !m_plot.AllLanesAutoFitToSelection();
m_plot.SetAllLanesAutoFitToSelection(enable, true);
break;
}
case PLOT_CMD_LANE_FIT_TIME:
// 「按网格时间轴展开」:展开时间轴后同时把该网格的 Y 轴自适应一次。
m_plot.FitTimeToLane(laneIndex);
break;
case PLOT_CMD_LANE_EDGE_COUNT:
// 「实验性功能 → 边沿计数」:切换本网格的横向光标与统计面板。
m_plot.ToggleLaneEdgeCount(laneIndex);
break;
case PLOT_CMD_LANE_EDGE_CENTER:
// 「实验性功能 → 边沿计数(中心轴)」:切换中心平均值曲线与统计面板。
m_plot.ToggleLaneEdgeCenterCount(laneIndex);
break;
case PLOT_CMD_LANE_MEAN_FILTER:
// 「实验性功能 → 均值滤波」:切换均值滤波曲线与参数面板。
m_plot.ToggleLaneMeanFilter(laneIndex);
break;
case PLOT_CMD_LANE_WEAK_PEAK:
// 「实验性功能 → 弱峰调整」:切换弱峰调整曲线与参数面板。
m_plot.ToggleLaneWeakPeak(laneIndex);
break;
case PLOT_CMD_LANE_FREE_MARKER:
// 「实验性功能 → 自由标记」:切换网格内跟随鼠标的纵向光标与固定数据点面板。
m_plot.ToggleLaneFreeMarker(laneIndex);
break;
case PLOT_CMD_LANE_RESAMPLE:
// 「实验性功能 → 重采样」:切换重采样参数面板。
m_plot.ToggleLaneResample(laneIndex);
break;
case PLOT_CMD_LANE_CLIP:
// 「实验性功能 → 选区截取」:切换选区截取面板。
m_plot.ToggleLaneClip(laneIndex);
break;
case PLOT_CMD_LANE_SPLIT:
// 「拆分当前网格」:信号按个拆成独立网格,新网格顺次插入其下方。
m_plot.SplitLane(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(true);
}
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;
}
}
} // namespace jdi