Event Handling with Swing Component Set

第1章提供了Swing组件集合的概述。在本章中,我们将会详细了解使用Swing组件的一个方面:事件处理。当使用Swing组件集合时,我们可以使用基于委托的事件处理机制,但是我们也可以使用其他的方法来响应用户的动作。在本章中,我们将会探索所有这些事件处理响应机制。我们同时也会了解到Swing是如何管理输入焦点以及控制输入焦点处理的相关技术。

当我们探索事件处理功能时,我们将会开始了解一些实际的Swing组件。在本章中,我们将会以最简单的方式来使用Swing组件。我们可以先阅读本书后面章节中所探讨的组件,然后再回到本章探讨事件处理。本书的后面章节中也包含每一个组件特定的事件处理的详细内容。

基于委托的事件处理

Sun在JDK.1.1及JavaBean的Java库中引入了基于委托的事件处理机制。尽管Java 1.0库中包含了遵循观察者行为设计模式的对象观察者对,但这并不是用户界面编程的长久解决方案。

事件委托模型

基于委托的事件处理机制是观察者设计模式的一种特殊形式。当一个观察者希望希望一个被监视的对象状态何时发生变化以及状态变化是什么时可以使用观察者模式。在基于委托的事件处理机制中,观察者并不监听状态改变,而是监听事件发生。

图2-1显示了在Java库中与事件处理的特定类相关的修改后的观察者模式结构。模式中的通用Subject管理一个用于Subject可以生成的事件的通用观察者对象列表。列表中的观察者对象必须提供一个特定的接口,通过这个接口Subject参与者可以通知他们。当观察者对象所感兴趣的事件在Subject参与者中发生时,所有已注册的观察者对象都会被通知到。在Java世界中,观察者对象要实现的接口必须扩展java.util.EventListener接口。Subject参与者必须创建的事件需要扩展java.util.EventObject类。

Swing_2_1.png

Swing_2_1.png

为了使得讨论更为清晰,下面我们由非设计模式的角度来了解基于委托的事件处理机制。GUI组件(JavaBean)管理一个监听器列表,每一个监听器会有用于监听器类型的一对方法:addXXXListener()与removeXXXListener()。当组件中有事件发生时,组件会通知所有注册的事件监听器。任何对该事件感兴趣的观察者类需要向组件注册一个相应接口的实现器。当事件发生时,所有的实现都被通知。图2-2尝过了这个过程。

Swing_2_2.png

Swing_2_2.png

作为观察者的事件监听器

使用事件监听器来处理事件分为三步:

  1. 定义一个类来实现相应的监听器接口(这包括为所有的接口方法提供实现)。
  2. 创建这个监听器的一个实例。
  3. 将监听器注册到我们所感兴趣的的事件的组件上。

下面我们通过创建一个简单的通过输出消息来响应选择的按钮来了解一下这三个特定步骤。

定义监听器

要为了一个可选择的按钮设置事件处理,我们需要创建一个ActionListener,因为JButton在被选中时会生成ActionEvent对象。

class AnActionListener implements ActionListener {
  public void actionPerformed(ActionEvent actionEvent) {
    System.out.println("I was selected.");
  }
}

创建监听器实例

接下来我们简单的创建一个我们所定义的监听器的实例。

ActionListener actionListener = new AnActionListener();

如果我们为事件监听器使用匿名内联类,我们就可以组合步骤1与2:

ActionListener actionListener = new ActionListener() {
  public void actionPerformed(ActionEvent actionEvent) {
    System.out.println("I was selected.");
  }
};

向组件注册监听器

一旦我们创建了监听器,我们就可以将其与相应的组件相关联。假定我们已经创建JButton,并将其引用存入在变量button中,我们可以通过调用按钮的addActionListener()方法来实现:

button.addActionListener(actionListener);

如果我们当前所定义的类就是实现监听器接口的类,那么我们就不需要创建一个单独的监听器实例。我们只需要将作为监听器的类与组件相关联。如下面的示例代码所示:

public class YourClass implements ActionListener {
  ... // Other code for your class
  public void actionPerformed(ActionEvent actionEvent) {
    System.out.println("I was selected.");
  }
  // Code within some method
   JButton button = new JButton(...);
   button.addActionListener(this);
  // More code within some method
}

以如上所示创建监听器并将其与组件相关联的方法使用事件处理器是响应Swing组件事件的基本方法。哪一个监听器与哪一个组件配合使用将会在后面的章节中描述相应的组件时进行讨论。在下面的内容中,我们将会了解到响应事件的一些其他方法。

多线程的Swing事件处理

为了提高其效率并降低其复杂性,所有的Swing组件都被设计为非线程安全的。尽管这听起比较恐怖,他只是简单的意味着对Swing组件的所有访问需要由一个单一线程完成--事件分发线程。如果我们并不确定我们位于一个特定的线程中,我们可以使用public static boolean isDispatchThread()方法请求EventQueue类或是通过public static boolean isEventDispatchThread()方法请求SwingUtilities类。后者只是作为前者的代理。

通过EventQueue类的帮助,我们可以创建Runnable对象在事件分发线程上执行来正确的访问组件。如果我们需要在事件分发线程上执行一个任务,但是我们并不需要结果也不会关心任务何时完成时,我们可以使用EventQueue的public static void invokeLater(Runnable runnable)方法。如果是相反的情况,直到任务结束并返回值时我们才能继承我们的工作,我们可以使用EventQueue的public static void invokeAndWait(Runnable runnable)方法。获取值的代码要由我们来完成,而并不是invokeAndWait()方法的返回值。

为了演示创建一个基于Swing程序的正确方法,列表2-1演示了一个用于可选中按钮的源代码。

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class ButtonSample {
  public static void main(String args[]) {
    Runnable runner = new Runnable() {
      public void run() {
        JFrame frame = new JFrame("Button Sample");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JButton button = new JButton("Select Me");
        // Define ActionListener
        ActionListener actionListener = new ActionListener() {
          public void actionPerformed(ActionEvent actionEvent) {
            System.out.println("I was selected.");
          }
        };
        // Attach listeners
        button.addActionListener(actionListener);
        frame.add(button, BorderLayout.SOUTH);
        frame.setSize(300, 100);
        frame.setVisible(true);
      }
    };
    EventQueue.invokeLater(runner);
  }
}

代码所生成的按钮如图2-3所示。

Swing_2_3.png

Swing_2_3.png

首先,我们来看一下invokeLater()方法。他需要一个Runnable对象作为参数。我们创建一个Runnable对象并传递给invokeLater()方法。在当前事件分发完成之后,Runnable对象就会执行。

Runnable runnable = new Runnable() {
  public void run() {
    // Do work to be done
  }
}
EventQueue.invokeLater(runnable);

如果我们希望我们的Swing GUI创建是线程安全的,那么我们所有的Swing代码就应该遵循这种模式。如果我们需要访问命令行参数,只需要在参数声明前添加final关键字就可以了:public static void main(final String args[])。这看起已经超出了一个简单的示例,但是这可以保证我们程序的线程安全性,确保所有的Swing组件的访问都是通过事件分发线程完成的。(然而调用repaint(),revalidate()以及invalidate()并不需要通过事件分发线程完成。)

列表2-1中另外一个需要解释的代码行就是

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

