11.3.4 GridBagLayout布局管理器

GridBagLayout布局管理器的功能最强大,但也最复杂。

GridBagLayoutGridLayout布局管理器的不同

GridLayout布局管理器不同的是,GridBagLayout布局管理器中,一个组件可以跨越一个或多个网格,并可以设置各网格的大小互不相同,从而增加了布局的灵活性。当窗口的大小发生变化时,GridLayout布局管理器也可以准确地控制窗口各部分的拉伸。

为了处理GridLayoutGUI组件的大小、跨越性,Java提供了GridBagConstraints对象,该对象与特定的GUI组件关联,用于控制该GUI组件的大小、跨越性。

使用GridBagLayout布局管理器的步骤

使用GridBagLayout布局管理器的步骤如下:

  1. 创建GridBagLayout布局管理器,并指定GUI容器使用该布局管理器:
    1
    2
    GridBagLayout gb = new GridBagLayout();
    container.setLayout(gb);
  2. 创建GridBagConstraints对象,并设置该对象的相关属性(用于设置受该对象控制的GUI组件的大小、跨越性等)。
    1
    2
    3
    4
    gbc.grid=2;       //设置受该对象控制的GUI组件位于网格的横向索引
    gbc.grid=1; //设置受该对象控制的GUI组件位于网格的纵向索引
    gbc.gridwidth=2; //设置受该对象控制的GUI组件横向跨越多少网格
    gbc.gridheight=1; //设置受该对象控制的GUI组件纵向跨越多少网格
  3. 调用GridBagLayout对象的方法来建立GridBagConstraints对象和受控制组件之间的关联
    1
    gb.setConstraints(c,gbc);//设置c组件受gbc对象控制
  4. 添加组件,与采用普通布局管理器添加组件的方法完全一样:
    1
    container.add(c);

如果需要向一个容器中添加多个GUI组件,则需要多次重复步骤2~4。由于GridBagConstraints对象可以多次重用,所以实际上只需要创建一个GridBagConstraints对象,每次添加GUI组件之前先改变GridBagConstraints对象的属性即可

从上面介绍中可以看出,使用GridBagLayout布局管理器的关键在于GridBagConstraints,它才是精确控制每个GUI组件的核心类,该类具有如下几个属性。

  • gridxgridy
    • 设置受该对象控制的GUI组件左上角所在网格的横向索引、纵向索引(GridLayout左上角网格的索引为0、0)。这两个值还可以是GridBagConstraints.RELATIVE(默认值),它表明当前组件紧跟在上一个组件之后。
  • gridheightgridwidth:
    • 设置受该对象控制的GUI组件横向、纵向跨越多少个网格,两个属性值的默认值都是1。
    • 如果设置这两个属性值为:GridBagConstraints.REMANDER,这表明受该对象控制的GUI组件是横向、纵向最后一个组件;
    • 如果设置这两个属性值为:GridBagConstraints.RELATIVE,这表明受该对象控制的GUI组件是横向、纵向倒数第二个组件。
  • fill:
    • 设置受该对象控制的GUI组件如何占据空白区域。该属性的取值如下
    • GridBagConstraints.NONEGUI组件不扩大
    • GridBagConstraints.HORIZONTALGUI组件水平扩大以占据空白区域
    • GridBagConstraints.VERTICALGUI组件垂直扩大以占据空白区域。
    • GridBagConstraints.BOTHGUI件水平、垂直同时扩大以占据空白区域
  • ipadxipady:设置受该对象控制的GUI组件横向、纵向内部填充的大小,即在该组件最小尺寸的基础上还需要增大多少。如果设置了这两个属性,则组件横向大小为最小宽度再加ipade*2像素,纵向大小为最小高度再加ipad*2像素。
  • insets:设置受该对象控制的GUI组件的外部填充的大小,即该组件边界和显示区域边界之间的距离。
  • anchor:设置受该对象控制的GUI组件在其显示区域中的定位方式。定位方式如下
    • GridBagConstraints.CENTER(中间)
    • GridBagConstraints.NORTH(上中)
    • GridBagConstraints.NORTHWEST(左上角)
    • GridBagConstraints.NORTHEAST(右上角)
    • GridBagConstraints.SOUTH(下中)
    • GridBagConstraints.SOUTHEAST(右下角)
    • GridBagConstraints.SOUTHWEST(左下角)
    • GridBagConstraints.EAST(右中)
    • GridBagConstraints.WEST(左中)
  • weightweighty:设置受该对象控制的GUI组件占据多余空间的水平、垂直增加比例(也叫权重,即weight的直译),这两个属性的默认值是0,即该组件不占据多余空间。假设某个容器的水平线上包括三个GUI组件,它们的水平增加比例分别是1、2、3,但容器宽度增加60像素时,则第一个组件宽度增加10像素,第二个组件宽度增加20像素,第三个组件宽度增加30像素。如果其增加比例为0,则表示不会增加。

设置某个组件随容器的增大而增大

如果希望某个组件的大小随容器的増大而増大,则必须同时设置控制该组件GridBagConstraints对象的fill属性和weightweighty属性。

程序示例

下面的例子程序示范了如何使用GridBagLayout布局管理器来管理窗口中的10个按钮:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import java.awt.*;

