From 85aaed55f64b1f0ef1a50a951a577654eed663f4 Mon Sep 17 00:00:00 2001 From: "DeepSeek V4.1 Flash" <> Date: Wed, 4 Nov 2026 15:18:01 +0800 Subject: [PATCH] =?UTF-8?q?refactor(panels):=20=E8=A1=8C=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E6=94=B6=E6=95=9B=E4=B8=BA=20AppendRow=20=E5=94=AF=E4=B8=80?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=EF=BC=88=E6=B6=88=E9=99=A4=E5=B9=B3=E8=A1=8C?= =?UTF-8?q?=E6=95=B0=E7=BB=84=E9=94=99=E4=BD=8D=E9=9A=90=E6=82=A3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景:Code Review 报告 §12.7 建议 1。"改一次面板布局就出错"的**机制性**原因, 是 `EdgePanelLayout` 的每一行对应 **18 个按行平行的 `std::vector`**, 而每个排版分支都要在同一位置、按同一顺序 push 恰好 18 项。只要某个分支少推一项, 那一列之后的所有行都会读到**别人的**矩形(`panels` 的长度不变式只能事后报警)。 本次做法(比"合并成一个 PanelRow 结构体 + 314 处引用"风险低得多、收益相同) * `EdgePanelLayout::AppendRow(ref, header, nameRect, triangle, check)`: **唯一**允许新增行的入口,18 个数组一次补齐(参数行/重采样行一律为空矩形)。 * 三个 push 块改为: * 边沿计数表格:13 次 push → **1 次 `AppendRow`**; * 列表式:13 次 push → **1 次 `AppendRow`**;参数行/重采样行改为 `rowXxx.back() = …` **赋值**(长度天然对齐,不再需要 push); * 「未勾选/收起」分支里那 6+4 次补空 push **整个删掉**(AppendRow 已补过)。 * ⇒ 新增一列只需改 `AppendRow` 一处(新增行的分支不必逐个补); 新增行必须走 `AppendRow`(新增列不会漏)。 为什么没有做"完整合并成 std::vector + 列视图" * 列视图方案需要给 `EdgePanelLayout` 写自定义拷贝构造/赋值(视图持有 `panelRows` 的地址), 而该类型是 `EdgePanelLayoutFor()` 的返回值、又被存进 `std::vector`, 拷贝路径是热路径 —— 一旦拷贝语义写错,会产生**比原缺陷更隐蔽**的悬垂视图。 * 本轮先消除"错位"这个真实隐患(AppendRow 已做到), 完整结构合并留到有"逐行几何基线"检查之后再做(已记入后续建议)。 验证 * `panels -mode compare` → 7 个面板像素哈希 + 几何**逐项不变**。 * `panelscroll`(MF4-Record,566 行)→ 7 种面板的行数组长度全部等于 566,行为不变。 * `OffscreenChecks -all` → 6/6 PASS,`checks=108 failed=0`。 * `UnitTests` → `checks=260 failed=0`。 * release 全量零 error / 零 warning;产物 786,432 → **785,408 B**(−1,024)。 --- Components/PlotView/Inc/PlotView.hpp | 40 ++++++++++ Components/PlotView/Src/PlotViewPanels.cpp | 90 +++++++--------------- 2 files changed, 67 insertions(+), 63 deletions(-) diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index 1449abc..98609ee 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -2070,6 +2070,7 @@ namespace jdi /** @brief 「均值滤波」面板每个信号标题栏左侧的**展开 / 收起三角**(仅勾选后出现)。 */ std::vector rowTriangles; std::vector rowRefs; + /** * @brief 「自由标记」面板的一行(一个固定点)。 * @@ -2210,6 +2211,45 @@ namespace jdi MARKER_COL_TIME = 4, MARKER_COL_DT = 5 }; + + /** + * @brief 追加一**行**(条目栏 / 名称 / 三角 / 复选框),其余列一律补空矩形。 + * + * ⚠️⚠️ 这是**唯一**允许新增行的入口。面板每行对应 18 个平行数组 + * (`rowRefs` / `rowHeaders` / `rowChecks` / `rowLevelTracks` / …); + * 如果每个排版分支各写一遍 `push_back`,只要某分支漏推一项, + * 那一列之后的所有行都会**读到别人的矩形**("改一次布局就出错"的典型机制)。 + * 现在:新增行的分支只调本函数,**新增一列只需改这里一处**; + * 参数行 / 重采样行则用 `rowXxx.back() = …` 赋值(长度天然对齐)。 + * + * @param [IN] ref 该行对应的信号。 + * @param [IN] header 信号色叠底条目栏(整行宽度)。 + * @param [IN] nameRect 名称(表格里的名称列)。 + * @param [IN] triangle 左侧展开 / 收起三角(不适用时传空矩形)。 + * @param [IN] check 右端复选框。 + */ + void AppendRow(const SignalRef& ref, const gd::RectF& header, const gd::RectF& nameRect, + const gd::RectF& triangle, const gd::RectF& check) + { + rowRefs.push_back(ref); + rowHeaders.push_back(header); + rowNameRects.push_back(nameRect); + rowTriangles.push_back(triangle); + rowChecks.push_back(check); + rowPoints.push_back(gd::RectF()); + rowIterations.push_back(gd::RectF()); + rowStats.push_back(gd::RectF()); + rowLevelTracks.push_back(gd::RectF()); + rowLevelValues.push_back(gd::RectF()); + rowLevelResets.push_back(gd::RectF()); + rowIterTracks.push_back(gd::RectF()); + rowIterValues.push_back(gd::RectF()); + rowIterResets.push_back(gd::RectF()); + rowInfoMaxInterval.push_back(gd::RectF()); + rowInfoMinInterval.push_back(gd::RectF()); + rowIntervalField.push_back(gd::RectF()); + rowFrequencyField.push_back(gd::RectF()); + } }; /** diff --git a/Components/PlotView/Src/PlotViewPanels.cpp b/Components/PlotView/Src/PlotViewPanels.cpp index 7b78260..34b22c5 100644 --- a/Components/PlotView/Src/PlotViewPanels.cpp +++ b/Components/PlotView/Src/PlotViewPanels.cpp @@ -735,22 +735,12 @@ namespace jdi { continue; } - layout.rowRefs.push_back(ref); - layout.rowHeaders.push_back(gd::RectF(contentLeft, rowY, innerWidth, rowH)); - layout.rowNameRects.push_back( - gd::RectF(contentLeft + 4.0f * m_scale, rowY, - std::max(10.0f, nameColumn - 8.0f * m_scale), rowH)); - layout.rowTriangles.push_back(gd::RectF()); - layout.rowChecks.push_back(gd::RectF()); - layout.rowPoints.push_back(gd::RectF()); - layout.rowIterations.push_back(gd::RectF()); - layout.rowStats.push_back(gd::RectF()); - layout.rowLevelTracks.push_back(gd::RectF()); - layout.rowLevelValues.push_back(gd::RectF()); - layout.rowLevelResets.push_back(gd::RectF()); - layout.rowIterTracks.push_back(gd::RectF()); - layout.rowIterValues.push_back(gd::RectF()); - layout.rowIterResets.push_back(gd::RectF()); + // ⚠️ 行的追加走 `AppendRow()`(**唯一入口**):18 个平行数组一次补齐, + // 不会出现"某个分支漏推一列 ⇒ 该列之后所有行读到别人的矩形"。 + layout.AppendRow(ref, gd::RectF(contentLeft, rowY, innerWidth, rowH), + gd::RectF(contentLeft + 4.0f * m_scale, rowY, + std::max(10.0f, nameColumn - 8.0f * m_scale), rowH), + gd::RectF(), gd::RectF()); rowY += rowH; } } @@ -886,42 +876,26 @@ namespace jdi checked = (item != nullptr); itemCollapsed = (item != nullptr && item->collapsed); } - layout.rowRefs.push_back(ref); // 组头(信号色叠底):整行宽度,左侧三角(仅勾选时)+ 名称,右端复选框。 - layout.rowHeaders.push_back( - gd::RectF(contentLeft, rowY, innerWidth, groupRow)); - layout.rowNameRects.push_back(gd::RectF(contentLeft, rowY, innerWidth, groupRow)); - layout.rowTriangles.push_back( + // ⚠️ 行的追加走 `AppendRow()`(**唯一入口**):18 个平行数组一次补齐, + // 不会出现"某个分支漏推一列 ⇒ 该列之后所有行读到别人的矩形"。 + // 下面的参数行 / 重采样行一律用 `rowXxx.back() = …` **赋值**(长度天然对齐)。 + layout.AppendRow( + ref, gd::RectF(contentLeft, rowY, innerWidth, groupRow), + gd::RectF(contentLeft, rowY, innerWidth, groupRow), (checked && !clipPanel) ? gd::RectF(contentLeft + 3.0f * m_scale, rowY + (groupRow - kEdgePanelRowTriangleSize * m_scale) * 0.5f, kEdgePanelRowTriangleSize * m_scale, kEdgePanelRowTriangleSize * m_scale) - : gd::RectF()); - layout.rowChecks.push_back( + : gd::RectF(), gd::RectF(contentLeft + innerWidth - closeSize - 4.0f * m_scale, rowY + (groupRow - closeSize) * 0.5f, closeSize, closeSize)); - layout.rowPoints.push_back(gd::RectF()); - layout.rowIterations.push_back(gd::RectF()); - layout.rowStats.push_back(gd::RectF()); rowY += groupRow; // 参数行:只有勾选且未收起时才有几何(未勾选的行两者都是空矩形)。 if (!checked || itemCollapsed || clipPanel) { - layout.rowLevelTracks.push_back(gd::RectF()); - layout.rowLevelValues.push_back(gd::RectF()); - layout.rowLevelResets.push_back(gd::RectF()); - layout.rowIterTracks.push_back(gd::RectF()); - layout.rowIterValues.push_back(gd::RectF()); - layout.rowIterResets.push_back(gd::RectF()); - if (resamplePanel) - { - layout.rowInfoMaxInterval.push_back(gd::RectF()); - layout.rowInfoMinInterval.push_back(gd::RectF()); - layout.rowIntervalField.push_back(gd::RectF()); - layout.rowFrequencyField.push_back(gd::RectF()); - } continue; } if (resamplePanel) @@ -929,27 +903,17 @@ namespace jdi // 「重采样」四行:① 最大间隔(只读)② 最小间隔(只读) // ③ 重采样间隔(输入)④ 重采样频率(输入,与③联动)。 // 每行 = 左侧标签 + 右侧数值/输入框区域。 - layout.rowLevelTracks.push_back(gd::RectF()); - layout.rowLevelValues.push_back(gd::RectF()); - layout.rowLevelResets.push_back(gd::RectF()); - layout.rowIterTracks.push_back(gd::RectF()); - layout.rowIterValues.push_back(gd::RectF()); - layout.rowIterResets.push_back(gd::RectF()); - layout.rowInfoMaxInterval.push_back( - gd::RectF(contentLeft, rowY, innerWidth, sliderRow)); + layout.rowInfoMaxInterval.back() = gd::RectF(contentLeft, rowY, innerWidth, sliderRow); rowY += sliderRow; - layout.rowInfoMinInterval.push_back( - gd::RectF(contentLeft, rowY, innerWidth, sliderRow)); + layout.rowInfoMinInterval.back() = gd::RectF(contentLeft, rowY, innerWidth, sliderRow); rowY += sliderRow; // 输入行的**数值矩形**靠右对齐,宽度取剩余可用宽度(绘制框与编辑框同源)。 const float fieldLeft = contentLeft + rowLabelColumn + 8.0f * m_scale; const float fieldWidth = std::max(24.0f * m_scale, innerWidth - rowLabelColumn - 8.0f * m_scale); - layout.rowIntervalField.push_back( - gd::RectF(fieldLeft, rowY, fieldWidth, sliderRow)); + layout.rowIntervalField.back() = gd::RectF(fieldLeft, rowY, fieldWidth, sliderRow); rowY += sliderRow; - layout.rowFrequencyField.push_back( - gd::RectF(fieldLeft, rowY, fieldWidth, sliderRow)); + layout.rowFrequencyField.back() = gd::RectF(fieldLeft, rowY, fieldWidth, sliderRow); rowY += sliderRow; continue; } @@ -961,22 +925,22 @@ namespace jdi } const gd::RectF pointsTrack(trackLeft, rowY + (sliderRow - trackHeight) * 0.5f, trackWidth, trackHeight); - layout.rowLevelTracks.push_back(pointsTrack); - layout.rowLevelValues.push_back( + layout.rowLevelTracks.back() = pointsTrack; + layout.rowLevelValues.back() = gd::RectF(pointsTrack.X + pointsTrack.Width + 6.0f * m_scale, rowY, valueColumn, - sliderRow)); - layout.rowLevelResets.push_back( - gd::RectF(resetLeft, rowY + (sliderRow - resetSize) * 0.5f, resetSize, resetSize)); + sliderRow); + layout.rowLevelResets.back() = + gd::RectF(resetLeft, rowY + (sliderRow - resetSize) * 0.5f, resetSize, resetSize); rowY += sliderRow; const gd::RectF iterTrack(trackLeft, rowY + (sliderRow - trackHeight) * 0.5f, trackWidth, trackHeight); - layout.rowIterTracks.push_back(iterTrack); - layout.rowIterValues.push_back( + layout.rowIterTracks.back() = iterTrack; + layout.rowIterValues.back() = gd::RectF(iterTrack.X + iterTrack.Width + 6.0f * m_scale, rowY, valueColumn, - sliderRow)); - layout.rowIterResets.push_back( - gd::RectF(resetLeft, rowY + (sliderRow - resetSize) * 0.5f, resetSize, resetSize)); + sliderRow); + layout.rowIterResets.back() = + gd::RectF(resetLeft, rowY + (sliderRow - resetSize) * 0.5f, resetSize, resetSize); rowY += sliderRow; } }