在默认情况下,如果我们图2-3中所示的容器中标题栏上的小X,程序并不会关闭;相反,框架会不可见。将默认的关闭操作设置为JFrame.EXIT_ON_CLOSE可以使得程序在会用户点击X时关闭。在第8章中我们探讨JFrame类时我们会了解到更多的信息。

使用SwingUtilities用于鼠标按钮标识

Swing组件集合包含了一个名为SwingUtilities的工具类,这个类提供了一个通用帮助方法集合。在本书中,当这个类的特定方法集合起来有用时,我们会间断的遇到这个类。对于列表2-1中的按钮示例,我们所感兴趣的方法是与确定选中哪个鼠标按钮相关的方法。

MouseInputListener接口由七个方法组成:mouseClick(MouseEvent), mouseEntered(MouseEvent), mouseExited(MouseEvent), mousePressed(MouseEvent)以及MouseListener中的mouseRelease(MouseEvent),MouseMotionListener中的mouseDragged(MouseEvent)与mouseMove(MouseEvent)。如果我们需要确定当事件发生时哪一个鼠标按钮被选中(或是释放),我们可以检测MouseEvent的modifiers属性,并将其与InputEvent类中的各种掩码设置常量进行对比。

例如,要检测鼠标按下事件中是否是鼠标中键被按下,我们可以在我们的鼠标监听器mousePressed()方法中使用下面的代码:

public void mousePressed(MouseEvent mouseEvent) {
  int modifiers = mouseEvent.getModifiers();
  if ((modifiers & InputEvent.BUTTON2_MASK) == InputEvent.BUTTON2_MASK) {
    System.out.println("Middle button pressed.");
  }
}

尽管这种方法可以工作得很好,然而SwingUtilities类提供三个方法可以使得这个过程更为简单:

SwingUtilities.isLeftMouseButton(MouseEvent mouseEvent)
SwingUtilities.isMiddleMouseButton(MouseEvent mouseEvent)
SwingUtilities.isRightMouseButton(MouseEvent mouseEvent)

现在我们不需要手动获取标识并与掩码进行对比,我们可以请求SwingUtilities来完这些工作,如下所示:

if (SwingUtilities.isMiddleMouseButton(mouseEvent)) {
  System.out.println("Middle button released.");
}

这可以使得我们的代码变得更容易阅读与维护。

列表2-2包含了一个更新的ButtonSample,在其中添加了另一个监听器来检测哪一个鼠标按钮被按下。

/**
 *
 */
package swingstudy.ch02;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

/**
 * @author lenovo
 *
 */
