布局系统总览
DirectSurface UI 使用约束传递模型:父节点向子节点传递 BoxConstraints,子节点在约束内计算 size,父节点再为子节点设置 offset。业务代码应使用布局组件表达结构,不要在 performPaint() 中手工计算页面坐标。
可运行布局示例
UniformGrid 等分布局
Anchor / Canvas 锚点定位
Box / Panel 盒模型
Visibility / Clip 状态与裁剪
统一的组件布局属性
所有 RenderBox 组件都直接拥有以下属性,不再通过单子节点包装器补充尺寸、外边距或对齐能力:
| 属性 | 类型 | 说明 |
|---|---|---|
width / height |
number | undefined |
期望内容尺寸;父级 tight 约束优先。 |
minWidth / maxWidth |
number | undefined |
水平方向尺寸边界。 |
minHeight / maxHeight |
number | undefined |
垂直方向尺寸边界。 |
margin |
EdgeInsetsInput |
组件绘制区域之外、由直接父布局保留的空间。 |
horizontalAlignment |
BoxAlignment |
在父布局分配槽位中的水平位置。 |
verticalAlignment |
BoxAlignment |
在父布局分配槽位中的垂直位置。 |
BoxAlignment 为 'start' | 'center' | 'end' | 'stretch'。EdgeInsetsInput 支持数字和对象:
const title = new RenderText('患者列表')title.width = 240title.margin = { left: 12, right: 12, bottom: 8 }title.horizontalAlignment = 'center'
size 始终表示组件自身的绘制尺寸;outerSize 表示 size + margin。这一区分让背景、边框和命中区域不会错误地覆盖外边距。
自身对齐、父级分布和默认策略
horizontalAlignment / verticalAlignment 描述的是“组件自身如何放入直接父级分配的槽位”,不是“如何对齐自己的子组件”。属性值为 undefined 时,由直接父布局选择适合当前布局算法的默认策略;不同父容器不必使用同一个默认值。
| 直接父布局 | 未设置子项 alignment 时的默认策略 |
|---|---|
RenderStackPanel |
主轴由排列和 mainAxisAlignment 统一分布;交叉轴使用 crossAxisAlignment,默认 start。 |
RenderGridPanel / RenderUniformGrid |
单元格内水平、垂直均为 stretch。 |
RenderAdaptiveGridPanel |
单元格内水平 stretch、垂直 start。 |
RenderDockPanel |
Top/Bottom 水平 stretch;Left/Right 垂直 stretch;Fill 双轴 stretch。另一个方向默认 start。 |
RenderWrapPanel |
行内和行间位置由面板的 alignment / runAlignment 决定;行内交叉轴默认 start。 |
RenderMasonryPanel |
列内水平使用 itemHorizontalAlignment,默认 stretch;垂直位置由瀑布流顺序决定。 |
RenderAnchor / RenderCanvas |
使用 top/right/bottom/left/centerX/centerY,普通 alignment 不参与锚点定位。 |
RenderOverlayHost |
base 始终接受宿主的双轴 tight 约束并填满宿主;overlay 默认双轴 center。 |
除 RenderAnchor / RenderCanvas 的锚点轴和 RenderOverlayHost 的 base 外,子组件显式设置的 alignment 会覆盖有意义轴上的父级默认策略。alignment 只有在父级分配的槽位大于组件实际尺寸时才会产生可见偏移;如果组件已经占满槽位,设置 center 不会改变画面。
约束优先级
尺寸按以下顺序解析:
- 父布局给出的 tight 约束最高优先级,例如 Grid 轨道、Dock fill 和 StackPanel flex 分配。
- 在父约束允许时使用组件的
width/height。 minWidth、maxWidth、minHeight、maxHeight对结果限幅。- 未指定期望尺寸时使用组件自然尺寸。
因此,固定大小直接设置组件属性即可,不需要额外的 SizedBox;局部居中直接设置 alignment,不需要额外的 Align 或 Center。
在 Grid、UniformGrid、Dock、Wrap、AdaptiveGrid、Masonry 和 overlay 槽位中,显式 width / height 会阻止对应轴上的默认 stretch 拉伸;如果没有再显式设置 center 或 end,固定尺寸组件仍停在槽位起点。需要固定尺寸居中时应同时设置尺寸边界和 alignment。父级明确给出 tight 约束时仍以父约束为准,例如 StackPanel 的 flex 主轴和 RenderOverlayHost 的 base。
全页居中表单
全页居中通常使用单格 Grid 提供完整页面槽位,再给表单容器设置宽度边界和自身 alignment。内层 Grid 只负责字段的行列对齐:
import { RenderBorder, RenderGridPanel, gridAuto, gridFr } from 'ds-ui' const formGrid = new RenderGridPanel({ columns: [gridFr(1)], rows: [gridAuto()],}) const host = new RenderGridPanel({ columns: [gridFr(1)], rows: [gridFr(1)], padding: 24,}) const formCard = new RenderBorder({ child: formGrid })formCard.maxWidth = 520formCard.horizontalAlignment = 'center'formCard.verticalAlignment = 'center' host.addChild(formCard)
maxWidth 很重要:如果表单自身也是带 fr 列的 Grid,它会在没有宽度边界时占满外层单元格,此时虽然 alignment 已生效,但没有剩余空间可供居中。
Padding 属于容器
padding 表示容器边界与内容槽位之间的内部空间,只由能够承载内容的容器提供。主布局面板继承 RenderPanel,原生支持 padding:
RenderStackPanelRenderGridPanelRenderDockPanelRenderWrapPanelRenderUniformGridRenderAdaptiveGridPanelRenderMasonryPanelRenderAnchor/RenderCanvas
单子节点视觉容器 RenderBorder 同样原生支持 padding。
const content = new RenderStackPanel({ orientation: 'vertical', spacing: 12, padding: 24, crossAxisAlignment: 'stretch',}) const saveButton = new RenderButton({ label: '保存', variant: 'primary' })saveButton.margin = { top: 8 }content.addChild(saveButton)
常规兄弟间距优先用 spacing、rowGap、columnGap;某一个子节点需要例外间距时才设置该节点的 margin。
未显式配置间距时,布局阶段使用当前主题的 itemSpacing。由于公开 getter 没有布局主题上下文,此时读取 spacing、rowGap 或 columnGap 会得到 0;它表示“没有显式数值”,不是最终生效的主题间距。
常用结构容器
| 场景 | 推荐组件 | 说明 |
|---|---|---|
| 页面纵向结构、工具条、按钮组 | RenderStackPanel |
单方向排列,支持 flex 剩余空间分配。 |
| 顶部、底部、左右侧栏和主内容 | RenderDockPanel |
子项只声明 dock 方位,尺寸使用子组件自身属性。 |
| 表单、属性面板、严格行列对齐 | RenderGridPanel |
支持 px/fr/auto 轨道、跨行跨列;对齐使用子组件自身属性。 |
| 等宽等高入口 | RenderUniformGrid |
所有单元格尺寸一致。 |
| 标签、筛选条件自动换行 | RenderWrapPanel |
空间不足时自动换行。 |
| 响应式卡片列 | RenderAdaptiveGridPanel |
根据可用宽度计算列数。 |
| 高度不一致的卡片 | RenderMasonryPanel |
新项目进入当前最短列。 |
| 绝对定位、覆盖层、浮动按钮 | RenderAnchor / RenderCanvas |
attached data 只描述边缘和中心锚点。 |
| 普通内容滚动 | RenderScrollViewer |
单子节点滚动容器;复杂数据组件通常自己管理滚动。 |
父布局附加数据只描述父级关系:
- StackPanel:
flex - DockPanel:
dock - GridPanel:
row、column、rowSpan、columnSpan - Anchor:
top、right、bottom、left、centerX、centerY
宽高、最小/最大尺寸、margin 和 alignment 始终属于子组件本身,避免同一能力出现两套状态来源。
StackPanel 与 flex
使用 addChild(child, flex) 分配剩余主轴空间:
const page = new RenderStackPanel({ orientation: 'vertical', spacing: 8, padding: 16, crossAxisAlignment: 'stretch',}) page.addChild(new RenderPageHeader({ title: '订单列表' }))page.addChild(new RenderTextBox({ placeholder: '搜索订单' }))page.addChild(new RenderDataGrid({ columns: [], rows: [] }), 1)
flex = 0 在主轴按自然尺寸布局;flex > 0 按权重分配有限的剩余空间。主轴无界时 flex 子项也退化为自然尺寸。需要纯空白占位时使用 RenderSpacer。
保留的行为型包装组件
以下组件保留,因为它们提供的是独立行为,不是重复的盒模型属性:
| 组件 | 独立职责 |
|---|---|
RenderVisibility |
在 visible、hidden、collapsed 之间切换布局和命中行为。 |
RenderClip |
裁剪子节点绘制和命中区域。 |
RenderAppContextScope |
向子树提供局部 app context。 |
RenderCommandScope |
向子树提供局部命令作用域。 |
滚动边界
RenderScrollViewer 适合普通内容。RenderDataGrid、RenderTreeView、RenderPlainTextEditor、RenderMarkdownViewer 和 RenderMedicalRecordEditor 通常自己管理滚动,不要在同一方向重复嵌套滚动容器。