11.5.7 匿名内部类实现监听器

大部分时候,事件处理器都没有复用价值(可复用代码通常会被抽象成业务逻辑方法),因此大部分事件监听器只是临时使用一次,所以使用匿名内部类形式的事件监听器更合适。实际上,这种形式是目前使用最广泛的事件监听器形式。

程序示例 匿名内部类实现监听器

下面程序使用匿名内部类来创建事件监听器:

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 java.awt.event.*;

public class AnonymousEventHandler {
private Frame f = new Frame("测试");
private TextArea ta = new TextArea(6, 40);

public void init() {
// 以匿名内部类的形式来创建事件监听器对象
f.addWindowListener(new WindowAdapter() {
// 实现事件处理方法
public void windowClosing(WindowEvent e) {
System.out.println("用户试图关闭窗口!\n");
System.exit(0);
}
});
f.add(ta);
f.pack();
f.setVisible(true);
}

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

上面程序中使用匿名内部类创建了一个事件监听器对象,“new 监听器接口()”或“new 事件适配器()”的形式就是用于创建匿名内部类形式的事件监听器。

如果事件监听器接口内只包含一个方法,通常会使用Lambda表达式代替匿名内部类创建监听器对象,这样就可以避免烦琐的匿名内部类代码。遗憾的是,如果要通过继承事件适配器来创建事件监听器,那就无法使用Lambda表达式

11.5.6 类本身作为事件监听器类

类本身作为事件监听器类这种形式使用GUI界面类直接作为监听器类,可以直接在GUI界面类中定义事件处理器方法。这种形式非常简洁,也是早期AWT事件编程里比较喜欢采用的形式。但这种做法有如下两个缺点。

  • 这种形式可能造成混乱的程序结构,GUI界面的职责主要是完成界面初始化工作,但此时还需包含事件处理器方法,从而降低了程序的可读性
  • 如果GUI界面类需要继承事件适配器,将会导致该GUI界面类不能继承其他父类。

程序示例 使用GUI界面作为事件监听器

下面程序使用GUI界面类作为事件监听器类:

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
import java.awt.*;
import java.awt.event.*;

// GUI界面类继承WindowAdapter作为事件监听器类
public class SimpleEventHandler extends WindowAdapter {
private Frame f = new Frame("测试");
private TextArea ta = new TextArea(6, 40);

public void init() {
// 将该类的默认对象作为事件监听器对象
f.addWindowListener(this);
f.add(ta);
f.pack();
f.setVisible(true);
}

// GUI界面类直接包含事件处理器方法
public void windowClosing(WindowEvent e) {
System.out.println("用户关闭窗口!\n");
System.exit(0);
}

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

上面程序让GUI界面类继承了WindowAdapter事件适配器,从而可以在该GUI界面类中直接定义事件处理器方法:windowClosing。当为某个组件添加该事件监听器对象时,直接使用this作为事件监听器对象即可。

11.5.4 使用内部类实现监听器

事件监听器是一个特殊的Java对象,实现事件监听器对象有如下几种形式。

  • 内部类形式:将事件监听器类定义成当前类的内部类。
  • 外部类形式:将事件监听器类定义成一个外部类。
  • 类本身作为事件监听器类:让当前类本身实现监听器接口或继承事件适配器
  • 匿名内部类形式:使用匿名内部类创建事件监听器对象

前面示例程序中的所有事件监听器类都是内部类形式,使用内部类可以很好地复用该监听器类,如MultiListener.java程序所示;

内部类形式的监听器类可以自由访问外部类的所有GUI组件这也是内部类的两个优势。

11.5.5 使用外部类实现监听器

使用外部类定义事件监听器类的形式比较少见,主要有如下两个原因。