public class ButtonSample2 {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Button Sample");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                JButton button =  new JButton("Select Me");

                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        System.out.println("I was selected");
                    }
                };

                MouseListener mouseListener = new MouseAdapter() {
                    public void mousePressed(MouseEvent event) {
                        int modifiers = event.getModifiers();

                        if((modifiers & InputEvent.BUTTON1_MASK) == InputEvent.BUTTON1_MASK) {
                            System.out.println("Left button is pressed");
                        }

                        if((modifiers & InputEvent.BUTTON2_MASK) == InputEvent.BUTTON2_MASK) {
                            System.out.println("Middle button is pressed");
                        }

                        if((modifiers & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK) {
                            System.out.println("Right button is pressed");
                        }
                    }

                    public void mouseReleased(MouseEvent event) {
                        if(SwingUtilities.isLeftMouseButton(event)) {
                            System.out.println("Left button is released");
                        }

                        if(SwingUtilities.isMiddleMouseButton(event)) {
                            System.out.println("Middle button is released");
                        }

                        if(SwingUtilities.isRightMouseButton(event)) {
                            System.out.println("Right button is released");
                        }
                    }
                };

                button.addActionListener(actionListener);
                button.addMouseListener(mouseListener);

                frame.add(button, BorderLayout.SOUTH);
                frame.setSize(300,100);
                frame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

使用属性变化监听器作为观察者

除了基本的事件委托机制以外,JavaBean引入另一种观察者设计模式的变体,这次是通过属性变化监听器。PropertyChangeListener实现是观察者模式的确切表示。每一个观察者观察Subject的一个属性的变化。当Subject中发生变化时,观察者会被通知新的状态。图2-4显示了与JavaBean库中用于属性变化处理的特定类相关的观察者模式结构。在这种情况下,可观察的Subject具有一个add/remove属性变化监听器方法集合以及一个状态被监视的属性。

Swing_2_4.png

Swing_2_4.png

使用PropertyChangeListener,注册的监听器集合是在PropertyChangeSupport类中进行管理的。当监视的属性值变化时,这个支持类会通知所有的注册监听器新的以及旧的属性状态值。

通过向支持这种监听器类型的各种组件注册PropertyChangleListener对象,我们可以减少在初始化监听设置之后必须生成的代码量。例如,绑定Swing组件的背景颜色,意味着可以向组件注册一个PropertyChangeListener,当背景设置发生变化时就可以得到通知。当组件的背景属性值发生变化时,监听者就可以得到通知,从而可以使得观察者将其背景颜色设置为一个新的设置。所以,如果我们希望我们程序中的所有组件具有相同的背景颜色,我们可以将他们注册到一个组件。然后,当那个组件改变其背景颜色时,所有其他的组件都会被通知这种改变,并修改其背景为新的设置。

列表2-3中的程序演示了PropertyChangeListener的使用。他创建了两个按钮。当任意一个被选中时,被选中按钮的背景就会修改为一个随机的颜色值。第二个按钮监听第一个按钮的属性变化。当第一个按钮的背景颜色变化时,第二个按钮的背景颜色也会修改为新值。第一个按钮并没有监听第二个按钮的属性变化,所以当第二个按钮被选中并改变及背景颜色时,这种变化并不会传播到第一个按钮。

/**
 *
 */
package swingstudy.ch02;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Random;

import javax.swing.JButton;
import javax.swing.JFrame;

/**
 * @author lenovo
 *
 */
public class ButtonSample3 {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Button Sample");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                final JButton button1 = new JButton("Select Me");
                final JButton button2 = new JButton("No Select Me");

                final Random random = new Random();

                ActionListener actionListener = new ActionListener(){
                    public void actionPerformed(ActionEvent event) {
                        JButton button = (JButton)event.getSource();
                        int red = random.nextInt(255);
                        int green = random.nextInt(255);
                        int blue = random.nextInt(255);
                        button.setBackground(new Color(red, green, blue));
                    }
                };

                PropertyChangeListener propertyChangeListener = new PropertyChangeListener() {
                    public void propertyChange(PropertyChangeEvent event) {
                        String property = event.getPropertyName();
                        if("background".equals(property)) {
                            button2.setBackground((Color)event.getNewValue());
                        }
                    }
                };

                button1.addActionListener(actionListener);
                button1.addPropertyChangeListener(propertyChangeListener);
                button2.addActionListener(actionListener);

                frame.add(button1, BorderLayout.NORTH);
                frame.add(button2, BorderLayout.SOUTH);
                frame.setSize(300, 100);
                frame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

尽管这个例子只是实现了按钮选中中的一个颜色变化,想像一下,如果第一个按钮的背景颜色是由上百个不同位置变化的,而不是一个动作监听器。如果没有属性变化监听器,这些位置中的每一个都需要改变第二个按钮的背景颜色。借助于属性变化监听器,他只需要修改基本对象的背景颜色--在这种情况下是第一个按钮。然后这种变化会自动传播到其他的组件。

Swing库同时也会使用ChangeEvent/ChangeListener对来表示状态变化。尽管其与PropertyChangeEvent/PropertyChangeListener对相类似,但是ChangeEvent并不会带有新的以及旧的数据值设置。我们可以将其看作是属性变化监听器的一个轻量级版本。当多个属性值发生变化时ChangeEvent会非常有用,因为ChangeEvent并不需要包装变化。

管理监听器列表

如果我们正在创建我们自己的组件并且希望这些组件触发事件,我们需要维护一个要通知的监听器列表。如果监听器列表是用于AWT事件的,我们可以使用AWTEventMulticaster类用于列表管理。对于Swing库而言,如果事件并不是一个预定义的AWT事件类型,我们需要自己管理监听器列表。通过使用javax.swing.event包中的EventListenerList类,我们不再需要手动管理监听器列表,也无需担心线程安全。而且如果我们需要获取监听器列表,我们可以通过public EventLIstener[] getListener(Class listenerType)来请求Component,或者是类似于JButton的getActionListeners()方法的类型特定方法。这使得我们可以由一个内部管理列表中移除监听器,从而有助于垃圾回收。

AWTEventMulticaster类

无论我们是否意识到,AWTEventMulticaster类被AWT组件用来管理事件监听器列表。这个类实现了所有的AWT事件监听器(ActionListener, AdjustmentListener, ComponentListener, ContainerListener, FocusListener, HierarchyBoundsListener, HierarchyListener, InputMethodListener, ItemListener, KeyListener, MouseListener, MouseMotionListener, MouseWheelListener, TextListener, WindowFocusListener, WindowListener以及WindowStatListener)。无论何时我们调用组件的方法来添加或是移除一个监听器时,AWTEventMulticaster都会被用来作为支持。

如果我们希望创建我们自己的组件并且管理用于AWT事件/监听器对的监听器列表,我们可以使用AWTEventMulticaster。作为一个示例,我们来看一下如何创建一个通用组件,当按键在组件内部按下时,这个组件会生成一个ActionEvent对象。这个组件使用KeyEvent的public static String getKeyText(int keyCode)方法来将按键代码转换相应的文本字符串,并且将这个文本字符串作为ActionEvent的动作命令回传。因为这个组件是作为ActionListener观察者的源,他需要一对添加/移除方法来处理监听器的注册。这也就是AWTEventMulticaster类的用处所在,因为他会管理由我们的监听器列表变量中监听器的添加或移除:

private ActionListener actionListenerList = null;
public void addActionListener(ActionListener actionListener) {
  actionListenerList = AWTEventMulticaster.add(
    actionListenerList, actionListener);
}
public void removeActionListener(ActionListener actionListener) {
  actionListenerList = AWTEventMulticaster.remove(
    actionListenerList, actionListener);
}

类定义的其余部分描述了如何处理内部事件。为了向ActionListener发送击键需要注册一个内部的KeyListener。另外,组件必须能够获得输入焦点;否则,所有的击键都会到达其他的组件。完整的类定义如列表2-4所示。用于监听器通知的代码行以粗体显示。这一行通知所有的已注册的监听器。

/**
 *
 */
package swingstudy.ch02;

import java.awt.AWTEventMulticaster;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

import javax.swing.JComponent;

/**
 * @author lenovo
 *
 */
public class KeyTextComponent extends JComponent{

    private ActionListener actionListenerList = null;

    public KeyTextComponent() {
        setBackground(Color.CYAN);
        KeyListener internalKeyListener = new KeyAdapter() {
            public void keyPressed(KeyEvent event) {
                if(actionListenerList != null) {
                    int keyCode = event.getKeyCode();
                    String keyText = event.getKeyText(keyCode);
                    ActionEvent actionEvent = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, keyText);
                    actionListenerList.actionPerformed(actionEvent);
                }
            }
        };

        MouseListener internalMouseListener = new MouseAdapter() {
            public void mousePressed(MouseEvent event) {
                requestFocusInWindow();
            }
        };

        addKeyListener(internalKeyListener);
        addMouseListener(internalMouseListener);
    }

    public void addActionListener(ActionListener actionListener) {
        actionListenerList = AWTEventMulticaster.add(actionListenerList, actionListener);
    }

    public void removeActionListener(ActionListener actionListener) {
        actionListenerList = AWTEventMulticaster.remove(actionListenerList, actionListener);
    }

    public boolean isFocusable() {
        return true;
    }
}

图2-5显示所有的组件。图中上部分是组件,而下底部则是一个文本输入框。为了显示按下键的文本字符串,向更新文本框的KeyTextComponent注册了一个ActionListener。

示例源码如列表2-5所示。

/**
 *
 */
package swingstudy.ch02;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JFrame;
import javax.swing.JTextField;

/**
 * @author lenovo
 *
 */
public class KeyTextTester {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Key Text Sample");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                KeyTextComponent keyTextComponent = new KeyTextComponent();
                final JTextField textField = new JTextField();

                ActionListener actionListener =  new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        String keyText = event.getActionCommand();
                        textField.setText(keyText);
                    }
                };

                keyTextComponent.addActionListener(actionListener);

                frame.add(keyTextComponent, BorderLayout.CENTER);
                frame.add(textField, BorderLayout.SOUTH);
                frame.setSize(300, 200);
                frame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

EventListenerList类

尽管AWTEventMulticaster类很容易使用,然而他却并不能用于管理自定义的事件监听器列表或是javax.swing.event中的Swing事件器。我们可以创建一个这个类的自定义扩展用于处理我们需要管理的每一种类型的事件监听器列表,或者我们可以将列表存储在一个如Vector或是LinkedList的数据结构中。尽管使用Vector或是LinkedList可以工作得很好,当我们使用这种方法时,我们需要考虑同步问题。如果我们没有正确的编写列表管理,监听器通知也许会发生错误的监听器集合。

为了简化这种情况,Swing组件库包含了一个特殊的事件监听吕地类,EventListenerList。这个类的一个实例可以管理一个组件的所有不同的事件监听器。为了演示这个类的用法,我们来看一下如何使用EventListenerList替换AWTEventMulticaster来重写前面的例子。注意,在这个特定例子中,使用AWTEventMulticaster类实际上是一种更为简单的解决方法。然而,想像一个类似的情况下,在这种情况下事件监听器并不是一个预定义的AWT事件监听器或者是我们需要维护多个监听器列表。

添加或是移除监听器类似于在前面的例子中AWTEventMulticaster所用的技术。我们需要创建一个合适的变量类型-这次是EventListenerList-同时定义添加与移除监听器方法。这两种方法之间的主要区别在于初始的EventListenerList并不为null,而另一个初始时则是null。首先必须创建一个到空的EventListenerList的引用。这避免了在后面多次检测null列表变量的需要。添加与移除监听器的方法也有一些不同。因为EventListenerList可以管理任意类型的监听器列表,当我们添加或是移除监听器时,我们必须提供起作用的监听器类类型。

