第5章:布局管理器 - 不用手算像素的艺术

第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自由定位复杂布局拼图

费曼测试:你能解释清楚吗?

  1. FillLayout 和 RowLayout 的区别?

- FillLayout:强制均分,所有控件一样大小 - RowLayout:自然排列,控件保持自己大小

  1. grabExcessHorizontalSpace 的作用?

- 占据剩余空间,窗口变大时控件自动拉伸

  1. FormLayout 的附件是什么?

- 控件之间的连接关系,相对定位

  1. 何时使用 GridLayout?

- 表单、表格等需要网格对齐的布局

下一章预告

现在你已经学会了布局管理器,
可以轻松排列控件,
但控件还是"死"的,不会响应用户操作。

下一章,我们将学习事件处理,
让控件"活"起来,
响应用户的每一次点击、输入、移动。

练习题:

  1. 创建一个登录表单,包含:用户名、密码、登录按钮、取消按钮,使用 GridLayout。
  2. 创建一个工具栏,包含 5 个按钮,使用 RowLayout 或 FillLayout。
  3. 创建一个复杂布局,中心一个文本框,周围 4 个按钮,使用 FormLayout。

(提示:登录表单通常用 2 列的 GridLayout,标签在左,输入框在右)

← 返回目录