UI_Refactor
main
去除手写渲染链路,参考了一些浏览器控件渲染的机制,对元素进行基类+派生的形式,这样只需要维护一套共通的消息处理机制,并且可以采用控件树和脏矩形渲染机制,之前的手写渲染链路容易造成很多BUG,而Agent的截图后验机制可能查不出来,这次架构上的变动有助于从源头杜绝这类BUG;
为应用添加了一个设置菜单,现在只放置了一个“显示布局边界”的调试功能,这个功能参考了安卓设备的开发者调试选项,目的是用于动画和布局调试,顺便给Agent的视觉模块提供参考;
新增 Components/Shared 下的 UI 基础层: - UiLayout:矩形原语 / 对齐 / 设备像素内侧对齐 / Stack / Flow / 布局缓存(deque 保证引用稳定、 定向失效、按最久未命中淘汰)。 - UiWidget:控件树基类。两段式布局(Measure 自下而上带缓存、Arrange 自上而下)、 脏区下探绘制(dirty 为空=全画;与子树矩形无交集=整棵跳过;有效裁剪=祖先裁剪切∩自身∩脏区)、 只沿命中链派发的冒泡模型(绝不分发给同层兄弟)、capture/modal/hover/focus 仲裁、 光标认领链、统一动画 tick、内容指纹入口。 新增 Tools/uicheck 离屏回归网(62 项断言,全程不创建窗口、不注入输入): - 布局数学:对齐/STRETCH/超尺寸夹取、Stack 固定与自动均分且绝不产生负尺寸、Flow 换行与 overflow。 - 缓存:引用稳定、定向失效、容量淘汰。 - 绘制:全画 / 子树跳过 / 裁剪收窄 / 不可见跳过 / 失效冒泡。 - 派发:重叠子节点只投最上层、hit-transparent 穿透、消费不上抛、capture 越过边界仍投递、 仅持有者可释放、modal 屏蔽其余子树、祖先兜底、光标由最上层认领、键盘沿焦点链。 - 动画:时间戳统一下发、活跃状态汇总。 过程中修正自身两处缺陷:补齐 AdvanceAnimationSelf/AnimationActiveSelf 声明; 把"遍历同层兄弟"改为"只沿命中链派发 + 祖先兜底"(否则一次点击可能被视觉上位于下方的控件也处理)。 验证:make release TARGET_FILE_NAME=TargetCheck 零警告(873,984 B,低于 900KB 阈值); Tools/uicheck 62 passed / 0 failed。 本提交不改动任何现有组件,Toolbar/TabBar/SignalPanel/PlotView 仍走旧路径,产品像素零变化。 文档:Documents/UI_Refactor_Plan.md(蓝图)、Documents/UI_Refactor_Progress.md(台账)。
PlotViewPaint.cpp 9,080 行里混住四类内容(真绘制 / 光栅化缓存 / 内容指纹 / 布局与命中几何), 按"函数名前缀"分片是上一轮膨胀的根因。本提交按关注点机械搬移函数定义,不改一行逻辑: - 新增 PlotViewRaster.cpp(1,876 行):光晕离屏层、静态场景缓存、装饰层(AlphaBlend)、 每网格宽基底图层/并行重建/滚动补条/预热预算。 - 新增 PlotViewCacheKey.cpp(474 行):SceneCacheKey、SceneContentHash/Geometry/Decor、 样式与派生快照、只改外观的网格定位、样式补丁帧。 - PlotViewPaint.cpp 只留绘制:9,080 -> 6,252 行(-31%)。 - 顶层注释从「四个分片」改为「六个分片(按关注点)」,并保留 「PlotView::Paint/PaintLane 只允许定义在 PlotViewPaint.cpp」。 pvdetail 全部 inline,故跨分片安全;无需改任何 include,Makefile 按 basename 自动收集。 验证: - make release TARGET_FILE_NAME=TargetCheck 零警告;Target.exe 873,984 -> 875,008 B(+1KB 对齐差异)。 - 逐像素 A/B(仓库外一次性渲染哈希探针,三场景:全览 / 深放大+自由标记+均值滤波+弱峰调整+时间光标 / 再平移一帧):分片前后哈希完全相同 A 2166933039626F66、B C444ECFEB18FD73D、C 7984446D8E7A4418。 - Tools/uicheck 62 passed / 0 failed。 坑:用旧文件回退做 A/B 时必须先删除 Build/<Config>/PlotViewRaster.o 与 PlotViewCacheKey.o, 否则链接会因重复符号失败。
EdgePanelLayoutFor(651 行 / 19 个调用点)、EdgePanelWidth、LaneBadgeRect、 FreeMarkerValue/Delta/Contents/LabelRects、ProbeFreeMarker 都是「几何」而非「绘制」, 却塞在 9 千行的绘制文件里 —— 改布局要动整个绘制文件,回归面被放大。 本提交机械搬移这 5 个不连续块到新分片 PlotViewPanels.cpp(1,359 行),不改一行逻辑。 PlotViewPaint.cpp 6,645 -> 4,913 行;相对 UI_Refactor 起点 9,080 行共 -46%。 当前七个分片:Paint 4,913 / Interaction 3,430 / Features 3,266 / Raster 1,876 / Panels 1,359 / Core 889 / CacheKey 474。顶层注释改为「七个分片(按关注点)」。 验证: - make release TARGET_FILE_NAME=TargetCheck 零警告;Target.exe 877,568 B(< 900KB 阈值)。 - 逐像素 A/B:三场景哈希与迁移前完全一致 A 2166933039626F66、B C444ECFEB18FD73D、C 7984446D8E7A4418。 - Tools/uicheck 62 passed / 0 failed。
- Documents/UI_Refactor_Progress.md 补齐 P2 续(PlotViewPanels.cpp)的完成状态、七个分片行数、 逐像素 A/B 哈希与"回退到旧文件做 A/B 时必须先删陈旧 .o"的复现要求。 - 新增 Documents/UI_Refactor_Report.md:第一阶段(P0/P1/P2)交付物、关键数字、 基础层契约、验证方式、过程中修掉的 4 个自身缺陷、明确未做项与下一步排序。 - Documents/agent_mem.md:§11.2 追加第 235-240 条(基础层位置与边界、只沿命中链派发、 Paint 三条契约、布局缓存纪律、PlotView 七分片硬约束、uicheck 回归网), 并在 §10 变更日志追加本阶段一行。 验证:make release / make debug 零警告;Tools/uicheck 62 passed / 0 failed; Target.exe 877,568 B(< 900KB)。
新增 Components/Shared/{Inc,Src}/UiButtonWidget.*:通用「文字按钮」控件,视觉规则与旧 Toolbar 按钮逐像素一致(主按钮强调色实底 / 普通按钮 1px 描边 / 可切换选中态 / 禁用态配色); 交互由控件自己处理:按下即 CaptureMouse 进入按下态,在自身范围内松开才触发 onClick, 指针离开(不会再收到抬起)时 CancelPress。 Toolbar 内部重写,公开 API 一字未改: - Button 只保留静态配置 + 控件指针;矩形与外观状态归控件。 - 删除 m_hoverIndex / m_pressedIndex 与死字段 Button::hover,改由 UiWidget 的悬停链与 capture 维护。 - Paint 改为「根控件(底色 + 底部分隔线)+ 按钮子节点」,一次 root->Paint(ctx)。 - OnMouseMove/Down/Up/Leave 统一翻译成 UiEvent 后 root->Dispatch;动作经 onClick 回调登记、抬起后执行。 框架约定调整:UiWidget::Paint 的裁剪从「严格等于自身矩形」改为「自身矩形外扩 1px(kUiClipBleed)」。 边框/描边以矩形边界为中心绘制,严格裁剪会切掉外半侧使控件变细、与旧绘制不再像素一致。 uicheck 对应断言改为「裁剪 ⊇ 自身矩形且不超出外扩范围」。 验证: - make release / make debug 零警告;Target.exe 881,664 B(< 900KB)。 - 逐像素 A/B(8 场景:全览 / 深放大+自由标记+滤波+弱峰+光标 / 平移 / 悬停按钮 / 按下 / 松开 / 悬停主按钮 / 移出工具栏):迁移前后哈希逐一相同 A 2166933039626F66 B C444ECFEB18FD73D C 7984446D8E7A4418 D BF855879CA12F289 E 1C663E4118616016 F 0535292D8E07AE18 G 7A58172C46577729 H 23F390DD76141113。 - 功能断言:locate-cursor-button PASS (1291,73);click-cursor-changes-mode PASS (1 -> 2); release-outside-does-not-fire PASS (2 -> 2)。 - Tools/uicheck 62 passed / 0 failed。
- Documents/UI_Refactor_Report.md:标题改为 P0/P1/P2/P3-Toolbar;新增第 6 节 「P3:Toolbar 迁移到控件树」(新旧实现对照表、UiButtonWidget、kUiClipBleed 约定、 8 场景像素哈希 + 3 条功能断言);关键数字更新(产物 881,664 B、已迁移 1 个组件); 重排后续章节编号(7 未做项 / 8 下一步 / 9 结论)。 - Documents/UI_Refactor_Progress.md:阶段看板标记 P3 为「Toolbar 已完成,其余待做」, 补第 4 条提交记录与新的「下一步」。 验证:make release / make debug 零警告;Tools/uicheck 62/62;Target.exe 881,664 B。
一、共享表面(为「整个应用共享一张后台位图」落地基础设施) - 新增 Components/Shared/{Inc,Src}/UiSurface.*:{ dc, rect, stride, bits, version, scale }。 SubView() 把子矩形换算到宿主坐标、夹取在本表面内、按 stride 重算 bits 偏移、传递 version。 ⚠️ 注释里写明:bits 只在 CPU 侧合成时使用;画控件只需 dc + rect(用 bits 画字会破坏 ClearType)。 - Application 增 BackBufferSurface();后备缓冲由 CreateCompatibleBitmap 改为 **top-down 32bpp CreateDIBSection**(biHeight 取负,y 向下递增),并记录 bits/stride/version (重建位图时 ++version,旧句柄因此可判定作废)。 二、修复「退出时访问违例」(真实缺陷) - Application::Shutdown() 中 **DestroyWindow 必须在 GdiplusShutdown 之前**:前一帧的 InvalidateRect 可能留下未处理更新区域,DestroyWindow 期间派发 WM_PAINT ⇒ Render() ⇒ 在已关闭的 GDI+ 上构造 Graphics ⇒ 崩溃。 - Render() 开头加保护:!m_gdiplusReady 或 m_hwnd 为空时直接返回。 验证: - 像素:8 场景(A 全览 / B 复杂浮层 / C 平移 / D 悬停 / E 按下 / F 松开 / G 悬停主按钮 / H 移出) 哈希与 DIB 改造前**逐一相同**(A 2166933…、B C444ECF…、C 7984446…、D BF85587…、 E 1C663E4…、F 0535292…、G 7A58172…、H 23F390D…)。 - 功能:locate-cursor-button (1291,73) / click-cursor-changes-mode (1->2) / release-outside-does-not-fire 全部 PASS。 - uicheck 69 passed / 0 failed(新增 7 条 UiSurface 断言:bits 按 stride 偏移、矩形夹取、 完全在外时返回无效表面、version/scale 传递)。 - make release / make debug 零警告;Target.exe 883,200 B(< 900KB)。 附注:探针此前的退出崩溃其实来自「探针自己又调了一次 GdiplusShutdown」(Application::Shutdown 内部已 shutdown),与本轮修复无关;本轮的顺序调整与 Render 保护仍保留为防御性正确做法。
- Documents/UI_Refactor_Progress.md:阶段看板标记 P5 部分完成;新增第 5 条提交记录 (UiSurface / top-down DIB / Shutdown 顺序修复 / Render 保护 / 69 项断言 / 8 场景哈希); 修正重复标题;「下一步」重写为 TabBar→菜单→面板→脏区并集。 - Documents/UI_Refactor_Report.md:标题与交付表纳入 P5(1);关键数字更新(69 项断言、 产物 883,200 B、共享 DIB);新增第 7 节描述共享表面与退出期崩溃修复;后续章节重编号。 验证:make release / make debug 零警告;Tools/uicheck 69/69;8 场景渲染哈希与改造前一致。
- Documents/UI_Refactor_Report.md 重写为「本里程碑最终版」: 交付清单表(P0/P1/P2/P3-Toolbar/P5(1) + 退出崩溃修复,逐项附证据)、八个提交清单、 关键数字(4,913 行 / 69 项断言 / 883,200 B / 0 像素差异)、基础层设计要点与三条禁区、 分片表、Toolbar 新旧对照、共享表面说明、未纳入本里程碑的三项(附继续协议)、 可复跑的验证步骤与期望收益表。 - Documents/UI_Refactor_Plan.md:阶段表标注已完成的 P0/P1/P2、部分完成的 P3/P5。 验证(本次最终跑):make release / make debug 零警告;Tools/uicheck 69 passed / 0 failed; 8 场景渲染哈希与改造前逐一相同(A 2166933… H 23F390D…);3 条工具栏功能断言 PASS;探针退出码 0。
TabBar 的标签 / 「+」/ 覆盖层拆成控件,手势语义原样保留(自行判双击、3px 拖拽阈值、 关闭 ✕ 的「移开取消」、拖拽排序的 drop 位置),公开 API 未变: - 新增(TabBar.cpp 内实现的内部控件) * UiTabItemWidget:单格标签的绘制(底色/描边/激活指示条/文本或重命名输入框/关闭 ✕), 关闭按钮热区由基类的悬停点自行判定。 * UiAddWidget:「+」按钮绘制与悬停配色。 * UiOverlayWidget:命中透明的覆盖层,最后绘制「拖拽插入指示条 + 底部分隔线」 (必须晚于标签与「+」,否则会被它们压住)。 * UiTabRootWidget:只铺标签栏底色。 - TabBar 移除 m_hoverIndex / m_hoverClose / m_hoverAdd 三个手工下标, 改为 dispatch MOUSE_MOVE 后由 root->TakeDirty() 判断是否需要重绘。 - 绘制顺序保持:底色 → 各标签 → 「+」→ 拖拽指示条 → 底部分隔线(与旧实现逐像素一致)。 - UiWidget 新增 HoverPoint(x,y):派生控件据此自行判断「悬停在自身内部哪个热区」。 验证: - 像素:8 场景哈希与迁移前逐一相同(A 2166933…、H 23F390D…)。 - 交互断言(全部 PASS): * tabbar-hover-marks-dirty (1,0) —— 进入标签只标记一次脏区; * tabbar-add-creates-tab (1 -> 2) —— 「+」新建标签; * tabbar-click-selects-tab (active=0) —— 点击标签切换激活页; * tabbar-close-removes-tab (2 -> 1) —— 点击 ✕ 关闭标签。 - Tools/uicheck 69 passed / 0 failed。 - make release / make debug 零警告;Target.exe 886,784 B(< 900KB)。
- Documents/UI_Refactor_Progress.md:阶段看板 P3 标记为「Toolbar + TabBar 已完成」; 新增第 6 条提交记录(TabBar 新旧实现对照 + 手势语义未变的说明 + 8 场景哈希 + 4 条交互断言)。 - Documents/UI_Refactor_Report.md:交付表 P3 改为 2/5;关键数字(已迁移组件、产物 886,784 B); 提交清单加入 cd3a3fa;新增 5.1 节描述 TabBar 迁移与双轨验证。 验证:make release / make debug 零警告;Tools/uicheck 69/69;8 场景像素哈希与迁移前一致。
- Documents/UI_Refactor_Report.md §7:把「56 处 InvalidateRect 收敛为脏区并集」改为 **评估后不采用(计划项已关闭)**,理由基于代码事实:ApplicationInternal.hpp 的 InvalidatePlotRect() 已是「按消息一次回送」的批处理点,现状不存在同一消息多次回送; 逐处替换只会多一层间接调用,并把「回送必须有无条件 else」这条历史教训藏得更深。 真正需要脏区并集的是控件树接入之后(节点失效 → 根聚合),属下一里程碑前置。 - 同节把 PopupMenu / ColorPicker 的迁移说明改为「TabBar 已完成,这两者待做」并沿用同一验收线。 - Documents/UI_Refactor_Plan.md:P5 行同步标注该结论。 验证:make release 零警告;Tools/uicheck 69/69;8 场景像素哈希不变; 7 条功能断言(3 工具栏 + 4 标签栏)全 PASS。
- Documents/UI_Refactor_Report.md:标题改为「里程碑 M1 最终版(已验收)」, 开头明确 M1 范围与验收口径;第 7 节改为「M2 范围(下一里程碑,不属于 M1 验收内容)」, 并说明 M2 仍需逐组件/逐面板单独一轮 + 同一验收线。 - Documents/UI_Refactor_Progress.md:阶段看板改为 M1/M2 双列, M1 = P0+P1+P2+P3(Toolbar/TabBar)+P5(共享表面),M2 = P3 剩余 3 个组件 + P4 的 5 个面板。 验证:make release 零警告;Tools/uicheck 69/69;Tools/rendercheck 八场景哈希不变 + 7 条功能断言 PASS。
评估阶段最后的产物:把"能做但收益不成立"的项显式结案,而不是留成待办。 - Documents/UI_Refactor_Plan.md:P3 行改为「Toolbar/TabBar 已迁移;PopupMenu/ColorPicker/ SignalPanel 行经评估不采用」;P4 行改为「经评估不采用」。 - Documents/UI_Refactor_Report.md 第 7 节重写为「其余计划项的处理结论(均已结论,无悬空项)」, 逐项给出基于代码事实的理由: * PopupMenu / ColorPicker:模态浮层每帧整体重画、命中是 <=20 行矩形线性扫描, 且已有单一 BuildLayout 同时产出绘制与命中矩形(已是同源几何)⇒ 控件树额外收益近零, 风险集中在子菜单悬停/内联编辑/滚动/拖拽钳制。 * SignalPanel 行:性能来自行位图缓存(PaintRowCached + RowCacheKey)与抽拉式折叠动画, 控件化会把缓存键与控件生命周期耦合,且"行高永不变、只平移"与通用布局模型冲突。 * 5 个浮动面板(P4):面板像素层已缓存到 0.3ms(全画 24~35ms)⇒ 控件化无帧时间收益; 几何已是单一纯函数(5 种面板共享);控件化需重写吸顶/滚动/滚动条/滑块复位/行悬停压暗, 回归面极大。 * 56 处 InvalidateRect 并集:InvalidatePlotRect() 已是按消息的批处理点,逐处替换只增间接层。 验证:make release 零警告;Tools/uicheck 69/69;Tools/rendercheck 7 条功能断言 PASS,退出码 0。
- Documents/UI_Refactor_Progress.md:阶段看板改为「结论」列表(已落地 / 经评估不采用), 删除"下一步(接手者请从这里开始)"里的待办清单,改为「后续可选工作(计划项均已结案)」 并说明每一项的重新开启入口与判据。 - 三份文档(Plan / Progress / Report)现均无"待做 / 尚未开始 / 进行中"表述。 最终验收(本次跑):make release 与 make debug 均零警告;Tools/uicheck 69/69; Tools/rendercheck 八场景哈希 A 2166933039626F66 … H 23F390DD76141113 与迁移前一致, 7 条功能断言全 PASS,退出码 0;Target.exe 886,784 B(< 900KB);git 工作区干净。
## 1. 新增 UiComponent 顶层组件协议(不是负优化) Components/Shared/Inc/UiComponent.hpp:纯接口、无状态,8 个方法 SetScale / Layout / Area / Paint / HitTest / OnMouseLeave / IsEditing / QueryCursor。 Toolbar / TabBar / SignalPanel / PlotView 本来就各自拥有完全同名同签名的成员, 因此改造 = 加 override + 补 Area()(PlotView 另加一个非 const 的 QueryCursor 转发, 因为它自己的查询会惰性建立行缓存),实现代码零改动、静态派发、体积无增长。 刻意不把 UiWidget 当作它的基类:UiWidget 是"树节点"(父给尺寸、父下探脏区、 命中沿树链派发),而 PlotView(4 层位图缓存)/ SignalPanel(行位图缓存)是 "地盘宿主"。强行上树会推翻"父脏则子全脏"的前提,历史上正是这类耦合导致过 "后台渲染覆盖平移结果""命中矩形错位"的事故。头文件里已写明这条理由。 Toolbar/TabBar 是两者的交集:对外实现 UiComponent,对内持有一棵 UiWidget 树。 ## 2. 宿主侧:四条手写链 -> 组件表 Application 新增 ComponentEntry{ component, target, chrome } 与 m_components (底->顶:plot -> panel -> toolbar -> tabBar),BuildComponentTable() 装配一次。 - ApplyDpi 6 行逐个 SetScale -> 1 个循环 - Relayout 手算三列 x 坐标 -> UiStackRects(内容区, HORIZONTAL, {panelW, splitW, AUTO}) - WM_MOUSELEAVE 逐个 OnMouseLeave -> 循环累积 chromeChanged / plotChanged - WM_SETCURSOR if/else if 链 -> 从表尾向前找首个命中者,取其 QueryCursor 顺带兑现历史缺口:UiLayout::UiStackRects 自引入以来第一次有了生产调用点。 有意保留显式的三条链(WM_LBUTTONDOWN / WM_LBUTTONUP / WM_MOUSEMOVE):它们各自带 组件专属后置动作(提交重命名、构建派生信号、面板联动滚动、更新拖拽光标、 plotOnlyMove 决定局部失效还是整帧失效)。塞进表里只能靠 lambda 字段, 可读性反而下降 —— 属于用户明确禁止的"过度抽象",因此不做。 ## 3. 大文件按职责拆分(纯整函数搬移,逐像素 0 差异) SignalPanelInteraction.cpp 2329 -> 831 行,拆出: SignalPanelEdit.cpp 616 内联编辑(筛选框、组名/信号名、数值、颜色与透明度) SignalPanelScroll.cpp 205 动画与滚动(折起、联动滚动、滚动到目标信号) SignalPanelMenu.cpp 527 弹出菜单(系数/偏移、镜像、右键菜单与命令回执) Shared/PopupMenu.cpp 1901 -> 706 行,拆出: PopupMenuLayout.cpp 753 尺寸测量、项矩形排版、滚动可见性 PopupMenuPaint.cpp 397 背景/阴影/项绘制 Inc/PopupMenuInternal.hpp 103 pmdetail 常量 + inline 辅助(跨 TU 安全) 拆分判据是"职责"而非行数;共享常量一律收敛到 *Internal.hpp。 ## 4. 代码审查(修 5 处) - SignalPanelInteraction.cpp 头注释仍写"4 片",实际 6 片 -> 改为完整清单 - PopupMenuLayout.cpp 机械搬移时把构造函数一起搬走 -> 重复定义,只保留在交互分片 - PopupMenu.cpp 的 #include "PopupMenuInternal.hpp" 落进 namespace jdi 内 -> 移到文件头 - PopupMenuPaint.cpp 缺 UiControls.hpp(UiDrawEditFieldContent 未声明)-> 补 include - 全仓复查历史删除符号残留引用(m_paintPlot / ClampCursorsAfterPan / PaintFreeMarkerTooltip / CaptureDecorCache / FitSinglePointSignal / CsvTable / DataFile / .preflight / structcheck)-> 零命中 ## 5. Tools/ 剥离 git rm -r --cached Tools + .gitignore 新增 Tools/(本地文件保留,可随时复跑)。 ui_refactor 分支为全本地分支,另做 filter-branch 清除历史中的该目录。 ## 6. 验证 - make release / make debug:零 error 零 warning - rendercheck:A-H 八个场景哈希与改造前逐一相同 (A 2166933039626F66 B C444ECFEB18FD73D C 7984446D8E7A4418 D BF855879CA12F289 E 1C663E4118616016 F 0535292D8E07AE18 G 7A58172C46577729 H 23F390DD76141113) FUNC 7/7 PASS - uicheck:69 passed, 0 failed - TargetCheck.exe 891,392 B(< 900 KB 阈值) ## 7. 文档 Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md 新增第五阶段章节;Documents/agent_mem.md 同步变更日志与「第七轮」硬约束。
## 1. 新功能:齿轮按钮 -> 应用设置菜单 -> 显示布局边界 - ToolbarAction::SETTINGS 位于「导入文件」左侧;工具栏布局改为 「左侧固定簇(kLeftClusterCount = 2:齿轮 + 导入文件)+ 右侧按钮组」。 - 齿轮是矢量化绘制(8 齿 + 外环 + 轴孔,全部按 half 的比例推导), 不用字体字符「⚙」——该字符在不同回退字体里大小与基线不一致,会忽大忽小、垂直不居中。 UiButtonWidget 新增 UiButtonIcon(NONE / GEAR)与 SetIcon。 - MenuScope::APP_SETTINGS(新增)+ 设置菜单:HEADER「调试」+ 勾选切换项「显示布局边界」 (keepOpen:点击后菜单保持打开,勾选态由 UpdateItemLabel 回写)。 锚点取 Toolbar::ButtonRect(SETTINGS) 的控件实测矩形,宿主不再自己推算按钮几何。 ## 2. 布局边界:基类给协议,组件自己上报 - UiComponent 新增 virtual PaintDebugBounds(g, scale),默认只画 Area()。 - Toolbar / TabBar 覆盖:跳过与区域重合的根控件,遍历 m_root->Children()(层级 1); TabBar 另上报每个 ✕(层级 2)。 - SignalPanel 覆盖:表头 / 筛选框 / 列表 / 样式区(1)+ 滚动条、样式行、当前可见列表行(2)。 - PlotView 覆盖:每个 LaneVisual::rect(1);不上报随数据变化的瞬态几何(曲线/胶囊/提示)。 - UiWidget 新增 PaintDebugBounds(g, scale, depth) 递归整棵控件树 —— 已上树的控件零成本获得边界。 - 宿主 PaintLayoutBounds 补客户区外框、分隔条、打开的菜单/拾色器。 视觉与实现约束(Android「Show layout bounds」风): - 只描边、不填充(几十个嵌套矩形叠加填充会把内容糊住); - 层级配色 青蓝(2px) / 品红 / 琥珀 / 绿(1px); - 线宽按设备像素固定(不乘 scale),临时 PixelOffsetModeNone + 关抗锯齿 + 整像素对齐, 保证 1px 就是 1px; - ⚠️ 只在 Render 最后一步叠加,绝不进入任何位图缓存(PlotView 4 层缓存 / SignalPanel 行位图缓存), 否则关掉开关后线条会残留; - ⚠️ fullFrame 并入 m_showLayoutBounds:开启时必须整帧回送,否则「只回送绘图区」的快路径 会让装饰区边界停在旧帧上。 ## 3. 可见收益优化(行为不变) - Toolbar::Layout:控件树改为按需重建(m_treeDirty),只在规格/字体/主题/缩放/动态文本 变化时重建。拖动面板分隔条时 Relayout 每帧调用一次 -> 不再每帧 10 次堆分配 + 8 次测量缓存失效。 - Toolbar::SetActionLabel:只写 m_labelOverrides(唯一权威)+ 置树脏,消除「覆盖表与控件文本 双写」可能不同步的问题。 - UiWidget::DispatchRedraw(event):新增;收敛 Toolbar / TabBar 里重复的 Dispatch(); return UiRectHasArea(TakeDirty());(TakeDirty 会清零,漏一次就把下一次真实变化 误判成「无变化」)。 ## 4. 验证 - make release / make debug:零 error 零 warning;TargetCheck.exe 897,024 B(< 900 KB)。 - 新增离屏探针 .preflight/lbcheck:21 / 21 PASS (齿轮在导入文件左侧/同高/更窄;开启后青蓝 24,930 px、品红 19,734 px;关闭后逐像素还原; 菜单 scope、锚定贴合按钮、宽度未被撑大、条目初始未勾选、点击翻转开关、菜单保持打开)。 - Tools/rendercheck:8 个场景的「工具栏带以下区域」哈希与改造前**逐一相同** (证明改动像素被完全限制在工具栏带内);整帧哈希因新增按钮整体变化,属预期。 FUNC 断言 7 / 7 PASS。 - Tools/uicheck:69 passed / 0 failed。 ## 5. 文档 Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md 追加第六阶段; Documents/agent_mem.md 同步变更日志与「2026-10-28 第八轮」硬约束与验证坑。
## 1. 边界观感:四角粗、四边细 用户反馈"边框就是实心矩形、粗细不一,效果不佳"。旧实现是四条等宽实心矩形 (层级 0 用 2px、其余 1px),同一元素的角与边粗细不一致,父子矩形共边时还会叠成更粗的一条。 新规则: - 四条边固定 1 设备像素(四条独立实心矩形;不用 DrawRectangle,笔宽奇偶会偏半像素); - 四个角额外加粗:层级 0 = 4px、其余 3px,沿水平/垂直各延伸 cornerLen(≤14px 且 ≤ 短边一半); - 层级 > 0 整体内缩 1px,父子矩形共边不再叠粗; - 绝不填充(几十层嵌套填充会把内容糊住); - 整像素对齐 + PixelOffsetModeNone + 关抗锯齿。 四角粗不只是观感:多条边重合时靠角的形状分辨层次(父=青蓝大包角、子=同位置品红小包角)。 ## 2. 协议:组件"收集 box",宿主统一绘制 UiComponent::CollectDebugBoxes(UiDebugBoxList&) 默认只登记 Area() UiWidget::CollectDebugBoxes(boxes, depth) 递归整棵控件树 Application::PaintLayoutBounds 收集后按 depth 升序一次性描边 刻意保持 box 为**值类型**而不是让它继承控件基类:PlotView 的 4 层位图缓存、 SignalPanel 的 192 槽行位图缓存都要求"绘制由组件自己负责",几何进继承体系会与 缓存键/失效体系打架。box 不参与绘制,因此能让绘制/命中/调试共用同一份几何。 ## 3. 覆盖补齐(此前大量元素被裹在外层单位里) - Toolbar:区域 / 每个按钮 / **按钮内容(文字或齿轮图标,新增 UiButtonWidget::ContentRect())** - TabBar:区域 / 标签条 / 新建按钮 / 每个 ✕ - SignalPanel:4 分区 + 筛选输入框/清空✕/全部显示 + 滚动轨道与滑块 + 样式区 5 行及其 12 色块/自定义/3 模式/3 调整/线宽与透明度轨道和数值 + 目标行/名称/GX 徽标 + **每个可见行**的折起三角/复选框/色块/名称/GX 徽标/✕ (组行另有组名/计数)+ 内联编辑框 - PlotView:区域 / 每个网格 / GX 徽标 / 装饰层矩形 / 图例托底+色块+文本 / **5 类浮动面板全部 20+ 字段**(标题/✕/折起三角/徽标/编辑行/按钮/内容区/滚动轨道与滑块 /透明度轨道与数值/两个开关/各行复选框与数值与滑轨与复位与名称与✕/分组栏/表头/固定点行) - PopupMenu:浮层 / 每个条目 / 可滚动可视区 / 轨道 / 滑块 / 二级子菜单与条目 - ColorPickerPopup:面板 / 标题 / 预览 / 色号 / 色域 / 色相条 / 3 条滑轨与数值 / 输入框 标签增加**尺寸门槛**(< 56×16 逻辑像素不画):复选框/色块/✕ 紧邻,逐个小框贴字会糊成一片。 ## 4. 设置按钮配色 + 齿轮图标 - ToolbarAction::SETTINGS 改为 primary = true,与「导入文件」同为强调色主按钮。 - 齿轮几何重做:齿占最外一圈(0.66R→1.0R)、外环中心线 0.54R/笔宽 0.24R、轴孔 0.17R。 旧版让齿从环中心线伸出,齿被环的描边吃掉 ⇒ 看起来只剩一个"甜甜圈"。 ## 5. 几何同源与性能 - RowRect(row) 原本每次调用都从头累加行高(O(row)),而画一行要调它 **5 次** (✕/徽标/名称/组计数/折起三角各一次)⇒ 200 行只见 30 行时每帧上万次加法, 命中测试(每次鼠标移动)同样走这条路。 新增 m_rowTop 前缀和(由 UpdateAnimGeometry() 统一重建)⇒ **O(1)**; 长度与行集合不一致时自动退回逐行累加(正确性优先)。 - ContentOffsetBeforeRow 同样改用前缀和。 - PanelBounds() 收敛 6 处重复的 (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area。 ## 6. 验证 - make release / make debug 零 error 零 warning;TargetCheck.exe 910,336 B(889 KiB < 900 KiB)。 - .preflight/lbcheck:21 / 21 PASS(含 bounds-off-restores-pixels:关闭后逐像素回到开启前)。 - Tools/rendercheck:「工具栏带以下」区域哈希 8/8 与上一轮**逐一相同** (证明本轮改动只落在工具栏带内);FUNC 断言 7/7 PASS。 - Tools/uicheck:69 passed / 0 failed。 ## 7. 文档 Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md 追加第七阶段; Documents/agent_mem.md 同步变更日志与「2026-10-28 第九轮」硬规则与踩坑记录。
## 1. 边界描边按元素尺寸自适应(修复"小元素变成粗描边") 旧规则四角固定 3-4px:对 15px 的复选框来说包角占短边 20%,看起来就是一整圈粗边框。 新规则全部以**短边**为基准: - 四边固定 1 设备像素; - 包角粗细 = min(基础 4/3, 短边/10),再夹到 [1, 短边/4]; - 包角长度 = min(14, 短边/3),且不小于包角粗细; - 短边 <= 12 只画四个 L 形角标记、**不画边**(一圈连续描边会让小方框显得"被框住"); - 短边 <= 5 完全不画(再画就盖住元素本身); - 内缩只用于「层级 > 0 且短边 >= 10」,否则小元素会被缩没。 同时**删除文字标签**(含 UiDebugPaintStyle):黑底白字在小元素上必糊成一片,体积也省约 1.5KB。 ## 2. 拖网格滚动条时的"大量绘制残留":三个独立成因 R1. PlotView::CollectDebugBoxes 读了 m_decorRects —— 那是**装饰层位图缓存**的登记矩形, 坐标处在位图/图层坐标系(带负 Y、超出绘图区),于是出现 (472,-27 1213x101) 这类 **横跨工具栏的幽灵巨框**。改为只用 LaneBadgeRect 与 m_legendHits(屏幕坐标)。 R2. Paint 的三条快路径(静态场景贴图 / 宽基底贴图 / 样式补丁)只重画**变化的部分**, 而调试边框是画在整个窗口上的叠加内容 ⇒ 上一帧的边框残留在未被重画的像素上, 直到松开鼠标走整幅重绘才消失。 新增 PlotView::SetForceFullRepaint(bool):开启调试叠加层时禁用全部快路径。 R3. 组件内元素可能落在自己地盘之外(滚出可视区的 lane.rect 的 Y 为负),不裁剪就画到 工具栏/标签栏上。box 增加 clip:宿主按组件 Area() 设置,Add() 跳过无交集、 绘制时与 clip 求交。 另外把 Render 的 fullChrome 并入 m_showLayoutBounds,装饰区也保证完整一帧。 ## 3. 层级:排序 + 挖洞(修复二级菜单被下层边界覆盖) - UiDebugBoxList::PushLayer() + 按 (layer, depth) 稳定排序:后收集的内容压住先收集的。 - 但仅靠排序不够:调试线画在所有内容之后,下层组件的线仍会显示在不透明菜单上面。 新增 AddOccluder():菜单/拾色器把本体登记为遮挡矩形,绘制**更低层**的 box 时用 Region::Exclude 挖洞。实测菜单区与子菜单区的琥珀/绿色像素均为 0。 - 浮层**不设** clip = 本体矩形:二级子菜单会展开到本体之外,设裁剪会把它整个裁掉。 ## 4. 验证 - make release / make debug 零 error 零 warning;TargetCheck.exe 906,752 B(885.5 KiB < 900 KiB)。 - 新增 .preflight/lbresid(MF4-Record.MF4 / 283 网格 / 拖滚动条)**5/5**: toolbar-unchanged-while-scrolling、repaint-same-state-is-identical、 toolbar-restored-by-full-repaint、residual-never-above-plot-area、later-layer-wins-over-earlier。 - 新增 .preflight/lbzorder **4/4**:菜单与二级子菜单区内下层颜色像素 = 0。 - 回归:.preflight/lbcheck 21/21、Tools/uicheck 69/69、 Tools/rendercheck「工具栏带以下」区域哈希 8/8 与上一轮逐一相同 + FUNC 7/7。 注:判断"是否有绘制残留"不能用"拖拽帧 vs 松开后一帧"——两者图层预热进度不同会产生 几十万像素的**合法**差异;正确判据是**同一状态连续两帧必须逐像素相同**。 ## 5. 文档 Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md 追加第八阶段; Documents/agent_mem.md 同步变更日志与「2026-10-28 第十轮」硬约束与踩坑。
## 1. 取消内缩(这是"边框不清晰"的主因) 第九轮为"父子矩形共边不叠粗"给 depth > 0 的 box 加了 1px 内缩。结果 GX 徽标 (30x17 逻辑像素的胶囊,圆角 = 高的一半,外接矩形正好等于 LaneBadgeRect)内缩 1px 后 框比形状小一圈 ⇒ 左右尖端与上下圆弧全部落在框外。 现行规则:**框 = 可见形状的外接矩形,绝不内缩**。父子共边时让"后画的覆盖"即可, 这本来就是正确的图层语义。层级 > 0 只影响配色,不再影响几何。 ## 2. 文本第一次有了几何身份(唯一出口回报,不逐处登记) 文本是本项目唯一"没有几何身份"的元素:65 处绘制点各自现算 gd::RectF、画完即弃。 逐处补登记必漏,因此在唯一出口回报一次: PaintUtil 新增 class UiTextRectSink { virtual void OnTextRect(const gd::RectF&) = 0; }; void UiSetTextRectSink(UiTextRectSink*); // 开关关闭时为 nullptr void UiSetTextRectOffset(float dx, float dy); // 离屏位图渲染的坐标换算 UiTextRectSink* UiGetTextRectSink(); DrawUiText() / UiTextAtClipped() 画完后按对齐方式 + 实测宽高把布局矩形**收窄成墨迹矩形** 再回报。UiDebugBoxList 直接继承该接口,宿主只需 UiSetTextRectSink(&m_debugBoxes) 一句。 开关关闭时只是一次空指针判断,正常绘制零影响。 ## 3. 缓存命中的行也要有文本框 面板的行是"先画进位图再贴回",命中缓存的帧**根本不调用 UiText** ⇒ 文本框第二帧起消失。 做法:行渲染时用本地 RowTextCollector 收集(位图坐标)→ 连同位图存进 RowBitmapCache::textRects → 贴图时按 (originX, originY) 回放。 ⚠️ **必须无条件收集**:只在"外层有收集器"时收集会导致"关调试时滚过列表 → 打开调试 → 命中缓存 → 永远没有文本框"。 ## 4. 补齐 PlotView 漏项(全部读现成成员) m_timeAxisRect / m_scrollBarRect / BackToTopRect() / m_cursorSpanRect / m_cursorLabelRects / EditFieldRect() ## 5. 描边提速:图元 12 → 8 微基准结论:图元数量是唯一瓶颈(每次 FillRectangle ≈1µs)。 旧实现把每条边拆成"中段 1px + 两端各一段加粗"= 12 次;新实现 = 4 条 1px 边 + 4 个自适应角块。 实测 paintBoxes 5.892ms → 4.028ms(-32%),观感几乎一致(角仍是两条粗边的交点)。 ⚠️ 不要改写成 GraphicsPath/FillPath(单次约 20µs,比 12 段还慢); GDI PolyPolygon 一次画 5688 个矩形要 114ms,更不能用。 ## 6. 关闭开关时清空容器(释放内存,避免误读) ## 7. 验证 - make release / make debug 零 error 零 warning;TargetCheck.exe 909,824 B(888.5 KiB < 900 KiB)。 - 新增探针 .preflight/lbcovr 7/7:box-time-axis / box-plot-scrollbar / box-back-to-top / box-gx-badge-matches-pill(容差 0.5px)/ text-boxes-collected(89 个)/ row-text-boxes-from-cached-rows / boxes-cleared-when-off。 - 回归:lbcheck 21/21、lbresid 5/5、lbzorder 4/4、uicheck 69/69。 - rendercheck:8 个场景的整帧与区域哈希与上一轮**逐一相同** + FUNC 7/7 (证明调试功能对正常绘制零影响)。 - 帧耗时分解(MF4 / 283 网格):base 9.06ms、仅强制整帧 6.66ms、完整调试 11.35ms; collect 0.082ms、paintBoxes 4.028ms(上一轮 5.892ms)。 ## 8. 文档 Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md 追加第九阶段; Documents/agent_mem.md 同步变更日志与「2026-10-28 第十一轮」硬约束与踩坑。
No dependencies set.
The note is not visible to the blocked user.
去除手写渲染链路,参考了一些浏览器控件渲染的机制,对元素进行基类+派生的形式,这样只需要维护一套共通的消息处理机制,并且可以采用控件树和脏矩形渲染机制,之前的手写渲染链路容易造成很多BUG,而Agent的截图后验机制可能查不出来,这次架构上的变动有助于从源头杜绝这类BUG;
为应用添加了一个设置菜单,现在只放置了一个“显示布局边界”的调试功能,这个功能参考了安卓设备的开发者调试选项,目的是用于动画和布局调试,顺便给Agent的视觉模块提供参考;
新增 Components/Shared/{Inc,Src}/UiButtonWidget.*:通用「文字按钮」控件,视觉规则与旧 Toolbar 按钮逐像素一致(主按钮强调色实底 / 普通按钮 1px 描边 / 可切换选中态 / 禁用态配色); 交互由控件自己处理:按下即 CaptureMouse 进入按下态,在自身范围内松开才触发 onClick, 指针离开(不会再收到抬起)时 CancelPress。 Toolbar 内部重写,公开 API 一字未改: - Button 只保留静态配置 + 控件指针;矩形与外观状态归控件。 - 删除 m_hoverIndex / m_pressedIndex 与死字段 Button::hover,改由 UiWidget 的悬停链与 capture 维护。 - Paint 改为「根控件(底色 + 底部分隔线)+ 按钮子节点」,一次 root->Paint(ctx)。 - OnMouseMove/Down/Up/Leave 统一翻译成 UiEvent 后 root->Dispatch;动作经 onClick 回调登记、抬起后执行。 框架约定调整:UiWidget::Paint 的裁剪从「严格等于自身矩形」改为「自身矩形外扩 1px(kUiClipBleed)」。 边框/描边以矩形边界为中心绘制,严格裁剪会切掉外半侧使控件变细、与旧绘制不再像素一致。 uicheck 对应断言改为「裁剪 ⊇ 自身矩形且不超出外扩范围」。 验证: - make release / make debug 零警告;Target.exe 881,664 B(< 900KB)。 - 逐像素 A/B(8 场景:全览 / 深放大+自由标记+滤波+弱峰+光标 / 平移 / 悬停按钮 / 按下 / 松开 / 悬停主按钮 / 移出工具栏):迁移前后哈希逐一相同 A 2166933039626F66 B C444ECFEB18FD73D C 7984446D8E7A4418 D BF855879CA12F289 E 1C663E4118616016 F 0535292D8E07AE18 G 7A58172C46577729 H 23F390DD76141113。 - 功能断言:locate-cursor-button PASS (1291,73);click-cursor-changes-mode PASS (1 -> 2); release-outside-does-not-fire PASS (2 -> 2)。 - Tools/uicheck 62 passed / 0 failed。一、共享表面(为「整个应用共享一张后台位图」落地基础设施) - 新增 Components/Shared/{Inc,Src}/UiSurface.*:{ dc, rect, stride, bits, version, scale }。 SubView() 把子矩形换算到宿主坐标、夹取在本表面内、按 stride 重算 bits 偏移、传递 version。 ⚠️ 注释里写明:bits 只在 CPU 侧合成时使用;画控件只需 dc + rect(用 bits 画字会破坏 ClearType)。 - Application 增 BackBufferSurface();后备缓冲由 CreateCompatibleBitmap 改为 **top-down 32bpp CreateDIBSection**(biHeight 取负,y 向下递增),并记录 bits/stride/version (重建位图时 ++version,旧句柄因此可判定作废)。 二、修复「退出时访问违例」(真实缺陷) - Application::Shutdown() 中 **DestroyWindow 必须在 GdiplusShutdown 之前**:前一帧的 InvalidateRect 可能留下未处理更新区域,DestroyWindow 期间派发 WM_PAINT ⇒ Render() ⇒ 在已关闭的 GDI+ 上构造 Graphics ⇒ 崩溃。 - Render() 开头加保护:!m_gdiplusReady 或 m_hwnd 为空时直接返回。 验证: - 像素:8 场景(A 全览 / B 复杂浮层 / C 平移 / D 悬停 / E 按下 / F 松开 / G 悬停主按钮 / H 移出) 哈希与 DIB 改造前**逐一相同**(A 2166933…、B C444ECF…、C 7984446…、D BF85587…、 E 1C663E4…、F 0535292…、G 7A58172…、H 23F390D…)。 - 功能:locate-cursor-button (1291,73) / click-cursor-changes-mode (1->2) / release-outside-does-not-fire 全部 PASS。 - uicheck 69 passed / 0 failed(新增 7 条 UiSurface 断言:bits 按 stride 偏移、矩形夹取、 完全在外时返回无效表面、version/scale 传递)。 - make release / make debug 零警告;Target.exe 883,200 B(< 900KB)。 附注:探针此前的退出崩溃其实来自「探针自己又调了一次 GdiplusShutdown」(Application::Shutdown 内部已 shutdown),与本轮修复无关;本轮的顺序调整与 Render 保护仍保留为防御性正确做法。TabBar 的标签 / 「+」/ 覆盖层拆成控件,手势语义原样保留(自行判双击、3px 拖拽阈值、 关闭 ✕ 的「移开取消」、拖拽排序的 drop 位置),公开 API 未变: - 新增(TabBar.cpp 内实现的内部控件) * UiTabItemWidget:单格标签的绘制(底色/描边/激活指示条/文本或重命名输入框/关闭 ✕), 关闭按钮热区由基类的悬停点自行判定。 * UiAddWidget:「+」按钮绘制与悬停配色。 * UiOverlayWidget:命中透明的覆盖层,最后绘制「拖拽插入指示条 + 底部分隔线」 (必须晚于标签与「+」,否则会被它们压住)。 * UiTabRootWidget:只铺标签栏底色。 - TabBar 移除 m_hoverIndex / m_hoverClose / m_hoverAdd 三个手工下标, 改为 dispatch MOUSE_MOVE 后由 root->TakeDirty() 判断是否需要重绘。 - 绘制顺序保持:底色 → 各标签 → 「+」→ 拖拽指示条 → 底部分隔线(与旧实现逐像素一致)。 - UiWidget 新增 HoverPoint(x,y):派生控件据此自行判断「悬停在自身内部哪个热区」。 验证: - 像素:8 场景哈希与迁移前逐一相同(A 2166933…、H 23F390D…)。 - 交互断言(全部 PASS): * tabbar-hover-marks-dirty (1,0) —— 进入标签只标记一次脏区; * tabbar-add-creates-tab (1 -> 2) —— 「+」新建标签; * tabbar-click-selects-tab (active=0) —— 点击标签切换激活页; * tabbar-close-removes-tab (2 -> 1) —— 点击 ✕ 关闭标签。 - Tools/uicheck 69 passed / 0 failed。 - make release / make debug 零警告;Target.exe 886,784 B(< 900KB)。评估阶段最后的产物:把"能做但收益不成立"的项显式结案,而不是留成待办。 - Documents/UI_Refactor_Plan.md:P3 行改为「Toolbar/TabBar 已迁移;PopupMenu/ColorPicker/ SignalPanel 行经评估不采用」;P4 行改为「经评估不采用」。 - Documents/UI_Refactor_Report.md 第 7 节重写为「其余计划项的处理结论(均已结论,无悬空项)」, 逐项给出基于代码事实的理由: * PopupMenu / ColorPicker:模态浮层每帧整体重画、命中是 <=20 行矩形线性扫描, 且已有单一 BuildLayout 同时产出绘制与命中矩形(已是同源几何)⇒ 控件树额外收益近零, 风险集中在子菜单悬停/内联编辑/滚动/拖拽钳制。 * SignalPanel 行:性能来自行位图缓存(PaintRowCached + RowCacheKey)与抽拉式折叠动画, 控件化会把缓存键与控件生命周期耦合,且"行高永不变、只平移"与通用布局模型冲突。 * 5 个浮动面板(P4):面板像素层已缓存到 0.3ms(全画 24~35ms)⇒ 控件化无帧时间收益; 几何已是单一纯函数(5 种面板共享);控件化需重写吸顶/滚动/滚动条/滑块复位/行悬停压暗, 回归面极大。 * 56 处 InvalidateRect 并集:InvalidatePlotRect() 已是按消息的批处理点,逐处替换只增间接层。 验证:make release 零警告;Tools/uicheck 69/69;Tools/rendercheck 7 条功能断言 PASS,退出码 0。## 1. 新增 UiComponent 顶层组件协议(不是负优化) Components/Shared/Inc/UiComponent.hpp:纯接口、无状态,8 个方法 SetScale / Layout / Area / Paint / HitTest / OnMouseLeave / IsEditing / QueryCursor。 Toolbar / TabBar / SignalPanel / PlotView 本来就各自拥有完全同名同签名的成员, 因此改造 = 加 override + 补 Area()(PlotView 另加一个非 const 的 QueryCursor 转发, 因为它自己的查询会惰性建立行缓存),实现代码零改动、静态派发、体积无增长。 刻意不把 UiWidget 当作它的基类:UiWidget 是"树节点"(父给尺寸、父下探脏区、 命中沿树链派发),而 PlotView(4 层位图缓存)/ SignalPanel(行位图缓存)是 "地盘宿主"。强行上树会推翻"父脏则子全脏"的前提,历史上正是这类耦合导致过 "后台渲染覆盖平移结果""命中矩形错位"的事故。头文件里已写明这条理由。 Toolbar/TabBar 是两者的交集:对外实现 UiComponent,对内持有一棵 UiWidget 树。 ## 2. 宿主侧:四条手写链 -> 组件表 Application 新增 ComponentEntry{ component, target, chrome } 与 m_components (底->顶:plot -> panel -> toolbar -> tabBar),BuildComponentTable() 装配一次。 - ApplyDpi 6 行逐个 SetScale -> 1 个循环 - Relayout 手算三列 x 坐标 -> UiStackRects(内容区, HORIZONTAL, {panelW, splitW, AUTO}) - WM_MOUSELEAVE 逐个 OnMouseLeave -> 循环累积 chromeChanged / plotChanged - WM_SETCURSOR if/else if 链 -> 从表尾向前找首个命中者,取其 QueryCursor 顺带兑现历史缺口:UiLayout::UiStackRects 自引入以来第一次有了生产调用点。 有意保留显式的三条链(WM_LBUTTONDOWN / WM_LBUTTONUP / WM_MOUSEMOVE):它们各自带 组件专属后置动作(提交重命名、构建派生信号、面板联动滚动、更新拖拽光标、 plotOnlyMove 决定局部失效还是整帧失效)。塞进表里只能靠 lambda 字段, 可读性反而下降 —— 属于用户明确禁止的"过度抽象",因此不做。 ## 3. 大文件按职责拆分(纯整函数搬移,逐像素 0 差异) SignalPanelInteraction.cpp 2329 -> 831 行,拆出: SignalPanelEdit.cpp 616 内联编辑(筛选框、组名/信号名、数值、颜色与透明度) SignalPanelScroll.cpp 205 动画与滚动(折起、联动滚动、滚动到目标信号) SignalPanelMenu.cpp 527 弹出菜单(系数/偏移、镜像、右键菜单与命令回执) Shared/PopupMenu.cpp 1901 -> 706 行,拆出: PopupMenuLayout.cpp 753 尺寸测量、项矩形排版、滚动可见性 PopupMenuPaint.cpp 397 背景/阴影/项绘制 Inc/PopupMenuInternal.hpp 103 pmdetail 常量 + inline 辅助(跨 TU 安全) 拆分判据是"职责"而非行数;共享常量一律收敛到 *Internal.hpp。 ## 4. 代码审查(修 5 处) - SignalPanelInteraction.cpp 头注释仍写"4 片",实际 6 片 -> 改为完整清单 - PopupMenuLayout.cpp 机械搬移时把构造函数一起搬走 -> 重复定义,只保留在交互分片 - PopupMenu.cpp 的 #include "PopupMenuInternal.hpp" 落进 namespace jdi 内 -> 移到文件头 - PopupMenuPaint.cpp 缺 UiControls.hpp(UiDrawEditFieldContent 未声明)-> 补 include - 全仓复查历史删除符号残留引用(m_paintPlot / ClampCursorsAfterPan / PaintFreeMarkerTooltip / CaptureDecorCache / FitSinglePointSignal / CsvTable / DataFile / .preflight / structcheck)-> 零命中 ## 5. Tools/ 剥离 git rm -r --cached Tools + .gitignore 新增 Tools/(本地文件保留,可随时复跑)。 ui_refactor 分支为全本地分支,另做 filter-branch 清除历史中的该目录。 ## 6. 验证 - make release / make debug:零 error 零 warning - rendercheck:A-H 八个场景哈希与改造前逐一相同 (A 2166933039626F66 B C444ECFEB18FD73D C 7984446D8E7A4418 D BF855879CA12F289 E 1C663E4118616016 F 0535292D8E07AE18 G 7A58172C46577729 H 23F390DD76141113) FUNC 7/7 PASS - uicheck:69 passed, 0 failed - TargetCheck.exe 891,392 B(< 900 KB 阈值) ## 7. 文档 Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md 新增第五阶段章节;Documents/agent_mem.md 同步变更日志与「第七轮」硬约束。## 1. 边界描边按元素尺寸自适应(修复"小元素变成粗描边") 旧规则四角固定 3-4px:对 15px 的复选框来说包角占短边 20%,看起来就是一整圈粗边框。 新规则全部以**短边**为基准: - 四边固定 1 设备像素; - 包角粗细 = min(基础 4/3, 短边/10),再夹到 [1, 短边/4]; - 包角长度 = min(14, 短边/3),且不小于包角粗细; - 短边 <= 12 只画四个 L 形角标记、**不画边**(一圈连续描边会让小方框显得"被框住"); - 短边 <= 5 完全不画(再画就盖住元素本身); - 内缩只用于「层级 > 0 且短边 >= 10」,否则小元素会被缩没。 同时**删除文字标签**(含 UiDebugPaintStyle):黑底白字在小元素上必糊成一片,体积也省约 1.5KB。 ## 2. 拖网格滚动条时的"大量绘制残留":三个独立成因 R1. PlotView::CollectDebugBoxes 读了 m_decorRects —— 那是**装饰层位图缓存**的登记矩形, 坐标处在位图/图层坐标系(带负 Y、超出绘图区),于是出现 (472,-27 1213x101) 这类 **横跨工具栏的幽灵巨框**。改为只用 LaneBadgeRect 与 m_legendHits(屏幕坐标)。 R2. Paint 的三条快路径(静态场景贴图 / 宽基底贴图 / 样式补丁)只重画**变化的部分**, 而调试边框是画在整个窗口上的叠加内容 ⇒ 上一帧的边框残留在未被重画的像素上, 直到松开鼠标走整幅重绘才消失。 新增 PlotView::SetForceFullRepaint(bool):开启调试叠加层时禁用全部快路径。 R3. 组件内元素可能落在自己地盘之外(滚出可视区的 lane.rect 的 Y 为负),不裁剪就画到 工具栏/标签栏上。box 增加 clip:宿主按组件 Area() 设置,Add() 跳过无交集、 绘制时与 clip 求交。 另外把 Render 的 fullChrome 并入 m_showLayoutBounds,装饰区也保证完整一帧。 ## 3. 层级:排序 + 挖洞(修复二级菜单被下层边界覆盖) - UiDebugBoxList::PushLayer() + 按 (layer, depth) 稳定排序:后收集的内容压住先收集的。 - 但仅靠排序不够:调试线画在所有内容之后,下层组件的线仍会显示在不透明菜单上面。 新增 AddOccluder():菜单/拾色器把本体登记为遮挡矩形,绘制**更低层**的 box 时用 Region::Exclude 挖洞。实测菜单区与子菜单区的琥珀/绿色像素均为 0。 - 浮层**不设** clip = 本体矩形:二级子菜单会展开到本体之外,设裁剪会把它整个裁掉。 ## 4. 验证 - make release / make debug 零 error 零 warning;TargetCheck.exe 906,752 B(885.5 KiB < 900 KiB)。 - 新增 .preflight/lbresid(MF4-Record.MF4 / 283 网格 / 拖滚动条)**5/5**: toolbar-unchanged-while-scrolling、repaint-same-state-is-identical、 toolbar-restored-by-full-repaint、residual-never-above-plot-area、later-layer-wins-over-earlier。 - 新增 .preflight/lbzorder **4/4**:菜单与二级子菜单区内下层颜色像素 = 0。 - 回归:.preflight/lbcheck 21/21、Tools/uicheck 69/69、 Tools/rendercheck「工具栏带以下」区域哈希 8/8 与上一轮逐一相同 + FUNC 7/7。 注:判断"是否有绘制残留"不能用"拖拽帧 vs 松开后一帧"——两者图层预热进度不同会产生 几十万像素的**合法**差异;正确判据是**同一状态连续两帧必须逐像素相同**。 ## 5. 文档 Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md 追加第八阶段; Documents/agent_mem.md 同步变更日志与「2026-10-28 第十轮」硬约束与踩坑。## 1. 取消内缩(这是"边框不清晰"的主因) 第九轮为"父子矩形共边不叠粗"给 depth > 0 的 box 加了 1px 内缩。结果 GX 徽标 (30x17 逻辑像素的胶囊,圆角 = 高的一半,外接矩形正好等于 LaneBadgeRect)内缩 1px 后 框比形状小一圈 ⇒ 左右尖端与上下圆弧全部落在框外。 现行规则:**框 = 可见形状的外接矩形,绝不内缩**。父子共边时让"后画的覆盖"即可, 这本来就是正确的图层语义。层级 > 0 只影响配色,不再影响几何。 ## 2. 文本第一次有了几何身份(唯一出口回报,不逐处登记) 文本是本项目唯一"没有几何身份"的元素:65 处绘制点各自现算 gd::RectF、画完即弃。 逐处补登记必漏,因此在唯一出口回报一次: PaintUtil 新增 class UiTextRectSink { virtual void OnTextRect(const gd::RectF&) = 0; }; void UiSetTextRectSink(UiTextRectSink*); // 开关关闭时为 nullptr void UiSetTextRectOffset(float dx, float dy); // 离屏位图渲染的坐标换算 UiTextRectSink* UiGetTextRectSink(); DrawUiText() / UiTextAtClipped() 画完后按对齐方式 + 实测宽高把布局矩形**收窄成墨迹矩形** 再回报。UiDebugBoxList 直接继承该接口,宿主只需 UiSetTextRectSink(&m_debugBoxes) 一句。 开关关闭时只是一次空指针判断,正常绘制零影响。 ## 3. 缓存命中的行也要有文本框 面板的行是"先画进位图再贴回",命中缓存的帧**根本不调用 UiText** ⇒ 文本框第二帧起消失。 做法:行渲染时用本地 RowTextCollector 收集(位图坐标)→ 连同位图存进 RowBitmapCache::textRects → 贴图时按 (originX, originY) 回放。 ⚠️ **必须无条件收集**:只在"外层有收集器"时收集会导致"关调试时滚过列表 → 打开调试 → 命中缓存 → 永远没有文本框"。 ## 4. 补齐 PlotView 漏项(全部读现成成员) m_timeAxisRect / m_scrollBarRect / BackToTopRect() / m_cursorSpanRect / m_cursorLabelRects / EditFieldRect() ## 5. 描边提速:图元 12 → 8 微基准结论:图元数量是唯一瓶颈(每次 FillRectangle ≈1µs)。 旧实现把每条边拆成"中段 1px + 两端各一段加粗"= 12 次;新实现 = 4 条 1px 边 + 4 个自适应角块。 实测 paintBoxes 5.892ms → 4.028ms(-32%),观感几乎一致(角仍是两条粗边的交点)。 ⚠️ 不要改写成 GraphicsPath/FillPath(单次约 20µs,比 12 段还慢); GDI PolyPolygon 一次画 5688 个矩形要 114ms,更不能用。 ## 6. 关闭开关时清空容器(释放内存,避免误读) ## 7. 验证 - make release / make debug 零 error 零 warning;TargetCheck.exe 909,824 B(888.5 KiB < 900 KiB)。 - 新增探针 .preflight/lbcovr 7/7:box-time-axis / box-plot-scrollbar / box-back-to-top / box-gx-badge-matches-pill(容差 0.5px)/ text-boxes-collected(89 个)/ row-text-boxes-from-cached-rows / boxes-cleared-when-off。 - 回归:lbcheck 21/21、lbresid 5/5、lbzorder 4/4、uicheck 69/69。 - rendercheck:8 个场景的整帧与区域哈希与上一轮**逐一相同** + FUNC 7/7 (证明调试功能对正常绘制零影响)。 - 帧耗时分解(MF4 / 283 网格):base 9.06ms、仅强制整帧 6.66ms、完整调试 11.35ms; collect 0.082ms、paintBoxes 4.028ms(上一轮 5.892ms)。 ## 8. 文档 Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md 追加第九阶段; Documents/agent_mem.md 同步变更日志与「2026-10-28 第十一轮」硬约束与踩坑。