EventListenerList actionListenerList = new EventListenerList();
public void addActionListener(ActionListener actionListener) {
  actionListenerList.add(ActionListener.class, actionListener);
}
public void removeActionListener(ActionListener actionListener) {
  actionListenerList.remove(ActionListener.class, actionListener);
}

这只留下了要处理的监听器通知。在这个类中并不存在通用方法来通知有事件发生的特定类型的监听器,所以我们必须创建我们自己的代码。fireActionPerformed(actionEvent)的调用将会替代前面例子中的actionListenerList.actionPerformed(actionEvent)。这行代码会以数据形式由列表中获取一份一个特定类型的所有监听器的拷贝(以线程安全方式)。然后我们需要在这个列表中循环并通知合适的监听器。

protected void fireActionPerformed(ActionEvent actionEvent) {
  EventListener listenerList[] =
    actionListenerList.getListeners(ActionListener.class);
  for (int i=0, n=listenerList.length; i<n; i++) {
    ((ActionListener)listenerList[i]).actionPerformed(actionEvent);
  }
}

新的改进类的完整代码显示在列表2-6中。当使用EventListenerList类时,不要忘记这个类位于javax.swing.event包中。除了组件类的名字,测试程序并没有改变。

/**
 *
 */
package swingstudy.ch02;

import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.EventListener;

import javax.swing.JComponent;
import javax.swing.event.EventListenerList;

/**
 * @author lenovo
 *
 */
public class KeyTextComponent2 extends JComponent{

    private EventListenerList actionListenerList = new EventListenerList();

    public KeyTextComponent2() {
        setBackground(Color.CYAN);

        KeyListener internalKeyListener = new KeyAdapter() {
            public void keyPressed(KeyEvent event) {
                if(actionListenerList != null) {
                    int keyCode = event.getKeyCode();
                    String keyText = event.getKeyText(keyCode);
                    ActionEvent actionEvent = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, keyText);
                    fireActionPerformed(actionEvent);
                }
            }
        };

        MouseListener internalMouseListener = new MouseAdapter() {
            public void mousePressed(MouseEvent event) {
                requestFocusInWindow();
            }
        };

        addKeyListener(internalKeyListener);
        addMouseListener(internalMouseListener);
    }

    public void addActionListener(ActionListener actionListener) {
        actionListenerList.add(ActionListener.class, actionListener);
    }

    public void removeActionListener(ActionListener actionListener) {
        actionListenerList.remove(ActionListener.class, actionListener);
    }

    public void fireActionPerformed(ActionEvent event) {
        EventListener[] listenerList = actionListenerList.getListeners(ActionListener.class);
        for(int i=0, n=listenerList.length; i<n; i++) {
            ((ActionListener)listenerList[i]).actionPerformed(event);
        }
    }

    public boolean isFocusable() {
        return true;
    }
}

Timer类

除了EventQueue的invokeAndWait()与invokeLater()方法外,我们还可以使用Timer类来创建在事件分发线程上执行的动作。Timer提供了一种在预定义的时间之后通知ActionListener的方法。计时器可以重复通知监听吕在,或者是只通知一次。

创建计时器对象

下面是用于创建在ActionListener调用之间指定毫秒时延的Timer的构造器:

public Timer(int delay, ActionListener actionListener); // 1 second

interval Timer timer = new Timer(1000, anActionListener);

使用计时器对象

在创建了Timer对象之后,我们需要启动start()。一旦启动了Timer,ActionListener就会在指定的时间之后得到通知。如果系统繁忙,延时会更长,但绝不会更短。

如果我们需要停止Timer,我们可以调用stop()方法。Timer同时还有一个restart()方法,这个方法会调用stop()与start(),重新启动时延间隔。

为了演示了的需要,列表2-8定义了一个只是简单的输出消息的ActionListener。然后我们创建一个Timer每半秒调用这个监听器。在我们创建计时器之后,我们需要启动这个计时器。

/**
 *
 */
package swingstudy.ch02;

import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.Timer;

/**
 * @author lenovo
 *
 */
public class TimerSample {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Runnable runner = new Runnable() {
            public void run() {
                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        System.out.println("Hello world timer");
                    }
                };

                Timer timer =  new Timer(500, actionListener);
                timer.start();
            }
        };

        EventQueue.invokeLater(runner);
    }

}

Timer属性

表2-1列出Timer的六个属性。四个允许我们自定义计时器的行为。running告诉我们计时器是否启动而没有停止,而actionListeners会为我们提供动作监听器列表。

属性名 数据类型 可访问性
actionListeners ActionListener[] 只读
coalesce boolean 读写
delay int 读写
initialDelay int 读写
repeats boolean 读写
running boolean 只读

Table: Timer属性

delay属性与构造函数的参数相同。如果我们改变一个运行计时器的时延,只有已存在的时延超时时才会使用新的时延。

initialDelay属性使得我们在第一次运行之后除了间隔时延以外还可以其他的启动时延。例如,如果我们在前一个小时并不希望执行一件任务,但是我们希望在之后每15分钟执行一次,我们就需要在启动计时器这前修改initialDelay设置。在默认情况下,在构造函数中initialDelay与delay属性设置为相同的设置。

repeats属性默认情况下设置为true,从而重复运行计时器。当设置为false时,计时器只通知动作监听器一次。然而我们需要重新启动restart()计显示器来再次触发监听器。非重复计时器可以用于在触发事件之后发生的一次通知。

coalesce属性允许一个繁忙的系统当已注册的ActionListener对象有新事件需要触发时丢弃还没有发生的通知。在默认情况下,coalesce的值设置为true。这就意味着如果一个计时器每500毫秒运行一次,但是系统十分繁忙且已经有2秒没有响应,计时器只需要发送一条消息,而不需要发送丢失的消息。如果这个属性设置为false,那么就需要发送四条消息。

除了所列出的属性以外,我们还可以用下面的代码来允许日志消息:

Timer.setLogTimers(true);

日志消息对于没有可视化元素的动作十分有用,使得我们知道事件的发生。

Swing特定的事件处理

请记住,Swing组件是构建在AWT库之上的,Swing组件库具有一些改进的功能从而使得事件处理更为简单。功能改进覆盖AWT核心事件处理特性之上,由基本的动作监听到焦点管理。

为了简化事件处理,Swing库使用Action接口扩展了原始的ActionListener接口来存储具有事件处理器的可视属性。这使得事件处理器的创建独立于可视化组件。然后,当Action在稍后与一个组件相关联时,组件直接由事件处理器自动获取信息(例如按钮标签)。这包括当Action被修改时更新标签的通知。AbstractAction与TextAction类实现了这个概念。

Swing库同时添加了KeyStroke类从而使得我们更容易的响应键盘事件。当一个特定的击键序列被按下时,我们可以通知组件必须响应特定的动作,而无需监听一个特定键的所有按键事件。这些击键到动作的映射存储在InputMap与ActionMap对象的组合中。当组件容器具有信息时,InputMap就会特例化ComponentInputMap。Swing文本组件借助于Keymap接口可以更容易的使用这些来存储击键到动作的映射。第16章更详细的描述了TextAction支持的映射,以及文本事件处理功能的其余部分。

