关于编写事件监听器的一般信息

原文: https://docs.oracle.com/javase/tutorial/uiswing/events/generalrules.html

本节讨论在应用程序中实现事件处理器时要记住的几个设计注意事项。然后,我们向您介绍描述每个事件的事件对象小对象。特别是,我们讨论EventObject,它是所有 AWT 和 Swing 事件的超类。接下来,我们介绍低级事件和语义事件的概念,建议您在可能的情况下更喜欢语义事件。本节的其余部分讨论了您可能在某些事件监听器中使用的实现技术,或者在由其他人或 GUI 构建器创建的事件监听器中查看的实现技术。

关于事件监听器要记住的最重要的规则是它们应该非常快速地执行。因为所有绘图和事件监听方法都在同一个线程中执行,所以一个缓慢的事件监听器方法可能会使程序看起来没有响应而且重绘速度很慢。如果由于事件需要执行一些冗长的操作,请通过启动另一个线程(或以某种方式向另一个线程发送请求)来执行操作。有关使用线程的帮助,请参阅 Swing 中的并发。

您有很多关于如何实现事件监听器的选择。我们不能推荐一种特定的方法,因为一种解决方案不适合所有情况。但是,即使您没有在程序中使用相同的解决方案,我们也可以为您提供一些提示并向您展示您可能会看到的一些技巧。

例如,您可以选择为不同类型的事件监听器实现单独的类。这可以是一个易于维护的架构,但许多类也可能意味着降低性能。

在设计程序时,您可能希望在不公开的类中实现事件监听器,但更隐藏的地方。私有实现是一种更安全的实现。

如果您有一种非常特殊的简单事件监听器,则可以通过使用EventHandler类来避免创建类。

每个事件监听器方法都有一个参数,一个继承自 EventObject 类的对象。尽管参数总是来自EventObject,但其类型通常更精确地指定。例如,处理鼠标事件的方法的参数是MouseEvent的实例,其中MouseEventEventObject的间接子类。

EventObject类定义了一个非常有用的方法:

Object getSource()

Returns the object that fired the event.

请注意,getSource方法返回Object。事件类有时会定义类似于getSource的方法,但它们具有更多受限制的返回类型。例如,ComponentEvent类定义getComponent方法,就像getSource返回触发事件的对象一样。区别在于getComponent总是返回Component。事件监听器的每个操作方法页面都提到您是否应该使用getSource或其他方法来获取事件源。

通常,事件类定义返回有关事件的信息的方法。例如,您可以查询MouseEvent对象,以获取有关事件发生位置,用户点击次数,按下了哪些修改键等信息。

事件可以分为两组:低级事件和语义事件。低级事件表示窗口系统发生或低级输入。其他一切都是语义事件。

低级事件的示例包括鼠标和键事件,这两者都直接来自用户输入。语义事件的示例包括动作和项目事件。语义事件可能由用户输入触发;例如,按钮通常在用户单击时触发动作事件,并且当用户按下 Enter 时,文本字段触发动作事件。但是,一些语义事件根本不是由低级别事件触发的。例如,当表模型从数据库接收新数据时,可能会触发表模型事件。

只要有可能,您应该监听语义事件而不是低级别事件。这样,您可以使代码尽可能健壮且可移植。例如,监听按钮上的动作事件而不是鼠标事件意味着当用户尝试使用键盘替代或特定于外观的手势激活按钮时,按钮将作出适当的反应。在处理复合组件(如组合框)时,必须坚持语义事件,因为您没有可靠的方法在可能用于构成复合的所有特定于外观的组件上注册监听器零件。

