第5章:布局管理器 - 不用手算像素的艺术
作者:步子哥 (steper@foxmail.com)
5.1 为什么要用布局?
手写坐标的痛苦
// 手写坐标的代码
Label label1 = new Label(shell, SWT.NONE);
label1.setText("标签 1");
label1.setBounds(10, 10, 100, 30);
Label label2 = new Label(shell, SWT.NONE);
label2.setText("标签 2");
label2.setBounds(10, 50, 100, 30);
Label label3 = new Label(shell, SWT.NONE);
label3.setText("标签 3");
label3.setBounds(10, 90, 100, 30);
费曼解释:手写坐标的问题
手写坐标 = 手工砌墙
- 每一块砖都要精确计算位置
- 想要加一块砖,后面的全部调整
- 换个大屏幕,布局乱套
类比:
手写坐标 = 手工记账
- 每笔账都要手写
- 想要修改,要擦了重写
- 换个账本格式,全部重来
布局管理器 = 自动排版系统
布局管理器 = 打印机排版
- 告诉打印机:"把这些字排好"
- 打印机自动计算位置
- 想要修改,打印机自动调整
类比:
布局管理器 = Excel 自动对齐
- 输入数据,自动对齐
- 调整列宽,自动重排
- 不用计算每个格子的位置
不同系统的字体差异
费曼解释:为什么布局在不同系统上有差异?
问题:
- Windows 的字体比 Linux 大
- Mac 的字体间距不同
- 用户可以自定义字体大小
后果:
- 手写坐标:一个系统刚好,另一个系统乱套
- 布局管理器:自动适应,始终整齐
类比:
手写坐标 = 定制西装
- 只适合一个人穿
- 别人穿不合身
布局管理器 = 均码 T 恤
- 谁穿都合适
- 自动适应身材
窗口缩放的考验
代码示例:手写坐标 vs 布局管理器
// 手写坐标:窗口缩放后,控件位置不变
Shell shell = new Shell(display);
shell.setSize(400, 300);
Button button = new Button(shell, SWT.PUSH);
button.setText("按钮");
button.setBounds(10, 10, 100, 30); // 固定位置
shell.open();
// 用户把窗口缩放到 800x600
// 按钮还在 (10, 10),后面一片空白
// 布局管理器:窗口缩放后,控件自动调整
Shell shell = new Shell(display);
shell.setLayout(new FillLayout()); // 使用 FillLayout
Button button = new Button(shell, SWT.PUSH);
button.setText("按钮"); // 不需要设置 bounds,自动填满
shell.open();
// 用户把窗口缩放到 800x600
// 按钮自动填满整个窗口
费曼解释:布局管理器如何响应缩放?
布局管理器 = 橡皮筋
- 窗口变大了,控件拉伸(橡皮筋拉伸)
- 窗口变小了,控件压缩(橡皮筋压缩)
- 始终填满可用空间
类比:
手写坐标 = 刚性的木头
- 尺寸固定,无法伸缩
布局管理器 = 有弹性的橡胶
- 可以拉伸,可以压缩
- 自动适应容器大小
5.2 FillLayout:填满一切
最简单的布局
Shell shell = new Shell(display);
shell.setLayout(new FillLayout()); // 设置布局管理器
Button button1 = new Button(shell, SWT.PUSH);
button1.setText("按钮 1");
Button button2 = new Button(shell, SWT.PUSH);
button2.setText("按钮 2");
费曼解释:FillLayout 的工作原理
FillLayout = 均分蛋糕
- 所有控件平分空间
- 每个人分到一样大
- 一刀切,简单粗暴
类比:
FillLayout = 排队分蛋糕
- 10 个人排一队
- 每人分到 1/10 块蛋糕
- 无论蛋糕多大,都是平均分
水平与垂直排列
// 水平排列(默认)
FillLayout horizontalLayout = new FillLayout(SWT.HORIZONTAL);
shell.setLayout(horizontalLayout);
Button button1 = new Button(shell, SWT.PUSH);
button1.setText("按钮 1");
Button button2 = new Button(shell, SWT.PUSH);
button2.setText("按钮 2");
// 垂直排列
FillLayout verticalLayout = new FillLayout(SWT.VERTICAL);
shell.setLayout(verticalLayout);
Button button3 = new Button(shell, SWT.PUSH);
button3.setText("按钮 3");
Button button4 = new Button(shell, SWT.PUSH);
button4.setText("按钮 4");
代码示例:FillLayout 对比
public class FillLayoutExample {
public static void main(String[] args) {
Display display = new Display();
Shell shell1 = new Shell(display);
shell1.setText("FillLayout 水平");
shell1.setLayout(new FillLayout(SWT.HORIZONTAL));
Button button1 = new Button(shell1, SWT.PUSH);
button1.setText("按钮 1");
Button button2 = new Button(shell1, SWT.PUSH);
button2.setText("按钮 2");
Button button3 = new Button(shell1, SWT.PUSH);
button3.setText("按钮 3");
shell1.setBounds(100, 100, 400, 100);
shell1.open();
Shell shell2 = new Shell(display);
shell2.setText("FillLayout 垂直");
shell2.setLayout(new FillLayout(SWT.VERTICAL));
Button button4 = new Button(shell2, SWT.PUSH);
button4.setText("按钮 1");
Button button5 = new Button(shell2, SWT.PUSH);
button5.setText("按钮 2");
Button button6 = new Button(shell2, SWT.PUSH);
button6.setText("按钮 3");
shell2.setBounds(100, 220, 100, 300);
shell2.open();
while (!shell1.isDisposed() && !shell2.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
}
间距控制
FillLayout layout = new FillLayout(SWT.HORIZONTAL);
layout.marginWidth = 10; // 左右边距
layout.marginHeight = 10; // 上下边距
layout.spacing = 5; // 控件间距
shell.setLayout(layout);
费曼解释:间距的作用
间距 = 相框的边框
- margin:相框和照片的间距
- spacing:照片和照片的间距
类比:
margin = 房间和门的距离
- 房间和门之间留点空隙
- 看起来更舒服
spacing = 门和门的距离
- 两扇门之间留点空隙
- 不至于挤在一起
代码示例:FillLayout 间距
public class FillLayoutSpacingExample {
public static void main(String[] args) {
Display display = new Display();
Shell shell1 = new Shell(display);
shell1.setText("无边距、无间距");
shell1.setLayout(new FillLayout(SWT.HORIZONTAL));
Button button1 = new Button(shell1, SWT.PUSH);
button1.setText("按钮 1");
Button button2 = new Button(shell1, SWT.PUSH);
button2.setText("按钮 2");
shell1.setBounds(100, 100, 400, 80);
shell1.open();
Shell shell2 = new Shell(display);
shell2.setText("有边距、有间距");
FillLayout layout = new FillLayout(SWT.HORIZONTAL);
layout.marginWidth = 10;
layout.marginHeight = 10;
layout.spacing = 5;
shell2.setLayout(layout);
Button button3 = new Button(shell2, SWT.PUSH);
button3.setText("按钮 1");
Button button4 = new Button(shell2, SWT.PUSH);
button4.setText("按钮 2");
shell2.setBounds(100, 200, 400, 80);
shell2.open();
while (!shell1.isDisposed() && !shell2.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
}
5.3 RowLayout:排队走
左右对齐、居中
RowLayout rowLayout = new RowLayout();
rowLayout.type = SWT.HORIZONTAL; // 水平排列
rowLayout.justify = true; // 左右对齐
shell.setLayout(rowLayout);
Button button1 = new Button(shell, SWT.PUSH);
button1.setText("按钮 1");
Button button2 = new Button(shell, SWT.PUSH);
button2.setText("按钮 2");
费曼解释:RowLayout 与 FillLayout 的区别
FillLayout = 强制均分
- 所有控件必须一样大小
- 不管控件内容多少
RowLayout = 自由排队
- 控件保持自己原来的大小
- 按照内容自然排列
类比:
FillLayout = 分蛋糕
- 强制每块一样大
- 不管谁吃多少
RowLayout = 排队买饭
- 每人按自己的饭量打饭
- 自然排队,不强求均分
自动换行
RowLayout rowLayout = new RowLayout(SWT.HORIZONTAL);
rowLayout.wrap = true; // 允许自动换行
shell.setLayout(rowLayout);
for (int i = 1; i <= 20; i++) {
Button button = new Button(shell, SWT.PUSH);
button.setText("按钮 " + i);
}
费曼解释:自动换行的原理
自动换行 = 写文章自动换行
- 写到行尾,自动换到下一行
- 不用数写了多少个字
类比:
RowLayout.wrap = 打字机的自动换行
- 按一下键,写一个字
- 写到行尾,自动跳到下一行
- 不用手动按回车
间距与边距
RowLayout rowLayout = new RowLayout();
rowLayout.marginLeft = 10; // 左边距
rowLayout.marginRight = 10; // 右边距
rowLayout.marginTop = 10; // 上边距
rowLayout.marginBottom = 10; // 下边距
rowLayout.spacing = 5; // 控件间距
shell.setLayout(rowLayout);
代码示例:RowLayout 完整示例
public class RowLayoutExample {
public static void main(String[] args) {
Display display = new Display();
Shell shell1 = new Shell(display);
shell1.setText("RowLayout 水平");
RowLayout horizontalLayout = new RowLayout(SWT.HORIZONTAL);
horizontalLayout.wrap = true;
horizontalLayout.spacing = 5;
shell1.setLayout(horizontalLayout);
for (int i = 1; i <= 10; i++) {
Button button = new Button(shell1, SWT.PUSH);
button.setText("按钮 " + i);
}
shell1.setBounds(100, 100, 300, 150);
shell1.open();
Shell shell2 = new Shell(display);
shell2.setText("RowLayout 垂直");
RowLayout verticalLayout = new RowLayout(SWT.VERTICAL);
verticalLayout.wrap = true;
verticalLayout.spacing = 5;
shell2.setLayout(verticalLayout);
for (int i = 1; i <= 10; i++) {
Button button = new Button(shell2, SWT.PUSH);
button.setText("按钮 " + i);
}
shell2.setBounds(450, 100, 150, 300);
shell2.open();
Shell shell3 = new Shell(display);
shell3.setText("RowLayout 居中对齐");
RowLayout centerLayout = new RowLayout(SWT.HORIZONTAL);
centerLayout.center = true;
shell3.setLayout(centerLayout);
for (int i = 1; i <= 5; i++) {
Button button = new Button(shell3, SWT.PUSH);
button.setText("按钮 " + i);
}
shell3.setBounds(100, 300, 300, 80);
shell3.open();
while (!shell1.isDisposed() && !shell2.isDisposed() && !shell3.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
}
5.4 GridLayout:网格的魔法(最常用)
行列的概念
GridLayout gridLayout = new GridLayout(2, false); // 2 列,不等宽
shell.setLayout(gridLayout);
Label label1 = new Label(shell, SWT.NONE);
label1.setText("姓名:");
GridData gridData1 = new GridData();
label1.setLayoutData(gridData1);
Text text1 = new Text(shell, SWT.BORDER);
GridData gridData2 = new GridData();
gridData2.horizontalAlignment = GridData.FILL;
gridData2.grabExcessHorizontalSpace = true;
text1.setLayoutData(gridData2);
Label label2 = new Label(shell, SWT.NONE);
label2.setText("邮箱:");
Text text2 = new Text(shell, SWT.BORDER);
GridData gridData4 = new GridData();
gridData4.horizontalAlignment = GridData.FILL;
gridData4.grabExcessHorizontalSpace = true;
text2.setLayoutData(gridData4);
费曼解释:GridLayout 的工作原理
GridLayout = 电子表格
- 把容器分成网格(表格)
- 每个控件占据一个或多个格子
- 可以控制控件的大小和对齐
类比:
GridLayout = 棋盘
- 棋盘分成格子
- 每个棋子占据一个格子
- 棋子可以移动到不同格子
跨列
GridLayout gridLayout = new GridLayout(2, false);
shell.setLayout(gridLayout);
// 占据 1 列
Label label = new Label(shell, SWT.NONE);
label.setText("姓名:");
// 占据 2 列
Text text = new Text(shell, SWT.BORDER);
GridData gridData = new GridData();
gridData.horizontalSpan = 2; // 跨 2 列
text.setLayoutData(gridData);
费曼解释:跨列的含义
跨列 = 合并单元格
- Excel 中可以合并多个单元格
- GridLayout 也可以让控件跨列
类比:
跨列 = 两扇门变成一扇大门
- 原本是两个格子
- 合并成一个大格子
- 可以放更大的东西
grabExcessHorizontalSpace
GridLayout gridLayout = new GridLayout(2, false);
shell.setLayout(gridLayout);
Label label = new Label(shell, SWT.NONE);
label.setText("姓名:");
Text text = new Text(shell, SWT.BORDER);
GridData gridData = new GridData();
gridData.horizontalAlignment = GridData.FILL;
gridData.grabExcessHorizontalSpace = true; // 占据剩余空间
text.setLayoutData(gridData);
费曼解释:grabExcessHorizontalSpace 是什么?
grabExcessHorizontalSpace = 占据剩余空间
- 窗口变大,控件自动拉伸
- 就像橡胶绳,可以拉伸
类比:
grabExcessHorizontalSpace = 弹性腰带
- 腰变粗,腰带自动拉伸
- 腰变细,腰带自动收缩
- 始终贴合身体
对齐方式
GridData gridData = new GridData();
gridData.horizontalAlignment = GridData.BEGINNING; // 左对齐
// gridData.horizontalAlignment = GridData.CENTER; // 居中
// gridData.horizontalAlignment = GridData.END; // 右对齐
// gridData.horizontalAlignment = GridData.FILL; // 填满
gridData.verticalAlignment = GridData.CENTER; // 垂直居中
control.setLayoutData(gridData);
代码示例:GridLayout 完整示例
public class GridLayoutExample {
public static void main(String[] args) {
Display display = new Display();
Shell shell = new Shell(display);
shell.setText("GridLayout 示例");
shell.setLayout(new GridLayout(3, false)); // 3 列,不等宽
// 第一行:标题(跨 3 列)
Label titleLabel = new Label(shell, SWT.BORDER | SWT.CENTER);
titleLabel.setText("这是一个表格布局示例");
GridData titleData = new GridData();
titleData.horizontalSpan = 3;
titleData.horizontalAlignment = GridData.FILL;
titleLabel.setLayoutData(titleData);
// 第二行:姓名
Label nameLabel = new Label(shell, SWT.NONE);
nameLabel.setText("姓名:");
Text nameText = new Text(shell, SWT.BORDER);
GridData nameData = new GridData();
nameData.horizontalSpan = 2;
nameData.horizontalAlignment = GridData.FILL;
nameData.grabExcessHorizontalSpace = true;
nameText.setLayoutData(nameData);
// 第三行:邮箱
Label emailLabel = new Label(shell, SWT.NONE);
emailLabel.setText("邮箱:");
Text emailText = new Text(shell, SWT.BORDER);
GridData emailData = new GridData();
emailData.horizontalSpan = 2;
emailData.horizontalAlignment = GridData.FILL;
emailData.grabExcessHorizontalSpace = true;
emailText.setLayoutData(emailData);
// 第四行:性别
Label genderLabel = new Label(shell, SWT.NONE);
genderLabel.setText("性别:");
Button maleRadio = new Button(shell, SWT.RADIO);
maleRadio.setText("男");
Button femaleRadio = new Button(shell, SWT.RADIO);
femaleRadio.setText("女");
femaleRadio.setSelection(true);
// 第五行:备注(跨 3 列)
Label remarkLabel = new Label(shell, SWT.NONE);
remarkLabel.setText("备注:");
Text remarkText = new Text(shell, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL);
GridData remarkData = new GridData();
remarkData.horizontalSpan = 2;
remarkData.horizontalAlignment = GridData.FILL;
remarkData.verticalAlignment = GridData.FILL;
remarkData.grabExcessHorizontalSpace = true;
remarkData.grabExcessVerticalSpace = true;
remarkText.setLayoutData(remarkData);
// 第六行:按钮
Button saveButton = new Button(shell, SWT.PUSH);
saveButton.setText("保存");
GridData saveData = new GridData();
saveData.horizontalAlignment = GridData.END;
saveButton.setLayoutData(saveData);
Button cancelButton = new Button(shell, SWT.PUSH);
cancelButton.setText("取消");
GridData cancelData = new GridData();
cancelData.horizontalAlignment = GridData.END;
cancelButton.setLayoutData(cancelData);
shell.setBounds(100, 100, 400, 300);
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
}
5.5 FormLayout:自由定位
附件的概念
FormLayout formLayout = new FormLayout();
shell.setLayout(formLayout);
Button button1 = new Button(shell, SWT.PUSH);
button1.setText("按钮 1");
FormData formData1 = new FormData();
formData1.left = new FormAttachment(0, 10); // 距离左边 10 像素
formData1.top = new FormAttachment(0, 10); // 距离顶部 10 像素
button1.setLayoutData(formData1);
Button button2 = new Button(shell, SWT.PUSH);
button2.setText("按钮 2");
FormData formData2 = new FormData();
formData2.left = new FormAttachment(button1, 10); // 距离 button1 右边 10 像素
formData2.top = new FormAttachment(button1, 0, SWT.TOP); // 顶部对齐 button1
button2.setLayoutData(formData2);
费曼解释:FormLayout 的工作原理
FormLayout = 拼图
- 每个控件是一块拼图
- 用"附件"把拼图连接起来
- 可以精确定位
类比:
FormLayout = 拉链
- 每个拉链齿是一个控件
- 用拉链把齿连起来
- 每个齿的位置都相对其他齿
相对定位的威力
// 按钮在按钮 1 的右边,距离 10 像素
formData2.left = new FormAttachment(button1, 10);
// 按钮在按钮 1 的底部,距离 5 像素
formData2.top = new FormAttachment(button1, 5, SWT.BOTTOM);
// 按钮在容器的中心
formData2.left = new FormAttachment(50, 0);
formData2.top = new FormAttachment(50, 0);
费曼解释:相对定位的优势
相对定位 = 站队
- "我站他后面 10 厘米"
- "我站她旁边 5 厘米"
- 不用说"我站在东边 100 厘米处"
类比:
相对定位 = 遛狗
- 狗在主人左边 1 米
- 主人走到哪,狗跟着到哪
- 不用说"狗在公园东门 100 米处"
百分比定位
// 控件在容器的 50% 处
FormData formData = new FormData();
formData.left = new FormAttachment(50, 0); // 左边在 50% 处
formData.top = new FormAttachment(50, 0); // 顶部在 50% 处
formData.width = 100; // 宽度 100
formData.height = 30; // 高度 30
control.setLayoutData(formData);
费曼解释:百分比定位的含义
百分比 = 半径的一半
- 容器宽度 400,50% = 200
- 控件左边缘在 200 处
- 容器变大到 800,50% = 400
- 控件左边缘自动移动到 400
类比:
百分比 = 镜子的反射
- 镜子(容器)变大
- 倒影(控件位置)自动调整
- 始终保持相对位置
代码示例:FormLayout 完整示例
public class FormLayoutExample {
public static void main(String[] args) {
Display display = new Display();
Shell shell = new Shell(display);
shell.setText("FormLayout 示例");
shell.setLayout(new FormLayout());
// 按钮 1:左上角
Button button1 = new Button(shell, SWT.PUSH);
button1.setText("按钮 1");
FormData formData1 = new FormData();
formData1.left = new FormAttachment(0, 10);
formData1.top = new FormAttachment(0, 10);
formData1.width = 80;
formData1.height = 30;
button1.setLayoutData(formData1);
// 按钮 2:按钮 1 的右边
Button button2 = new Button(shell, SWT.PUSH);
button2.setText("按钮 2");
FormData formData2 = new FormData();
formData2.left = new FormAttachment(button1, 10);
formData2.top = new FormAttachment(0, 10);
formData2.width = 80;
formData2.height = 30;
button2.setLayoutData(formData2);
// 按钮 3:按钮 1 的下方
Button button3 = new Button(shell, SWT.PUSH);
button3.setText("按钮 3");
FormData formData3 = new FormData();
formData3.left = new FormAttachment(0, 10);
formData3.top = new FormAttachment(button1, 10);
formData3.width = 80;
formData3.height = 30;
button3.setLayoutData(formData3);
// 按钮 4:中心
Button button4 = new Button(shell, SWT.PUSH);
button4.setText("中心");
FormData formData4 = new FormData();
formData4.left = new FormAttachment(50, 0); // 左边在 50%
formData4.top = new FormAttachment(50, 0); // 顶部在 50%
formData4.width = 80;
formData4.height = 30;
button4.setLayoutData(formData4);
// 文本框:底部,跨整个宽度
Text text = new Text(shell, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL);
FormData textData = new FormData();
textData.left = new FormAttachment(0, 10);
textData.right = new FormAttachment(100, -10); // 右边在 100%,回退 10 像素
textData.bottom = new FormAttachment(100, -10); // 底部在 100%,回退 10 像素
textData.height = 100;
text.setLayoutData(textData);
shell.setBounds(100, 100, 400, 300);
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
}
5.6 本章小结
布局管理器总结
| 布局 | 特点 | 适用场景 | 类比 |
|---|---|---|---|
| FillLayout | 均分空间 | 工具栏、状态栏 | 分蛋糕 |
| RowLayout | 排队排列 | 工具栏、按钮组 | 排队买饭 |
| GridLayout | 网格排列 | 表单、表格 | 棋盘 |
| FormLayout | 自由定位 | 复杂布局 | 拼图 |
费曼测试:你能解释清楚吗?
- FillLayout 和 RowLayout 的区别?
- FillLayout:强制均分,所有控件一样大小 - RowLayout:自然排列,控件保持自己大小
- grabExcessHorizontalSpace 的作用?
- 占据剩余空间,窗口变大时控件自动拉伸
- FormLayout 的附件是什么?
- 控件之间的连接关系,相对定位
- 何时使用 GridLayout?
- 表单、表格等需要网格对齐的布局
下一章预告
现在你已经学会了布局管理器,
可以轻松排列控件,
但控件还是"死"的,不会响应用户操作。
下一章,我们将学习事件处理,
让控件"活"起来,
响应用户的每一次点击、输入、移动。
练习题:
- 创建一个登录表单,包含:用户名、密码、登录按钮、取消按钮,使用 GridLayout。
- 创建一个工具栏,包含 5 个按钮,使用 RowLayout 或 FillLayout。
- 创建一个复杂布局,中心一个文本框,周围 4 个按钮,使用 FormLayout。
(提示:登录表单通常用 2 列的 GridLayout,标签在左,输入框在右)