当程序员向窗体上添加了按钮等组件之后就能够操作这些组件,但在20.3小节的各个案例中,虽然在窗体上添加了一些按钮,但点击这些按钮并没有任何反应,因此这些按钮也就成了毫无意义的“摆设”。如果希望按钮等组件能够在被操作时执行一段代码并产生一个动作,就必须为组件添加一个监听器并由监听器负责处理组件所产生的事件,本小节将详细讲解事件处理的相关原理。
20.4.1事件处理基本原理
组件被操作时会产生事件,在事件处理的过程中,主要涉及三类对象:
- 事件源:事件发生的场所,通常就是各个组件,例如按钮、窗口、菜单等。
- 事件:就是指组件上发生的特定事情,通常就是一次用户操作,如果程序需要获得组件上所发生事件的相关信息,都通过事件对象来取得。
- 事件监听器:负责监听事件源所发生的事件,并对各种事件做出响应处理。
当用户单击一个按钮,或者单击某个菜单项时,这些动作就会触发一个相应的事件,该事件会被封装成相应的事件对象,该事件会触发事件源上注册的事件监听器产生反应,事件监听器调用对应的事件处理器来做出相应的响应。
AWT的事件处理机制是一种委派式事件处理方式:作为事件源的普通组件将事件的处理工作委托给特定的对象,也就是事件监听器。当该事件源发生指定的事件时,就通知所委托的事件监听器,由事件监听器来处理这个事件。
每个组件均可以针对特定的事件指定一个或多个事件监听对象,每个事件监听器也可以监听一个或多个事件源。因为同一个事件源上可能发生多种事件,委派式事件处理方式可以把事件源上可能发生的不同的事件分别授权给不同的事件监听器来处理,同时也可以让一类事件都使用同一个事件监听器来处理。
委派式事件处理方式类似于人类社会的分工协作,例如某个单位发生了火灾,该单位通常不会自己处理该事件,而是将该事件委派给消防局处理,消防局就相当于事件监听器,同理,如果发生了打架斗殴事件,则委派给公安局处理,公安局相当于事件监听器。消防局、公安局也会同时监听多个单位的火灾、打架斗殴事件。这种委派式处理方式将事件源和事件监听器分离,从而提供更好的程序模型,有利于提高程序的可维护性。下面的图20-6展示了事件处理的流程图。
图20-6事件处理的流程图
在图20-6中,处理程序会以事件对象作为处理事件方法的参数,因此在处理程序中可以获得事件对象的各项属性。
20.4.2监听器简介
两个相同事件源产生事件之后其处理过程可能并不相同。例如窗体上某个按钮被单击后会向数据库中插入一条数据,而另一个按钮被单击后则删会除数据库中的一条数据。由此可以看出:每一个按钮被单击后所做的操作各不相同,因此程序员没有办法在监听器中编写出具体的处理代码,所以只能把处理按钮被单击事件定义成抽象方法,而处理按钮被单击的方法又被定义在监听器内,这样的话监听器也只能被定义成抽象类或者是接口类型。实际上,几乎所有的监听器都被定义成接口,这是因为接口具有多实现特性。
事件源添加监听器的方法名称通常都是addXXXListener,这些方法都被定义在组件中。事件源可以产生的事件类型很多,这些事件以不同的类进行定义,而监听这些事件的监听器名称也与事件的名称有高度吻合,下面的表20-3展示了常见事件名称、事件含义以及处理这些事件监听器的名称。
表20-3 常见事件及对应监听器
事件 | 事件意义 | 监听器 |
---|---|---|
ActionEvent | 动作事件 | ActionListener |
AdjustmentEvent | 滚动条调整事件 | AdjustmentListener |
ContainerEvent | 容器相关事件 | ContainerListener |
FocusEvent | 焦点变化事件 | FocusListener |
ItemEvent | 下拉框、列表框选项相关事件 | ItemListener |
KeyEvent | 键盘操作事件 | KeyListener |
MouseEvent | 鼠标操作事件 | MouseListener |
WindowEvent | 窗体变化事件 | WindowListener |
表20-3中,第一行的监听器ActionListener翻译成汉语是“动作监听器”,它的名字太过于抽象了,这个监听器是Java语言提供的一个处理各种组件发生频率最高事件的监听器。比如对于按钮组件来说发生频率最高的事件就是被按钮单击,那么ActionListener添加到按钮组件上,就是专门用来处理按钮单击事件的,再比如对于菜单组件来讲,发生最高频率的事件就是菜单当中某个菜单项被单击,那么ActionListener用到菜单组件上,就是处理菜单中某个菜单项被单击的操作,但是也有一些窗体组件并不支持ActionListener。ActionListener这个监听器当中只定义了一个actionPerformed()抽象方法,程序员只需要把处理事件的代码写到这个方法中就可以。下面的【例20_06】用两种方式实现了监听器,它们都能处理按钮被单击事件。
【例20_06 监听器】
Exam20_06.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
//定义一个监听鼠标事件的监听器
class Listener1 implements MouseListener {
@Override
public void mouseReleased(MouseEvent e) {
}
@Override
public void mousePressed(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseClicked(MouseEvent e) {
System.out.println("按钮1被鼠标单击!");
}
}
//定义一个ActionListener
class Listener2 implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("按钮2被鼠标单击!");
}
}
//定义一个窗体类
class Exam20_06Frame extends JFrame{
JButton button1;
JButton button2;
public Exam20_06Frame( ){
init();
}
private void init( ){
button1 = new JButton("按钮1");
button2 = new JButton("按钮2");
Container container = this.getContentPane();
//把内容面板设置为空布局
container.setLayout(null);
//设置button1的位置为(0,0)
button1.setLocation(0, 0);
//设置button1的大小为200,100
button1.setSize(200, 100);
//为按钮添加MouseListener
button1.addMouseListener(new Listener1());//①
//设置button2的位置为(240,50)
button2.setLocation(240, 50);
//设置button2的大小为100,200
button2.setSize(100, 200);
//为按钮添加ActionListener
button2.addActionListener(new Listener2());//②
container.add(button1);
container.add(button2);
}
}
public class Exam20_06 {
public static void main(String[] args) {
Exam20_06Frame frame = new Exam20_06Frame();
frame.setSize(400, 300);//设置窗体的大小
frame.setLocationRelativeTo(null);//设置窗体出现在屏幕正中间
frame.setTitle("Exam20_06Frame");//设置窗体的标题
//设置关闭窗体时同时停止程序
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);//设置窗体可见
}
}
【例20_06】中,用两种方式处理按钮被单击的事件。单击事件由鼠标单击操作产生,为了监听鼠标所产生的事件,第一种方式是为按钮添加代表鼠标事件的MouseListener,由于MouseListener是一个接口,因此需要定义一个MouseListener的实现类Listener1实现监听器。可以看出:MouseListener接口中定义了很多抽象方法,mouseClicked()方法是鼠标单击事件的处理方法。程序员只需要在mouseClicked()方法中写上代码,这段代码将会在鼠标单击时自动执行,本例在mouseClicked()方法里编写了一条输出语句,并且在语句①中为按钮添加了这个监听器对象,因此在窗体上点击按钮时控制台上会输出“按钮1被鼠标单击!”。此外还可以看出:所有鼠标事件相关的处理方法都以鼠标事件MouseEvent作为参数。
第二种方式以ActionListener作为监听器,由于按钮最常出现的事件就是被鼠标单击,因此ActionListener能够处理这个事件。处理事件时在控制台上输出“按钮2被鼠标单击!”各位读者可以自行运行本例以观察按钮1和按钮2被单击后的效果。
20.4.3监听器的实现方式
监听器一般都是以接口形式存在,程序员只要定义一个接口的实现类,并且在实现类中实现接口所定义的抽象方法就可以实现监听器。监听器在处理事件时往往要对窗体上的其他组件进行操作,而这些组件往往又是以窗体类属性的方式存在的,例如在【例20_06】中,窗体上的两个按钮组件button1和button2都被定义成了Exam20_06Frame类的属性,而属性的操作和访问又受到访问权限的影响,因此编程时必须考虑如何合理的实现监听器。除此之外,监听器在处理事件时往往还会操作一些重要的数据,所以监听器的实现方式还直接关系到应用程序的安全性。
通常情况下,监听器有以下4种实现方式:
- 匿名内部类实现
- 普通内部类实现
- 窗体直接充当监听器
- 外部类实现
如果窗体上的按钮对象是button,为这个按钮添加的监听器是ActionListener,那么使用匿名内部类对象作为其监听器的代码是:
class MyFrame extends JFrame{
JButton button;
private void init( ){
......
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
//事件处理代码
}
});
......
}
}
使用匿名内部类实现监听器能够非常方便的操作以属性形式定义的窗体组件以及其他窗体类的属性,并且匿名内部类实现的监听器只能被添加到一个组件上,这是因为一个匿名类对象只能出现在一个地方。一个监听器对象只能出现在一个地方的特性既有优点也有缺点,优点是能够保证监听器不被其他组件混用,这能够提高代码的安全性,缺点是一个监听器没有被重复利用的机会。此外,匿名内部类实现的监听器会使代码的结构性变差,虽然如此,由于匿名内部类实现的监听器安全性最高,所以这种方式是实现监听器的首选。
使用普通内部类也能实现监听器,其代码结构如下:
class MyFrame extends JFrame{
JButton button;
//普通内部类实现的监听器
class MyListener implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
//事件处理代码
}
}
private void init( ){
//添加监听器
button.addActionListener(new MyListener());
}
}
普通内部类实现的监听器也能非常方便的操作以属性形式定义的窗体组件以及其他窗体类的属性,并且这种监听器能够同时被添加到多个组件上,提高了代码的可重用性。此外,因为普通内部类的定义和创建对象的代码是分开的,所以耦合度也会降低,代码结构清晰,但因为这种监听器可以被多个组件使用,不属专于某个组件,因此也会导致安全性降低。
第三种实现监听器的方式比较特殊,就是用窗体类直接充当监听器,这种情况下,窗体类不仅要继承JFrame类,还要实现监听器接口,代码如下:
class MyFrame extends JFrame implements ActionListener{
JButton button;
//普通内部类实现的监听器
@Override
public void actionPerformed(ActionEvent e) {
//事件处理代码
}
private void init( ){
//添加监听器
button.addActionListener(this);
}
}
使用窗体类直接充当监听器的优点是:把窗体当作监听器,处理事件的方法就直接出现在窗体类当中,这样处理事件的方法无论操作窗体的哪个属性都不受访问权限的影响。此外,因为没有使用内部类,所以代码结构变得清晰、优美。但这种实现方式的缺点也是显而易见的:首先,窗体类的角色过于复杂。以上代码中的MyFrame类本来是表示一个窗体,但是它又充当了监听器的角色。如果窗体上还存在其他用键盘操作的组件,比如说文本框,为了处理这些组件的键盘事件就必须让MyFrame还同时实现KeyListener。以此类推,MyFrame这个类很可能承担多种类型的角色,这样的话,它必然会同时实现多个监听器接口,这也直接导致在窗体内部会出现大量处理事件的代码。其次,监听器接口中定义的处理事件的方法都被定义成public,因此窗体充当监听器也会导致这些方法能够被公开访问,降低了安全性。
监听器的第4实现种方式是使用外部类定义监听器,【例20_06】就是用这种方式实现的监听器。这种方式实现的监听器不能访问窗体类中被定义为private的属性,因此在实际开发过程中很少使用这种方式实现监听器。
20.4.4适配器类
大部分监听器接口中都定义了好几个抽象方法,例如处理鼠标事件的MouseListener接口就定义了5个抽象方法,而程序员在实现监听器接口时要把这些抽象方法全部都实现了,因为不实现这些抽象方法就无法定义出一个非抽象的类,但有的时候,程序员只是希望实现某种具体的事件类型处理方法,这种情况下为了实现监听器接口,就不得不在实现类中定义很多空方法。例如:
class Listener1 implements MouseListener {
@Override
public void mouseReleased(MouseEvent e) {
}
@Override
public void mousePressed(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseClicked(MouseEvent e) {
System.out.println("按钮1被鼠标单击!");
}
}
以上代码实现了一个监听器接口,可以看出:程序员希望只处理鼠标单击这个事件,但为了实现监听器接口,不得不以空方法的形式实现其他4个抽象方法,这显然造成了程序中出现大量无用的空闲代码。
为了解决这个问题,Java语言又提供了相应的适配器类。适配器类实际上就是监听器接口的默认实现类,这些实现类中都提供了监听器接口中抽象方法的空实现。程序员在实现监听器的时候,不是去直接实现这些监听器接口,而是去继承这些实现类,在继承实现类的时候,重写那些真正需要处理事件的方法。
Java语言中,大部分监听器接口都有对应的适配器类,但也有一些没有,监听器与适配器类的关系如表20-4所示。
表20-4监听器与适配器
监听器接口 | 适配器类 |
---|---|
ActionListener | 无 |
AdjustmentListener | 无 |
ContainerListener | ContainerAdapter |
FocusListener | FocusAdapter |
ItemListener | 无 |
KeyListener | KeyAdapter |
MouseListener | MouseAdapter |
WindowListener | WindowAdapter |
下面的【例20_07】展示了用继承适配器类的方式定义监听器。
【例20_07 适配器】
Exam20_07.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
//以继承适配器类的方式实现监听器
class MyMouseAdapter extends MouseAdapter {
@Override
public void mouseClicked(MouseEvent e) {
System.out.println("按钮被鼠标单击!");
}
}
//定义一个窗体类
class Exam20_07Frame extends JFrame{
JButton button;
public Exam20_07Frame( ){
init();
}
private void init( ){
button = new JButton("按钮");
Container container = this.getContentPane();
container.setLayout(null);
button.setLocation(0, 0);
button.setSize(200, 100);
button.addMouseListener(new MyMouseAdapter());//①
container.add(button);
}
}
public class Exam20_07{
public static void main(String[] args) {
Exam20_07Frame frame = new Exam20_07Frame();
frame.setSize(400, 300);//设置窗体的大小
frame.setLocationRelativeTo(null);//设置窗体出现在屏幕正中间
frame.setTitle("Exam20_07Frame");//设置窗体的标题
//设置关闭窗体时同时停止程序
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);//设置窗体可见
}
}
可以看出:以继承适配器类的方式实现监听器能够大量的减少无用代码。