Serial Studio 手动布局模式:Figma 式对齐辅助线与 48×48 密集小窗口支持(Spec 0010)实现解析
【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio
手动布局模式(关闭自动布局)是 Serial Studio 中最终仪表盘版式编排的场所——冻结功能(Spec 0007)的存在正是为了让手工排好的版式被锁定并交付给操作员。但过去这一模式没有任何精度工具:拖动窗口时唯一的辅助是靠近画布边缘时出现的半屏/四分之一屏 Aero 式吸附矩形,既没有窗口与窗口之间的边缘/中心对齐,也没有等距辅助、尺寸匹配和位置读数。本文以仓库中的设计规格文档 doc/claude/specs/0010-manual-layout-guides/spec.md 为核心骨架,结合源码实现,系统讲解该特性的需求定义(R1–R11)、验收标准(AC1–AC12)、约束不变量,以及它在 QML 与 C++ 层的真实落地方式,帮助你掌握在 Serial Studio 中快速拼出像素级整齐仪表盘的操作方法与原理。
一、背景与动机:为什么手动布局需要精度工具
规格文档开篇点明了两个并存的问题:
手动模式缺乏精度工具。自动布局关闭后,手工拼版是最终版式的产出环节,但拖动时唯一的辅助是 Aero 风格吸附——仅在画布边缘提供半屏/四分之一屏矩形。没有窗口与邻居之间的边缘或中心吸附、没有等距辅助、没有位置和尺寸读数。排一排仪表只能靠肉眼估像素偏移,在"最需要精致"的交付环节反而最容易排得参差不齐。
356×320 的最小尺寸限制了密集面板。当时每个仪表盘窗口强制 356×320 的最小尺寸,而仪表盘面板(instrument panel)需要小瓦片——紧凑的 LED 簇、窄条、小数字读数——现有的尺寸下限让密集面板无法实现。值得注意的是,窗口部件自身已经具备降级渲染能力:标题条在宽度低于 96 px 时隐藏,工具栏在高度低于某个阈值时隐藏,因此限制来自窗口外壳(delegate),而非部件内容。
从源码看,这一论断完全成立:app/qml/MainWindow/Panes/Dashboard/WidgetDelegate.qml 中窗口的最小尺寸直接由布局模式决定:
minimumWidth/minimumHeight在自动布局时为356/320,手动模式则放宽到48/48。
二、目标与非目标
2.1 目标(Goals)
规格为手动模式定义了完整的精度工具集:
- 拖动窗口时,任意窗口的边缘/中心可与其他窗口的边缘/中心、画布边缘、画布水平/垂直中心线对齐,并在对齐发生时以辅助线(guide line)可视化确认——移动与缩放都生效;
- 构建行/列时获得等距辅助:当新产生的间距与相邻间距一致时,拖动吸附到该间距并高亮匹配的间距;
- 缩放时可将一个窗口的宽/高匹配到另一个窗口,尺寸相等时给出视觉提示;
- 通过画布上的
x/y/w×h像素读数徽章(badge),随时获知被操作窗口的精确几何; - 可选的背景网格,开启后移动/缩放吸附到网格线,满足固定节奏的排版需求;
- 按住修饰键可完全自由放置(本次手势内挂起全部吸附);
- 手动模式下窗口最小可缩放到 48×48 px,足以容纳密集仪表瓦片,同时仍显示标题栏按钮。
2.2 非目标(Non-Goals)
明确划清的边界同样重要:
- 不改自动布局模式:其 packing、sizing、拖动重排行为保持现状,包括既有最小尺寸假设;
- 不改外部弹窗窗口:其 356×320 最小尺寸与操作系统管理行为保持不变;
- Aero 吸附被移除而非改造:半屏/四分之一屏矩形从手动模式彻底删除,不重设计、不藏在修饰键后面——手动模式变为"纯自由放置 + 智能辅助线";
- 不做 Figma 式可拖拽标尺线(用户可放置的持久参考线),也不做多选/分组对齐:辅助线只作用于正在操作的单个窗口;
- 不重设计小尺寸下的部件渲染:部件保持既有降级行为(隐藏标题、工具栏、标签),48×48 下看起来极简是可接受的;
- 冻结模式不变:冻结时所有操纵保持完全禁用。
三、核心功能需求详解(R1–R11)
规格共定义了 11 条需求,是特性的完整行为契约。
R1 / R2:移动与缩放时的边缘/中心吸附
拖动窗口(移动)时,拖动位置在小阈值内吸附到以下目标集:
- 其他可见窗口的边缘/中心;
- 画布边缘;
- 画布水平/垂直中心线。
吸附成立时,沿对齐的全长渲染一条辅助线。缩放时(R2),移动中的边对同一目标集吸附,反馈相同;不移动的边在整个手势中绝不位移。
源码层面,吸附核心实现在 core/Ui/UI/SnapGuides.h:
kSnapThreshold = 6(第 34 行)——吸附阈值,即"候选对齐线捕获手势的像素距离",6 px 意味着轻微靠近即可吸附,但日常拖动依然顺滑;GuideKind(Edge/Center)与GuideLine——每条辅助线表达为画布坐标中的 1 px 矩形;resolveMoveSnap()/resolveResizeSnap()(第 102–103 行)——分别解析移动与缩放吸附,输出SnapResult(吸附后的几何 + 证明它的可视化信息:辅助线、间距、尺寸匹配窗口)。
缩放时的"不移动边绝不位移"由 core/Ui/UI/WindowManager/WindowGeometry.h 中的MovingEdges与computeResizedGeometry()保证:每次只按手势增量重新计算被移动的边,固定边不参与计算。
R3:等间距吸附
拖动时,若被拖动窗口与邻居之间的间距可以匹配"该邻居沿同轴与下一个窗口之间的既有间距",则位置吸附到等分间距,并可视化指示匹配的间距。规格明确该特性仅限移动手势(Figma 同样将其限定在移动,见"未决问题")。
实现上,SpacingIndicator(core/Ui/UI/SnapGuides.h)携带gap像素值,渲染时在每个间距条上显示具体像素数。
R4:尺寸匹配吸附
缩放时,若进行中的宽度或高度进入另一可见窗口宽度/高度的阈值范围,则尺寸吸附到相等,并高亮匹配窗口。SnapResult中的sizeMatch字段承载这一信息,SnapOverlay将其发布为sizeMatchRect+sizeMatchVisible属性。
R5:实时几何读数徽章
任何手动移动/缩放手势期间,一个紧凑徽章实时显示窗口当前 x、y、宽度、高度的画布像素值,手势结束即消失。WindowManager的manualGestureGeometry属性提供该数据,SnapOverlay::publishManualGesture()负责发布。这一徽章同时也是验收手段——AC1/AC4/AC5 都依赖它来核对最终落点是否与目标对齐"相差 0 px"。
R6:可选背景网格
用户可开启手动模式下的渲染背景网格,并可配置单元格大小;开启后移动与缩放吸附到网格线。网格吸附与智能辅助线可组合,两者都在范围内时智能辅助线优先。该偏好跨会话持久化。
实现落点非常具体:
- 网格绘制在 app/qml/MainWindow/Panes/Dashboard/DashboardCanvas.qml:一个
Canvas元素垫在窗口之下、壁纸之上,仅在尺寸/网格大小/主题变化时重绘,颜色取自主题色canvas_grid; - 开关与尺寸位于画布右键菜单(同文件第 356–402 行):
Show Grid菜单项,网格尺寸提供8 / 16 / 24 / 32 / 64 px五档; - 持久化在 core/Ui/UI/WindowManager.cpp:通过
QSettings读写WindowManager_GridEnabled(默认false)与WindowManager_GridSize(默认16,取值被qBound(2, …, 256)钳制); - 网格吸附的量化函数是
SnapGuides::snapToGrid(value, gridSize)。
R7:Alt 键临时绕过吸附
移动或缩放时按住 Alt 挂起全部吸附(辅助线、等距、尺寸匹配、网格);手势中途松开 Alt 立即恢复。这是"精度不粘手"设计约束的关键出口——吸附必须让像素级对齐成为轻松默认,但绝不允许与用户对抗。
R8:移除 Aero 半屏/四分之一屏吸附
半屏/四分之一屏吸附矩形不再出现在手动模式;把窗口拖到画布边缘附近时,就停在被(可能已吸附的)拖动所放置的确切位置。画布上仍保留了snapIndicator属性与渲染代码(DashboardCanvas.qml),但其职责已转为"真实吸附几何的指示框"(按被触碰边内缩 1 px),而非旧的占位吸附矩形。
R9:48×48 手动最小尺寸
手动模式下窗口最小可缩放到 48×48 px 且不能更小;在该尺寸下标题栏按钮依然可见可点。自动布局与外部窗口保持既有下限;将小尺寸版式切回自动布局不得破坏 packing。
WidgetDelegate.qml 中的实现就是一句话的分支:windowManager.autoLayoutEnabled ? 356 : 48。标题栏按钮在 48 px 下的可用性由MiniWindow外壳的 chrome 折叠逻辑保证(见"约束与不变量")。
R10:辅助线仅限手动模式
任何辅助线、徽章、网格、吸附行为都不出现在自动布局模式或冻结状态;仅悬停时也不出现(只活跃于移动/缩放手势进行中)。DashboardCanvas.qml 中手势叠加层的可见性条件即为_wm.manualGestureActive && !_wm.autoLayoutEnabled && !_wm.frozen,网格同理(第 496 行)。
R11:布局持久化保持既有结构
保存的手动几何(包括小于 356×320 的尺寸)按既有方式持久化与恢复(按项目、按画布尺寸桶),旧版保存的布局原样加载。
这由 core/Ui/UI/WindowManager/WindowLayoutStore.h 承担:它拥有手动布局的参考形态——每个窗口的矩形、测量时的画布尺寸、等待窗口注册的待定几何,并以稳定的(widgetType, relativeIndex)身份为键,因此布局能在项目重载导致窗口重新编号后依然正确恢复。
四、源码级实现剖析
规格文档是"WHAT 与 WHY"(Phase 1 of 4),实现细节由 plan 承载。当前仓库中该特性状态为done,全部 12 条验收标准已勾选。以下按层拆解真实代码。
4.1 SnapGuides:吸附核心
core/Ui/UI/SnapGuides.h 是纯算法的吸附引擎,核心数据结构:
SnapInput:一次吸附解析的输入——候选几何、画布范围、兄弟窗口矩形集、阈值、最小尺寸(仅限缩放)、网格开关与尺寸、siblingSpacing(与邻居齐平吸附的间隙)、smartGuidesEnabled(关闭时只做网格量化);SnapResult:输出——吸附后的矩形、尺寸匹配矩形、辅助线列表、等距指示列表;snapToFraction()/fractionLabel():分数吸附与标签——除规格需求外,源码还实现了"扳手分数缩放预览"(footprint 上标注以画布分数表示的宽/高),属于同一精度工具家族的额外落地。
4.2 SnapOverlay 与 WindowManager:手势状态机与可视化发布
core/Ui/UI/WindowManager/SnapOverlay.h 拥有手动移动/缩放手势的全部视觉反馈:对齐辅助线、等距指示、尺寸匹配兄弟、分数预览与实时徽章。设计上强调按需发布——每个发布器只在其值真正变化时发信号,因此"解析结果与上次相同的视觉"不会触发无谓的 QML 重新求值。
core/Ui/UI/WindowManager.h 中的WindowManager(QQuickItem)则拥有窗口注册表、交互状态机与 QML 表面,把 QML 绑定的叠加层属性全部转发出去:
alignmentGuides/spacingIndicators/sizeMatchRect/sizeMatchVisible/fractionPreviewRect/fractionPreviewLabel—— 手势叠加层数据;manualGestureActive/manualGestureGeometry—— 手势活跃状态与徽章几何;gridEnabled/gridSize—— 网格开关与尺寸;snapIndicator/snapIndicatorVisible—— 吸附指示框。
窗口几何工具(WindowGeometry.h)提供了关键常量与函数:kResizeMargin = 8(窗口边缘 8 px 像素带内开始缩放而非拖动)、ResizeEdge八方向枚举、computeResizedGeometry()、clampResizeToCanvas()、constrainGeometry()。缩放时固定边不移位的保证、画布边界钳制都在此层完成。
4.3 DashboardCanvas:网格、吸附指示与手势叠加层
app/qml/MainWindow/Panes/Dashboard/DashboardCanvas.qml 是特性在 QML 侧的集中展示区:
- 网格:见 R6 章节,
Canvas用Cpp_ThemeManager.colors["canvas_grid"]画 1 px 网格线; - 吸附指示框(第 632–655 行):
_snapIndicator按真实吸附几何渲染,边框按触碰边内缩 1 px,颜色取主题snap_indicator_background/snap_indicator_border; - 手势叠加层(第 661–775 行):
_gestureOverlay仅在手动模式且非冻结时可见,内含四个视觉族——- 辅助线:
Repeater遍历_wm.alignmentGuides,用主题色highlight渲染每条线,统一透明度 0.8; - 等距指示:遍历
_wm.spacingIndicators,半透明高亮 + 居中显示间距像素数; - 尺寸匹配:2 px 描边的
sizeMatchRect高亮匹配兄弟; - 分数预览:标注落点 footprint 的宽/高画布分数,标签允许溢出 footprint 并被钳制在画布内。
- 辅助线:
所有颜色均来自Cpp_ThemeManager,满足"视觉跟随主题、无硬编码颜色"的约束,也保证在用户自定义背景图片上依然可读。
4.4 WidgetDelegate:48×48 的落地
如 R9 所述,WidgetDelegate.qml 用一行三元表达式区分两种模式的最小尺寸。规格约束要求"标题栏与按钮在 48 px 宽度下保持可用,放不下的 chrome 必须优雅折叠而非溢出或不可点击"——这是MiniWindow外壳(caption 栏)的职责,按钮保持可见可点,是 AC9 验收的核心。
4.5 WindowLayoutStore:持久化存储形态
如 R11 所述,WindowLayoutStore.h 以(widgetType, relativeIndex)稳定身份为键保存手动几何与参考画布尺寸,serialize()/applyManualLayout()/applySavedGeometries()构成保存-恢复链路;preload()处理"几何先于窗口注册到达"的时序问题。旧布局字节级兼容加载,小窗口布局在旧版 356×320 下限上最多被钳制放大,不会崩溃或错位恢复。
五、验收标准(AC1–AC12)
规格附带的验收标准全部为已勾选状态,是手动验证与回归的完整清单:
| 编号 | 验收内容(应用内) |
|---|---|
| AC1 | 拖动窗口缓慢经过另一窗口左边缘、上边缘与水平中心,各自产生可见吸附 + 辅助线,落点与目标对齐相差 0 px(用 R5 徽章核对) |
| AC2 | 缩放右边缘靠近邻居右边缘,吸附齐平,且左边缘整个手势不移动 |
| AC3 | 三窗口一行,拖动第三个直到间距匹配第一对的间距,间距指示出现,落点后两个间距像素级相等 |
| AC4 | 缩放窗口宽度匹配兄弟宽度,尺寸匹配提示出现,徽章中两者宽度读数一致 |
| AC5 | 徽章手势开始即出现、实时跟踪、与最终保存几何一致、松开即消失 |
| AC6 | 开启指定单元格的网格,移动落在网格线上;关闭后恢复自由放置;开关状态在应用重启后保留 |
| AC7 | 拖动过程中按住 Alt 经过已知吸附对齐不产生吸附与辅助线;手势内松开 Alt 重新启用 |
| AC8 | 手动模式下贴画布四边拖动,从不出现旧的半屏/四分之一屏吸附矩形 |
| AC9 | 窗口精确缩到 48×48 并停住;关闭/最小化/最大化按钮在该尺寸下可见可点;48×48 窗口以相同几何保存/重载 |
| AC10 | 存在多个小于 356×320 窗口时切回自动布局,packing 成功、无重叠或零尺寸窗口;切回恢复保存的手动几何(既有往返行为) |
| AC11 | 冻结开启时任何手势/辅助线/徽章/网格交互均不可用;自动布局开启时新视觉永不出现 |
| AC12 | 回归:--benchmark-hotpath门控不变(该特性仅为交互期 UI,帧路径上无任何运行代码) |
其中 AC6 的"重启后保留"在源码中由 WindowManager.cpp 的QSettings读写落实;AC12 是架构约束的回归兜底——吸附与网格计算只发生在手势期间,telemetry 帧路径完全不受影响。
对应的单元测试可见 app/tests/tst_snap_overlay.cpp(钉死 SnapOverlay 的发布契约:每个发布器存储什么、触发哪个变更信号、重复发布不变值保持静默)与 app/tests/tst_window_geometry.cpp(几何计算与边界行为)。
六、约束与不变量
- 决定性约束:精度但不粘手。吸附必须让像素级对齐成为轻松默认,同时保持普通拖动的流畅——小阈值、即时视觉反馈、可靠的旁路(Alt)。一个与用户对抗的吸附系统不如没有。
- 性能边界。辅助线计算与渲染只运行于活跃手势期间、交互频率下;不向遥测帧路径添加任何工作;30+ 窗口的面板在拖动时不得卡顿。
- 冻结输入门控绝对化。冻结继续中止/拒绝所有操纵路径,任何新手势逻辑不得改变它。
- 持久化兼容。手动几何沿用既有存储形态(按画布尺寸桶、按项目);旧布局字节级兼容加载;小窗口布局在旧版 356×320 下限上只做钳制放大。
- 48 px 下的 chrome。标题栏、按钮与命中区域在 48 px 宽度下必须可用;放不下的 chrome 优雅折叠,不溢出、不可点击失禁。
- 主题与可读性。辅助线/网格视觉跟随当前主题(无硬编码颜色),且在用户设置背景图片上保持可读。
- 免费功能。该特性的任何部分均不设 Pro/版本门槛。
- RTL 兼容。辅助线与徽章须在
LayoutMirroring生效时正确渲染。
七、未决问题(Open Questions)
规格保留了两个开放决策,均已由实现给出答案:
- 网格默认单元格大小与开关位置——规格让 plan 提议、维护者选择。实现选择了画布右键菜单承载开关(
Show Grid)与五档尺寸(8/16/24/32/64 px),默认 16 px(见 WindowManager.cpp 与 DashboardCanvas.qml)。 - 等间距吸附是否扩展至缩放——规格跟随 Figma,仅限移动手势,当前实现保持一致。
八、总结:如何用好这套精度工具
从使用视角归纳,Spec 0010 让手动布局模式具备了完整的设计工具体验:
- 对齐:拖动或缩放窗口,靠近另一窗口的边缘/中心、画布边缘/中心线时,6 px 阈值内自动吸附,辅助线即时确认——AC1/AC2 是它的操作范本;
- 等距与尺寸:排行/列时让新间距匹配既有间距(R3),缩放时让宽度/高度匹配兄弟(R4);
- 读数:任何手势中都可用徽章核对精确 x/y/w×h(R5);
- 网格:右键画布 →
Show Grid,选 8–64 px 档位,获得固定节奏的吸附(R6,偏好跨会话保留); - 自由放置:按住 Alt 挂起全部吸附(R7);
- 密集面板:手动模式下把窗口缩到 48×48,拼出 LED 簇、窄条、小数字读数等紧凑瓦片(R9);切回自动布局或冻结模式时,所有辅助视觉自动消失、布局按既有结构安全往返(R10/R11)。
这套特性是"设计工具式精度"与"运行时仪表盘"结合的范例:规格把行为契约写到验收标准级,实现则用SnapGuides的纯算法核心 +SnapOverlay/WindowManager的按需发布 +DashboardCanvas的主题化渲染三层结构落地,并通过 tst_snap_overlay.cpp 与 tst_window_geometry.cpp 钉死行为边界,值得作为 Qt Quick 桌面应用交互精度设计的参考实现。
【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考