public class GridBagTest {
private Frame f = new Frame("测试窗口");
private GridBagLayout gb = new GridBagLayout();
private GridBagConstraints gbc = new GridBagConstraints();
private Button[] bs = new Button[10];

public void init() {
f.setLayout(gb);
for (int i = 0; i < bs.length; i++) {
bs[i] = new Button("按钮" + i);
}
// 所有组件都可以横向、纵向上扩大
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1;
addButton(bs[0]);
addButton(bs[1]);
addButton(bs[2]);
// 该GridBagConstraints控制的GUI组件将会成为横向最后一个元素
gbc.gridwidth = GridBagConstraints.REMAINDER;
addButton(bs[3]);
// 该GridBagConstraints控制的GUI组件将横向上不会扩大
gbc.weightx = 0;
addButton(bs[4]);
// 该GridBagConstraints控制的GUI组件将横跨两个网格
gbc.gridwidth = 2;
addButton(bs[5]);
// 该GridBagConstraints控制的GUI组件将横跨一个网格
gbc.gridwidth = 1;
// 该GridBagConstraints控制的GUI组件将纵向跨两个网格
gbc.gridheight = 2;
// 该GridBagConstraints控制的GUI组件将会成为横向最后一个元素
gbc.gridwidth = GridBagConstraints.REMAINDER;
addButton(bs[6]);
// 该GridBagConstraints控制的GUI组件将横向跨越一个网格,纵向跨越2个网格。
gbc.gridwidth = 1;
gbc.gridheight = 2;
// 该GridBagConstraints控制的GUI组件纵向扩大的权重是1
gbc.weighty = 1;
addButton(bs[7]);
// 设置下面的按钮在纵向上不会扩大
gbc.weighty = 0;
// 该GridBagConstraints控制的GUI组件将会成为横向最后一个元素
gbc.gridwidth = GridBagConstraints.REMAINDER;
// 该GridBagConstraints控制的GUI组件将纵向上横跨一个网格
gbc.gridheight = 1;
addButton(bs[8]);
addButton(bs[9]);
f.pack();
f.setVisible(true);
}

private void addButton(Button button) {
gb.setConstraints(button, gbc);
f.add(button);
}

public static void main(String[] args) {
new GridBagTest().init();
}
}

运行上面程序,会看到如图11.12所示的窗口。

图11.12

从图11.12中可以看出,虽然设置了按钮4、按钮5横向上不会扩大,但因为按钮4、按钮5的宽度会受上一行4个按钮的影响,所以它们实际上依然会变大;同理,虽然设置了按钮8、按钮9纵向上不会扩大,但因为受按钮7的影响,所以按钮9纵向上依然会变大(但按钮8不会变高)。

推荐使用init方法完成界面的初始化工作

上面程序把需要重复访问的AWT组件设置成成员变量,然后使用init()方法来完成界面的初始化工作,这种做法比前面那种在main方法里把AWT组件定义成局部变量的方式更好。

11.3.3 GridLayout布局管理器

GridLayout布局管理器将容器分割成纵横线分隔的网格,每个网格所占的区域大小相同。当向使用GridLayout布局管理器的容器中添加组件时,默认从左向右、从上向下依次添加到每个网格中。与FlowLayout不同的是,放置在GridLayout布局管理器中的各组件的大小由组件所处的区域来决定(每个组件将自动占满整个区域)。

GridLayout构造器

GridLayout有如下两个构造器:

方法 描述
GridLayout() Creates a grid layout with a default of one column per component, in a single row.
GridLayout(int rows, int cols) 采用指定的行数、列数,默认的横向间距、默认的纵向间距 将容器分割成多个网格。
GridLayout(int rows, int cols, int hgap, int vgap) 采用指定的行数、指定的列数,指定的横向间距、指定的纵向间距将容器分割成多个网格。

程序示例 BorderLayout+GridLayout计算器

如下程序结合BorderLayoutGridLayout开发了一个计算器的可视化窗口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.awt.*;
import static java.awt.BorderLayout.*;

public class GridLayoutTest {
public static void main(String[] args) {
Frame f = new Frame("计算器");
Panel p1 = new Panel();
p1.add(new TextField(30));
f.add(p1, NORTH);
Panel p2 = new Panel();
// 设置Panel使用GridLayout布局管理器
p2.setLayout(new GridLayout(3, 5, 4, 4));
String[] name = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "-", "*", "/", "." };
// 向Panel中依次添加15个按钮
for (int i = 0; i < name.length; i++) {
p2.add(new Button(name[i]));
}
// 默认将Panel对象添加Frame窗口的中间
f.add(p2);
// 设置窗口为最佳大小
f.pack();
// 将窗口显示出来(Frame对象默认处于隐藏状态)
f.setVisible(true);
}
}

上面程序的Frame采用默认的BorderLayout布局管理器,程序向BorderLayout中只添加了两个组件:NORTH区域添加了一个文本框,CENTER区域添加了一个Pane容器,该容器釆用GridLayout布局管理器,Panel容器中添加了15个按钮。运行上面程序,会看到如图11.11所示的运行窗口:

图11.11

多布局管理器嵌套使用

图11.11所示的效果是结合两种布局管理器的例子:Frame使用BorderLayout布局管理器,CENTER区域的Panel使用GridLayout布局管理器。实际上,大部分应用窗口都不能使用一个布局管理器直接做出来,必须采用这种嵌套的方式。

11.3.2 BorderLayout布局管理器

BorderLayout五个部分

