[关闭]
@Darcy 2017-08-11T11:13:55.000000Z 字数 13354 阅读 2533

第六章 窗口事件

JavaForKids

程序在运行的过程中会发生很多事件:比如用户点击了窗口上的一个按钮,然后浏览器跳到另外一个页面。我肯定你试过点击在第五章中的计算器上的按钮了吧,不过这些按钮们还没准备好去响应你的动作。

每个窗口组件都可以处理一系列的事件,用我们的话说,就是监听这些事件。你的程序必须通过被称为监听器的Java类去注册窗口组件。你的组件应该只去监听他们感兴趣的事件。例如,当用户移动光标经过计算器的按钮时,对于用户按下按钮的具体位置是不重要的,只要它是在该按钮的表面就可以了。这也就是你不必去注册按钮的MouseMotionListener事件的原因。在另外一些场景中,MouseMotionListener监听器对实现很多绘图程序来说是很有用的。[1]

计算器的按钮只需要注册ActionListener这个监听器就可以处理按钮点击事件了。所有这些监听器都是称为接口(interface)的特殊Java类。

1. 接口

大部分类都定义了执行各种动作的方法,比如响应按钮点击,响应鼠标移动等。这些动作的集合称为类的行为。

接口是一种特殊的类,它们只是命名了一系列的空方法,而没有实际实现这些方法的代码,例如:

  1. interface MouseMotionListener {
  2. void mouseDragged(MouseEvent e);
  3. void mouseMoved(MouseEvent e);
  4. }

正如你所见的, mouseDragged()mouseMoved()方法没有任何实现代码 -- 它们只是在MouseMotionListener里面声明了一下而已。但是如果你的类需要在鼠标移动或者拖动时做出响应,那它就必须去实现这个接口。关键词implements 表示这个类一定会包含在这个接口中声明的方法,比如:

  1. import java.awt.event.MouseMotionListener;
  2. class MyDrawingPad implements MouseMotionListener{
  3. public void mouseDragged(MouseEvent e){
  4. }
  5. public void mouseMoved(MouseEvent e){
  6. }
  7. }

你可能会好奇,为什么要那么麻烦创建一个什么也不干的接口?原因就是,一旦创建了接口,那么它就可以被很多类复用了。举个例子,当其他类(或者JVM本身)看见MyDrawingPad这个类实现了MouseMotionListener这个接口时,它们肯定知道这个类必定包含了mouseDragged()mouseMoved()这两个方法。每当用户移动鼠标时,JVM就会调用mouseMoved(),然后执行你写在里面的代码了。试想如果程序员Joe决定起一个方法名为mouseMoved(),但是Mary起了一个moveMouse(),Peter更喜欢mouseCrawling()? 在这种情况下,JVM可能会非常困惑而不知道该调用哪个方法去响应鼠标的动作了。

一个Java类可能会实现多个接口,比如它可能需要响应鼠标的移动和按钮的点击:

  1. class MyDrawingProgram implements MouseMotionListener, ActionListener{
  2. //你必须在这个类中加入在这两个接口中定义的方法
  3. }

在了解Java的接口知识后,你终于可以去创建你自己的接口了, 但这相对来说比较复杂,我们先跳过这部分。

2. 动作监听

让我们再次回到我们的计算器上。如果你已经完成了之前章节的任务,那么界面的部分已经是完成了。现在, 我们将要创建另外的监听类来监听用户点击按钮的行为,并执行一些动作。实际上,我们本来可以直接在Calcalator.java上直接添加点击事件的相关执行代码,但是一个好的程序员总是会把可视界面和执行部分分开在不同的类中。

我们把第二个类命名为CalculatorEngine, 它必须要实现java.awt.ActionListener这个接口,这个接口只声明了一个方法 - actionPerformed(ActionEvent)。JVM会在用户点击按钮时调用这个方法。

请创建一个简单的类,如下面代码 :

  1. import java.awt.event.ActionListener;
  2. public class CalculatorEngine implements ActionListener {
  3. }