KeyboardFocusManager与DefaultKeyboardFocusManager,借助于FocusTraversalPolicy及其实现的帮助,管理焦点子系统。InputVerifier用于用户输入验证。这些内容都会在本章稍后的Swing组件管理部分进行讨论。

Action接口

Action接口是ActionListener接口的扩展,他可以非常灵活的用于定义与作为触发代理的组件相独立的共享事件处理器。这个接口实现了ActionListener,并且定义了一个查询表数据结构,其键值作为属性。然后,当Action与一个组件相关联时,这些显示属性会自动的传递到Action。下面是接口定义:

public interface Action implements ActionListener {
  // Constants
  public final static String ACCELERATOR_KEY;
  public final static String ACTION_COMMAND_KEY;
  public final static String DEFAULT;
  public final static String LONG_DESCRIPTION;
  public final static String MNEMONIC_KEY;
  public final static String NAME;
  public final static String SHORT_DESCRIPTION;
  public final static String SMALL_ICON;  // Listeners
  public void addPropertyChangeListener(PropertyChangeListener listener);
  public void removePropertyChangeListener(PropertyChangeListener listener);
  // Properties
  public boolean isEnabled();
  public void setEnabled(boolean newValue);
  // Other methods
  public Object getValue(String key);
  public void putValue(String key, Object value);
}

因为Action仅是一个接口,Swing提供了一个类来实现这个接口,这就是AbstractAction。

AbstractAction类

AbstractAction类提供了Action接口的一个默认实现。这就是属性行为实现的地方。

使用Action

一旦我们通过继承定义一个AbstractAction并且提供一个public void actionPerformed(ActionEvent actionEvent)方法,我们就可以将其传递给一些特殊的Swing组件。JCheckBox,JToggleButton,JMenuItem,JCheckBoxMenuItem以及JRadioButtonMenuItem提供了由动作创建组件的构造函数,而Swing文本组件通过Keymap,InputMap以及ActionMap对Action对象提供了内建支持。

当具有关联Action的组件被添加到相应的Swing容器中时,选中会触发Action的actionPerformed(ActionEvent actionEvent)方法的调用。组件的显示是通过添加到内部数据结构的属性元素来定义的。了为演示的需要,列表2-8提供了一个具有Print标签以及一个图标的Action。当其被激活时,会输出一个Hello, World消息。

import java.awt.event.*;
import javax.swing.*;
public class PrintHelloAction extends AbstractAction {
  private static final Icon printIcon = new ImageIcon("Print.gif");
  PrintHelloAction() {
    super("Print", printIcon);
    putValue(Action.SHORT_DESCRIPTION, "Hello, World");
  }
  public void actionPerformed(ActionEvent actionEvent) {
    System.out.println("Hello, World");
  }
}

一旦定义了Action,我们就可以创建Action并将其与我们所希望的组件相关联。

Action printAction = new PrintHelloAction();
menu.add(new JMenuItem(printAction));
toolbar.add(new JButton(printAction));

在我们将Action与对象相关联之后,如果我们发现我们需要修改Action的属性,我们只需要在一个地方修改其设置 。因为所有的属性都是绑定的,他们会传播到使用Action的任意组件。例如,禁止Action(printAction.setEnabled(false))将会禁止分别在JMenu与JToolBar上所创建的JMenuItem与JButton。相应的,通过printAction.putValue(Action.NAME, “Hello, World”)修改Action的名字将会修改相关联组件的文本标签。

图2-6JToolBar与JMenu上的PrintHelloAction的样子。可选中的按钮用来允许或是禁止Action,同时也可以修改其名字。

Swing_2_6.png

Swing_2_6.png

此示例的完整代码显示在列表2-9中。不要担心工具栏与菜单栏的创建。我们将会在第6章对其进行详细的讨论。

/**
 *
 */
package swingstudy.ch02;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JToolBar;

/**
 * @author lenovo
 *
 */
public class ActionTester {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Action Sample");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                final Action printAction = new PrintHelloAction();

                JMenuBar menuBar = new JMenuBar();
                JMenu menu = new JMenu("File");
                menuBar.add(menu);
                menu.add(new JMenuItem(printAction));

                JToolBar toolBar = new JToolBar();
                toolBar.add(new JButton(printAction));

                JButton enableButton = new JButton("Enable");
                ActionListener enableActionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        printAction.setEnabled(true);
                    }
                };
                enableButton.addActionListener(enableActionListener);

                JButton disableButton = new JButton("Disable");
                ActionListener disableActionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        printAction.setEnabled(false);
                    }
                };
                disableButton.addActionListener(disableActionListener);

                JButton relabelButton = new JButton("Relabel");
                ActionListener relabelActionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        printAction.putValue(Action.NAME, "Hello, World");
                    }
                };
                relabelButton.addActionListener(relabelActionListener);

                JPanel buttonPanel = new JPanel();
                buttonPanel.add(enableButton);
                buttonPanel.add(disableButton);
                buttonPanel.add(relabelButton);

                frame.setJMenuBar(menuBar);

                frame.add(toolBar, BorderLayout.SOUTH);
                frame.add(buttonPanel, BorderLayout.NORTH);

                frame.setSize(300, 200);
                frame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

AbstractAction属性

正如表2-2所示,AbstractAction类有三个可用的属性。

属性名 数据类型 访问性
enabled boolean 读写绑定
keys Object[] 只读
propertyChangeListeners PropertyChangeListener[] 只读

Table: Table 2-2 AbstractionAction属性

其余的绑定属性通过putValue(String key, Object value)放置在查询表中。获取当前的keys属性设置可以使得我们查看可以进行哪些设置,而不需要进行单独请求。表2-3描述了可以用作键值的Action预定义常量集合。我们也可以添加我们自己的常量,从而在以后动作发生时进行查询。

常量 描述
NAME Action名字,用作按钮标签
SMALL_ICON Action图标,用作按钮标签
SHORT_DESCRIPTION Action的简短描述;可以用作提示文本,但是默认情况下并不会
LONG_DESCRIPTION Action的长描述;可以用作访问功能(查看第22章)
ACCELERATOR KeyStroke字符串;可以用Action的快捷键
ACTION_COMMAND_KEY InputMap键;映射到与JComponent相关的ActionMap中的Action
MNEMONIC_KEY 按键代码;可以用作Action的快捷键
DEFAULT 可以用于我们自定义属性的未用常量

Table: Table 2-3 AbstractAction查询属性键

一旦一个属性已经存放在查询表中,我们可以通过public Object getValue(String key)进行获取。其作用方式类似于java.util.Hashtable类或是java.util.Map接口,区别在于:如果表中存在一个键值,那么我们尝试存入一个具有null值的key/value对,则查询表会移除这个键值。

KeyStroke类

KeyStroke类以及特定JComponent的inputMap与actionMap属性提供了一个简单的替换可以向组件注册KeyListener对象并监听特定键的按下。KeyStroke使得我们可以定义一个简单的按键集合,例如Shift-Ctrl-P或是F4。然后我们可以通过将其注册到组件来激活按键,并且在组件识别出时通知按键进行动作,从而通知ActionListener。

在我们探讨如何创建按键之前,我们先来了解一下可以激活按键的不同条件,从而添加不同的输入映射。有三个条件可以激活已注册的按键,并JComponent中的四个常量可以提供帮助。第四个用于未定义的状态。表2-4中列出了可用的四个常量。