BorderLayout将容器分为EASTSOUTHWESTNORTHCENTER五个区域,普通组件可以被放置在这5个区域的任意一个中。BorderLayout布局管理器的布局示意图如图11.8所示。

图11.8

当改变使用BorderLayout的容器大小时

  • NORTHSOUTHCENTER区域的会在水平方向上调整,
  • EASTWESTCENTER区域的会在垂直方向上调整。

BorderLayout特点

使用BorderLayout有如下两个注意点。

  1. 当向使用BorderLayout布局管理器的容器中添加组件时,需要指定要添加到哪个区域中。如果没有指定添加到哪个区域中,则默认添加到中间区域中。
  2. 如果向同一个区域中添加多个组件时,后放入的组件会覆盖先放入的组件

默认使用BorderLayout布局管理器的组件

FrameDialogScrollPane默认使用BorderLayout布局管理器.

BorderLayout构造器

BorderLayout有如下两个构造器

方法 描述
BorderLayout() 使用默认的水平间距、垂直间距创建BorderLayout布局管理器。
BorderLayout(int hgap, int vgap) 使用指定的水平间距、垂直间距创建BorderLayout布局管理器。

使用静态常量指定组件的添加位置

当向使用BorderLayout布局管理器的容器中添加组件时,应该使用BorderLayout类的几个静态常量来指定添加到哪个区域中。BorderLayout有如下几个静态常量:EAST(东)、NORTH(北)、WEST(西)、SOUTH(南)、CENTER(中)。

程序示例

如下例子程序示范了BorderLayout的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.awt.*;
import static java.awt.BorderLayout.*;

public class BorderLayoutTest {
public static void main(String[] args) {
Frame f = new Frame("测试窗口");
// 设置Frame容器使用BorderLayout布局管理器
f.setLayout(new BorderLayout(30, 5));
f.add(new Button("南"), SOUTH);
f.add(new Button("北"), NORTH);
// 默认添加到中间
f.add(new Button("中"));
f.add(new Button("东"), EAST);
f.add(new Button("西"), WEST);
// 设置窗口为最佳大小
f.pack();
// 将窗口显示出来(Frame对象默认处于隐藏状态)
f.setVisible(true);
}
}

运行上面程序,会看到如图11.9所示的运行窗口:

图11.9

从图11.9中可以看出,当使用BorderLayout布局管理器时,每个区域的组件都会尽量去占据整个区域,所以中间的按钮比较大

BorderLayout放入的组件可以少于五个

BorderLayout最多只能放置5个组件,但可以放置少于5个组件,如果某个区域没有放置组件该区域并不会出现空白,旁边区域的组件会自动占据该区域,从而保证窗口有较好的外观。

BorderLayout如何放入超过5个的实际组件

虽然BorderLayout最多只能放置5个组件,但因为容器也是一个组件,所以我们可以先向Panel里添加多个组件,再把Panel添加到BorderLayout布局管理器中,从而让BorderLayout布局管理中的实际组件数远远超出5个

程序示例

下面程序可以证实这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.awt.*;
import static java.awt.BorderLayout.*;

public class BorderLayoutTest2 {
public static void main(String[] args) {
Frame f = new Frame("测试窗口");
// 设置Frame容器使用BorderLayout布局管理器
f.setLayout(new BorderLayout(30, 5));
f.add(new Button("南"), SOUTH);
f.add(new Button("北"), NORTH);
// 创建一个Panel对象
Panel p = new Panel();
// 向Panel对象中添加两个组件
p.add(new TextField(20));
p.add(new Button("单击我"));
// 默认添加到中间,向中间添加一个Panel容器
f.add(p);
f.add(new Button("东"), EAST);
// 设置窗口为最佳大小
f.pack();
// 将窗口显示出来(Frame对象默认处于隐藏状态)
f.setVisible(true);
}
}

上面程序没有向WEST区域添加组件,但向CENTER区域添加了一个Panel容器,该Panel容器中包含了一个文本框和一个按钮。运行上面程序,会看到如图11.10所示的窗口界面

图11.10

从图11.10中可以看出CENTER区域占用了WEST区域,CENTER区域是一个Panel,Panel中包含了两个组件。

11.3 布局管理器

为了使生成的图形用户界面具有良好的平台无关性,Java语言提供了布局管理器这个工具来管理组件在容器中的布局,而不使用直接设置组件位置和大小的方式。
例如通过如下语句定义了一个标签(Label):

1
Label hello = new Label("Hello Java");

为了让这个hello标签里刚好可以容纳“Hello Java”字符串,也就是实现该标签的最佳大小(既没有冗余空间,也没有内容被遮挡),Windows可能应该设置为长100像素,高20像素,但换到UNIX上,则可能需要设置为长120像素,高24像素。当一个应用程序从Windows移植到UNIX上时,程序需要做大量的工作来调整图形界面。

对于不同的组件而言,它们都有一个最佳大小,这个最佳大小通常是平台相关的,程序在不同平台上运行时,相同内容的大小可能不一样。如果让程序员手动控制每个组件的大小、位置,这将给编程带来巨大的困难,为了解决这个问题,Java提供了LayoutManager,LayoutManager可以根据运行平台来调整组件的大小,程序员要做的,只是为容器选择合适的布局管理器。

设置布局管理器

所有的AWT容器都有默认的布局管理器,如果没有为容器指定布局管理器,则该容器使用默认的布局管理器。为容器指定布局管理器通过调用容器对象setlayout方法来完成:

如下代码所示

1
container.setLayout(new XxxLayout());

setLayout方法

方法 描述
void setLayout(LayoutManager mgr) Sets the layout manager for this container.

常用布局管理器

AWT提供了FlowLayoutBorderLayoutGridLayoutGridBagLayoutCardLayout这5个常用的布局管理器,

Swing还提供了一个BoxLayout布局管理器。下面将详细介绍这几个布局管理器

11.3.1 FlowLayout布局管理器

FlowLayout布局管理器中,组件像水流一样向某方向流动(排列),遇到障碍(边界)就折回,重头开始排列。在默认情况下,FlowLayout布局管理器从左向右排列所有组件,遇到边界就会折回下行重新开始

当读者在电脑上输入一篇文章时,所使用的就是FlowLayout布局管理器,所有的文字默认从左向右排列,遇到边界就会折回下一行重新开始。AWT中的FlowLayout布局管理器与此完全类似,只是此时排列的是AWT组件,而不是文字。

构造器

FlowLayout有如下三个构造器

方法 描述
FlowLayout() 创建对齐方式,垂直间距、水平间距都默认FlowLayout布局管理器。
FlowLayout(int align) 创建指定的对齐方式,垂直间距、水平间距默认FlowLayout布局管理器。
FlowLayout(int align, int hgap, int vgap) 创建 指定对齐方式指定的垂直间距、指定水平间距FlowLayout布局管理器

参数说明

上面三个构造器的

  • hgapvgap代表水平间距、垂直间距,为这两个参数传入整数值即可。
  • align表明FlowLayout中组件的排列方向(从左向右、从右向左、从中间向两边等),align参数应该使用FlowLayout类的静态常量FlowLayout.LEFTFlowLayout.CENTERFlowLayout.RIGHT

默认使用FlowLayout布局管理器的组件

PanelApplet默认使用FlowLayout布局管理器。

程序示例

下面程序将一个Frame改为使用FlowLayout布局管理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.awt.*;

public class FlowLayoutTest {
public static void main(String[] args) {
Frame f = new Frame("测试窗口");
// 设置Frame容器使用FlowLayout布局管理器
f.setLayout(new FlowLayout(FlowLayout.LEFT, 20, 5));
// 向窗口中添加10个按钮
for (int i = 0; i < 10; i++) {
f.add(new Button("按钮" + i));
}
// 设置窗口为最佳大小
f.pack();
// 将窗口显示出来(Frame对象默认处于隐藏状态)
f.setVisible(true);
}
}

运行上面程序,会看到如图11.7所示的窗口效果。

图片

图11.7显示了各组件左对齐、水平间距为20、垂直间距为5的分布效果。

pack方法 自动调整窗口大小

上面程序中执行了f.pack();代码,pack()方法是Window容器提供的一个方法,该方法用于将窗口调整到最佳大小。通过Java编写图形用户界面程序时,很少直接设置窗口的大小,通常都是调用pack()方法来将窗口调整到最佳大小。

11.2 AWT容器

容器(Container)是Component的子类,因此容器对象本身也是一个组件,具有组件的所有性质,可以调用Component类的所有方法。

Component类

设置组件大小位置可见性的方法

Component类提供了如下几个常用方法来设置组件的大小、位置和可见性等。

方法 描述
void setLocation(int x, int y) 设置组件的位置
void setLocation(Point p) Moves this component to a new location.
void setSize(int width, int height) 设置组件的大小
void setSize(Dimension d) Resizes this component so that it has width d.width and height d.height
void setBounds(int x, int y, int width, int height) 同时设置组件的位置、大小
void setBounds(Rectangle r) Moves and resizes this component to conform to the new bounding rectangle r
void setVisible(boolean b) 设置该组件的可见性

Container类

访问容器内的组件的方法

容器还可以盛装其他组件,容器类(Container)提供了如下几个常用方法来访问容器里的组件:

方法 描述
Component add(Component comp) Appends the specified component to the end of this container.
Component add(Component comp, int index) Adds the specified component to this container at the given position.
Component getComponent(int n) Gets the nth component in this container.
int getComponentCount() Gets the number of components in this panel.
Component[] getComponents() Gets all the components in this container.

AWT容器分类

AWT主要提供了如下两种主要的容器类型。

  1. Window:可独立存在的顶级窗口。
  2. Panel:可作为容器容纳其他组件,但不能独立存在,必须被添加到其他容器中(如WindowPanel或者Applet等)。

AWT容器继承关系

AWT容器的继承关系图,如图11.3所示。

图11.3

图11.3中显示了AWT容器之间的继承层次,其中FrameDialogAWT编程中常用的组件。

Frame

Frame代表常见的窗口,它是Window类的子类,具有如下几个特点

  • Frame对象有标题,允许通过拖拉来改变窗口的位置、大小
  • 初始化时为不可见,可用setVisible(true)使其显示出来
  • 默认使用BorderLayout作为其布局管理器。

程序示例 创建一个窗口

下面的例子程序通过Frame创建了一个窗口。

1
2
3
4
5
6
7
8
9
10
11
import java.awt.*;

public class FrameTest {
public static void main(String[] args) {
Frame f = new Frame("测试窗口");
// 设置窗口的大小、位置
f.setBounds(30, 30, 250, 200);
// 将窗口显示出来(Frame对象默认处于隐藏状态)
f.setVisible(true);
}
}