如果你尝试去编译这个类,你会得到一个错误信息,大概意思是: 这个类必须实现actionPerformed(ActionEvent e)这个方法。让我们来修复这个错误:

  1. import java.awt.event.ActionListener;
  2. import java.awt.event.ActionEvent;
  3. public class CalculatorEngine implements ActionListener {
  4. public void actionPerformed(ActionEvent e){
  5. // 方法中什么都不写也是可以的,但这样当JVM调用它的时候什么事情都不会发生哦。
  6. }
  7. }

这个类的下个版本将会在actionPerformed()中弹出一个消息框。你可以使用JOptionPane这个类和它的方法showConfirmDialog()来显示任何信息。比如CalculatorEngine这个类显示如下的信息框:

这里有许多不同版本的showConfirmDialog()方法,我们将会使用带了四个参数的那个。在下面的代码中,第一个参数null代表了在这个消息框中没有父窗口,第二参数是消息的内容,第三个是标题,第四个让你可以选择包含在消息框中的按钮类型(PLAIN_MESSAGE代表在消息框中只显示一个OK按钮)。

  1. import java.awt.event.ActionListener;
  2. import java.awt.event.ActionEvent;
  3. import javax.swing.JOptionPane;
  4. public class CalculatorEngine implements ActionListener {
  5. public void actionPerformed(ActionEvent e){
  6. JOptionPane.showConfirmDialog(null, "Something happened...",
  7. "Just a test", JOptionPane.PLAIN_MESSAGE);
  8. }
  9. }

在下一个小节中,我会向你们介绍如何去编译和运行我们下个版本的计算器, 这个计算器会用消息框来展示:Something happened...

3. 为组件注册ActionListener

我们写在actionPerformed()里的代码会在什么情况和被谁调用呢? 答案就是JVM本身会调用这个方法,也就是当你为计算器的按钮注册了CalculatorEngine这个类。如何去注册这个监听器呢? 在Calculator.java的构造方法最后加上下面两句代码即可。

  1. CalculatorEngine calcEngine = new CalculatorEngine();
  2. button0.addActionListener(calcEngine);

从此刻开始,每次用户点击button0的时候,JVM就会调用 CalculatorEngine对象中的actionPerformed()方法。现在编译并运行Calculator这个类,点击按钮0 - 它就会弹出内容是Something happened...的消息框了。而其他的按钮没有反应是因为它们还没有注册我们的监听器。好了,接着像上面一样让每个按钮都动起来吧。

  1. button1.addActionListener(calcEngine);
  2. button2.addActionListener(calcEngine);
  3. button3.addActionListener(calcEngine);
  4. button4.addActionListener(calcEngine);
  5. ...

4. 事件源是什么?

下一步,我们让listener变得更加聪明一点 - 它会根据所点击按钮的不同来显示不同的消息框。当一个动作事件发生时,JVM会调用监听器类上的actionPerformed(ActionEvent)方法,它会通过参数ActionEvent来传递一些有用的信息。你可以通过这些信息来做出相应的处理。

4.1 转型

在下一个例子中,我们会通过调用类ActionEvent上的getSource()方法来找出哪个按钮是处于按下的状态的 - 变量e就是对这个存在内存某处的对象的引用。但是根据Java的文档说明[2],这个方法返回的事件源实例是个Object类型: 包括所有视窗组件在内的一切Java类的父类。它这么做的原因是为了接受更加广泛的实例类型,这样所有的组件都能通过它返回自己了。但是在这里, 无疑我们自己是知道返回的肯定是JButton类型啦! 这也是为什么我们会将JButton写在小括号中放到方法调用语句的前面,这种方式可以将返回的Object类型转型为JButton类型:

  1. JButton clickedButton = (JButton) e.getSource();

我们在等号的左边声明了一个JButton类型的变量,尽管getSouce()方法返回的对象是Object类型, 但是我们告诉JVM: 不用担心,我知道这肯定是一个JButton类型的实例。

