在Swing事件模型中,组件可以发起(触发)一个事件。每种事件的类型由不同的类表示。当事件被触发时,它将被一个或多个“监听器”接收,监听器负责处理事件。所以,事件发生的地方可以与事件处理的地方分离开。既然是以这种方式使用Swing组件,那么就只需编写组件收到事件时将被调用的代码,所以这是一个分离接口与实现的极佳例子。
所谓事件监听器,就是一个“实现特定类型的监听器接口”的类对象。所以程序员要做的就是,先创建一个监听器对象,然后把它注册到触发事件的组件。这个注册动作是通过调用触发事件的组件addXXXListener()方法来完成的,这里用XXX表示监听器所监听的事件类型。通过观察addListener方法的名称,就可以很容易地知道其能够处理的事件类型,要是你把所监听事件的类型搞错了,在编译期间就会发现有错误。在本章的后面将会学习到,JavaBean也是使用addListener方法名称来判断某个Bean所能处理的事件类型。
然后,所有的事件处理逻辑都将被置于监听器类的内部。要编写一个监听器类,唯一的要求就是必须实现相应的接口。可以创建一个全局的监听器类,不过有时写成内部类会更有用。这不仅是因为将监听器类放在它们所服务的用户接口类或者业务逻辑类的内部时,可以在逻辑上对其进行分组,而且还因为(将在后面看到)内部类对象含有一个对其外部类对象的引用,这就为跨越类和子系统边界的调用提供了一种优雅的方式。
到目前为止,在本章的所有例子中已经使用了Swing事件模型,本节余下的部分将补充这个模型的细节。
一、事件与监听器的类型
所有Swing组件都具有addXXXListener()和removeXXXListener()方法。这样就可以为每个组件添加或移除相应类型的监听器。注意,每个方法的“XXX”还表示方法所能接受的参数,比如addMyListener(MyListener)。下表包含互相关联的基本事件、监听器以及通过提供addXXXListener()和removeXXXListener()方法来支持这些事件的基本组件。记住,事件模型是可以扩展的,所以将来你也许会遇到表格里没有列出的事件和监听器。
事件、监听器接口以及“添加”和“移除”方法 | 支持此事件的组件 |
ActionEvent ActionListener addActionListener() removeActionListener() | JButton、JList、JTextField、JMenuItem及其派生类,包括JCheckBoxMenuItem、JMenu和JRadioButtonMenu Item。 |
AdjustmentEvent AdjustmentListener addAdjustmentListener() removeAdjustmentListener() | JScrollbar以及你编写的任何实现Adjustable接口的类。 |
ComponentEvent ComponentListener addComponentListener() removeComponentListener() | Component及其派生类,包括JButton、JCheckBox、JComboBox、Container、JPanel、JApplet、JScrollPane、Window、JDialog、JFileDialog、JFrame、JLabel、JList、JScrollbar、JTextArea和JTextField。 |
ContainerEvent ContainerListener addContainerListener() removeContainerListener() | Container及其派生类,包括JScrollPane、Window、JDialog、JFileDialog和JFrame。 |
FocusEvent FocusListener addFocusListener() removeFocusListener() | Component及其派生类。 |
KeyEvent KeyListener addKeyListener() removeKeyListener() | Component及其派生类。 |
MouseEvent(包括单击和移动) MouseListener addMouseListener() removeMouseListener() | Component及其派生类。 |
MouseEvent(包括单击和移动) MouseMotionListener addMouseMotionListener() removeMouseMotionListener() | Component及其派生类。 |
WindowEvent WindowListener addWindowListener() removeWindowListener() | Window及其派生类,包括JDialog、JFileDialog和JFrame |
ItemEvent ItemListener addItemListener() removeItemListener() | JCheckBox、JCheckBoxMenuItem、JComboBox、JList以及任何实现了ItemSelectable接口的类。 |
TextEvent TextListener addTextListener() removeTextListener() | 任何从JTextComponent导出的类,包括JtextArea和JTextField。 |
你可以观察到,每种组件所支持的事件类型都是固定的。为每个组件列出其支持的所有事件是相当困难的。一个比较简单的方法是修改“类型信息”章节的ShowMethods.java程序,这样它就可以显示出你所输入的任意Swing组件所支持的所有事件监听器。
“类型信息”章中介绍了反射机制,并且使用反射对指定的类查找其方法:既可以查找所有方法的列表,也可以查找“方法名称符合你所提供的关键字的”部分方法。反射的神奇之处在于它能自动得到一个类的所有方法,而不用遍历类的整个继承层次并在每个层次检查基类。所以,它为编程提供了极具价值并可以节省时间的工具;因为大多数Java方法的名称非常详细且具有描述性,所以可以找出包含你所感兴趣的关键字的方法名称。当找到了所要找的方法后,就可以在JDK文档里查看其细节了。
下面是ShowMethods.java的更好用的GUI版本,专门用来查找Swing组件里的addListener方法:
package gui;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.Method;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import com.buba.util.SwingConsole;
public class ShowAddListeners extends JFrame {
private JTextField name = new JTextField(25);
private JTextArea results = new JTextArea(40, 65);
private static Pattern addListener = Pattern.compile("(add\\w+?Listener\\(.*?\\))");
private static Pattern qualifier = Pattern.compile("\\w+\\.");
class NameL implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
String nm = name.getText().trim();
if (nm.length() == 0) {
results.setText("No match");
return;
}
Class<?> kind = null;
try {
kind = Class.forName("javax.swing." + nm);
} catch (ClassNotFoundException ex) {
results.setText("No match");
return;
}
Method[] methods = kind.getMethods();
results.setText("");
for (Method m : methods) {
Matcher matcher = addListener.matcher(m.toString());
if (matcher.find())
results.append(qualifier.matcher(matcher.group(1)).replaceAll("") + "\n");
}
}
}
public ShowAddListeners() {
NameL nameListener = new NameL();
name.addActionListener(nameListener);
JPanel top = new JPanel();
top.add(new JLabel("Swing class name (press Enter):"));
top.add(name);
add(BorderLayout.NORTH, top);
add(new JScrollPane(results));
// 初始数据和测试
name.setText("JTextArea");
nameListener.actionPerformed(new ActionEvent("", 0, ""));
}
public static void main(String[] args) {
SwingConsole.run(new ShowAddListeners(), 500, 400);
}
}
在name JTextField中输入要查找的Swing组件类的名称。查找的结果将使用正则表达式进行匹配,最终结果显示在JTextArea中。
注意,这里没有使用按钮或者别的组件来表明你希望启动查找。这是由于JTextField被ActionListener所监听。当你做出更改并按下“回车”键后,列表马上就得到了更新。如果文本域的内容非空,将把此内容作为Class.forName()的参数,以用来查找这个类。如果名称不正确,Class.forName()方法将失败,即抛出异常。这个异常将被捕获,并把JTextArea内容设置为No match(不匹配)。如果输入正确的名称(注意大小写),Class.forName()将成功返回,然后getMethods()方法将返回一个Method对象的数组。
这里使用了两个正则表达式。第一个是addListener,它查找的模式为:以add开头,后面跟任意字母,然后接Listener,最后是括号内的参数列表。注意,整个正则表达式用“非转义”的括号包围,意思是当发生匹配的时候,它可以作为一个正则表达式“组”来访问。在NameL.ActionPerformed()中,通过把每个Method对象都以字符串形式传递给Pattern.matcher()方法,创建一个Matcher对象。当在此对象上调用find()的时候,只有发生了匹配,才会返回真,这时你可以通过调用group(1)来选择第一个匹配的包含在括号中的表达式组。这样得到的字符串仍然包含限定词,为了把限定词剔除掉,需要使用qualifier Pattern对象,这与ShowMethods.java中的做法很相似。
在构造器的末尾,在name中设置一个初始值,然后触发事件,对初始数据进行一次测试。
这个程序为查询Swing组件所支持的事件类型提供了一种便利方式。一旦知道了某个组件支持哪些事件,不用参考任何资源就可以处理这个事件了。你只要:
- 获取事件类的名称,并移除单词“Event”,然后将剩下的部分加上单词“Listener”,得到的就是内部类必须实现的监听器接口。
- 实现上面的接口,为要捕获的事件编写出方法。比如,你可能要查找鼠标移动,所以你可以为MouseMotionListener接口的mouseMoved()方法编写代码(自然必须同时实现接口的其他方法,不过很快你会学到一种简单的方式)。
- 为第二步编写的监听器类创建一个对象。然后通过调用方法向组件注册这个对象——方法名为“add”前缀加上监听器名称,比如addMouseMotionListener()。
下面是一些监听器接口:
监听器接口及其适配器 | 接口中的方法 |
ActionListener | actionPerformed(ActionEvent) |
AdjustmentListener | adjustmentValueChanged(AdjustmentEvent) |
ComponentListener ComponentAdapter | componentHidden(ComponentEvent) componentShown(ComponentEvent) componentMoved(ComponentEvent) ComponentResized(ComponentEvent) |
ContainerListener ContainerAdapter | componentAdded(ContainerEvent) componentRemoved(ContainerEvent) |
FocusListener FocusAdapter | focusGained(FocusEvent) focusLost(FocusEvent) |
KeyListener KeyAdapter | keyPressed(KeyEvent) keyReleased(KeyEvent) keyTyped(KeyEvent) |
MouseListener MouseAdapter | mouseClicked(MouseEvent) mouseEntered(MouseEvent) mouseExited(MouseEvent) mousePressed(MouseEvent) mouseReleased(MouseEvent) |
MouseMotionListener MouseMotionAdapter | mouseDragged(MouseEvent) mouseMoved(MouseEvent) |
WindowListener WindowAdapter | windowOpened(WindowEvent) windowClosing(WindowEvent) windowClosed(WindowEvent) windowActivated(WindowEvent) windowDeactivated(WindowEvent) windowIconified(WindowEvent) windowDeiconified(WindowEvent) |
ItemListener | itemStateChanged(ItemEvent) |
这并不是完整的列表,部分原因是由于事件模型允许你编写自己的事件类型和相应的监听器。所以,人们常常会遇到含有自定义事件的库,本章学习到的知识可以帮助你理解如何使用这些事件。
(1)使用监听器适配器来进行简化
在上面的表中可以发现,某些监听器接口只有一个方法。这种接口实现起来很简单。不过,具有多个方法的监听器接口使用起来却不太方便。比如,如果你想捕获一个鼠标单击事件(例如,某个按钮还没有替你捕获该事件),那么就需要为mouseClicked()方法编写代码。但是因为MouseListener是一个接口,所以尽管接口里的其他方法对你来说没有任何用处,但是你还是必须要实现所有这些方法。这非常烦人。
要解决这个问题,某些(不是所有的)含有多个方法的监听器接口提供了相应的适配器(可以在上面的表中看到具体的名称)。适配器为接口里的每个方法都提供了默认的空实现。现在你要做的就是从适配器继承,然后仅覆盖那些需要修改的方法。比如,你要用的典型的MouseListener像这样:
class MyMouseListener extends MouseAdapter {
public void mouseClicked(MouseEvent e) {
// Respond to mouse click...
}
}
适配器的出发点就是为了使编写监听器类变得更容易。不过,适配器也有某种形式的缺陷。假设你写了一个与前面类似的MouseAdapter:
class MyMouseListener extends MouseAdapter {
public void MouseClicked(MouseEvent e) {
// Respond to mouse click...
}
}
这个适配器将不起作用,而且要想找出问题根源也非常困难,这足以让你发现。因为除了鼠标单击的时候方法没有被调用以外,程序的编译和运行十分良好。你能发现这个问题吗?它出在方法的名称上:这里的名称是MouseClicked()而没有写成mouseClicked()。这个简单的大小写错误导致加入了一个新方法,它不是关闭视窗的时候所应该调用的方法,所以无法得到所希望的结果。尽管使用接口有些不方便,但可以保证方法被正确实现。
要想保证实际上的确是覆盖了某个方法,一种改进的方法是在这段代码上面使用内建的@Override注解。
二、跟踪多个事件
作为一个有趣的实验,也为了向读者证明这些事件确实可以被触发,编写一个程序,使其能够跟踪JButton除了“是否被按下”事件以外的行为,将会显得很有价值。这个例子还向读者演示如何从JButton中继承出自己的按钮对象。
在下面的代码中,MyButton是TrackEvent类的内部类,所以MyButton能访问父窗口,并操作其文本区域,这正是能够把状态信息写到父窗口的文本区域所必需的。当然,这是一个受限的解决方案,因为MyButton被局限于只能与TrackEvent一起使用,这种情况有时称为“高耦合”代码:
package gui;
import java.awt.Color;
import java.awt.GridLayout;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.HashMap;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
import com.buba.util.SwingConsole;
public class TrackEvent extends JFrame {
private HashMap<String, JTextField> h = new HashMap<>();
private String[] event = { "focusGained", "focusLost", "keyPressed", "keyReleased", "keyTyped", "mouseClicked",
"mouseEntered", "mouseExited", "mousePressed", "mouseReleased", "mouseDragged", "mouseMoved" };
private MyButton b1 = new MyButton(Color.BLUE, "test1"), b2 = new MyButton(Color.RED, "test2");
class MyButton extends JButton {
void report(String field, String msg) {
h.get(field).setText(msg);
}
FocusListener fl = new FocusListener() {
@Override
public void focusGained(FocusEvent e) {
report("focusGained", e.paramString());
}
@Override
public void focusLost(FocusEvent e) {
report("focusLost", e.paramString());
}
};
KeyListener kl = new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
report("keyTyped", e.paramString());
}
@Override
public void keyPressed(KeyEvent e) {
report("keyPressed", e.paramString());
}
@Override
public void keyReleased(KeyEvent e) {
report("keyReleased", e.paramString());
}
};
MouseListener ml = new MouseListener() {
@Override
public void mouseClicked(MouseEvent e) {
report("mouseClicked", e.paramString());
}
@Override
public void mousePressed(MouseEvent e) {
report("mousePressed", e.paramString());
}
@Override
public void mouseReleased(MouseEvent e) {
report("mouseReleased", e.paramString());
}
@Override
public void mouseEntered(MouseEvent e) {
report("mouseEntered", e.paramString());
}
@Override
public void mouseExited(MouseEvent e) {
report("mouseExited", e.paramString());
}
};
MouseMotionListener mml = new MouseMotionListener() {
@Override
public void mouseMoved(MouseEvent e) {
report("mouseMoved", e.paramString());
}
@Override
public void mouseDragged(MouseEvent e) {
report("mouseDragged", e.paramString());
}
};
public MyButton(Color color, String label) {
super(label);
setBackground(color);
addFocusListener(fl);
addKeyListener(kl);
addMouseListener(ml);
addMouseMotionListener(mml);
}
}
public TrackEvent() {
setLayout(new GridLayout(event.length + 1, 2));
for (String evt : event) {
JTextField t = new JTextField();
t.setEditable(false);
add(new JLabel(evt, JLabel.RIGHT));
add(t);
h.put(evt, t);
}
add(b1);
add(b2);
}
public static void main(String[] args) {
SwingConsole.run(new TrackEvent(), 700, 500);
}
}
在MyButton的构造器中,调用SetBackground()方法设置按钮的颜色。所有的监听器都是通过简单的方法调用进行注册的。
TrackEvent类包含了一个HashMap,它用来存放表示事件类型的字符串;以及一些JTextField,每个JTextField用来显示和相应事件有关的信息。当然,这种对应关系可以静态生成而不用放进HashMap,不过我认为你会同意这样做,因为如此一来使用和修改会容易得多。尤其是,如果要在TrackEvent中加入或删除新的事件类型,那么只要在event数组中加入或删除字符串即可,其他工作将自动完成。
调用report()的时候,将传给它事件的名称以及从事件中得到的参数字符串。它使用外部类中的HashMap对象h来查找与事件名称相关联的JTextField,然后把第二个参数放进该文本域。
运行这个例子很有趣,由此可以观察到程序中事件发生的实际情况。
如果本文对您有很大的帮助,还请点赞关注一下。