【IT168 技术文章】为应用程序创建一个图形用户界面(graphical user interface(GUI))的过程需要多个开发者和设计师紧密配合以达成设定好的目标。即使在配合最严密的产品小组中,这个过程也经常因为缺乏共同语言而受到影响,这种情况会导致错误理解、基础的简单任务的重复和不理想的结果。
在本文中,我们会讨论设计师和开发者是怎样受益于在 GUI 开发中结合 Java 布局管理器的。Swing 类库包括几个布局管理器,从非常简单的 FlowLayout 管理器到更复杂和灵活的 GridBagLayout 管理器。从我们的经验看,使用布局管理器能够帮助我们在设计和开发过程中得到一致,因为每个布局管理器带来了一组独特的、定义好的设计可能性,开发小组很容易实现这些可能性。
我们会使用一个 GUI 示例,并采用从最简单到最复杂的形式,最简单的形式中 GUI 构建不使用任何布局管理器,最复杂的形式中会使用高级的布局管理器,用来管理控件、列和 GUI 窗口中多余空间的分配。在文章结尾时,您应该会很好地理解布局管理器可以怎样积极地影响 GUI 设计,从而影响 GUI 开发过程。
设计和开发过程
在典型的 GUI 设计和开发过程中,设计师负责创建一系列图形以表示 GUI 的每个屏幕。通常是在一张白色书写板或一张纸上绘出这些图形,然后在一个如 Photoshop 或 Visual Basic 的绘图工具中做成模型。设计师在界面中使用字体、颜色和控件的布局,直到他或她对结果满意为止。这个做成模型的 GUI 就变成交给开发者或开发小组的规范的一部分,他们的任务是实现设计师理想中的 GUI。
在很多情况下,设计师的模型给开发者或开发小组带来很多问题,原因通常是一系列错误的假定。我们首先会讨论这些常见的假定,然后看看 Java 布局管理器能够怎样帮助我们解决它们。
导致错误设计的常见假定情况
首先,设计师可能假定如按钮和标签的控件中包含的字符串文字在大小上一致。如果要向不同国家或地区的用户配置这个应用程序,那么用户可见的字符串就很可能被翻译成那个国家或地区的语言。如果(这是很可能的)翻译过后的字符串的长度和设计师指定的长度不同,较长的字符串就会被控件截断,较短的字符串就会在两头加上多余的空格。
第二,设计师可能假定控件整体窗口的大小。用户经常希望能调整窗口的大小,将窗口调大以使用更大的区域,或将窗口调小从而同时使用更多可见的应用程序窗口。在两种情况中,应该对 GUI 控件的位置和大小都进行调整,从而最好地利用新的空间。当窗口扩展时,虽然用户可能不会希望控件按钮(它包含固定的文字字符串)增大,他们可能希望一个元素(如列表或表)增大,从而显示更多的行和更宽的列。
第三,设计师可能假定原型机上控件的外观会扩展到每个最终用户的机器。Java 语言承诺实现的一件事就是跨平台兼容性。为了达到这一点,“Java 基础类”(Java Foundation Classes(JFC))提供了一套可以运行在不同平台和操作系统上的 GUI 控件。为实现基本的如按钮和文本框的控件,开发者可以依赖“抽象窗口工具包”(Abstract Window Toolkit(AWT))类库。AWT 让 Java 语言能够在每个系统上使用本地(或重量级)窗口小部件(widget),但它只能提供基本的用户界面体验。
对于将使用树列表、表、工具栏、带图形的按钮和其它复杂控件的应用程序,开发者就要转向 JFC 和 Swing 类库。Swing 通过创建画布和实际地建立每个带有低级绘图和鼠标 API 调用的控件实现可移植性。因为没有使用本地窗口小部件,这被称为 轻量级或 模拟窗口小部件工具包。每个控件的实际绘图延迟到一个被称为 外观和感觉的对象。外观和感觉尽力模仿本机控件的外观和特征,所以用户在从本机应用程序切换到 Java 应用程序时会感到有一点不同。
不同的外观和感觉对控件的定位和绘图有很大区别,所以在 Windows 外观和感觉中有同一种外观的 GUI 以 Motif 或 Macintosh 的外观和感觉运行时会有非常不同的外观。没有考虑到这一点而设计和开发的 GUI 在您的 Windows 原型中可能看起来不错,但运行在另一个操作系统(如 Linux 或 Macintosh)上可能看起来很差。
解决这些假定的问题
前三个假定可以通过以一种单独的语言,用不可调整大小的窗口和固定的外观和感觉发布应用程序来避免。然而,显示设置提出的完全是另外一个问题。在部署应用程序的机器上,这些设置和原型机上的设置不一样。例如,用户可以在整个系统设置字体和字体大小以适合他(或她)的可访问性首选项。在这种情况下,用户可见字符串的长短和接收输入(如文本框)的控件会和设计时的不一样。
控件、窗口和字体根据系统的不同而改变,所以您在建立 GUI 时应该牢记这些变量,并根据它们对其作巧妙的调整。Web 应用程序设计师和开发者必须使用的最重要的概念是可流动性。以静态的模型设计 Java GUI 将弄巧成拙;相反,您必须在一个框架中工作,控件的位置和大小由很多变换的变量来管理。
在下面的几节中,我们会使用一个包括一个窗口和几个控件的简单的 GUI。我们会从一个不使用布局管理器的 GUI 设计开始。
不使用布局管理器操作控件
一个 GUI 包括一个有很多控件的顶层窗口。每个控件是一个 Java 类( java.awt.Component 的一个子类)的一个实例。控件的示例有文本框、标签、按钮和列表。最外层的控件,即有标题条和最小化和最大化按钮的控件,是 java.awt.Window 的一个子类。每一个控件都通过给定一个既包括长和宽,又包括 x 和 y 位置的长方形来定位。除了 Window 本身,每个控件有一个父控件。
图 1 展示了我们在本文中要一直使用的 GUI 的最初形式。
图 1. 一个简单的 GUI