也只有当我们 把getSource()返回的对象转型成JButton类型后,这个对象才能调用JButton的方法。

  1. public class CalculatorEngine implements ActionListener{
  2. public void actionPerformed(ActionEvent e){
  3. JButton clickedButton = (JButton)e.getSource();
  4. String cliedButtonLabel = clickedButton.getText();
  5. JOptionPane.showConfirmDialog(null , "You pressed "+cliedButtonLabel , "Just a test" , JOptionPane.PLAIN_MESSAGE);
  6. }
  7. }

例如,如果你点击按钮5,你会看到 下 面的消息框弹出来:

但是如果窗口事件不仅仅由按钮产生,还有其他类型的组件呢?我们肯定不想把每个对象都转型成JButton类型吧! 在这种情况下,我们可以通过一个特殊的Java操作符instanceof判别不同的类型,做出合适的转型。下面的例子就是首先检查事件对象的类型, 然后转型为对应的JButton或者JTextField类型。

  1. public void actionPerformed(ActionEvent evt) {
  2. JTextField myDisplayField = null;
  3. JButton clickedButton = null;
  4. Object eventSource = evt.getSource();
  5. if (eventSource instanceof JButton) {
  6. clickedButton = (JButton) eventSource;
  7. } else if (eventSource instanceof JTextField) {
  8. myDisplayField = (JTextField) eventSource;
  9. }
  10. }

我们的计算器会对不同的按钮点击进行不同的处理,下面的代码片段展示了如何去做到这点:

  1. public void actionPerformed(ActionEvent e) {
  2. Object src = e.getSource();
  3. if (src == buttonPlus) {
  4. // 加法操作的代码放到这里
  5. } else if (src == buttonMinus) {
  6. // 减法操作的代码放到这里
  7. // Code that subtracts numbers goes here
  8. } else if (src == buttonDivide) {
  9. // 除法操作的代码放到这里
  10. } else if (src == buttonMultiply) {
  11. // 乘法操作的代码放到这里
  12. }
  13. }

5. 如何在类之间传递数据

在现实中,在真实的计算中按下按钮不会弹出个消息框的,而是在顶部的文本框中显示数字。新的挑战来了 - 我们需要在类CalculatorEngineactionPerformed()方法中访问到类CalculatordisplayField属性。我们可以在CalculatorEngine中定义一个Calculator类型的变量来存储传过来的Calculator对象。

我们将在下个版本的CalculatorEngine中声明一个构造方法。这个构造方法会有一个Calculator类型的参数。不要感到惊讶,方法的参数可以是由你创建的类类型。

JVM会在内存中创建Calculator类的实例时调用这个类的构造方法。在类Calculator里面会初始化CalculatorEngine,并通过engine的构造方法把自己的引用传递过去:

  1. CalculatorEngine calcEngine = new CalculatorEngine(this);

this作为一个引用,引用了内存中Calculator的这个实例,CalculatorEngine的构造方法会 保存这个值在parent这个成员变量中, 最终可以在actionPerformed()方法中访问到CalculatordisplayField了。

  1. parent.displayField.getText();
  2. parent.displayField.setText(dispFieldText + clickedButtonLabel);

这两行代码是在下面的代码示例中摘取的:

  1. import java.awt.event.ActionListener;
  2. import java.awt.event.ActionEvent;
  3. import javax.swing.JButton;
  4. public class CalculatorEngine implements ActionListener {
  5. Calculator parent; // Calculator对象的引用
  6. // 通过构造方法传入计算器的窗口对象,并用成员变量parent来存储这个对象。
  7. CalculatorEngine(Calculator parent) {
  8. this.parent = parent;
  9. }
  10. public void actionPerformed(ActionEvent e) {
  11. // 获取动作源
  12. JButton clickedButton = (JButton) e.getSource();
  13. // 获取displayField文本框中的文字
  14. String dispFieldText = parent.displayField.getText();
  15. // 获取按钮上的字
  16. String clickedButtonLabel = clickedButton.getText();
  17. parent.displayField.setText(dispFieldText + clickedButtonLabel);
  18. }
  19. }