某些监听器接口包含多个方法。例如,MouseListener接口包含五种方法:mousePressedmouseReleasedmouseEnteredmouseExitedmouseClicked。即使您只关心鼠标点击,如果您的类直接实现MouseListener,那么您必须实现所有五种MouseListener方法。您不关心的事件的方法可能有空体。这是一个例子:

  1. //An example that implements a listener interface directly.
  2. public class MyClass implements MouseListener {
  3. ...
  4. someObject.addMouseListener(this);
  5. ...
  6. /* Empty method definition. */
  7. public void mousePressed(MouseEvent e) {
  8. }
  9. /* Empty method definition. */
  10. public void mouseReleased(MouseEvent e) {
  11. }
  12. /* Empty method definition. */
  13. public void mouseEntered(MouseEvent e) {
  14. }
  15. /* Empty method definition. */
  16. public void mouseExited(MouseEvent e) {
  17. }
  18. public void mouseClicked(MouseEvent e) {
  19. ...//Event listener implementation goes here...
  20. }
  21. }

由此产生的空方法体集合可以使代码更难以阅读和维护。为了帮助您避免实现空方法体,API 通常包含一个适配器类,用于具有多个方法的每个监听器接口。 (监听器 API 表列出了所有监听器及其适配器。)例如,MouseAdapter类实现MouseListener接口。适配器类实现其所有接口方法的空版本。

要使用适配器,您需要创建它的子类并仅覆盖感兴趣的方法,而不是直接实现监听器接口的所有方法。以下是修改前面代码以扩展MouseAdapter的示例。通过扩展MouseAdapter,它继承了MouseListener包含的所有五种方法的空定义。

  1. /*
  2. * An example of extending an adapter class instead of
  3. * directly implementing a listener interface.
  4. */
  5. public class MyClass extends MouseAdapter {
  6. ...
  7. someObject.addMouseListener(this);
  8. ...
  9. public void mouseClicked(MouseEvent e) {
  10. ...//Event listener implementation goes here...
  11. }
  12. }

如果要使用适配器类,但不希望公共类从适配器类继承,该怎么办?例如,假设您编写了一个 applet,并且希望Applet子类包含一些代码来处理鼠标事件。由于 Java 语言不允许多重继承,因此您的类不能扩展AppletMouseAdapter类。一个解决方案是在Applet子类中定义一个内部类,它扩展了MouseAdapter类。

内部类对于直接实现一个或多个接口的事件监听器也很有用。

  1. //An example of using an inner class.
  2. public class MyClass extends Applet {
  3. ...
  4. someObject.addMouseListener(new MyAdapter());
  5. ...
  6. class MyAdapter extends MouseAdapter {
  7. public void mouseClicked(MouseEvent e) {
  8. ...//Event listener implementation goes here...
  9. }
  10. }
  11. }

Performance note:

在考虑是否使用内部类时,请记住,应用程序启动时间和内存占用量通常与您加载的类数成正比。您创建的类越多,启动程序所需的时间越长,所需的内存就越多。作为应用程序开发人员,您必须将其与您可能具有的其他设计约束相平衡。我们并不是建议您将应用程序转换为单个整体类,以期缩短启动时间和内存占用,这将导致不必要的麻烦和维护负担。


您可以创建内部类而不指定名称,这称为匿名内部类。虽然初看起来可能看起来很奇怪,但匿名内部类可以使代码更容易阅读,因为类被定义在引用它的位置。但是,您需要权衡方便性与增加类数量可能带来的性能影响。

以下是使用匿名内部类的示例:

  1. //An example of using an anonymous inner class.
  2. public class MyClass extends Applet {
  3. ...
  4. someObject.addMouseListener(new MouseAdapter() {
  5. public void mouseClicked(MouseEvent e) {
  6. ...//Event listener implementation goes here...
  7. }
  8. });
  9. ...
  10. }
  11. }

Note:

匿名内部类的一个缺点是长期持久性机制无法看到它们。有关更多信息,请参阅 JavaBeans 中的 JavaBeans™软件包Bean 持久性课程的 API 文档。


即使您的事件监听器需要访问封闭类中的私有实例变量,内部类也可以工作。只要您不将内部类声明为static,内部类就可以引用实例变量和方法,就好像它的代码在包含类中一样。要使局部变量可用于内部类,只需将变量的副本保存为final局部变量。

要引用封闭实例,可以使用_EnclosingClass_ 。这。有关内部类的更多信息,请参见嵌套类