清单 1 中是创建图 1 所示窗口的代码。第一行是创建 Frame ,它是最外面窗口要使用的类。下面一行将 frame 的布局管理器设置为空。因为我们没有使用布局管理器,所以控件必须在 frame 内给自己定位。接下来,创建两个按钮并将其添加到 frame,给定其位置和大小。这里的 x 和 y 位置分别是 10,30 和 70,30。座标 x 从左到右,座标 y 从上到下。
清单 1. 有两个按钮的简单 GUI
2 frame.setLayout(null);
3 frame.setBounds(100,100,150,70);
4 Button button1 = new Button("Next");
5 frame.add(button1);
6 button1.setBounds(10,30,50,25);
7 Button button2 = new Button("Previous");
8 frame.add(button2);
9 button2.setBounds(70,30,50,25);
10 frame.setVisible(true);
11
固定座标的问题
根据绝对座标对控件定位(称为 绝对定位)的代码的问题是:标签内的字符串可能被翻译成另一种语言,或者用户可能调整了窗口的大小。在两种情况下,按钮都会保持固定在它们最初的位置。图 2 展示了结果,您可以看到当把 GUI 从英语翻译成法语时会发生什么,还有当用户调整窗口大小时会发生什么。
图 2 在简单 GUI 中使用固定坐标的结果

为了解决这个问题,我们将使用最简单的布局管理器 ― 流程布局管理器。
可内置一个子控件的控件(如窗口)是 java.awt.Container 的一个子类。每一个容器有一个布局管理器,每一个布局管理器负责为容器定位控件。最简单的布局控件 FlowLayout 以从左到右的方式对控件布局。
清单 2 中是这种布局管理器的代码,其中 frame 的布局设置为 FlowLayout 的一个实例,而且那两个按钮被添加到 frame。请注意,设置两个按钮界限来定位和调整它们大小的几行被删除了。当使用布局管理器时,控件不再是通过设置界限来个别地定位。计算它所有子控件的位置和大小是通过布局管理器完成的。
清单 2. 使用流程布局管理器的简单 GUI
2 frame.setLayout(new FlowLayout());
3 frame.setBounds(100,100,160,70);
4 Button button1 = new Button("Suivante");
5 frame.add(button1);
6 Button button2 = new Button("Precedente");
7 frame.add(button2);
8 frame.setVisible(true);
9
如图 3 所示,流程布局管理器将两个按钮定位在可用空间的中间。每个按钮的标签被分配了足够的空间,在两边留下刚好适量的空格。
图 3. FlowLayout 从左到右对控件进行布局

清单 2 不包含设置按钮大小的代码,这可能让您想知道流程布局管理器是怎样判断每个按钮的适当宽度的。这是通过 Component 类的 getPreferredSize() 方法完成的。首选大小是包含宽度和高度的 Dimension 的一个实例。 Button 类被编写成检查其标签和其字体的规格并计算显示整个标签所需的宽度。如果标签变长,首选大小也会改变,布局管理器就会使用新的首选大小。图 4 说明的是一个从英语翻译到法语的 GUI,按钮的大小被增加以适应新的字符串。
图 4. FlowLayout 调整按钮的首选大小

