1. 一行控件排不齐多半是布局方式选错了第一次在 SWT 里画一堆按钮很多人的第一反应是给每个控件setBounds(x, y, w, h)手动摆位置。写完那一刻看着挺整齐等到窗口被拖动、字体换成高 DPI、界面语言切换成长短不一的文案整排控件立刻散架有的压在边框上有的伸到窗口外面有的被裁掉一半。RowLayout 存在的意义就是把这套人肉算坐标的活接过去让父容器按行或按列自动把子控件排列开宽度、高度、间距都由布局管理器算。RowLayout 是 SWT 里最早出现、也是最容易被低估的布局管理器之一。它不像 GridLayout 那样有行列网格和跨列概念也不像 FormLayout 那样需要给每个控件写约束表达式它做的事情非常朴素把所有子控件沿着一个方向依次摆放一行放不下就换行或者干脆不换行、让内容溢出。正因为朴素才会有大量看起来应该生效却没生效的细节——fill和pack打架、wrap打开后父容器高度塌陷、RowData设了却没反应、动态增删控件后界面不刷新。这篇内容适合两类人看一类是刚接触 SWT、正在用桌面端做内部工具或者调试面板的开发者需要快速把界面搭起来另一类是用惯了其他布局体系、切到 SWT 后觉得这些参数怎么都不按常理出牌的老手。下面我会把 RowLayout 的每个参数拆开讲清楚它到底在控制什么、什么时候生效、和哪个参数互斥再结合几个真实的界面场景给出可以直接抄的配置最后把我在动态界面和跨平台打包时踩过的坑完整复盘一遍。1.1 RowLayout 在 SWT 布局体系里的真实定位SWT 自带的布局管理器有五个常用的FillLayout、RowLayout、GridLayout、FormLayout加上功能更垂直的 StackLayout。这几个不是互相替代的关系而是各有各的舒适区。FillLayout 最粗暴把子控件全部填满父容器一个占满、两个对半分适合只有一个主内容区或者一整行等宽按钮的场景。GridLayout 最通用有行列、有跨列、有列宽分配策略代价是每个控件都要配一个 GridData写得啰嗦。FormLayout 适合某个控件的右边紧邻另一个控件这类相对位置关系明确的表单。StackLayout 类似卡片同一时间只显示一个子控件。RowLayout 的舒适区卡在中间那一段子控件数量不定、宽度不固定、需要整体横向排开、但又不需要严格对齐成几列。典型场景是工具栏、标签云、搜索结果里的标签条、一组动态生成的筛选按钮、状态栏上左中右三段信息。这些地方的共同特征是我不知道会有几个控件也不知道每个多宽我只想让它们排成一行放不下就换行。它的底层计算逻辑不复杂父容器宽度方向累加每个子控件的首选宽度加上中间的spacing再加上两侧的margin得到一个总需求宽度高度方向取所有子控件里的最大值。开启wrap之后一旦累加宽度超过父容器可用宽度就往下一行放如此反复。这个模型决定了 RowLayout 的几个先天特点它对齐的粒度是行不是列所以同一行里第二个控件的左边缘永远不会和下一行第二个控件的左边缘自动对齐——需要这种效果就得上 GridLayout这个问题我在第 7 节会专门对比。1.2 一份能直接跑起来的最小配置先给一段能跑的最简代码后面所有参数讨论都基于这个结构。Composite bar new Composite(parent, SWT.NONE); RowLayout layout new RowLayout(SWT.HORIZONTAL); layout.wrap false; // 不换行超出部分直接截断 layout.pack true; // 每个控件用自身最佳尺寸 layout.fill false; // 不强行拉伸 layout.justify false; // 不做两端对齐 layout.marginWidth 8; layout.marginHeight 6; layout.spacing 6; bar.setLayout(layout); Button create new Button(bar, SWT.PUSH); create.setText(新建); Button open new Button(bar, SWT.PUSH); open.setText(打开); Button refresh new Button(bar, SWT.PUSH); refresh.setText(刷新);这段代码有几个必须注意的顺序问题写错了整段布局直接失效new Composite(...)必须在setLayout之前子控件必须setLayout之后创建父级角色的容器最后要调一次shell.pack()或者shell.open()才会真正走一遍布局。如果父容器本身还没拿到尺寸比如刚 new 出来还没显示RowLayout 会以 0 宽度去算wrap为 true 时每个控件各占一行界面看起来就是竖着叠了一列这是新手最常见的第一眼困惑。RowLayout 的所有参数改动都不会自动生效。改完layout.spacing或者layout.marginWidth之后必须调parent.layout(true)或者重设一次 layout界面才会重新排。另外提醒一句Composite的样式常量里SWT.NONE和SWT.BORDER只是画不画边框的区别和布局无关。真正决定排布的是挂在它身上的 Layout 对象这个对象是有状态的同一个 RowLayout 实例不要往两个容器上挂会互相干扰。2. fill、pack、justify 三个开关的联动规则RowLayout 里有三个布尔字段最容易让人迷糊pack、fill、justify。名字看起来都是填充/塞满的意思实际语义完全不同而且它们之间存在明确的生效优先级。我见过不少人把三个全设成 true然后抱怨为什么没有效果——因为它们的组合里有一部分是互斥的。理解这三个开关最有效的方式是想象一排书架上放书。pack决定每本书是按自己的实际厚度放还是被平均分配格位fill决定书是被拉高到和书架同高还是保持原高度justify决定书之间的空隙是如何分配。2.1 pack 为 true 时fill 实际上是哑火的这是 RowLayout 里第一条硬规则pack true时每个子控件严格按照它自己computeSize()返回的首选尺寸摆放fill的设置会被忽略。原因很直白——既然已经决定用你自己的尺寸就不存在拉大填满这回事。想要fill生效必须先把pack关掉RowLayout layout new RowLayout(SWT.HORIZONTAL); layout.pack false; // 交给布局管理器分配空间 layout.fill true; // 子控件在交叉轴上填满在SWT.HORIZONTAL模式下fill控制的是垂直方向每个子控件高度被拉到整行最高控件的水平。这个效果在一行里混着 Button、Text、Label高度参差不齐的场景里非常直观不开fill按钮 24 像素高、输入框 22 像素高、标签 18 像素高视觉上像一排参差的牙齿开了fill之后全部对齐成统一高度看起来是完整的工具条。但fill有一个副作用要注意它会把控件拉伸成和这一行里最高的那个一样高如果某个控件比如多行文本或者带图标的按钮天生比别的控件高出一截其余控件会跟着被撑高整条工具条变得很臃肿。这种时候更稳妥的做法不是开fill而是把过高的控件单独用RowData.height掐住或者拆成两个 Composite 分别排。SWT.VERTICAL模式下fill的作用方向反过来控制的是水平方向所有子控件宽度被拉成整列最宽的那个。垂直 RowLayout 用得比水平少但做侧边栏菜单、竖向排列的过滤条件时非常好用fill true能让菜单项宽度统一不会出现长短不一的按钮。2.2 justify 与 center 的适用边界justify的行为经常被误解成两端对齐。更准确的描述是当某一行没有把父容器宽度占满、行尾还留着空白时justify会把这段空白平均分摊到控件之间让整行在视觉上铺满。它和 CSS 里的space-between不完全一样——SWT 的实现是均匀分配多余空间而不是只把空白塞到最两头。justify真正有价值的场景是wrap true、每行控件数量不一的标签云。开启之后每一行都会自动铺满整个宽度不会出现第一行排满、第二行只放两个、右边空一大块的缺口。关掉它缺口会老老实实留在行尾。如果只有一行控件想让它们整体居中而不是铺开用justify效果会很怪。SWT 较新版本的 RowLayout 有一个center字段置 true 后整行内容居中但不同 SWT 版本对这个字段的支持程度有差异跨版本分发时建议别依赖它。更稳的做法是在 RowLayout 外面套一层同样宽度的 Composite用FillLayout居中或者干脆接受左对齐。pack、fill、justify三者的关系可以用一张表说清楚packfilljustify最终表现true任意false每个控件保持自身首选尺寸行尾可能有空白true任意true控件自身尺寸多余空白分摊到控件之间falsetruefalse控件在交叉轴被拉满主轴按可用空间分配falsefalsefalse控件保持首选尺寸但空间分配权归布局管理器falsetruetrue交叉轴拉满、主轴铺满视觉最满的组合实际项目里我绝大多数时候用的是pack true 其余关闭因为现代桌面 UI 里控件的自然尺寸通常已经是设计好的强行拉伸反而破坏留白节奏。fill true只在做工具条和表头这类明显需要对齐的场景才开。3. wrap 换行背后的尺寸连锁反应wrap是 RowLayout 里唯一一个默认值为true的布尔字段。这个默认值坑过很多人明明只想排一行结果窗口一缩控件突然换行界面高度却没有跟着长第二行控件被活生生裁掉一半。wrap true的含义是父容器宽度不够时往下一行续排。听起来很智能问题在于 RowLayout 计算父容器首选高度时是按所有控件排成一行的假设去算的——它不会预先知道你会在第几个控件处换行也就无法给出一个准确的多行高度。所以在wrap true的容器上调用pack()得到的高度往往只够显示一行多一点。3.1 什么时候必须关掉 wrap判断标准很简单这排控件的数量在这块区域里是不是稳定的、能不能保证一行放得下。如果能直接关掉wrap让所有东西老实待在一行宽度不够时由上层处理比如整体横向滚动。工具栏、状态栏、对话框底部那一排确定性的确定/取消按钮组都该关掉wrap。开着它带来的唯一收益是极端窄窗口下还能看到控件代价是布局高度不可预测而且换行后的视觉非常难看——一个确认按钮被挤到第二行左边怎么看都是 bug。RowLayout toolbarLayout new RowLayout(SWT.HORIZONTAL); toolbarLayout.wrap false; toolbarLayout.pack true;3.2 换行高度塌陷的两种解法真正需要wrap true的场景通常是内容数量不可控的地方标签列表、最近打开的文件、动态生成的筛选条件。这种情况下得自己接管高度计算有两个做法。第一种让容器跟着内容长高。给 RowLayout 的宿主容器在父级布局里留一个可增长的位置比如 GridLayout 的grabExcessVerticalSpace false然后在每次增删控件后调用layout(true, true)再让父级layout()一遍。这要求父级也用了自动布局整条链路都得通。第二种固定容器高度配合 ScrolledComposite。这是我更推荐的做法因为它把内容可能很长这个不确定性关进了一个有边界的盒子里ScrolledComposite sc new ScrolledComposite(parent, SWT.V_SCROLL); sc.setExpandHorizontal(true); sc.setExpandVertical(false); Composite inner new Composite(sc, SWT.NONE); RowLayout layout new RowLayout(SWT.HORIZONTAL); layout.wrap true; layout.pack true; layout.spacing 8; layout.marginWidth 10; layout.marginHeight 10; inner.setLayout(layout); // ... 往 inner 里塞标签 inner.setSize(inner.computeSize(sc.getClientArea().width, SWT.DEFAULT)); sc.setContent(inner); sc.setMinHeight(inner.computeSize(SWT.DEFAULT, SWT.DEFAULT).y);关键在那句setMinHeight不给 ScrolledComposite 设最小高度内容区在滚动容器里可能被压成 0 高什么都看不见。另外每次往inner里加控件之后都要重新算一遍inner.setSize(inner.computeSize(宽, SWT.DEFAULT))否则滚动条不会跟着更新。wrap开着的时候justify才有明确意义。两者搭配能做出整齐的标签云只开wrap不开justify每行尾部参差不齐的缺口会非常显眼。还有一个容易忽略的点wrap的换行判断依赖父容器的客户端宽度也就是扣掉 margin 之后的可用宽度。如果 margin 设得很大而容器本身不宽实际可用空间会小到只能放一两个控件视觉上像是莫名其妙就换行了。调这个参数的时候先把 margin 归零验证一下能省掉不少排查时间。4. margin 家族与 spacing 的覆盖优先级RowLayout 的间距参数一共七个marginWidth、marginHeight、marginTop、marginBottom、marginLeft、marginRight、spacing。看着多规律其实很清晰——前两个是批量设置中间四个是单边精调spacing管的是控件之间的缝隙。默认值方面RowLayout 的marginWidth、marginHeight、spacing都是 3 像素。这个默认值在现代 UI 里偏小直接拿来用会显得非常局促控件几乎贴着边框。我一般把 margin 放到 8 到 12spacing 放到 6 到 10。4.1 marginWidth 是左右批量单边设置会覆盖它marginWidth一次性设置左右两个方向的边距marginHeight设置上下。单独设置marginLeft、marginRight等字段时会覆盖marginWidth对应方向的值。RowLayout layout new RowLayout(SWT.HORIZONTAL); layout.marginWidth 10; // 左右各 10 layout.marginHeight 8; // 上下各 8 layout.marginLeft 20; // 左边单独拉大到 20覆盖 marginWidth这里有个隐蔽的顺序陷阱marginWidth赋值时会同时写marginLeft和marginRight两个字段。所以如果先设marginLeft 20再设marginWidth 10那个 20 会被冲掉。反过来才是预期结果。这个行为在源码里是写死的用的时候记牢批量设置放前面单边微调放后面。marginTop对齐上边框这事儿在工具条里特别常见——想让按钮贴着窗口标题栏但留一点小缝就把marginTop单独调小其余保持正常。这种一边紧三边松的视觉处理比统一调 margin 出来的效果精致得多成本就是多写一行赋值。4.2 spacing 设成负数可以做出分组视觉效果spacing是相邻控件之间的间隔默认 3。它的值可以是负数这一点知道的人不多——负的 spacing 会让控件互相重叠在某些自定义绘制控件上能做出阴影叠加、标签角标压边的效果。不过常规业务界面里不建议动这个重叠带来的点击区域问题很难调试。更实用的手法是用 Composite 分组来制造差异化间距。比如工具条上有三组按钮文件操作、编辑操作、视图操作组内间距 4组间间距 16RowLayout root new RowLayout(SWT.HORIZONTAL); root.spacing 16; // 组与组之间 root.marginWidth 10; Composite fileGroup new Composite(bar, SWT.NONE); RowLayout g1 new RowLayout(SWT.HORIZONTAL); g1.marginWidth 0; g1.marginHeight 0; g1.spacing 4; fileGroup.setLayout(g1); // 组内按钮都挂在 fileGroup 上这个技巧的本质是让每个分组 Composite 只贡献一个控件宽度给外层 RowLayout组内的间距由内层自己管。这比去调每个按钮的 margin 要干净得多而且分组在视觉上更符合真实的产品形态。需要提醒的是内层 Composite 必须把自己的marginWidth、marginHeight归零否则外层的 16 间距加上内层的默认 3 边距实际视觉间距会变成 22比你想要的大一圈。这个边距叠加是排版对不上设计稿时最常被忽略的原因。5. RowData 的三个字段width、height、excludeRowLayout 本身只决定怎么排具体某个控件要多大、要不要参与排布全在它身上的RowData里。RowData 结构极简就三个字段width、height、exclude。但每一个都有明确的优先级规则搞混了就会出现设了没反应。先给个基础写法Text search new Text(bar, SWT.BORDER | SWT.SEARCH); RowData rd new RowData(); rd.width 200; rd.height 24; search.setLayoutData(rd);RowData也提供了RowData(int width, int height)的便捷构造用SWT.DEFAULT表示该维度不限search.setLayoutData(new RowData(200, SWT.DEFAULT));5.1 width 和 height 的优先级高于 fillRowData.width和RowData.height一旦设置对应维度就以它为准fill在那一维上失效。这给了我们一种精确控制的手段整个工具条开fill true让所有控件等高但某个特别宽的搜索框通过RowData.width 220单独定宽两不冲突。不过有个坑必须点出来单行 Text 的高度最终由字体度量决定RowData.height只是把它占的格子撑大控件本身不会跟着变高。这意味着你给单行 Text 设height 40视觉上会看到输入框仍然矮矮的待在格子中间上下留出一截空白。想要真正加高输入框得换SWT.MULTI | SWT.BORDER的多行文本或者改用自定义绘制控件。同理RowData.width对 Button 不一定能精确到位。Button 的宽度在某些平台的系统主题下由主题引擎给出最小宽度RowData.width小于这个最小值时会被顶回去。做等宽按钮组时如果发现宽度对不齐先确认是不是踩了这个最小值限制别急着怀疑布局算错了。5.2 exclude 是动态界面的唯一正确做法exclude true的含义是这个控件在布局计算时被完全忽略就好像它不存在同时控件本身保持不销毁。这个字段是做动态显隐的关键。新手常犯的错误是用setVisible(false)隐藏控件。在 SWT 里setVisible(false)只影响绘制不影响布局——控件还在占的宽度还在界面上会留下一块空白。正确姿势是两者一起用Button advanced new Button(bar, SWT.PUSH); advanced.setText(高级选项); RowData rd new RowData(); rd.exclude true; // 默认就不参与排布 advanced.setLayoutData(rd); advanced.setVisible(false); // 用户点了展开之后 rd.exclude false; advanced.setVisible(true); bar.layout(true, true); // 必须重排否则界面不会变这段代码构成了一个标准的显隐切换模板。两个要点exclude和setVisible必须成对修改只改一个会出现看不见但占位或者看得到但布局没算的错乱改完必须调layout(true, true)第一个参数表示尺寸变了需要重新计算第二个表示递归处理子级。layout(true)会清掉布局缓存并重算layout()只是把已有的缓存应用一次。动态增删控件、改exclude、改RowData之后一律用layout(true, true)。exclude还有一个隐藏用法做成占位符。把某个控件exclude true但setVisible(true)保持可见它会悬浮在自己的位置上而不影响其他控件的排布。这个特性可以用来做浮层提示、角标不过要自己处理它和其他控件的重叠区域业务代码里能不用就不用。6. 布局数据丢失一次完整的排查链路我在一个内部工具上遇到过一个很典型的故障界面上有个动态筛选区用户勾选条件后追加一个标签按钮取消勾选就移除。大部分时候正常但偶尔会出现标签全部挤在左上角、宽高都是 0的情况。前后排查了两天才定位到根因这里把整条链路完整复盘比直接给答案更有参考价值。6.1 现象拆解与第一轮假设现象是标签按钮确实被创建出来了也能点击响应事件但尺寸是 0×0全叠在容器左上角。从 SWT 的角度看尺寸为 0 意味着computeSize没有拿到有效结果或者布局管理器根本没被触发。第一轮我怀疑的是RowData被覆盖。检查代码发现标签的创建逻辑里有这么一段Button tag new Button(container, SWT.PUSH); tag.setText(name); container.setLayout(rowLayout); // 问题在这里 tag.setLayoutData(new RowData(SWT.DEFAULT, 26));setLayout被放在了子控件创建之后。SWT 的规则是父容器在设置 Layout 的那一刻会把当前所有子控件的布局数据纳入管理在此之前通过setLayoutData挂上去的数据会被这次setLayout覆盖成默认值。所以标签的 RowData 在setLayout时被冲掉了宽高全部回落到默认表现就是 0 尺寸。这个坑的阴险之处在于它只在特定调用顺序下出现。正常路径下容器是在初始化时一次性setLayout之后只加子控件完全正常出问题的是那个新建筛选区的分支——它先创建子控件再挂布局顺序反了结果就只在特定操作路径上崩。修复很直接把setLayout提到所有子控件创建之前或者干脆在容器构造完成后立刻设置Composite container new Composite(parent, SWT.NONE); RowLayout rowLayout new RowLayout(SWT.HORIZONTAL); rowLayout.pack true; container.setLayout(rowLayout); // 先设置 Button tag new Button(container, SWT.PUSH); tag.setLayoutData(new RowData(SWT.DEFAULT, 26)); // 后设置数据6.2 第二个坑增删控件后没有触发重排修完顺序问题后新的症状出现了标签的尺寸正常了但删除一个标签之后剩下的标签位置没有重排中间空出一个洞。原因很简单——删控件用的是dispose()但删除之后没有调layout(true, true)。SWT 不会监听子控件的增删事件自动重排必须显式触发。完整的增删模板应该是这样的void addTag(String name) { Button tag new Button(container, SWT.PUSH); tag.setText(name); tag.setLayoutData(new RowData(SWT.DEFAULT, 26)); tag.addListener(SWT.Selection, e - removeTag(tag)); container.layout(true, true); } void removeTag(Button tag) { tag.dispose(); container.layout(true, true); }两个layout调用缺一不可。我当时的代码只在addTag里加了removeTag里忘了写而测试的时候又总是连续添加、不删除所以没测出来。排查到这里我把整个动态区域的刷新统一收口成了一个方法所有涉及增删、显隐、改尺寸的操作都走它从源头上杜绝遗漏private void refreshLayout() { container.layout(true, true); Composite ancestor container.getParent(); while (ancestor ! null ancestor.getLayout() ! null) { ancestor.layout(true, true); ancestor ancestor.getParent(); } }向上逐级刷新看起来有点粗暴但动态界面的容器嵌套往往有好几层ScrolledComposite → 内容区 → 分组 → 标签区只刷新最内层的话外层的滚动条范围和最小高度都不会更新界面表现就是内容加满了但页面不滚。这个循环把整条链路一次性推平实践下来最省心。逐级刷新只对用了自动布局的祖先有效。如果中间某层是绝对定位没setLayout到这里就可以停了继续往上刷也没用。6.3 顺带暴露的第三个问题pack 与 setSize 的顺序同一批故障里还有一个边缘情况主窗口在显示之前调了pack()之后又调了setSize(900, 600)。这两个调用都会触发重排但如果子控件的布局数据是在这中间被修改的最终尺寸可能和预期差一截。处理办法是把所有布局相关的改动集中在窗口显示之前完成pack()放在最后一步之后不要再改RowData// 所有控件创建、RowData 设置、显隐状态初始化 buildUi(shell); shell.pack(); shell.setMinimumSize(shell.getSize()); shell.open();setMinimumSize用pack之后的尺寸作为下限能防止用户把窗口拖到比内容还小、导致 RowLayout 里所有控件被挤变形。这一行在带动态内容的窗口上几乎必加。7. 该换掉 RowLayout 的几种信号RowLayout 好用但它解决的问题边界很清晰。判断是不是该换布局我一般看三个信号需不需要列对齐、需不需要跨行跨列、需不需要内容按比例分配空间。只要命中任何一个就别硬用 RowLayout 往上堆嵌套 Composite 了——嵌套三层之后维护成本会指数级上升。7.1 需要列对齐时直接上 GridLayout最典型的误用场景是表单左边一列标签右边一列输入框。有人用 RowLayout 排一行标签 输入框的组合然后把多行组合垂直堆起来结果因为每个标签文字长度不同右列的输入框全部错位。这时候无论怎么调pack和fill都救不回来因为 RowLayout 压根没有列的概念。正确的做法是 GridLayout两列第一列用GridData固定宽度靠右对齐Composite form new Composite(parent, SWT.NONE); GridLayout gl new GridLayout(2, false); gl.horizontalSpacing 10; gl.verticalSpacing 8; gl.marginWidth 12; gl.marginHeight 12; form.setLayout(gl); Label l1 new Label(form, SWT.NONE); l1.setText(用户名); l1.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false)); Text t1 new Text(form, SWT.BORDER); GridData gd new GridData(SWT.FILL, SWT.CENTER, true, false); gd.widthHint 200; t1.setLayoutData(gd);同一份界面换到 RowLayout 里每一行都要单独一个 Composite每个 Composite 的标签还要手动设成等宽代码量翻三倍还不稳。这就是布局选错的典型代价。7.2 三种常用布局的取舍对照维度RowLayoutGridLayoutFillLayout排列方式单方向依次排列可换行行列网格可跨行列全部均分填满列对齐不支持原生支持不支持动态增删控件友好只需重排友好但列数固定只适合数量固定参数复杂度低三个布尔加几个边距中高每个控件配 GridData极低典型场景工具栏、标签云、状态栏表单、设置页、复杂面板单内容区、等宽按钮组空间分配能力弱强可设 grabExcess均分从表里能看出一条清晰的选型路径先问内容是不是固定数量的等分是就用 FillLayout再问需不需要严格对齐需要就用 GridLayout剩下的、只需要把东西顺次排开的地方才轮到 RowLayout。我个人的经验是一个中大型桌面应用的界面里GridLayout 大概占到六到七成RowLayout 占到两到三成FillLayout 和 FormLayout 分掉剩下的零头。RowLayout 用得最舒服的地方永远是那些横向一串、数量不定、不需要对齐的局部区域把它当主力布局去搭整个窗口最后一定会被列对齐问题逼回来。7.3 几个跨平台相关的实际情况最后说几个只在真实分发时才会遇到的问题。第一个是边距的视觉差异。同一个marginWidth 8在不同系统主题下的观感差别不小系统级控件的内边距本身就不一样。如果界面是给内部用的影响不大如果是会分发给不同环境使用的工具建议把边距集中成几个常量方便统一微调别散落在几十处直接写数字。第二个是控件最小尺寸的限制。Button、Combo、Spinner 这些原生控件在部分平台上存在由系统主题规定的最小高度和宽度RowData设得比这个值小会被顶回来。做紧凑型工具条时先跑一遍确认实际渲染尺寸再定你的spacing和margin否则算出来的总宽度和实际对不上整条工具条会莫名超宽。第三个是pack()的时机。pack()会按当前所有子控件的首选尺寸一次性算好窗口大小但这个首选尺寸依赖字体。如果应用在运行过程中动态换了字体或者切换了语言pack()的结果会失效需要重新调一次。字体切换后界面文字被裁掉这件事根源基本都是这里。我在实际项目里的处理方式是窗口首次显示前pack()一次并记录尺寸作为最小尺寸之后所有字体、语言、主题的变化都只调layout(true, true)逐级刷新不再重新pack()。这样窗口大小保持稳定用户拖好的位置不会因为换个语言就被重置体验上舒服很多。