运行上面程序,会看到一个简单的窗口。点击窗口右上角的“×”按钮,该窗口不会关闭,这是因为还未为该窗口编写任何事件响应。如果想关闭该窗口,可以通过关闭运行该程序的命令行窗口来关闭该窗口。

Panel

PanelAWT中另一个典型的容器,Panel代表不能独立存在、必须放在其他容器中的容器Panel外在表现为一个矩形区域,该区域内可盛装其他组件。Panel容器存在的意义在于为其他组件提供空间,Panel容器具有如下几个特点

  • 可作为容器来盛装其他组件,为放置组件提供空间。
  • 不能单独存在,必须放置到其他容器中。
  • 默认使用FlowLayout作为其布局管理器。

程序示例 创建Panel

下面的例子程序使用Panel作为容器来盛装一个文本框和一个按钮,并将该Panel对象添加到Frame对象中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.awt.*;

public class PanelTest {
public static void main(String[] args) {
Frame f = new Frame("测试窗口");
// 创建一个Panel容器
Panel p = new Panel();
// 向Panel容器中添加两个组件
p.add(new TextField(20));
p.add(new Button("单击我"));
// 将Panel容器添加到Frame窗口中
f.add(p);
// 设置窗口的大小、位置
f.setBounds(30, 30, 250, 120);
// 将窗口显示出来(Frame对象默认处于隐藏状态)
f.setVisible(true);
}
}

编译、运行上面程序,会看到如图11.5所示的运行窗口:

图11.5

从图11.5中可以看出,使用AWT创建窗口很简单,程序只需要通过Frame创建,然后再创建一些AWT组件,把这些组件添加到Frame创建的窗口中即可。

ScrollPane

ScrollPane是一个带滚动条的容器,它也不能独立存在,必须被添加到其他容器中。ScrollPane容器具有如下几个特点。

  • 可作为容器来盛装其他组件,当组件占用空间过大时,ScrollPane自动产生滚动条。当然也可以通过指定特定的构造器参数来指定默认具有滚动条。
  • 不能单独存在,必须放置到其他容器中。
  • 默认使用BorderLayout作为其布局管理器。ScrollPane通常用于盛装其他容器,所以通常不允许改变ScrollPane的布局管理器

程序示例 创建ScrollPane

下面的例子程序使用ScrollPane容器来代替Panel容器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.awt.*;

public class ScrollPaneTest {
public static void main(String[] args) {
Frame f = new Frame("测试窗口");
// 创建一个ScrollPane容器,指定总是具有滚动条
ScrollPane sp = new ScrollPane(ScrollPane.SCROLLBARS_ALWAYS);
// 向ScrollPane容器中添加两个组件
sp.add(new TextField(20));
sp.add(new Button("单击我"));
// 将ScrollPane容器添加到Frame对象中
f.add(sp);
// 设置窗口的大小、位置
f.setBounds(30, 30, 250, 120);
// 将窗口显示出来(Frame对象默认处于隐藏状态)
f.setVisible(true);
}
}

运行上面程序,会看到如图11.6所示的窗口:

图11.6

图11.6所示的窗口中具有水平、垂直滚动条,这符合使用ScrollPane后的效果。程序明明向ScrollPane容器中添加了一个文本框和一个按钮,但只能看到一个按钮,却看不到文本框,这是为什么呢?这是因为ScrollPane使用BorderLayout布局管理器的缘故,而Borderlayout导致了该容器中只有一个组件被显示出来。下一节将向读者详细介绍布局管理器的知识。

11.1 Java9改进的GUI(图形用户界面)和AWT

GUI的必要性

前面介绍的所有程序都是基于命令行的,基于命令行的程序可能只有一些“专业”的计算机人士才会使用。例如前面编写的五子棋、梭哈等程序,恐怕只有程序员自己才愿意玩这么“糟糕”的游戏,很少有最终用户愿意对着黑糊糊的命令行界面敲命令。

相反,如果为程序提供直观的图形用户界面(Graphics User Interface,GUI),最终用户通过鼠标拖动、单击等动作就可以操作整个应用,整个应用程序就会受欢迎得多(实际上,Windows之所以广为人知,其最初的吸引力就是来自于它所提供的图形用户界面)。作为一个程序设计者,必须优先考虑用户的感受,一定要让用户感到“爽”,程序才会被需要、被使用,这样的程序才有价值。

AWT

JDK1.0发布时,Sun提供了一套基本的GUI类库,这个GUI类库希望可以在所有平台下都能运行,这套基本类库被称为“抽象窗口工具集(Abstract Window Toolkit)”,它为Java应用程序提供了基本的图形组件。AWT是窗口框架,它从不同平台的窗口系统中抽取出共同组件,当程序运行时,将这些组件的创建和动作委托给程序所在的运行平台。简而言之,当使用AWT编写图形界面应用时,程序仅指定了界面组件的位置和行为,并未提供真正的实现,JVM调用操作系统本地的图形界面来创建和平台一致的对等体

使用AWT创建的图形界面应用和所在的运行平台有相同的界面风格,比如在Windows操作系统上它就表现出Windows风格;在UNIX操作系统上,它就表现出UNIX风格。Sun希望采用这种方式来实现“Write Once,Run Anywhere”的目标。

AWT存在的问题

但在实际应用中,AWT出现了如下几个问题。