在查询每个控件的首选大小之后,布局管理器根据可用空间设置每个控件的位置。控件的定位让它们不会互相重叠。当窗口变大时, FlowLayout 在可用空间的中心重新定位控件。如果没有足够的空间并排放置控件,布局管理器会将其上下排列,如图 5 所示。
图 5. FlowLayout 将控件上下排列

动态调整 GUI 窗口大小
在上面的 清单 1 和清单 2 中,窗口的实际大小被硬编码成宽度为 160,高度为 70。这会出问题,因为控件本身的大小和位置会根据其中包含的文本而增大或缩小。如果控件对窗口来说变得太大,就会被截断;如果它变得太小,周围的就会有多余空间。图 6 展示了这样的结果,其中字符串对于窗口来说变得太大了。
图 6. 使用静态窗口大小的结果

Window 组件提供 pack() 方法来解决这个问题。当向窗口发送 pack() 时,窗口会调整自身大小,从而提供足够的空间来显示其框架中包含的所有控件。清单 3 展示了写入了 pack() 方法的简单的 GUI。
清单 3. 使用 pack() 方法的简单的 GUI
2 frame.setLayout(new FlowLayout());
3 Button button1 = new Button("Advance Forward");
4 frame.add(button1);
5 Button button2 = new Button(" Revert to Previous");
6 frame.add(button2);
7 frame.pack();
8 frame.setVisible(true);
9
当使用了 pack() 后, Window 会调整自身大小,让控件不会被剪切。
使用方法计算首选大小
从上面的示例应该可以看出,设计 GUI 屏幕时,总是指定固定的大小是危险的。此外,控件的位置不应该以 x 和 y 或宽度和高度的形式表示。每个控件包含动态计算首选大小的方法,应该使用这些方法来给您的 GUI 带来最大的流动性。表 1 展示了计算控件大小的方法的一些示例。
表 1. 计算控件大小的方法控件 描述 方法 按钮 显示当前标签文本 setLabel(String)或 setText(String) 文本 显示很多字符 setColumns(int) 标签 显示标签的文本 setLabel(String)或 setText(String) 文本域 显示很多字符的行和列 setRows(int)和 setColumns(int) 列表 显示很多行 根据添加的行数计算
清单 4 展示了以上每个简单的 GUI 的方法的构造。
清单 4. 使用计算控件首选大小方法的简单的 GUI
2 frame.setLayout(new FlowLayout());
3 Button button1 = new Button("Next");
4 frame.add(button1);
5 TextField text1 = new TextField();
6 text1.setColumns(10);
7 frame.add(text1);
8 Label label1 = new Label("First Name:");
9 frame.add(label1);
10 TextArea textArea1 = new TextArea("This is some text in a text area");
11 textArea1.setRows(2);
12 textArea1.setColumns(10);
13 frame.add(textArea1);
14 List list1 = new List();
15 list1.add("FirstItem");
16 list1.add("SecondItem");
17 list1.add("ThirdItem");
18 list1.add("FourthItem");
19 list1.add("FifthItem");
20 frame.add(list1);
21 frame.pack();
22 frame.setVisible(true);
23
图 7 展示了以上方法对我们的 GUI 的影响
图 7. 使用首选大小方法的结果

直到现在,我们处理的都是最简单的 GUI。我们已经向您展示了怎样不使用布局管理器构建 GUI,每个控件通过 setBounds(Rectangle) 方法接收确定的位置和大小。我们还向您展示了如何使用最简单的布局管理器 FlowLayout ,它使用 getPreferredSize() 方法和单独的控件方法来设定每个控件的大小和位置。使用 FlowLayout ,您只控制组件布局的顺序。对于更多的控件,您将必须使用被称为 约束布局管理器。
约束布局管理器有一个与其关联的约束对象。无论组件在何时被添加到容器中,都是通过一个约束对象来完成,这个对象可以被看成是一个布局提示。 GridBagLayout 是约束布局管理器的一个示例,它有约束对象 GridBagConstraints 。 GridBagLayout 让您将窗口分成一系列逻辑的行和列。
清单 5 展示了我们会怎样将 GridBagLayout 添加到 FlowLayout 示例中使用的五个控件的每一个中。 GridBagConstraints 对象是向 frame 添加每个组件的 add(Component,Object) 方法的第二个参数。
清单 5. 计算首选大小的控件方法
2 frame.setLayout(new GridBagLayout());
3 GridBagConstraints constraints = new GridBagConstraints();
4 Button button1 = new Button("Next");
5 frame.add(button1 , constraints);
6 TextField text1 = new TextField();
7 text1.setColumns(10);
8 constraints.gridx = 1;
9 frame.add(text1 , constraints);
10 Label label1 = new Label("First Name:");
11 constraints.gridx = 2;
12 frame.add(label1 , constraints );
13 TextArea textArea1 = new TextArea("This is some text in a text area");
14 textArea1.setRows(2);
15 textArea1.setColumns(10);
16 constarints.gridx = 0;
17 constraints.gridy = 1;
18 frame.add(textArea1 , constraints );
19 List list1 = new List();
20 list1.add("FirstItem");
21 list1.add("SecondItem");
22 list1.add("ThirdItem");
23 list1.add("FourthItem");
24 list1.add("FifthItem");
25 constraints.gridx = 1;
26 frame.add(list1 , constraints );
27 frame.pack();
28 frame.setVisible(true);
29 List list1 = new List();
30 list1.add("FirstItem");
31 list1.add("SecondItem");
32 list1.add("ThirdItem");
33 list1.add("FourthItem");
34 list1.add("FifthItem");
35 constraints.gridx = 1;
36 frame.add(list1 , constraints );
37 frame.pack();
38 frame.setVisible(true);
39
图 8 展示了使用 GridBagLayout 怎样导致 GUI 窗口上控件分离成列。
图 8. 控件使用 GridBagLayout 的结果