  • 事件监听器通常属于特定的GUI界面,定义成外部类不利于提高程序的内聚性。
  • 外部类形式的事件监听器不能自由访问创建GUI界面类中的组件,编程不够简洁。

但如果某个事件监听器确实需要被多个GUI界面所共享,而且主要是完成某种业务逻辑的实现,则可以考虑使用外部类形式来定义事件监听器类。

程序示例 外部类作为事件监听器

下面程序定义了一个外部类作为事件监听器类,该事件监听器实现了发送邮件的功能。

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 java.awt.event.*;

public class MailerListener implements ActionListener {
// 该TextField文本框用于输入发送邮件的地址
private TextField mailAddress;

public MailerListener() {
}

public MailerListener(TextField mailAddress) {
this.mailAddress = mailAddress;
}

public void setMailAddress(TextField mailAddress) {
this.mailAddress = mailAddress;
}

// 实现发送邮件
public void actionPerformed(ActionEvent e) {
System.out.println("程序向“" + mailAddress.getText() + "”发送邮件...");
// 发送邮件的真实实现
}
}

上面的事件监听器类没有与任何GUI界面耦合,创建该监听器对象时传入一个TextField对象,该文本框里的字符串将被作为收件人地址。

下面程序使用了该事件监听器来监听窗口中的按钮。

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
import java.awt.*;
import java.awt.event.*;

public class SendMailer {
private Frame f = new Frame("测试");
private TextField tf = new TextField(40);
private Button send = new Button("发送");

public void init() {
// 使用MailerListener对象作为事件监听器
send.addActionListener(new MailerListener(tf));
f.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
f.add(tf);
f.add(send, BorderLayout.SOUTH);
f.pack();
f.setVisible(true);
}

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

上面程序为“发送”按钮添加事件监听器时,将该窗口中的TextField对象传入事件监听器,从而允许事件监听器访问该文本框里的内容。运行上面程序,会看到如图11.23所示的运行界面。

这里有一张图片

11.5.3 事件适配器

事件适配器的作用

事件适配器是监听器接口的空实现
事件适配器实现了监听器接口,并为该接口里的每个方法都提供了实现,这种实现是一种空实现(方法体内没有任何代码的实现)。

当需要创建监听器时,可以通过继承事件适配器,而不是实现监听器接口。因为事件适配器已经为监听器接口的每个方法提供了空实现,所以程序自己的监听器无须实现监听器接口里的每个方法,只需要重写自己感兴趣的方法,从而可以简化事件监听器的实现类代码

只有一个方法的监听器接口直接重写方法即可

如果某个监听器接口只有一个方法,则该监听器接口就无须提供适配器,因为该接口对应的监听器别无选择,只能重写该方法!如果不重写该方法,就没有必要实现该监听器。

表11.2 监听器接口和事件适配器对应表

监听器接口 事件适配器
ContainerListener ContainerAdapter
MouseListener MouseAdapter
FocusListener FocusAdapter
MouseMotionListener MouseMotionAdapter
ComponentListener ComponentAdapter
WindowListener WindowAdapter
KeyListener KeyAdapter

虽然表11.2中只列出了常用的监听器接口对应的事件适配器,实际上,所有包含多个方法的监听器接口都有对应的事件适配器,包括Swing中的监听器接口也是如此。

从表11.2中可以看出,所有包含多个方法的监听器接口都有一个对应的适配器,但只包含一个方法的监听器接口则没有对应的适配器

程序示例

下面程序通过事件适配器来创建事件监听器

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 java.awt.event.*;

public class WindowAdapterTest {
private Frame f = new Frame("测试");
private TextArea ta = new TextArea(6, 40);

public void init() {
f.addWindowListener(new MyListener());
f.add(ta);
f.pack();
f.setVisible(true);
}

class MyListener extends WindowAdapter {
public void windowClosing(WindowEvent e) {
System.out.println("用户关闭窗口!\n");
System.exit(0);
}
}

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

从上面程序中可以看出,窗口监听器继承WindowAdapter事件适配器,只需要重写windowClosing方法即可,这样当用户单击关闭按钮“×”时退出程序。

11.5.2 事件和事件监听器

这里有一张图片

从图11.20中可以看出,当外部动作在AWT组件上进行操作时,系统会自动生成事件对象,这个事件对象是EventObject子类的实例,该事件对象会触发注册到事件源上的事件监听器。

AWT事件机制涉及三个成员: 事件源、事件和事件监听器,其中事件源最容易创建,只要通过new来创建一个AWT组件,该组件就是事件源;事件是由系统自动产生的,无须程序员关心。所以,实现事件监听器是整个事件处理的核心

事件监听器必须实现事件监听器接口,AWT提供了大量的事件监听器接口用于实现不同类型的事件监听器,用于监听不同类型的事件。AWT中提供了丰富的事件类,用于封装不同组件上所发生的特定操作—AWT的事件类都是AWTEvent类的子类,AWTEventEventObject的子类。

EventObject类代表更广义的事件对象,包括Swing组件上所触发的事件、数据库连接所触发的事件等

AWT事件分类

AWT事件分为两大类:

低级事件和高级事件

1. 低级事件

低级事件是指基于特定动作的事件。比如进入、点击、拖放等动作的鼠标事件,当组件得到焦点、失去焦点时触发焦点事件

事件 描述
ComponentEvent 组件事件,当组件尺寸发生变化、位置发生移动、显示/隐藏状态发生改变时触发该事件。
ContainerEvent 容器事件,当容器里发生添加组件、删除组件时触发该事件。
WindowEvent 窗口事件,当窗口状态发生改变(如打开、关闭、最大化、最小化)时触发该事件。
FocusEvent 焦点事件,当组件得到焦点或失去焦点时触发该事件。
KeyEvent 键盘事件,当按键被按下、松开、单击时触发该事件。
MouseEvent 鼠标事件,当进行单击、按下、松开、移动鼠标等动作时触发该事件。
PaintEvent 组件绘制事件,该事件是一个特殊的事件类型,当GUI组件调用update/paint方法来呈现自身时触发该事件,PaintEvent事件并非专用于事件处理模型。

2. 高级事件(语义事件)

高级事件是基于语义的事件,它可以不和特定的动作相关联,而依赖于触发此事件的类。比如,在TextField中按Enter键会触发ActionEvent事件,在滑动条上移动滑块会触发AdjustmentEvent事件,选中项目列表的某一项就会触发ItemEvent事件。

事件 描述
ActionEvent 动作事件,当按钮、菜单项被单击,在TextField中按Enter键时触发该事件。
AdjustmentEvent 调节事件,在滑动条上移动滑块以调节数值时触发该事件。
ItemEvent 选项事件,当用户选中某项,或取消选中某项时触发该事件。
TextEvent 文本事件,当文本框、文本域里的文本发生改变时触发该事件

AWT事件继承层次图如图11.21所示

图片

图11.21中使用粗线框圈出的那些事件是常用的AWT事件。对于没有用粗线框圈出的事件,程序员很少使用它们,它们可能被作为事件基类或作为系统内部实现来使用。

不同的事件需要使用不同的监听器监听,不同的监听器需要实现不同的监听器接口,当指定事件发生后,事件监听器就会调用所包含的事件处理器(实例方法)来处理事件。

表11.1 事件、监听器接口和处理器之间的对应关系

事件 监听器接口 处理器和触发时机
ActionEvent ActionListener actionPerformed
AdjustmentEvent AdjustmentListener adjustmentValueChanged
ContainerEvent Containerlistener
  • componentAdded: 向容器中添加组件时触发
  • componentRemoved: 从容器中删除组件时触发
FocusEvent FocusListener
  • focusGained: 组件得到焦点时触发
  • focusLost: 组件失去焦点时触发
ComponentEvent ComponentListener
  • componentHidden: 组件被隐藏时触发
  • componentMoved: 组件位置发生改变时触发
  • componentResized: 组件大小发生改变时触发
  • componentShown: 组件被显示时触发
KeyEvent KeyListener
  • keyPressed: 按下某个按键时触发
  • keyReleased: 松开某个按键时触发
  • keyTyped: 单击某个按键时触发
MouseEvent MouseListener
  • mouseClicked: 在某个组件上单击鼠标键时触发
  • mouseEntered: 鼠标进入某个组件时触发
  • mouseExited: 鼠标离开某个组件时触发
  • mousePressed: 在某个组件上按下鼠标键时触发
  • mouseReleased: 在某个组件上松开鼠标键时触发
MouseEvent MouseMotionListener
  • mouseDragged: 在某个组件上移动鼠标,且按下鼠标键时触发
  • mouseMoved: 在某个组件上移动鼠标,且没有按下鼠标键时触发
TextEvent TextListener textValueChanged: 文本组件里的文本发生改变时触发
ItemEvent ItemListener itemStateChanged: 某项被选中或取消选中时触发
WindowEvent WindowListener
  • windowActivated: 窗口被激活时触发
  • windowClosed: 窗口调用dispose()即将关闭时触发
  • windowClosing: 用户单击窗口右上角的“x”按钮时触发
  • windowDeactivated: 窗口失去激活时触发
  • windowDeiconified: 窗口被恢复时触发
  • windowIconified:窗口最小化时触发
  • windowOpened: 窗口首次被打开时触发

通过表11.1可以大致知道常用组件可能发生哪些事件,以及该事件对应的监听器接口,通过实现该监听器接口就可以实现对应的事件处理器,然后通过addXxxlistener()方法将事件监听器注册给指定的组件(事件源)。当事件源组件上发生特定事件时,被注册到该组件的事件监听器里的对应方法(事件处理器)将被触发。

ActionListenerAdjustmentListener等事件监听器接口只包含一个抽象方法,这种接口也就是前面介绍的函数式接口,因此可用Lambda表达式来创建监听器对象

事件处理中的事件处理器方法由系统负责调用

实际上,可以如下理解事件处理模型: 当事件源组件上发生事件时,系统将会执行该事件源组件的所有监听器里的对应方法。与前面编程方式不同的是,普通Java程序里的方法由程序主动调用,事件处理中的事件处理器方法由系统负责调用

程序示例 一个监听器监听多个组件

下面程序示范了一个监听器监听多个组件,一个组件被多个监听器监听的效果

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
import java.awt.*;
import java.awt.event.*;

public class MultiListener {
private Frame f = new Frame("测试");
private TextArea ta = new TextArea(6, 40);
private Button b1 = new Button("按钮一");
private Button b2 = new Button("按钮二");

public void init() {
// 创建FirstListener监听器的实例
FirstListener fl = new FirstListener();
// 给b1按钮注册两个事件监听器
b1.addActionListener(fl);
b1.addActionListener(new SecondListener());
// 将f1事件监听器注册给b2按钮
b2.addActionListener(fl);
f.add(ta);
Panel p = new Panel();
p.add(b1);
p.add(b2);
f.add(p, BorderLayout.SOUTH);
f.pack();
f.setVisible(true);
}

class FirstListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
ta.append("第一个事件监听器被触发,事件源是:" + e.getActionCommand() + "\n");
}
}

class SecondListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
ta.append("单击了“" + e.getActionCommand() + "”按钮\n");
}
}

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

上面程序中b1按钮增加了两个事件监听器,当用户单击b1按钮时,两个监听器的actionPerform()方法都会被触发;而且f1监听器同时监听b1b2两个按钮,当b1b2任意一个按钮被单击时,fl监听器的actionPerform()方法都会被触发。

上面程序中调用了ActionEvent对象的getActionCommand()方法,用于获取被单击按钮上的文本。

运行上面程序,分别单击“按钮一”、“按钮二”一次,将看到如图11.2所示的窗口效果

这里有一张图片

程序示例 窗口监听器 可关闭窗体

下面程序为窗口添加窗口监听器,从而示范窗口监听器的用法,并允许用户单击窗口右上角的“×”按钮来结束程序。

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
import java.awt.*;
import java.awt.event.*;

public class WindowListenerTest {
private Frame f = new Frame("测试");
private TextArea ta = new TextArea(6, 40);

public void init() {
// 为窗口添加窗口事件监听器
f.addWindowListener(new MyListener());
f.add(ta);
f.pack();
f.setVisible(true);
}

// 实现一个窗口监听器类
class MyListener implements WindowListener {
public void windowActivated(WindowEvent e) {
ta.append("窗口被激活!\n");
}

public void windowClosed(WindowEvent e) {
ta.append("窗口被成功关闭!\n");
}

public void windowClosing(WindowEvent e) {
ta.append("用户关闭窗口!\n");
System.exit(0);
}

public void windowDeactivated(WindowEvent e) {
ta.append("窗口失去焦点!\n");
}

public void windowDeiconified(WindowEvent e) {
ta.append("窗口被恢复!\n");
}

public void windowIconified(WindowEvent e) {
ta.append("窗口被最小化!\n");
}

public void windowOpened(WindowEvent e) {
ta.append("窗口初次被打开!\n");
}
}

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

上面程序详细监听了窗口的每个动作,当用户单击窗口右上角的每个按钮时,程序都会做出相应的响应,当用户单击窗口中的关闭按钮“×”时,程序将正常退出。

事件适配器

大部分时候,程序无须监听窗口的每个动作,只需要为用户单击窗口中的关闭按钮“×”提供响应即可。无须为每个窗口事件提供响应,程序只想重写windowClosing事件处理器,但因为该监听器实现了WindowListener接口,实现该接口就不得不实现该接口里的每个抽象方法,这是非常烦琐的事情。为此,AWT提供了事件适配器

11.5 事件处理

前面介绍了如何放置各种组件,从而得到了丰富多彩的图形界面,但这些界面还不能响应用户的任何操作。比如单击前面所有窗口右上角的“×”按钮,但窗口依然不会关闭。因为在AWT编程中,所有事件必须由特定对象(事件监听器)来处理,而Frame和组件本身并没有事件处理能力

11.5.1 Java事件模型的流程

为了使图形界面能够接收用户的操作,必须给各个组件加上事件处理机制。

事件处理三要素

在事件处理的过程中,主要涉及三类对象。