常量 描述
WHEN_FOCUSED 当实际的组件获得输入焦点时激活按键
WHEN_IN_FOCUSED_WINDOW 当组件所在的窗口获得输入焦点时激活按键
WHEN_ANCESTOR_OF_FOCUSED_COMPONENT 当在组件或是在组件的容器中按下时激活按键
UNDEFINED_CONDITION 用于没有定义条件的情况

Table: 按键注册条件

创建按键

KeyStroke类是AWTKeyStroke的子类,并且没有公开的构造函数。我们可以通过下面的方法来创建一个按键:

public static KeyStroke getKeyStroke(char keyChar)
public static KeyStroke getKeyStroke(String representation)
public static KeyStroke getKeyStroke(int keyCode, int modifiers)
public static KeyStroke getKeyStroke(int keyCode, int modifiers,
  boolean onKeyRelease)
public static KeyStroke getKeyStrokeForEvent(KeyEvent keyEvent)

列表中的第一个版本,public static KeyStroke getKeyStroke(char keyChar),可以使得我们由一个char变量创建按键,例如Z。

KeyStroke space = KeyStroke.getKeyStroke('Z');

public static KeyStroke getKeyStroke(String representation)版本是最有趣的版本。他可以使得我们通过一个文本字符串来指定按键,例如”control F4”。字符串的标识符集合为shift, control, meta, alt, button1, button2与button3以及可以指定的多标识符。字符串的其余部分来自KeyEvent类的VK_*常量。例如,下面的代三为Ctrl-Alt-7定义了一个按键:

KeyStroke controlAlt7 = KeyStroke.getKeyStroke("control alt 7");

public static KeyStroke getKeyStroke(int keyCode, int modifiers)public static KeyStroke getKeyStroke(int keyCode, int modifiers,boolean onKeyRelease)是两个最为直接的方法。他允许我们直接指定VK_*常量 以及用于标识符的InputEvent掩码(没有标识符时为0)。当没有指定时,onKeyRelease为false。

KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, true);
KeyStroke shiftF4 = KeyStroke.getKeyStroke(KeyEvent.VK_F4, InputEvent.SHIFT_MASK);

列表中的最后一个版本,public static KeyStroke getKeyStrokeForEvent(KeyEvent keyEvent),将特定的KeyEvent直接映射到KeyStroke。当我们希望允许用户使用按键来激活事件时,这个方法就十分有用。我们要求用户为某一事件按下一个键,然后注册KeyEvent,从而下次按键发生时,事件就会被激活。

KeyStroke fromKeyEvent = KeyStroke.getKeyStrokeForEvent(keyEvent);

注册按键

在我们创建了按键之后,我们需要将其注册到组件。当我们向组件注册一个按键时,我们提供一个当按键按下(或是释放)时要调用的Action。注册要提供一个由按键到Action的映射。首先,我们通过getInputMap(condition)方法获取基于焦点激活条件组件的相应的InputMap。如果没有指定条件,则假定为WHEN_FOCUSED。然后我们在InputMap中添加一个由按键到文本字符串的映射:

component.getInputMap().put(keystroke, string)

如果我们知道已存在动作的动作字符串,我们就可以使用这个字符串;否则我们要定义这个字符串。然后我们使用ActionMap将字符串映射到Action:

component.getActionMap.put(string, action)

我们可以通过共享ActionMap实例来在组件之间共享动作。列表2-10的例子中创建了四个按钮,每一个都注册了不同的按键以及不同的焦点激活条件。按钮标签表明了按键激活条件。Action只是简单的输出消息并激活按钮标签。

/**
 *
 */
package swingstudy.ch02;

import java.awt.EventQueue;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.KeyStroke;

/**
 * @author lenovo
 *
 */
public class KeyStrokeSample {

    private static final String ACTION_KEY = "theAction";

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("KeyStroke Sample");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                JButton buttonA = new JButton("<html><center>FOCUSED<br>control alt 7");
                JButton buttonB = new JButton("<html><center>FOCUS/RELEASE<br>VK_ENTER");
                JButton buttonC = new JButton("<html><center>ANCESTOR<br>VK_F4+SHIFT_MASK");
                JButton buttonD = new JButton("<html><center>WINDOW<br>' '");

                Action actionListener = new AbstractAction() {
                    public void actionPerformed(ActionEvent event) {
                        JButton source = (JButton)event.getSource();
                        System.out.println("Activated: "+source.getText());
                    }
                };

                KeyStroke controlAlt7 = KeyStroke.getKeyStroke("control alt 7");
                InputMap inputMap = buttonA.getInputMap();
                inputMap.put(controlAlt7, ACTION_KEY);
                ActionMap actionMap = buttonA.getActionMap();
                actionMap.put(ACTION_KEY, actionListener);

                KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, true);
                inputMap = buttonB.getInputMap();
                inputMap.put(enter, ACTION_KEY);
                buttonB.setActionMap(actionMap);

                KeyStroke shiftF4 = KeyStroke.getKeyStroke(KeyEvent.VK_F4, InputEvent.SHIFT_MASK);
                inputMap = buttonC.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
                inputMap.put(shiftF4, ACTION_KEY);
                buttonC.setActionMap(actionMap);

                KeyStroke space = KeyStroke.getKeyStroke(' ');
                inputMap = buttonD.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
                inputMap.put(space, ACTION_KEY);
                buttonD.setActionMap(actionMap);

                frame.setLayout(new GridLayout(2,2));
                frame.add(buttonA);
                frame.add(buttonB);
                frame.add(buttonC);
                frame.add(buttonD);

                frame.setSize(400, 200);
                frame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

图2-7显示了程序运行时的样子。

Swing_2_7.png

Swing_2_7.png

使用快捷键

Swing库也可以使用KeyStroke对象用于一些内部功能。两个这样的功能为记忆键与快捷键,其工作如下:

  • 在组件记忆键中,标签中的一个字符以下划线出现。当这个字符平台特定的热键组合被按下时,组件就会被激活。例如,在图2-8所示的窗体中按下Alt-A则会在Windows XP平台下选中About按钮。
  • 菜单快捷键可以在菜单条目不可见的情况下激活条目。例如,在图2-8所示的窗体中按下Ctrl-P将会在File菜单不可见的情况下选中Print菜单条目。
Swing_2_8.png

Swing_2_8.png

我们将会在第6章了解更多关于记忆键与快捷键的内容。

Swing焦点管理

术语焦点是指一个组件获得输入焦点。当一个组件具有输入焦点时,他就会作为所有按键事件的源,例如文本输入。另外,一些特定的组件还有一些可视的标识来表明他们具有输入焦点,如图2-9所示。当特定的组件具有输入焦点时,除了使用鼠标选中,我们还可以使用键盘按键触发选中。例如,对于按钮,可以按下空格来激活。

Swing_2_9.png

Swing_2_9.png

在焦点管理中一个重要的概念就是焦点环,他映射了在一个特定的容器中的组件闭集合的焦点遍历顺序。下面的类是焦点管理中的主要角色:

  • FocusTraversalPolicy:定义了用来确定下一个与前一个可获得焦点的组件的java.awt类。
  • KeyboardFocusManager:用作键盘浏览与焦点变化的控制器的java.awt类。要请求焦点变化,我们通知管理器改变可获得焦点的组件;我们并不会在一个特定的组件上请求焦点。