控件布置和列的大小设置
Next 按钮被放在左上格,gridx 为 0,gridy 为 0。文本域的 gridx 约束被设置为 1,所以文本域被放在按钮的下一列。gridx 为 2 的标签被放在再下一个(或第三个)列。
接下来我们看到文本域与被放在 0 号列,它还包括按钮。 GridBagLayout 使用每一列中最大的控件的首选大小来设置该列的大小。这保证了控件在处于对于它不够宽的列或不够高的行中时不会被截断。
对于按钮和文本域,两者都在同一列中,文本域是两者中较大的。列被调整大小以适应文本域,这意味着按钮周围会有剩余的空间。在上一个示例中,这是通过在列的中心放置较小的控件来解决的。
我们更希望得到的效果可能是在列的左侧放置按钮。同样,文本框应该锚定在列的左侧或右侧。
在列的一侧锚定控件保证了控件总是在列增大时增大,从而让用户有可用空间。您可以通过设置 GridBagConstraints 对象的锚定字段来锚定控件。其值为您想将控件固定到的列边缘的方向点。例如,要将按钮固定在左侧,您将使用:
2
这条命令会将控件的左侧锚定到列的左侧。如果和被锚定的控件在同一行中的所有控件的首选高度都比这个控件大,那么按照缺省它也会被垂直放置在中心。如果您想将控件固定到一个特定的角落,可以使用方向点的组合,如下面的示例所示:
2
要让控件锚定在左边同时填满所有可用的空间,可以使用填充属性,如下所示:
2
如果您想既填满水平的也填满垂直的,那么应该使用 Both 值:
2
我们使用同一个 GUI 示例想做的另一件事是使用列表框标签下面的列。这将让列表能够跨越两列。创建这种效果的参数是 GridBagConstraints 的 Gridwidth 字段,如下所示:
2
使用 gridheight 字段,您既可以占用不止一列,也可以指定使用不止一行。在代码中包括所有这些字段的结果如图 9 所示。
图 9. 向控件添加 GridBagConstraints 的结果

下面是包括所有新字段的代码。
清单 6. 添加了 GridBagConstraints 的简单的 GUI
2 frame.setLayout(new GridBagLayout());
3 GridBagConstraints constraints = new GridBagConstraints();
4 Button button1 = new Button("Next");
5 constraints.anchor = GridBagConstraints.WEST;
6 frame.add(button1 , constraints);
7 TextField text1 = new TextField();
8 text1.setColumns(10);
9 constraints.gridx = 1;
10 constraints.fill = GridBagConstraints.HORIZONTAL;
11 frame.add(text1 , constraints);
12 Label label1 = new Label("First Name:");
13 constraints.gridx = 2;
14 frame.add(label1 , constraints );
15 TextArea textArea1 = new TextArea("This is some text in a text area");
16 textArea1.setRows(2);
17 textArea1.setColumns(10);
18 constraints.gridx = 0;
19 constraints.gridy = 1;
20 frame.add(textArea1 , constraints );
21 List list1 = new List();
22 list1.add("FirstItem");
23 list1.add("SecondItem");
24 list1.add("ThirdItem");
25 list1.add("FourthItem");
26 list1.add("FifthItem");
27 constraints.gridx = 1;
28 constraints.gridwidth = 2;
29 frame.add(list1 , constraints );
30 frame.pack();
31 frame.setVisible(true);
32
在窗口中使用剩余空间
迄今为止,我们讨论了使用控件和列的方法;我们想要讨论的另一个元素是窗口中的整体空间,还有在 GUI 的设计和开发中是怎样使用这个空间的。为了说明 GUI 设计中对空间的考虑是多么重要,图 10 展示了简单的 GUI 按缺省定位(即在中心),而且不考虑窗口的流动性时看起来怎样。
图 10. 空间利用很差的 GUI