  • EventSource(事件源):事件发生的场所,通常就是各个组件,例如按钮、窗口、菜单等。
  • Event(事件):事件封装了GUI组件上发生的特定事情(通常就是一次用户操作)。如果程序需要获得GUI组件上所发生事件的相关信息,都通过Event对象来取得
  • EventListener(事件监听器):负责监听事件源所发生的事件,并对各种事件做出响应处理。

当用户单击一个按钮,或者单击某个菜单项,或者单击窗口右上角的状态按钮时,这些动作就会触发一个相应的事件,该事件由AWT封装成相应的Event对象,该事件会触发事件源上注册的事件监听器(特殊的Java对象),事件监听器调用对应的事件处理器(事件监听器里的实例方法)来做出相应的响应。

委派式事件处理方式

AWT的事件处理机制是一种委派式(Delegation)事件处理方式:
普通组件(事件源)将事件的处理工作委托给特定的对象(事件监听器);当该事件源发生指定的事件时,就通知所委托的事件监听器,由事件监听器来处理这个事件。

每个组件均可以针对特定的事件指定一个或多个事件监听对象,每个事件监听器也可以监听一个或多个事件源。因为同一个事件源上可能发生多种事件,委派式事件处理方式可以把事件源上可能发生的不同的事件分别授权给不同的事件监听器来处理;同时也可以让一类事件都使用同一个事件监听器来处理。

生活中的委派式事件处理方式

例如某个单位如果发生了打架斗殴事件,则委派给公安局(事件监听器)处理;
而公安局也会同时监听这些打架斗殴事件。

这种委派式处理方式将事件源和事件监听器分离,从而提供更好的程序模型,有利于提高程序的可维护性

图11.20显示了AWT的事件处理流程示意图

这里有一张图片

程序示例

下面以一个简单的HelloWorld程序来示范AWT事件处理。

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
import java.awt.*;
import java.awt.event.*;

public class EventQs {
private Frame f = new Frame("测试事件");
private Button ok = new Button("确定");
private TextField tf = new TextField(30);

public void init() {
// 注册事件监听器
ok.addActionListener(new OkListener()); // 1
f.add(tf);
f.add(ok, BorderLayout.SOUTH);
f.pack();
f.setVisible(true);
}

// 定义事件监听器类
class OkListener implements ActionListener // 2
{
// 下面定义的方法就是事件处理器,用于响应特定的事件
public void actionPerformed(ActionEvent e) // 3
{
System.out.println("用户单击了ok按钮");
tf.setText("Hello World");
}
}

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

上面程序 3 号代码定义的方法就是事件处理器。当程序中的Oκ按钮被单击时,该处理器被触发,将看到程序中t文本框内变为“HelloWorld”,而程序控制台打印出“用户单击了OK按钮”字符串。

运行效果如下图所示:

AWT事件处理机制步骤

从上面程序中可以看出,实现AWT事件处理机制的步骤如下:

  • 实现事件监听器类,该监听器类是一个特殊的Java类,必须实现一个XxxListener接口。
  • 创建普通组件(事件源),创建事件监听器对象。
  • 调用addXxxListener()方法将事件监听器对象注册给普通组件(事件源)。当事件源上发生指定事件时,AWT会触发事件监听器,由事件监听器调用相应的方法(事件处理器)来处理事件,事件源上所发生的事件会作为参数传入事件处理器。

11.4 AWT常用组件

AWT组件需要调用运行平台的图形界面来创建和平台一致的对等体,因此AWT只能使用所有平台都支持的公共组件,所以AWT只提供了一些常用的GUI组件。

11.4.1 基本组件

AWT提供了如下基本组件:

AWT组件 描述
Button 按钮,可接受单击操作。
Canvas 用于绘图的画布。
Checkbox 复选框组件(也可变成单选框组件)。
CheckboxGroup 用于将多个Checkbox组件组合成一组,一组Checkbox组件将只有一个可以被选中,即全部变成单选框组件。
Choice 下拉式选择框组件。
Frame 窗口,在GUI程序里通过该类创建窗口。
Label 标签类,用于放置提示性文本。
List 列表框组件,可以添加多项条目。
Panel 不能单独存在基本容器类,必须放到其他容器中。
Scrollbar 滑动条组件。如果需要用户输入位于某个范围的值,就可以使用滑动条组件,比如调色板中设置RGB的三个值所用的滑动条。当创建一个滑动条时,必须指定它的方向、初始值滑块的大小、最小值和最大值。
TextArea 多行文本域。
TextField 单行文本框。

这些AWT组件的用法比较简单,读者可以査阅API文档来获取它们各自的构造器、方法等详细信息。

程序示例

下面的例子程序示范了它们的基本用法。

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Checkbox;
import java.awt.CheckboxGroup;
import java.awt.Choice;
import java.awt.Color;
import java.awt.Frame;
import java.awt.List;
import java.awt.Panel;
import java.awt.TextArea;
import java.awt.TextField;

import javax.swing.Box;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;

public class CommonComponent {
// 创建窗体
Frame frame = new Frame("测试");
// 定义一个按钮
Button button = new Button("确认");
// 定义复选框组,复选组中的选项只能选择一项
CheckboxGroup checkboxGroup = new CheckboxGroup();
// 定义一个单选框(处于cbg组),默认选中
Checkbox maleCheckbox = new Checkbox("男", checkboxGroup, true);
// 定义一个单选框(处于cbg组),默认不选中
Checkbox femaleCheckbox = new Checkbox("女", checkboxGroup, false);
// 定义一个复选框,初始处于没有选中状态
Checkbox marriedCheckbox = new Checkbox("是否已婚?", false);
// 定义一个下拉选择框
Choice colorChoice = new Choice();
// 定义一个列表选择框,允许多选
List colorList = new List(6, true);
// 定义一个5行、20列的多行文本域
TextArea textArea = new TextArea(5, 20);
// 定义一个50列的单行文本域
TextField textField = new TextField(50);

public void init() {
textField.setText("textField");
textField.setBackground(Color.PINK);
textArea.setText("textArea");
textArea.setBackground(Color.CYAN);

// 给 下拉选择框 添加选项
colorChoice.add("红色");
colorChoice.add("绿色");
colorChoice.add("蓝色");
colorChoice.setBackground(Color.YELLOW);

// 给 列表选择框 添加选项
colorList.add("红色");
colorList.add("绿色");
colorList.add("蓝色");
colorList.setBackground(Color.ORANGE);

// 创建一个装载了文本框、按钮的Panel
Panel southInFrame = new Panel();
southInFrame.setBackground(Color.BLUE);
southInFrame.add(textField);
southInFrame.add(button);

// 放在窗体的南部(最下方)
frame.add(southInFrame, BorderLayout.SOUTH);

// 创建一个装载了下拉选择框、三个Checkbox的Panel
Panel checkPanel = new Panel();
checkPanel.setBackground(Color.green);
checkPanel.add(colorChoice);
checkPanel.add(maleCheckbox);
checkPanel.add(femaleCheckbox);
checkPanel.add(marriedCheckbox);

// 创建一个垂直排列组件的Box,盛装多行文本域、Panel
Box leftInTop = Box.createVerticalBox();
LineBorder lineBorder = new LineBorder(Color.BLACK, 2);
TitledBorder titledBorder = new TitledBorder(lineBorder, "leftInTop");
leftInTop.setBorder(titledBorder);
leftInTop.add(textArea);
leftInTop.add(checkPanel);

// 创建一个水平排列组件的Box,盛装topLeft、colorList
Box top = Box.createHorizontalBox();
LineBorder border = new LineBorder(Color.RED, 2);
TitledBorder titledBorder2 = new TitledBorder(border,"top");
top.setBorder(titledBorder2);
top.add(leftInTop);
top.add(colorList);

// 将top Box容器添加到窗口的中间
frame.add(top);
frame.pack();
frame.setVisible(true);
}

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

关于AWT常用组件的用法,以及布局管理器的用法,读者可以参考API文档来逐渐熟悉它们。旦掌握了它们的用法之后,就可以借助于IDE工具来设计GUI界面,使用IDE工具可以更快地设计出更美观的GUI界面。

运行效果如下图所示:

11.4.2 对话框(Dialog)

DialogWindow类的子类,是一个容器类,属于特殊组件。对话框是可以独立存在的顶级窗口,因此用法与普通窗口的用法几乎完全一样。但对话框有如下两点需要注意。