使用AWT做出的图形用户界面在所有的平台上都显得很丑陋,功能也非常有限。

AWT为了迎合所有主流操作系统的界面设计,AWT组件只能使用这些操作系统上图形界面组件的交集,所以不能使用特定操作系统上复杂的图形界面组件,最多只能使用4种字体。

AWT用的是非常笨拙的、非面向对象的编程模式。

Swing

IFC

1996年,Netscape公司开发了一套工作方式完全不同的GUI库,简称为IFC( Internet Foundation Classes),IFC这套GUI库的所有图形界面组件,例如文本框、按钮等都是绘制在空白窗口上的,只有窗口本身需要借助于操作系统的窗口实现

IFC真正实现了各种平台上的界面一致性。不久,SunNetscape合作完善了这种方法,并创建了一套新的用户界面库:Swing

JFC

AWTSwing、辅助功能API2D API以及拖放API共同组成了JFC(Java Foundation Classes,Java基础类库),其中Swing组件全面替代了Java1.0中的AWT组件,但保留了Java1.1中的AWT事件模型。总体上,AWT是图形用户界面编程的基础,Swing组件替代了绝大部分AWT组件,对AWT图形用户界面编程有极好的补充和加强。

Swing并没有完全替代AWT,而是建立在AWT基础之上,Swing仅提供了能力更强大的用户界面组件,即使是完全采用Swing编写的GUI程序,也依然需要使用AWT的事件处理机制。本章主要介绍AWT组件,这些AWT组件在Swing里将有对应的实现,二者用法基本相似,下一章会有更详细的介绍。

Java9后AWT和Swing组件可自适应屏幕

Java9AWTSwing组件可以自适应高分辨率屏。在Java9之前,如果使用高分辨率屏,由于这种屏幕的像素密度可能是传统显示设备的2~3倍(即单位面积里显示像素更多),而AWTSwing组件都是基于屏幕像素计算大小的,因此这些组件在高分辨率屏上比较小。

Java9对此进行了改进,如果AWTSwing组件在高分辨率屏上显示,那么组件的大小可能会以实际屏幕的2个或3个像素作为“逻辑像素”,这样就可保证AWTSwing组件在高分辨率屏上也具有正常大小。另外,Java9也支持OSX设备的视网膜屏。

简而言之,Java9改进后的AWTSwing组件完全可以在高分辨率屏、视网膜屏上具有正常大小

所有和AWT编程相关的类都放在java.awt包以及它的子包中,AWT编程中有两个基类:ComponentMenuComponent图11.1显示了AWT图形组件之间的继承关系

这里有一张图片

AWT元素分类

java.awt包中提供了两种基类表示图形界面元素:ComponentMenuComponent,其中

  • Component代表一个能以图形化方式显示出来,并可与用户交互的对象,例如Button代表一个按钮,TextField代表一个文本框等;
  • MenuComponent则代表图形界面的菜单组件,包括MenuBar(菜单条)、MenuItem(菜单项)等子类。

除此之外,AWT图形用户界面编程里还有两个重要的概念:ContainerLayoutManager

  • Container是一种特殊的Component,它代表一种容器,可以盛装普通的Component
  • LayoutManager则是容器管理其他组件布局的方式。

本章要点

  • 图形用户界面编程的概念
  • AWT的概念
  • AWT容器和常见布局管理器
  • 使用AWT基本组件
  • 使用对话框
  • 使用文件对话框
  • Java的事件机制
  • 事件源、事件、事件监听器的关系
  • 使用菜单条、菜单、菜单项创建菜单
  • 创建并使用右键菜单
  • 重写paint()方法实现绘图
  • 使用Graphics
  • 使用BufferedImageImagedIO处理位图
  • 使用剪贴板
  • 剪贴板数据风格
  • 拖放功能
  • 拖放目标与拖放源

本章和下一章的内容会比较“有趣”,因为可以看到非常熟悉的窗口、按钮、动画等效果,而这些图形界面元素不仅会让开发者感到更“有趣”,对最终用户也是一种诱惑,用户总是喜欢功能丰富、操作简单的应用,图形用户界面的程序就可以满足用户的这种渴望。

AWT Swing

Java使用AWTSwing类完成图形用户界面编程,其中AWT的全称是抽象窗口工具集(Abstract Window Toolkit),它是Sun最早提供的GUI库,这个GUI库提供了一些基本功能,但这个GUI库的功能比较有限,所以后来又提供了Swing库。通过使用AWTSwing提供的图形界面组件库,Java的图形用户界面编程非常简单,程序只要依次创建所需的图形组件,并以合适的方式将这些组件组织在一起,就可以开发出非常美观的用户界面。

事件处理

程序以一种“搭积木”的方式将这些图形用户组件组织在一起,就是实际可用的图形用户界面,但这些图形用户界面还不能与用户交互,为了实现图形用户界面与用户交互操作,还应为程序提供事件处理,事件处理负责让程序可以响应用户动作。

通过学习本章,读者应该能开发出简单的图形用户界面应用,并提供相应的事件响应机制。本章也会介绍Java中的图形处理、剪贴板操作等知识。