当你声明一个变量去存储一个特定的类实例的时候,这个变量的类型必须是这个类或者这个类的父类。
每个在Java中的类都继承了Object,如果类Fish是类Pet的子类,那么下面的几行语句都是正确的:

  1. Fish myFish = new Fish();
  2. Pet myFish = new Fish();
  3. Object myFish = new Fish();

6. 完成计算器的编写

让我们先设定计算器工作的 一些规则(算法):

  1. 用户点击数字按钮,产生第一个数字显示在文本框中。
  2. 如果用户点击了 + ,-, *, / 的按钮,则保存第一个数字和相应的运输操作在成员变量中,然后在文本框中清除数字。
  3. 接着用户输入第二个数字,并点击=号按钮。
  4. 把从文本框中获取到的字符串(String)值转化成double类型(可以存放带小数点的大数值类型),并与步骤二中存储的数字进行运算。
  5. 把步骤4算得的结果显示在文本框上,并存储到步骤2提到的存放成员变量中。

我们会在类CalculatorEngine里面把这些动作编成程序。当阅读下面的代码的时候,请记得actionPerformed()会在按钮点击后被执行,在这些方法调用中的数据会存储在变量selectedActioncurrentResult中。

  1. import java.awt.event.ActionListener;
  2. import java.awt.event.ActionEvent;
  3. import javax.swing.JButton;
  4. public class CalculatorEngine implements ActionListener {
  5. Calculator parent; // Calculator对象的引用
  6. char selectedAction = ' '; // +, -, /, or *
  7. double currentResult = 0;
  8. // 通过构造方法传入计算器的窗口对象,并用成员变量parent来存储这个对象。
  9. CalculatorEngine(Calculator parent) {
  10. this.parent = parent;
  11. }
  12. public void actionPerformed(ActionEvent e) {
  13. // 获取动作源
  14. JButton clickedButton = (JButton) e.getSource();
  15. String dispFieldText = parent.displayField.getText();
  16. double displayValue = 0;
  17. // 把文本框中的字符串不为空,就转化成数字
  18. if (!"".equals(dispFieldText)) {
  19. displayValue = Double.parseDouble(dispFieldText);
  20. }
  21. Object src = e.getSource();
  22. //如果点击的是运算按钮,则把文本框的值存到currentResult中,用selectedAction来存储运算的类型,并把文本框清空
  23. if (src == parent.buttonPlus) {
  24. selectedAction = '+';
  25. currentResult = displayValue;
  26. parent.displayField.setText("");
  27. } else if (src == parent.buttonMinus) {
  28. selectedAction = '-';
  29. currentResult = displayValue;
  30. parent.displayField.setText("");
  31. } else if (src == parent.buttonDivide) {
  32. selectedAction = '/';
  33. currentResult = displayValue;
  34. parent.displayField.setText("");
  35. } else if (src == parent.buttonMultiply) {
  36. selectedAction = '*';
  37. currentResult = displayValue;
  38. parent.displayField.setText("");
  39. } else if (src == parent.buttonEqual) {
  40. // 点击的是等号,则用上一次的值currentResult和selectedAction的运算类型进行运算,
  41. // 把结果存到currentResult, 并显示在文本框中。
  42. if (selectedAction == '+') {
  43. currentResult += displayValue;
  44. //把数字类型转化成字符串,并显示到文本框中
  45. parent.displayField.setText(String.valueOf(currentResult) );
  46. } else if (selectedAction == '-') {
  47. currentResult -= displayValue;
  48. parent.displayField.setText(String.valueOf(currentResult));
  49. } else if (selectedAction == '/') {
  50. currentResult /= displayValue;
  51. parent.displayField.setText(String.valueOf(currentResult));
  52. } else if (selectedAction == '*') {
  53. currentResult *= displayValue;
  54. parent.displayField.setText(String.valueOf(currentResult));
  55. }
  56. } else {
  57. // 如果点击的是数字按钮,则直接叠加显示到文本框中。
  58. String clickedButtonLabel = clickedButton.getText();
  59. parent.displayField.setText(dispFieldText + clickedButtonLabel);
  60. }
  61. }
  62. }

最终版本的计算器窗口看起来应该是这样子的:

Calculator会按以下步骤执行:
1. 创建和 显示所有的窗口组件
2. 创建一个事件监听器CalculotrEngine的实例
3. 把Calculator自己的引用传递给engine
4. 给所有能产生事件的组件注册这个监听器

这是最终版本的Calculotor类:

  1. import javax.swing.*;
  2. import java.awt.GridLayout;
  3. import java.awt.BorderLayout;
  4. public class Calculator {
  5. // 声明并创建所有的窗口控件
  6. JButton button0 = new JButton("0");
  7. JButton button1 = new JButton("1");
  8. JButton button2 = new JButton("2");
  9. JButton button3 = new JButton("3");
  10. JButton button4 = new JButton("4");
  11. JButton button5 = new JButton("5");
  12. JButton button6 = new JButton("6");
  13. JButton button7 = new JButton("7");
  14. JButton button8 = new JButton("8");
  15. JButton button9 = new JButton("9");
  16. JButton buttonPoint = new JButton(".");
  17. JButton buttonEqual = new JButton("=");
  18. JButton buttonPlus = new JButton("+");
  19. JButton buttonMinus = new JButton("-");
  20. JButton buttonDivide = new JButton("/");
  21. JButton buttonMultiply = new JButton("*");
  22. JPanel windowContent = new JPanel();
  23. JTextField displayField = new JTextField(30);
  24. // 构造方法
  25. Calculator() {
  26. //为这个面板设置边框布局
  27. BorderLayout bl = new BorderLayout();
  28. windowContent.setLayout(bl);
  29. //把文本框放到窗口的顶部
  30. windowContent.add(BorderLayout.NORTH, displayField);
  31. //创建一个布局为GridLayout的面板, 它可以容纳12个按钮: 10个数字,一个小数点和一个等号
  32. JPanel p1 = new JPanel();
  33. GridLayout gl = new GridLayout(4, 3);
  34. p1.setLayout(gl);
  35. p1.add(button1);
  36. p1.add(button2);
  37. p1.add(button3);
  38. p1.add(button4);
  39. p1.add(button5);
  40. p1.add(button6);
  41. p1.add(button7);
  42. p1.add(button8);
  43. p1.add(button9);
  44. p1.add(button0);
  45. p1.add(buttonPoint);
  46. p1.add(buttonEqual);
  47. //把面板p1添加到窗口的中间区域。
  48. windowContent.add(BorderLayout.CENTER, p1);
  49. //创建同样GridLayout布局的面板p2, 把加减乘除四个按钮添加到上面
  50. JPanel p2 = new JPanel();
  51. GridLayout gl2 = new GridLayout(4, 1);
  52. p2.setLayout(gl2);
  53. p2.add(buttonPlus);
  54. p2.add(buttonMinus);
  55. p2.add(buttonMultiply);
  56. p2.add(buttonDivide);
  57. //把面板p2添加到窗口的右边
  58. windowContent.add(BorderLayout.EAST, p2);
  59. //创建窗口框架,把内容面板添加到上面
  60. JFrame frame = new JFrame("Calculator");
  61. frame.setContentPane(windowContent);
  62. // 设置窗口自适应大小
  63. frame.pack();
  64. // 显示它
  65. frame.setVisible(true);
  66. // 初始化按钮的点击事件监听器,并把它添加到每个按钮上面
  67. CalculatorEngine calcEngine = new CalculatorEngine(this);
  68. button0.addActionListener(calcEngine);
  69. button1.addActionListener(calcEngine);
  70. button2.addActionListener(calcEngine);
  71. button3.addActionListener(calcEngine);
  72. button4.addActionListener(calcEngine);
  73. button5.addActionListener(calcEngine);
  74. button6.addActionListener(calcEngine);
  75. button7.addActionListener(calcEngine);
  76. button8.addActionListener(calcEngine);
  77. button9.addActionListener(calcEngine);
  78. buttonPoint.addActionListener(calcEngine);
  79. buttonPlus.addActionListener(calcEngine);
  80. buttonMinus.addActionListener(calcEngine);
  81. buttonDivide.addActionListener(calcEngine);
  82. buttonMultiply.addActionListener(calcEngine);
  83. buttonEqual.addActionListener(calcEngine);
  84. }
  85. public static void main(String[] args) {
  86. //初始化Calculator这个类
  87. Calculator calc = new Calculator();
  88. }
  89. }