  • 对话框通常依赖于其他窗口,就是通常有一个parent窗口。
  • 对话框有非模式(non-Modal)和模式(modal)两种,当某个模式对话框被打开之后,该模式对话框总是位于它依赖的窗口之上;在模式对话框被关闭之前,它依赖的窗口无法获得焦点。
  • owner:指定该对话框所依赖的窗口,既可以是窗口,也可以是对话框。
  • title:指定该对话框的窗口标题
  • modal:指定该对话框是否是模式的,可以是truefalse

程序示例 模式对话框 非模式对话框

下面的例子程序示范了模式对话框和非模式对话框的用法

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
import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Dialog;
import java.awt.Frame;

public class DialogTest
{
Frame f = new Frame("测试");
Dialog d1 = new Dialog(f, "模式对话框" , true);
Dialog d2 = new Dialog(f, "非模式对话框" , false);
Button b1 = new Button("打开模式对话框");
Button b2 = new Button("打开非模式对话框");
public void init()
{
d1.setBounds(20 , 30 , 300, 400);
d2.setBounds(20 , 30 , 300, 400);
b1.addActionListener(e -> d1.setVisible(true));
b2.addActionListener(e -> d2.setVisible(true));
f.add(b1);
f.add(b2 , BorderLayout.SOUTH);
f.pack();
f.setVisible(true);
}
public static void main(String[] args)
{
new DialogTest().init();
}
}

运行效果如下图所示:

图

上面程序创建了d1d2两个对话框,其中d1是一个模式对话框,而d2是一个非模式对话框(两个对话框都是空的)。该窗口中还提供了两个按钮,分别用于打开模式对话框和非模式对话框。

