画布内布局
一、坐标从哪来
组件的 left / top 是相对父容器的偏移,state.width/height 是自己的盒子。
渲染与命中都按这个盒子来,所以“位置算错”通常就是盒子算错。
引擎提供两种现成的排布方式:
import { ICEFlowLayout, ICEBoxLayout } from 'ice-render'; // 属于引擎,不在本库导出
panel.setLayout(new ICEFlowLayout({ gap: 8, align: 'left' })); // 流式(可换行)
panel.setLayout(new ICEBoxLayout({ axis: 'y', gap: 12, align: 'stretch' })); // 盒式(交叉轴拉满)
引擎侧本轮补上的两项能力(2026-09-15):
ICEBoxLayout.align:start(默认)/center/end/stretch(交叉轴撑满, 就是 Swing BoxLayout 的默认口径);ICEFlowLayout.crossAlign:行内交叉轴对齐(一行里矮的项居中/贴底); 它的getPreferredSize()也按 SwingpreferredLayoutSize口径计入换行。
本库的容器型组件里,这三个把排列交给了引擎布局器(自己只保留组件级策略):
| 组件 | 用的布局 | 组件自己保留的策略 |
|---|---|---|
ICELayout(顶栏/侧栏/内容/页脚) | ICEBorderLayout(north / west|east / center / south) | 区高/区宽声明;侧栏收起 = display:false |
ICEForm | ICEBoxLayout({ axis: 'y', align: 'stretch' }) | 高度 = 内容高度 |
ICESpace | 横向 → ICEBoxLayout;换行 → ICEFlowLayout;纵向 → ICEBoxLayout | 没给宽/高的那一轴按内容自适应 |
ICESegmented | block → ICEGridLayout({ cellSizing: 'equal' });否则 ICEBoxLayout(axis x) | 段宽(非 block 时按文字估算);内缩由 padding 承担 |
ICEScrollPane | 自持 ICEScrollPaneLayout(Swing ScrollPaneLayout 位) | 内容多大 / 能不能滚 / 滚动条显不显示 |
ICETabs | 自持 ICETabsLayout(Swing JTabbedPane 位) | 页签有哪些 / 要不要溢出 / 滚到哪 |
ICEPagination | ICEBoxLayout(axis x) | 页码窗口、高度(含 8px 下边距) |
ICEFormItem(复合叶子) | 自持 ICEFormItemLayout | 形态(水平/垂直)、标签宽、行高与间距 |
其余三个组件级排布工具仍是各自的语义(引擎布局器没有对应能力):
// 1) 间距容器:横/纵排列 + 交叉轴对齐 + 换行,尺寸按内容自适应(内部走引擎布局器)
const space = new ICESpace({ direction: 'horizontal', size: 8, align: 'center' });
space.addItem(saveButton).addItem(cancelButton).addItem(tag);
// 2) 24 栅格:一行放不下 24 格自动换行,gutter 计在列之间(需要跨列的分数列宽,引擎 GridLayout 表达不了)
const grid = new ICEGrid({ width: 480, gutter: 16 });
grid.addCol(new ICEGridCol({ span: 12, content: leftCard }));
grid.addCol(new ICEGridCol({ span: 12, content: rightCard }));
// 3) 分栏:可拖的分隔条,size 是第一栏像素宽,min 夹取(尺寸由拖拽驱动,不是布局算出来的)
const splitter = new ICESplitter({ width: 900, height: 460, size: 300, first: queue, second: detail });
选型建议:同一行/列的等距排列用 ICESpace;页面骨架(12/12、8/8/8)用 ICEGrid;
两块内容要手动分配空间用 ICESplitter;整页外壳用 ICELayout;表单纵向堆叠用 ICEForm。
要自己拼版式时直接用引擎的 ICEFlowLayout / ICEBoxLayout / ICEBorderLayout(它们直接写子节点的 left/top)。
⚠️ 这三个都是纯布局容器,构造时
interactive: false。自己写布局容器时也要这样: 容器如果晚于内部控件创建(zIndex 更高)又参与命中,会把内部控件的点击整个吃掉 (输入框点不进去、焦点环不出现)。
引擎布局不继承(2026-09-15 起,对齐 Swing)
给容器 setLayout() 只影响它自己怎么摆子项,不会把策略传给子容器 —— 对齐 Java Swing 的
Container.setLayout()(父布局只给子容器摆位置,子容器用自己的策略排自己的子项)。两条推论:
- 子容器要自动排布,自己
setLayout(...);不设就保持手摆坐标(本库多数组件就是这样)。 - 给面板挂布局不会再穿透组件内部。引擎 2.7 及以前会把策略递归灌给所有后代容器,而本库每个
组件都是
ICEGroup子类、内部零件(按钮文字、输入框前后缀 / 清除按钮)都在同一个childNodes里, 于是一次setLayout()等于把整个界面的内部零件重摆一遍(实测输入框的文本12 → 0、 清除 按钮(170,6) → (316,0))。回归用例见tests/engineLayout.integration.test.ts。
尺寸协商也走引擎的 Swing 口径:布局问子项的 getPreferredSize();容器没显式声明首选尺寸时
报布局算出的内容尺寸,声明过 setPreferredSize([w, h]) 就报声明值。构造期给的 width/height
只算边界(Swing 的 setBounds),要让父布局按你给的尺寸留位请用 setPreferredSize()。
组件的内部装饰走 painter,不进 childNodes
「给组件挂布局」能不能安心的另一半,取决于组件的 childNodes 里装了什么:
- 装内容(调用方传进来的节点、自己的子控件)→ 正确,布局就该排它们;
- 装装饰(组件自己画的造型)→ 布局会把装饰也当成内容排一遍。
装饰一律交给 painter(Swing 的 ComponentUI 位,见
写一个自己的组件):
ICEAvatar 的圆底与首字、ICESkeleton 的占位条已经迁过去,它们的 childNodes 现在是空的,
怎么挂布局都不会碰到装饰。