10.7 本章小结

  • 本章主要介绍了Java异常处理机制的相关知识,Java的异常处理主要依赖于trycatchfinallythrowthrows这5个关键字,本章详细讲解了这5个关键字的用法。
  • 本章还介绍了Java异常类之间的继承关系,并介绍了Checked异常和Runtime异常之间的区别。
  • 本章也详细介绍了Java 7对异常处理的增强。
  • 本章还详细讲解了实际开发中最常用的异常链异常转译
  • 本章最后从优化程序的角度,给出了实际应用中处理异常的几条基本规则。

10.6 异常处理规则

前面介绍了使用异常处理的优势、便捷之处,本节将进一步从程序性能优化、结构优化的角度给出异常处理的一般规则。成功的异常处理应该实现如下4个目标

  • 使程序代码混乱最小化。
  • 捕获并保留诊断信息。
  • 通知合适的人员。
  • 采用合适的方式结束异常活动。

下面介绍达到这种效果的基本准则。

10.6.1 不要过度使用异常

不可否认,Java的异常机制确实方便,但滥用异常机制也会带来一些负面影响。过度使用异常主要有两个方面。

  • 把异常和普通错误混淆在一起,不再编写任何错误处理代码,而是以简单地抛岀异常来代替所有的错误处理。
  • 使用异常处理来代替流程控制

熟悉了异常使用方法后,程序员可能不再愿意编写烦琐的错误处理代码,而是简单地抛出异常。实际上这样做是不对的,

  • 对于完全已知的错误,应该编写处理这种错误的代码,增加程序的健壮性;
  • 对于普通的错误,应该编写处理这种错误的代码,增加程序的健壮性。
  • 只有对外部的、不能确定和预知的运行时错误才使用异常

不要使用异常处理来代替正常的业务逻辑判断

必须指出:异常处理机制的初衷是将不可预期异常的处理代码和正常的业务逻辑处理代码分离,因此绝不要使用异常处理来代替正常的业务逻辑判断

不要使用异常处理来代替正常的程序流程控制

另外,异常机制的效率比正常的流程控制效率差,所以不要使用异常处理来代替正常的程序流程控制

例如,对于如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
//定义一个字符串数组
String[] arr ={"Hello","Java","Spring"};
// 使用异常处理来遍历arr数组的每个元素
try{
int i=0;
while(true){
System.out.println(arr[i++]);
}
}
catch(ArrayIndexOutOfBoundsException ex){

}

运行上面程序确实可以实现遍历ar数组元素的功能,但这种写法可读性较差,而且运行效率也不高。程序完全有能力避免产生ArrayIndexOutOfBoundsException异常,程序“故意”制造这种异常,然后使用catch块去捕获该异常,这是不应该的。将程序改为如下形式肯定要好得多:

1
2
3
4
5
6
//定义一个字符串数组
String[] arr ={"Hello","Java","Spring"};
for (int i = 0;i < arr.length; i++)
{
System.out.println(arr[i]);
}

总结

异常只应该用于处理非正常的情况,不要使用异常处理来代替正常的流程控制

对于一些完全可预知,而且处理方式清楚的错误,程序应该提供相应的错误处理代码,而不是将其笼统地称为异常。

10.6.2 不要使用过于庞大的try块

很多初学异常机制的读者喜欢在try块里放置大量的代码,在一个try块里放置大量的代码看上去很简单”,但这种”简单”只是一种假象,只是在编写程序时看上去比较简单。但因为try块里的代码过于庞大,业务过于复杂,就会造成try块中出现异常的可能性大大增加,从而导致分析异常原因的难度也大大增加。

而且当try块过于庞大时,就难免在try块后紧跟大量的catch块才可以针对不同的异常提供不同的处理逻辑。同一个try块后紧跟大量的catch块则需要分析它们之间的逻辑关系,反而增加了编程复杂度。

正确的做法是,把大块的try块分割成多个可能出现异常的程序段落,并把它们放在单独的try块中,从而分别捕获并处理异常

10.6.3 避免使用Catch All语句

所谓Catch All语句指的是一种异常捕获模块,它可以处理程序发生的所有可能异常。例如,如下代码片段:

1
2
3
4
5
6
7
8
9
try
{
//可能引发 Checked异常的代码
}
catch (Throwable t)
{
//进行异常处理
t.printstackTrace();
}

不可否认,每个程序员都曾经用过这种异常处理方式;但在编写关键程序时就应避免使用这种异常处理方式。这种catch All异常处理方式有如下两点不足之处。

  • 所有的异常都采用相同的处理方式,这将导致无法对不同的异常分情况处理,如果要分情况处理,则需要在catch块中使用分支语句进行控制,这是得不偿失的做法。
  • 这种捕获方式可能将程序中的错误、Runtime异常等可能导致程序终止的情况全部捕获到,从而”压制”了异常。如果出现了一些”关键”异常,那么此异常也会被”静悄悄”地忽略。

实际上,Catch All语句不过是一种通过避免错误处理而加快编程进度的机制,应尽量避免在实际应用中使用这种语句。

10.6.4 不要忽略捕获到的异常

不要忽略异常!既然已捕获到异常,那catch块理应做些有用的事情——处理并修复这个错误。catch块整个为空,或者仅仅打印出错信息都是不妥的

  • catch块为空就是假装不知道甚至瞒天过海,这是最可怕的事情——程序出了错误,所有的人都看不到任何异常,但整个应用可能已经彻底坏了。
  • 仅在catch块里打印错误跟踪栈信息稍微好一点,但仅仅比空白多了几行异常信息。