我们可以通过注册一个FocusListener来确定Swing何时获得输入焦点。监听器可以使得我们知道一个组件何时获得或是失去焦点,当其他组件获得输入焦点时哪个组件失去焦点,当其他组件失去焦点时哪个组件获得焦点。另外,由于某些原因还会出一临时的焦点变化,例如弹出菜单。当菜单消失时失去焦点的组件会重新获得焦点。

安装的焦点遍历策略描述了焦点如何在一个窗体的可获得焦点的组件之间移动。在默认情况下,下一个组件是以组件添加到容器中的顺序来定义的,如图2-10所示。对于Swing程序来说,这个焦点遍历由图中的左上角开始,经历每一行并到达右下角。这是默认的策略,LayoutFocusTraversalPolicy。当所有的组件位于相同的容器中时,这个遍历顺序被称之为焦点环,并且可以限制在容器中。

Swing_2_10.png

Swing_2_10.png

移动焦点

作为一个基本功能的例子,我们来看一下如何创建两个监听器来处理输入焦点:一个MouseListener,当鼠标进入某个组件区域时将焦点移动到该组件上,以及一个ActionListener,将输入焦点移动到一个组件上。

当鼠标进行组件时,MouseListener只需要调用requestFocusInWindow()。

import java.awt.*;
import java.awt.event.*;
public class MouseEnterFocusMover extends MouseAdapter {
  public void mouseEntered(MouseEvent mouseEvent) {
    Component component = mouseEvent.getComponent();
    if (!component.hasFocus()) {
      component.requestFocusInWindow();
    }
  }
}

对于ActionListener,我们需要调用KeyboardFocusManager的focusNextComponent()方法。

package swingstudy.ch02;

import java.awt.KeyboardFocusManager;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class ActionFocusMover implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent event) {
        // TODO Auto-generated method stub
        KeyboardFocusManager manager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
        manager.focusNextComponent();
    }

}