相对于只是浪费空白上剩余的空间,我们可以利用它来让控件变得更大。按照缺省,控件不会随着窗口大小的增大而增大,因为这对如按钮和标签的控件没有什么意义。然而,文本框和列表框就会得益于这种增大。
为了在窗口大小的扩展事件中指定增量,我们使用了 GridBagConstraints 的 weightx 和 weighty 字段。这些字段从 0 到 1.0 取值,缺省值为 0。当有剩余水平空间时, GridBagLayout 类查询每个控件的 weightx 字段,并按照 weightx 的比例在它们之间划分剩余空间。
使用简单的 GUI 示例,我们可以将 TextField 的 weightx 设为 0.5,列表框的 weightx 设为 1。这样做的结果是剩余水平空间按照 1 比 2 的比例划分给文本域和列表框。另外,将列表框的 weighty 设为 1.0 可以让其增大使用所有剩余垂直空间,如图 11 所示。
图 11. 使用 weightx 和 weighty 字段的结果

当使用 weightx 和 weighty 时,剩余空间不是分配给指定的控件,而是分配给列。控件是否使用剩余空间依赖于控件怎样锚定和填充。要让 图 9 中的列表框使用分配给列的剩余空间,它必须指定为 fill = BOTH ,从而让其水平和垂直地填满来占用剩余的空间。
清单 7 中是我们简单的 GUI 的最后的代码。这个简单的 GUI 是为了可移植性和可扩展性设计的。它在基本的参数(如字符串长度和窗口大小)改变时,利用一个高级的布局管理器来动态地定位和设定控件的大小。
清单 7. 最后的添加了 weightx 和 weighty 参数的简单的 GUI。
2 frame.setLayout(new GridBagLayout());
3 GridBagConstraints constraints = new GridBagConstraints();
4 Button button1 = new Button("Next");
5 constraints.anchor = GridBagConstraints.WEST;
6 frame.add(button1 , constraints);
7 TextField text1 = new TextField();
8 text1.setColumns(10);
9 constraints.gridx = 1;
10 constraints.fill = GridBagConstraints.HORIZONTAL;
11 frame.add(text1 , constraints);
12 Label label1 = new Label("First Name:");
13 constraints.gridx = 2;
14 frame.add(label1 , constraints );
15 TextArea textArea1 = new TextArea("This is some text in a text area");
16 textArea1.setRows(2);
17 textArea1.setColumns(10);
18 constraints.gridx = 0;
19 constraints.gridy = 1;
20 constraints.weightx = 0.5;
21 frame.add(textArea1 , constraints );
22 List list1 = new List();
23 list1.add("FirstItem");
24 list1.add("SecondItem");
25 list1.add("ThirdItem");
26 list1.add("FourthItem");
27 list1.add("FifthItem");
28 constraints.gridx = 1;
29 constraints.gridwidth = 2;
30 constraints.weightx = 1.0;
31 constraints.weighty = 1.0;
32 constraints.fill = GridBagConstraints.BOTH;
33 frame.add(list1 , constraints );
34 frame.pack();
35 frame.setVisible(true);
36
结论
在本文中,我们精确地指出了可能导致差的 GUI 设计和令人失望的开发过程的常见的错误和假设。我们还向您展示了 Java 布局管理器可以怎样纠正这些错误和假设,只要通过为 GUI 设计和开发提供了一种公用的而灵活的框架。使用布局管理器作为 GUI 设计过程的基础让设计师不仅为开发小组展现了原型 GUI 的屏幕截图,还可以更精确地为每个控件指定布局管理器的设置。
这种精确让开发小组在某些时刻(例如字体改变时、窗口大小改变时或应用程序运行在不同的操作系统上时)可以不必第二次猜测开发者的意图以了解 GUI 的外观应该是什么样的。相反,设计师和开发者可以合作来分析和理解 GUI 在运行时的不同状态下会怎样表现。最终,使用布局管理器可以使 GUI 更紧密地符合设计师设定的原始规范,同时更紧密地符合开发小组设定的运行时的需求。