通常建议对异常采取适当措施,比如:

  • 处理异常
    • 对异常进行合适的修复,然后绕过异常发生的地方继续执行;
    • 或者用别的数据进行计算,以代替期望的方法返回值;
    • 或者提示用户重新操作……
    • 总之,对于Checked异常,程序应该尽量修复。
  • 重新抛出新异常。把当前运行环境下能做的事情尽量做完,然后进行异常转译,把异常包装成当前层的异常,重新抛出给上层调用者。
  • 在合适的层处理异常。如果当前层不清楚如何处理异常,就不要在当前层使用catch语句来捕获该异常,直接使用throws声明抛出该异常,让上层调用者来负责处理该异常。

10.5 Java的异常跟踪栈

异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据printStackTrace方法的输出结果,开发者可以找到异常的源头,并跟踪到异常一路触发的过程。
看下面用于测试printStackTrace的例子程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class SelfException extends RuntimeException
{
SelfException(){}
SelfException(String msg)
{
super(msg);
}
}
public class PrintStackTraceTest
{
public static void main(String[] args)
{
firstMethod();
}
public static void firstMethod()
{
secondMethod();
}
public static void secondMethod()
{
thirdMethod();
}
public static void thirdMethod()
{
throw new SelfException("自定义异常信息");
}
}

上面程序中main方法调用firstMethodfirstMethod调用secondMethodsecondMethod调用thirdMethod, thirdMethod直接抛出一个SelfException异常。运行上面程序,会看到如下结果。

1
2
3
4
5
Exception in thread "main" SelfException: 自定义异常信息
at PrintStackTraceTest.thirdMethod(PrintStackTraceTest.java:25)
at PrintStackTraceTest.secondMethod(PrintStackTraceTest.java:21)
at PrintStackTraceTest.firstMethod(PrintStackTraceTest.java:17)
at PrintStackTraceTest.main(PrintStackTraceTest.java:13)

从上面结果可以以看出,异常从thirdMethod方法开始触发,传到secondMethod方法,再传到firstMethod方法,最后传到main方法,在main方法终止,这个过程就是Java的异常跟踪栈。

在面向对象的编程中,大多数复杂操作都会被分解成一系列方法调用。这样可以实现更好的可重用性,将每个可重用的代码单元定义成方法,将复杂任务逐渐分解为更易管理的小型子任务。由于一个大的业务功能需要由多个对象来共同实现,在最终编程模型中,很多对象将通过一系列方法调用来实现通信,执行任务。

所以,面向对象的应用程序运行时,经常会发生一系列方法调用,从而形成“方法调用栈”,异常的传播则相反:只要异常没有被完全捕获(包括异常没有被捕获,或异常被处理后重新抛出了新异常),异常就会从发生异常的方法逐渐 向外传播,首先传给该方法的调用者,该方法调用者再次传给其调用者…直至最后传到main方法,如果main方法依然没有处理该异常,JVM会中止该程序,并打印异常的跟踪栈信息。

异常跟踪栈信息

异常跟踪栈信息它记录了应用程序中执行停止的各个点。

第一行的信息详细显示了异常的类型和异常的详细消息。

接下来跟踪栈记录程序中所有的异常发生点,各行显示被调用方法中执行的停止位置,并标明类类中的方法名、与故障点对应的文件的行编号。一行行地往下看,跟踪栈总是最内部的被调用方法逐渐上传,直到最外部业务操作的起点,通常就是程序的入口main方法或Thread类的run方法(多线程的情形)。

多线程中发生异常的情形

下面例子程序示范了多线程程序中发生异常的情形。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ThreadExceptionTest implements Runnable
{
public void run()
{
firstMethod();
System.out.println("自定义线程正常结束");
}
public void firstMethod()
{
secondMethod();
}
public void secondMethod()
{
int a = 5;
int b = 0;
int c = a / b;
}
public static void main(String[] args)
{
new Thread(new ThreadExceptionTest()).start();
try
{
Thread.sleep(5000);
System.out.println("主线程睡眠结束");
} catch (InterruptedException e)
{
// 打印异常跟踪栈
e.printStackTrace();
}
System.out.println("main线程正常结束");
}

}

运行结果如下:

1
2
3
4
5
6
7
Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
at ThreadExceptionTest.secondMethod(ThreadExceptionTest.java:16)
at ThreadExceptionTest.firstMethod(ThreadExceptionTest.java:10)
at ThreadExceptionTest.run(ThreadExceptionTest.java:5)
at java.lang.Thread.run(Thread.java:745)
主线程睡眠结束
main线程正常结束

程序在Threadrun方法中出现了ArithmeticException异常,这个异常的源头是ThreadExcetpionTestsecondMethod方法,位于ThreadExcetpionTest.java文件的第16行代码。这个异常传播到Thread类的run方法就会结束

结论

对于多线程的情况,当异常传播到Thread类的run方法时,如果该异常没有得到处理,将会导致该线程中止运行

虽然printStackTrace方法可以很方便地用于追踪异常的发生情况,可以用它来调试程序,但在最后发布的程序中,应该避免使用它;而应该对捕获的异常进行适当的处理,而不是简单地将异常的跟踪栈信息打印出来。

总结

  • printStackTrace方法可以打印异常跟踪栈信息。
  • 线程中出现异常,如果不处理,则该线程会终止,
  • 线程中发生的异常不会结束整个程序。
  • 发生在一个线程中的异常不会影响其他线程。