EventHandler 类支持动态生成简单的单语句事件监听器。虽然EventHandler仅对特定类型的极其简单的事件监听器有用,但值得一提的是有两个原因。它对以下内容很有用:

  • 创建一个持久化可以看到的事件监听器,但不会使用事件监听器接口和方法阻塞自己的类。
  • 不添加应用程序中定义的类数可以帮助提高性能。

手动创建EventHandler很困难。必须仔细构建EventHandler。如果你犯了一个错误,你不会在编译时得到通知,它会在运行时抛出一个模糊的异常。因此,EventHandler最好由 GUI 构建器创建。 EventHandler应该仔细记录。否则,您将面临生成难以阅读的代码的风险。

EventHandler类旨在供交互式工具(如应用程序构建器)使用,这些工​​具允许开发人员在 bean 之间建立连接。通常,连接是从用户界面 bean(事件源)到应用程序逻辑 bean(目标)。这种最有效的连接将应用程序逻辑与用户界面隔离开来。例如,从 JCheckBox 到接受布尔值的方法的连接的EventHandler可以处理提取复选框的状态并将其直接传递给方法,以便该方法与用户界面层隔离。

内部类是处理来自用户界面的事件的另一种更通用的方法。 EventHandler类仅处理使用内部类可能的子集。但是,EventHandler在长期持久性方案中比内部类更好。此外,在大型应用程序中使用EventHandler,其中多次实现相同的接口可以减少应用程序的磁盘和内存占用。

使用EventHandler的示例EventHandler的最简单用法是安装一个监听器,该监听器在没有参数的情况下调用目标对象上的方法。在下面的示例中,我们创建一个 ActionListener,它在javax.swing.JFrame的实例上调用 toFront 方法。

  1. myButton.addActionListener(
  2. (ActionListener)EventHandler.create(ActionListener.class, frame, "toFront"));

按下 myButton 时,将执行语句 frame.toFront()。通过定义 ActionListener 接口的新实现并将其实例添加到按钮,可以获得相同的效果,并具有一些额外的编译时类型安全性:

  1. //Equivalent code using an inner class instead of EventHandler.
  2. myButton.addActionListener(new ActionListener() {
  3. public void actionPerformed(ActionEvent e) {
  4. frame.toFront();
  5. }
  6. });

EventHandler的下一个最简单的用法是从监听器接口中的方法的第一个参数(通常是事件对象)中提取属性值,并使用它来设置目标对象中属性的值。在下面的示例中,我们创建一个 ActionListener,它将 target(myButton)对象的 nextFocusableComponent 属性设置为事件的“source”属性的值。

  1. EventHandler.create(ActionListener.class, myButton, "nextFocusableComponent", "source")

这将对应于以下内部类实现:

  1. //Equivalent code using an inner class instead of EventHandler.
  2. new ActionListener() {
  3. public void actionPerformed(ActionEvent e) {
  4. myButton.setNextFocusableComponent((Component)e.getSource());
  5. }
  6. }

也可以创建一个只将传入事件对象传递给目标操作的EventHandler。如果第四个EventHandler.create参数是一个空字符串,那么事件只是传递:

  1. EventHandler.create(ActionListener.class, target, "doActionEvent", "")

这将对应于以下内部类实现:

  1. //Equivalent code using an inner class instead of EventHandler.
  2. new ActionListener() {
  3. public void actionPerformed(ActionEvent e) {
  4. target.doActionEvent(e);
  5. }
  6. }

可能EventHandler最常见的用法是从事件对象的源中提取属性值,并将此值设置为目标对象的属性值。在下面的示例中,我们创建一个 ActionListener,它将目标对象的“label”属性设置为事件源的“text”属性值(“source”属性的值)。

  1. EventHandler.create(ActionListener.class, myButton, "label", "source.text")

这将对应于以下内部类实现:

  1. //Equivalent code using an inner class instead of EventHandler.
  2. new ActionListener {
  3. public void actionPerformed(ActionEvent e) {
  4. myButton.setLabel(((JTextField)e.getSource()).getText());
  5. }
  6. }