ActionFocusMover与MouseEnterFocusMover显示了编程实现移动焦点的两种不同方法。ActionFocusMover使用KeyboardFocusManager用于遍历。在MouseEnterFocusMover中,调用requestFocusInWindow()意味着我们希望指定的组件获得焦点。然而,焦点获得可以关闭。如果组件不可以获取焦点,或者是因为focusable属性的默认设置为false或是我们调用component.setFocusable(false),那么该组件就会被略过,而下一个组件将会获得焦点;这个组件将会由Tab焦点环中移除。(想像一下滚动条并不在焦点环中,而拖拽来改变设置。

列表2-11中的程序使用这两个事件处理器来移动焦点。他创建了一个3X3的按钮网格,在其中每一个按钮都会有一个相关的鼠标监听器与一个焦点监听器。偶数按钮是可以选中,但是不可以获得焦点。

package swingstudy.ch02;

import java.awt.EventQueue;
import java.awt.GridLayout;
import java.awt.event.ActionListener;
import java.awt.event.MouseListener;

import javax.swing.JButton;
import javax.swing.JFrame;

public class FocusSample {

    public static void main(String[] args) {
        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Focus Sample");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                ActionListener actionListener = new ActionFocusMover();
                MouseListener mouseListener = new MouseEnterFocusMover();

                frame.setLayout(new GridLayout(3,3));

                for(int i=1; i<10; i++) {
                    JButton button = new JButton(Integer.toString(i));
                    button.addActionListener(actionListener);
                    button.addMouseListener(mouseListener);
                    if((i%2)==0) {
                        button.setFocusable(false);
                    }
                    frame.add(button);
                }

                frame.setSize(300, 200);
                frame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }
}

图2-11显示程序的主窗口。

Swing_2_11.png

Swing_2_11.png

检测焦点环

在Swing容器级别的一个自定义选项就是焦点环。记住,一个容器的焦点环就组件的闭集合的焦点遍历顺序的映射。我们可以通过将focusCycleRoot属性设置为true从而将焦点环限制在容器的边界之内,从而由内部容器内进行焦点遍历。然后,当在容器的最后一个组件内部按下Tab键时,焦点环会重新回到容器中的第一个组件上,而不会将输入焦点移到容器外部的第一个组件上。当在第一个组件内部按下Shift-Tab时,焦点环会移到容器中的最后一个组件上,而不会移动到外部容器的初始组件上。

图2-12显示如果我们将图2-10中的中间三个按钮放在这种方式限制的容器中时焦点顺序的样子。在这个循环中,我们通过按下Tab键向前移动并不能移动到第三行的第一个组件上。为了能够通过Tab键到达第二行容器,我们需要将focusTraversalPolicyProvider属性设置true。否则,因为面板会将遍历策略限制在第二行,Tab键并不能使得我们到达第三行。

列表2-12中的程序演示了图2-12所示的行为。其程序界面与图2-11所示的类似,只是行为不同。

package swingstudy.ch02;

import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;


public class FocusCycleSample {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Focus Cycle Sample");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                frame.setLayout(new GridBagLayout());
                GridBagConstraints constraints = new GridBagConstraints();
                constraints.weightx = 1.0;
                constraints.weighty = 1.0;
                constraints.gridwidth = 1;
                constraints.gridheight = 1;
                constraints.fill = GridBagConstraints.BOTH;

                // Row One
                constraints.gridy = 0;
                for(int i=0; i<3; i++) {
                    JButton button = new JButton(""+i);
                    constraints.gridx = i;
                    frame.add(button, constraints);
                }

                // Row Two
                JPanel panel = new JPanel();
                panel.setFocusCycleRoot(true);
                panel.setFocusTraversalPolicyProvider(true);
                panel.setLayout(new GridLayout(1,3));
                for(int i=0; i<3; i++) {
                    JButton button =  new JButton(""+(i+3));
                    panel.add(button);
                }
                constraints.gridx = 0;
                constraints.gridy = 1;
                constraints.gridwidth = 3;
                frame.add(panel, constraints);

                // Row Three
                constraints.gridy = 2;
                constraints.gridwidth = 1;
                for(int i=0; i<3; i++) {
                    JButton button = new JButton(""+(i+6));
                    constraints.gridx = i;
                    frame.add(button, constraints);
                }

                frame.setSize(300, 200);
                frame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

FocusTraversalPolicy类

FocusTraversalPolicy负责确定焦点遍历顺序。他可以使得我们指定顺序中的下一个与前一个组件。这个类提供了用于控制遍历顺序的六个方法:

getComponentAfter(Container aContainer, Component aComponent)
getComponentBefore(Container aContainer, Component aComponent)
getDefaultComponent(Container aContainer)
getFirstComponent(Container aContainer)
getInitialComponent(Window window)
getLastComponent(Container aContainer)

Swing提供了五个预定义的遍历策略,如表2-5所示。通过为我们的程序选择正确的遍历策略,或是编写我们自己的遍历策略,我们可以确定如何在屏幕上浏览。

策略 描述
ContainerOrderFocusTraversalPolicy 组件以他们被添加到容器中的顺序进行遍历。组件必须是可见的,可显示的,允许的以及可获取焦点的。
DefaultFocusTraversalPolicy AWT程序的默认策略,他扩展了ContainerOrderFocusTraversalPolicy通过组件等价(操作系统)来检测组件是否显示的设置了可获取焦点。等价的可获取焦点特性依赖于Java运行实现。
InternalFrameFocusTraversalPolicy JInternalFrame的特殊策略,可以确定基于默认框架组件的可获取焦点的组件。
SortingFocusTraversalPolicy 我们为策略函数提供一个Comparator来定义焦点环顺序。
LayoutFocusTraversalPolicy Swing程序的默认策略,他考虑组件的几何设置(高,宽,位置),然后由下到下,由左到右确定浏览顺序。由上至下,由左到右的顺序是通过我们的locale的当前ComponentOrientation设置来确定的。例如,Hebrew将会由右至左的顺序。

Table: 预定义的遍历策略

为了演示,列表2-13中的程序反转了Tab与Shift-Tab的功能。当我们运行这个程序时,其界面显示与前面的图2-11类似,具有3X3的按钮集合。然而,在这个版本中,初始焦点在第9个按钮,而按下Tab键则会使得我们到第8,第7个按钮,依次类推。Shift-Tab则是相反的顺序。

package swingstudy.ch02;

import java.awt.Component;
import java.awt.Container;
import java.awt.EventQueue;
import java.awt.FocusTraversalPolicy;
import java.awt.GridLayout;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.SortingFocusTraversalPolicy;

public class NextComponentSample {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Reverse Sample");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                frame.setLayout(new GridLayout(3,3));

                for(int i=9; i>0; i--) {
                    JButton button = new JButton(Integer.toString(i));
                    frame.add(button, 0);
                }

                final Container contentPane = frame.getContentPane();
                Comparator<Component> comp = new Comparator<Component>() {
                    public int compare(Component c1, Component c2) {
                        Component comps[] = contentPane.getComponents();
                        List list = Arrays.asList(comps);
                        int first = list.indexOf(c1);
                        int second = list.indexOf(c2);

                        return second-first;
                    }
                };

                FocusTraversalPolicy policy = new SortingFocusTraversalPolicy(comp);
                frame.setFocusTraversalPolicy(policy);

                frame.setSize(300, 200);
                frame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

KeyboardFocusManager类

AWT库中的抽象KeyboardFocusManager类用作Swing组件的输入焦点行为的控制机制框架。DefaultKeyboardFocusManager是具体实现。焦点管理器使得我们可以编程编程确认谁当前具有输入焦点并且可以修改。

具有当前输入焦点的组件被称之为焦点拥有者。这可以通过KeyboardFocusManager的focusOwner属性来访问。同时我们也可以发现focusedWindow与activeWindow属性。具有输入焦点的窗口是包含焦点拥有者的窗口,活动窗口或者是输入焦点所在的窗口或者是包含焦点拥有者的框架或对话框。

移动到前一个或是下一个组件的简单概念可以多种不同的方法支持。首先,我们可以使用Component与Container的简单API方法:

  • Component.transferFocus()
  • Component.transferFocusBackward()
  • Component.transferFocusUpCycle()
  • Component.transferFocusDownCycle()

前两个方法请求焦点分别是移动到下一个或是前一个组件。向上与向下环方法可以使得我们向上移出当前焦点环或向下进一个焦点环。

下面的方法直接映射到KeyboardFocusManager的方法:

  • focusNextComponent()
  • focusPreviousComponent()
  • upFocusCycle()
  • downFocusCycle()

相同的四个方法的第二个集合接受Component的第二个参数。如果没有指定组件,这些方法会基于当前的焦点拥有者改变具有焦点的组件。如果提供了组件,改变则是基于所提供的组件。

Tab与Shift-Tab是用键盘焦点遍历,因为他们被定义为如果不是全部也是绝大多数的组件的默认焦点遍历键。要定义我们自己的遍历键,我们可以通过Component的setFocusTraversalKeys()方法来替换或是添加一个键。不同的集合可以用于向前,向后,以及上一个环,分别由KeyboardFocusManager的FORWARD_TRAVERSAL_KEYS, BACKWARD_TRAVERSAL_KEYS以及UP_CYCLE_TRAVERSAL_KEYS常量指定。我们可以设置与获取每一个按键集合。例如,要为一个组件添加F3按键作为上一个环的按键,我们可以使用下面的代码:

Set<AWTKeyStroke> set = component.getFocusTraversalKeys(
  KeyboardFocusManager.UP_CYCLE_TRAVERSAL_KEYS);
KeyStroke stroke = KeyStroket.getKeyStroke("F3");
set.add(stroke);
component.setFocusTraversalKeys(KeyboardFocusManager.UP_CYCLE_TRAVERSAL_KEYS, set);

焦点遍历时验证输入

Swing提供了抽象的InputVerifier类用于任意JComponent的焦点遍历中的组件级别验证。只需要继承InputVerifier并提供我们自己的public boolean verify(JComponent)方法来验证组件的内容。

列表2-4提供了一个简单的数据文本框验证的例子,在其中显示了三个文本框,其中只有两个具有验证。除非文本框1与3是正确的,否则我们不能通过Tab按键离开。

package swingstudy.ch02;

import java.awt.BorderLayout;
import java.awt.EventQueue;

import javax.swing.InputVerifier;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JTextField;

public class VerifierSample {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Verifier Sample");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                JTextField textField1 = new JTextField();
                JTextField textField2 = new JTextField();
                JTextField textField3 = new JTextField();

                InputVerifier verifier = new InputVerifier() {
                    public boolean verify(JComponent comp) {
                        boolean returnValue;
                        JTextField textField = (JTextField)comp;
                        try {
                            Integer.parseInt(textField.getText());
                            returnValue = true;
                        }
                        catch (NumberFormatException e) {
                            returnValue = false;
                        }
                        return returnValue;
                    }
                };

                textField1.setInputVerifier(verifier);
                textField3.setInputVerifier(verifier);

                frame.add(textField1, BorderLayout.NORTH);
                frame.add(textField2, BorderLayout.CENTER);
                frame.add(textField3, BorderLayout.SOUTH);

                frame.setSize(300, 100);
                frame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

小结

在本章中,我们了解了使用Swing组件时进行事件处理的多种方法。因为Swing组件是构建在AWT组件之上的,我们可以使用这些组件中通常的基于的事件处理机制。然后我们了解了Swing组件的多线程限制以及如何使用EventQueue的invokeAndWait()与invokeLater()方法进行处理。我们同时也探讨了Swing组件如何使用JavaBean的PropertyChangeListener方法来进行绑定属性改变的通知。

除了探讨Swing组件与AWT组件的相似之外,我们同时也了解了Swing库所提供的多个新特性。我们探讨了Action接口以及他将事件处理任务完全由可视组件分离开来如何简化复杂的用户界面开发。我们了解了向组件注册KeyStroke对象来简化监听按键事件的技术。最后,我们探讨了Swing的焦点管理系统功能以及如何自定义焦点环并使用FocusTraversalPolicy与KeyboardFocusManager类,同时探讨了使用InputVerifier验证输入。

在第3章中,我们将会了解Swing组件集合的模型-视图-控制器(MVC)体系结构。我们将会了解到MVC如何使得我们的用户开发更为简单。

comments powered by Disqus