好了,现在可以编译这个工程,然后运行类Calculator. 它就像真实世界的计算器一样工作。

此处应有掌声!这是你写的第一个可以给很多人使用的程序 - 把它当作一个礼物送给你的朋友吧。

为了更好地理解这个程序是怎么工作的,我强烈建议你去了解程序的调试。 你可以先到附录B看看程序调试相关的内容,再回到这个章节继续阅读。

7. 其他的一些事件监听器

下面列举了自包java.awt中的其他一些监听器,你最好也去了解一下:

在下面的表格中你会看到相应名字的监听器接口,还有接口相关的一些方法:

接口 实现的方法
FocusListener focusGained(FocusEvent)
focusLost(FocusEvent)
ItemListener itemStateChanged(ItemEvent)
KeyListener keyPressed(KeyEvent)
keyReleased(KeyEvent)
keyTyped(KeyEvent)
MouseListener mouseClicked(MouseEvent)
mouseEntered(MouseEvent)
mouseExited(MouseEvent)
mousePressed(MouseEvent)
mouseReleased(MouseEvent)
MouseMotionListener mouseDragged(MouseEvent)
mouseMoved(MouseEvent)
WindowListener windowActivated (WindowEvent)
windowClosed(WindowEvent)
windowClosing(WindowEvent)
windowDeactivated (WindowEvent)
windowDeiconified(WindowEvent)
windowIconified(WindowEvent)
windowOpened(WindowEvent)

举个例子,FocusListener接口声明了两个方法: focusGained()focusLost(). 这意味着即使你只是对获取焦点感兴趣,你也必须要去包含另外一个空方法focusLost()。这看起来可能很烦人,Java在这里为每个监听器提供了特殊的适配器类,让事件的处理更加地简单。

8. 如何使用适配器

让我们假设你现在需要在用户关闭窗口的时候保存一些数据在磁盘上。根据上面的表格,实现WindowsListener需要去 实现它的七个方法。这意味着你只会在windowClosing()上写保存的代码,其他的六个则是空的。

java.awt中有很多适配器类,这些类已经实现了所有需要的方法(这些方法都是空实现)。其中的一个类就叫做WindowAdapter。你可以通过继承这个类来覆盖你感兴趣的方法。比如例子中的windowClosing()方法。

  1. class MyEventProcessor extends java.awt.WindowsAdapter {
  2. public void windowClosing(WindowEvent e) {
  3. // 保存数据到硬盘的代码可以写在这里
  4. }
  5. }

接下来就很简单了 -- 只需要在窗口类中注册这个事件监听器就可以了:

  1. MyEventProcessor myListener = new MyEventProcessor();
  2. addWindowListener(myListener);

我们也可以通过匿名内部类来达到相同的效果,但是这个主题不在这本书的讨论范围之内。

9. 扩展阅读

编写事件监听器:
http://docs.oracle.com/javase/tutorial/uiswing/events/

10. 练习

  1. 试着用我们上面写好的计算器来计算一个数除以0 - 结果会显示:Infinity。 修改CalcalatorEngine这个类,让它在用户做除0操作时提示:被除数不能为0。

11. 进一步练习

  1. 修改CalculatorEngine类,使我们的计算器接受输入时,同一个数字最多只能有一个小数点。

温馨提示: 阅读String类的indexOf()方法,利用它来找出是否已经存在小数点了。


創用 CC 授權條款
本著作係採用創用 CC 姓名標示-非商業性-禁止改作 2.5 中國大陸 授權條款授權.


[1] MouseMotionListener用于监听鼠标详细的动态,如果是需要鼠标的坐标信息,就可以注册这个监听器。比如一些绘图程序。
[2] getSource()方法文档说明:http://t.cn/RLH3V4O
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注