  • 打开模式对话框后鼠标无法激活原来的“测试窗口”;
  • 但打开非模式对话框后还可以激活原来的“测试窗口”。

在上面的程序中,不管是模式对话框还是非模式对话框,打开后都无法关闭它们,因为程序没有为这两个对话框编写事件监听器。

应该使用模式对话框的情况

如果主程序需要对话框里接收的输入值,则应该把该对话框设置成模式对话框,因为模式对话框会阻塞该程序;如果把对话框设置成非模式对话框,则可能造成对话框被打开了,但用户并没有操作该对话框,也没有向对话框里进行输入,这就会引起主程序的异常。

文件对话框

Dialog类还有一个子类:FileDialog,它代表一个文件对话框,用于打开或者保存文件。FileDialog也提供了几个构造器,可分别支持parenttitlemode三个构造参数,其中parenttitle指定文件对话框的所属父窗口和标题;而mode指定该窗口用于打开文件或保存文件,该参数支持如下两个参数值:FileDialog.LOADFileDialog.SAVE

FileDialog不能指定是模式对话框或非模式对话框,因为FileDialog依赖于运行平台的实现,如果运行平台的文件对话框是模式的,那么FileDialog也是模式的;否则就是非模式的

打开路径

FileDialog提供了如下两个方法来获取被打开保存文件的路径

方法 描述
String getDirectory() 获取FileDialog被打开/保存文件的绝对路径。
String getFile() 获取FileDialog被打开/保存文件的文件名。

程序示例 java调用平台的文件对话框

下面程序分别示范了使用FileDialog来创建打开保存文件的对话框

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
import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.FileDialog;
import java.awt.Frame;

public class FileDialogTest {
Frame f = new Frame("测试");
// 创建两个文件对话框
FileDialog load = new FileDialog(f, "选择需要打开文件", FileDialog.LOAD);
FileDialog save = new FileDialog(f, "选择保存文件的路径", FileDialog.SAVE);
Button b1 = new Button("打开文件");
Button b2 = new Button("保存文件");

public void init() {
b1.addActionListener(e -> {
load.setVisible(true);
// 打印出用户选择的文件路径和文件名
System.out.println("正在打开文件:" + load.getDirectory() + load.getFile());
});
b2.addActionListener(e -> {
save.setVisible(true);
// 打印出用户选择的文件路径和文件名
System.out.println("正在保存文件:" + save.getDirectory() + save.getFile());
});
f.add(b1);
f.add(b2, BorderLayout.SOUTH);
f.pack();
f.setVisible(true);
}

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

运行上面程序,单击主窗口中的“打开文件”按钮,将看到如图11.19所示的文件对话框窗口

图11.19

从图11.19可以看出,这个文件对话框本身就是Windows(即Java程序所在的运行平台)提供的文件对话框,所以当单击其中的图标、按钮等元素时,该对话框都能提供相应的动作。当选中某个文件后,单击“打开”按钮,将看到程序控制台打印出该文件的绝对路径(文件路径+文件名),这就是由FileDialoggetDirectorygetFile方法提供的。

11.3.7 BoxLayout布局管理器

GridBagLayout布局管理器虽然功能强大,但它实在太复杂了,所以Swing引入了一个新的布局管理器:BoxLayout,它保留了GridBagLayout的很多优点,但是却没那么复杂。BoxLayout可以在垂直和水平两个方向上摆放GUI组件。

BoxLayout构造器

BoxLayout提供了如下一个简单的构造器:

方法 描述
BoxLayout(Container target, int axis) 指定创建基于target容器的BoxLayout布局管理器,该布局管理器里的组件按axis方向排列。其中axisBoxLayout.XAXIS(横向)和BoxLayout.YAXIS(纵向)两个方向。

程序示例 使用BoxLayout布局管理器

下面程序简单示范了使用BoxLayout布局管理器来控制容器中按钮的布局

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

public class BoxLayoutTest {
private Frame f = new Frame("测试");

public void init() {
f.setLayout(new BoxLayout(f, BoxLayout.Y_AXIS));
// 下面按钮将会垂直排列
f.add(new Button("第一个按钮"));
f.add(new Button("按钮二"));
f.pack();
f.setVisible(true);
}

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

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

这里有一张图片

Box容器

BoxLayout通常和Box容器结合使用,Box是一个特殊的容器,它有点像Panel容器,但该容器默认使用BoxLayout布局管理器。Box提供了如下两个静态方法来创建Box对象

方法 描述
static Box createHorizontalBox() 创建一个水平排列组件的Box容器。
static Box createVerticalBox() Creates a Box that displays its components from top to bottom.

一旦获得了Box容器之后,就可以使用Box来盛装普通的GUI组件,然后将这些Box组件添加到其他容器中,从而形成整体的窗口布局。

程序示例 使用Box容器

下面的例子程序示范了如何使用Box容器:

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 javax.swing.*;

public class BoxTest {
private Frame f = new Frame("测试");
// 定义水平摆放组件的Box对象
private Box horizontal = Box.createHorizontalBox();
// 定义垂直摆放组件的Box对象
private Box vertical = Box.createVerticalBox();

public void init() {
horizontal.add(new Button("水平按钮一"));
horizontal.add(new Button("水平按钮二"));
vertical.add(new Button("垂直按钮一"));
vertical.add(new Button("垂直按钮二"));
f.add(horizontal, BorderLayout.NORTH);
f.add(vertical);
f.pack();
f.setVisible(true);
}

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

上面程序创建了一个水平摆放组件的Box容器和一个垂直摆放组件的Box容器,并将这两个Box容器添加到Frame窗口中。运行该程序会看到如图11.16所示的运行窗口

这里有一张图片

BoxLayout控制组件的间距的方式

BoxLayout没有提供设置间距的构造器和方法,因为BoxLayout采用另一种方式来控制组件的间距:
BoxLayout使用Glue(橡胶)Strut(支架)和RigidArea(刚性区域)的组件来控制狙件间的间距。其中:

  • Glue代表可以在横向、纵向两个方向上同时拉伸的空白组件(间距),
  • Strut代表可以在横向、纵向任意一个方向上拉伸的空白组件(间距),
  • RigidArea代表不可拉伸的空白组件(间距)

Box提供了如下5个静态方法来创建GlueStrutRigidArea

方法 描述
static Component createHorizontalGlue() 创建一条水平Glue(可在两个方向上同时拉伸的间距)
static Component createVerticalGlue() 创建一条垂直Glue(可在两个方向上同时拉伸的间距)。
static Component createHorizontalStrut(int width) 创建一条指定宽度的水平Strut(可在垂直方向上拉伸的间距)。
static Component createVerticalStrut(int height) 创建一条指定高度的垂直Strut(可在水平方向上拉伸的间距)
static Component createRigidArea(Dimension d) 创建指定宽度、高度的RigidArea(不可拉伸的间距)。

不管GlueStrutRigidArea的翻译多么奇怪,这些名称多么古怪,但读者没有必要去纠缠它们的名称,只要知道它们就是代表组件之间的几种间距即可。

上面5个方法都返回Component对象(代表间距),程序可以将这些分隔Component添加到两个普通的GUI组件之间,用以控制组件的间距。

程序示例

下面程序使用上面三种间距来分隔Box中的按钮:

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
import java.awt.*;
import javax.swing.*;

public class BoxSpaceTest {
private Frame f = new Frame("测试");
// 定义水平摆放组件的Box对象
private Box horizontal = Box.createHorizontalBox();
// 定义垂直摆放组件的Box对象
private Box vertical = Box.createVerticalBox();

public void init() {
horizontal.add(new Button("水平按钮一"));
horizontal.add(Box.createHorizontalGlue());
horizontal.add(new Button("水平按钮二"));
// 水平方向不可拉伸的间距,其宽度为10px
horizontal.add(Box.createHorizontalStrut(10));
horizontal.add(new Button("水平按钮三"));
vertical.add(new Button("垂直按钮一"));
vertical.add(Box.createVerticalGlue());
vertical.add(new Button("垂直按钮二"));
// 垂直方向不可拉伸的间距,其高度为10px
vertical.add(Box.createVerticalStrut(10));
vertical.add(new Button("垂直按钮三"));
f.add(horizontal, BorderLayout.NORTH);
f.add(vertical);
f.pack();
f.setVisible(true);
}

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

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

图11.17

Glue Strut RigidArea三者的区别

从图11.17中可以看出:

  • Glue可以在两个方向上同时拉伸,
  • Strut只能在一个方向上拉伸,
  • RigidArea则不可拉伸。

因为BoxLayoutSwing提供的布局管理器,所以用于管理Swing组件将会有更好的表现。

11.3.6 绝对定位

很多曾经学习过VBDelphi的读者可能比较怀念那种随意拖动控件的感觉,对Java的布局管理器非常不习惯。实际上,Java也提供了那种拖动控件的方式,即Java也可以对GUI组件进行绝对定位。

使用绝对定位的步骤

Java容器中采用绝对定位的步骤如下:

  1. Container的布局管理器设成nullsetLayout(null);
  2. 向容器中添加组件时,先调用setBounds()setSize()方法来设置组件的大小、位置,或者直接创建GUI组件时通过构造参数指定该组件的大小、位置,然后将该组件添加到容器中。

程序示例 绝对定位

下面程序示范了如何使用绝对定位来控制窗口中的GUI组件

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

public class NullLayoutTest {
Frame f = new Frame("测试窗口");
Button b1 = new Button("第一个按钮");
Button b2 = new Button("第二个按钮");

public void init() {
// 设置使用null布局管理器
f.setLayout(null);
// 下面强制设置每个按钮的大小、位置
b1.setBounds(20, 30, 90, 28);
f.add(b1);
b2.setBounds(50, 45, 120, 35);
f.add(b2);
f.setBounds(50, 50, 200, 100);
f.setVisible(true);
}

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

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

图11.4

从图11.14中可以看出,使用绝对定位时甚至可以使两个按钮重叠,可见使用绝对定位确实非常灵活,而且很简捷,但这种方式是以丧失跨平台特性作为代价的。

11.3.5 CardLayout布局管理器

CardLayout布局管理器以时间而非空间来管理它里面的组件,它将加入容器的所有组件看成一叠卡片,每次只有最上面的那个Component才可见。就好像一副扑克牌,它们叠在一起,每次只有最上面的一张扑克牌才可见。

CardLayout构造器

CardLayout提供了如下两个构造器。

方法 描述
CardLayout() 创建默认的CardLayout布局管理器。
CardLayout(int hgap, int vgap) 创建卡片与容器的左右边界的间距(hgap)、上下边界(vgap)的有指定间距的CardLayout布局管理器。

CardLayout用于控制组件可见的5个常用方法如下。

方法 描述
void first(Container target) 显示target容器中的第一张卡片。
void last(Container target) 显示target容器中的最后一张卡片。
void previous(Container target) 显示target容器中的前一张卡片。
void next(Container target) 显示target容器中的后一张卡片。
void show(Container target, String name) 显示target容器中指定名字的卡片。

程序示例 CardLayout布局管理器的用法

如下例子程序示范了CardLayout布局管理器的用法:

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
63
64
import java.awt.*;
import java.awt.event.*;

public class CardLayoutTest {
Frame f = new Frame("测试窗口");
String[] names = { "第一张", "第二张", "第三张", "第四张", "第五张" };
Panel pl = new Panel();

public void init() {
final CardLayout c = new CardLayout();
pl.setLayout(c);
for (int i = 0; i < names.length; i++) {
pl.add(names[i], new Button(names[i]));
}
Panel p = new Panel();
ActionListener listener = e -> {
switch (e.getActionCommand()) {
case "上一张":
c.previous(pl);
break;
case "下一张":
c.next(pl);
break;
case "第一张":
c.first(pl);
break;
case "最后一张":
c.last(pl);
break;
case "第三张":
c.show(pl, "第三张");
break;
}
};
// 控制显示上一张的按钮
Button previous = new Button("上一张");
previous.addActionListener(listener);
// 控制显示下一张的按钮
Button next = new Button("下一张");
next.addActionListener(listener);
// 控制显示第一张的按钮
Button first = new Button("第一张");
first.addActionListener(listener);
// 控制显示最后一张的按钮
Button last = new Button("最后一张");
last.addActionListener(listener);
// 控制根据Card名显示的按钮
Button third = new Button("第三张");
third.addActionListener(listener);
p.add(previous);
p.add(next);
p.add(first);
p.add(last);
p.add(third);
f.add(pl);
f.add(p, BorderLayout.SOUTH);
f.pack();
f.setVisible(true);
}

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

上面程序中通过Frame创建了一个窗口,该窗口被分为上下两个部分,其中上面的Panel使用CardLayout布局管理器,该Panel中放置了5张卡片,每张卡片里放一个按钮;下面的Panel使用FlowLayout布局管理器,依次放置了3个按钮,用于控制上面Pane中卡片的显示。运行上面程序,会看到如图11.13所示的运行窗口。

这里有一张图片

单击图11.13中的5个按钮,将可以看到上面Panel中的5张卡片发生改变。