Welcome to The Definitive Guide to Java Swing Third Edtion’s documentation!

Contents:

Swing Overiview

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如何使得我们的用户开发更为简单。

The Model-View-Controller Architecture

第2章探讨了如何处理Swing组件的事件生产者与消费者。我们了解了Swing组件的事件处理如何超出原始的AWT组件的事件处理功能。在本章中,我们会进一步深入Swing组件设计,来探讨称之为Model-View-Controller(MVC)的体系地构。

理解MVC流

在1980年后首次被引入Smalltalk后,MVC体系结构是第2章所描述的观察者模式的一种特殊形式。MVC的模型部分存放组件的状态,并且用作Subject。MVC的视图部分用作Subject的观察者来显示模型状态。视图创建控制器,其中定义了用户界面如何响应用户输入。

MVC通信

图3-1显示MVC元素如何进行通信-在这种情况下,使用Swing的多行文本组件JTextArea。由MVC的角度来看,JTextArea作为MVC体系结构中的视图部分。显示在组件内部的是一个Document,他是JTextArea的模型。Document存放JTextArea的状态信息,例如文本内容。在JTextArea内部是一个InputMap格式的控制器。他将键盘输入映射为ActionMap中的命令,并且这些命令被映射到可以通知Document的TextAction对象。当发生通知时,Document创建一个DocumentEvent并将其发送回JTextArea。

Swing_3_1.png

Swing_3_1.png

Swing组件的UI委托

这个例子演示了Swing世界中MVC体系结构的一个重要方面。在视图与控制器之间需要发生复杂的交互。Swing设计将这两个元素组合为一个委托对象来简化设计。这导致了每一个Swing组件具有一个负责渲染当前组件状态并处理用户输入事件的UI委托。

有时,用户事件会导致不影响模型的视图改变。例如,当标位置是视图的一个属性。模型并不关心光标位置,只关心文本内容。影响光标位置的用户输入并不会传递给模型。相反,影响Document内容的用户输入(例如按下回退键)会被传递。按下回退键会导致由模型中移除一个字符。正是由于这种结合,每一个Swing组件具有一个UI委托。

为了演示,图3-2显示了具有模型与UI委托的JTextArea组成。用于JTextArea的UI委托由TextUI接口开始,并在BasicTextUI中进行基本实现。相应的,由用于JTextArea的BasicTextAreaUI进行特例化。BasicTextAreaUI创建一个视图,或者是一个PlainView或者是一个WrappedPlainView。在模型一侧,事情则要相对简单得多。Document接口由AbstractDocument类来实现,然后由PlainDocument进行具体化。

文本组件将会在第15章与第16章进行更为全面的解释。正如图3-2所示,文本组件的使用涉及到许多内容。在大多数情况下,我们并不需要处理到图中所示的程度。然而,所有这些类都是在幕后进行作用的。MVC体系结构的UI委托部分将会在第20章我们如何自定义委托时再进行深入的讨论。

Swing_3_2.png

Swing_3_2.png

共享数据模型

因为数据模型只存储状态信息,我们可以在多个组件之间共享模型。然后每一个组件视图可以用来修改模型。

在图3-3所示的情况中,三个不同的JTextArea组件可以用来修改一个Document模型。如果用户修改了一个JTextArea的内容,模型就会发生变化,使得其他的文本区域自动的反映更新的文档状态。对于任意的Document视图并没有必须手动通知其他的视图共享模型。

Swing_3_3.png

Swing_3_3.png

数据模型的共享可以通过下面两种方法来实现:

  • 我们可以创建与任意组件相分离的数据模型并通知所有的组件使用这个数据模型。
  • 我们可以先创建一个组件,由第一个组件获取模型,然后与其他的组件进行共享。
package swingstudy.ch03;

import java.awt.Container;
import java.awt.EventQueue;

import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.text.Document;

public class ShareModel {

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

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

                Container content = frame.getContentPane();

                JTextArea textarea1 = new JTextArea();
                Document document = textarea1.getDocument();

                JTextArea textarea2 = new JTextArea(document);
                JTextArea textarea3 = new JTextArea(document);

                content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));

                content.add(new JScrollPane(textarea1));
                content.add(new JScrollPane(textarea2));
                content.add(new JScrollPane(textarea3));

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

        EventQueue.invokeLater(runner);
    }

}

图3-4显示了在编辑共享文档之后程序的样子。注意,三个文本区域具有查看(或是修改)文档不同部分的功能。例如,他们并不会只限于只在末尾部添加文本。这是因为每一个文本区域都会分别管理位置与光标。位置与光标是视图的属性,而不是模型的属性。

Swing_3_4.png

Swing_3_4.png

理解预定义的数据模型

当使用Swing组件时,理解每一个组件后面的数据模型是有益的,因为数据模型存储他们的状态。理解每一个组件的数据模型有助于我们将可见的组件(视图部分)与其逻辑(数据模型部分)相分离。例如,理解了这种分离,我们就会明白为什么JTextArea内部的光标位置不是数据模型部分,而是视图部分。

表3-1提供了一份Swing组件,每一个组件描述数据模型的接口以及特定实现的完整列表。如果某个组件并没有列出来,则该组件由其父类继承了数据模型,如AbstractButton。另外,在某些情况下,多个接口会用来描述一个组件,因为数据存储在一个模型中,而数据的选择是在另一个模型中。例如JComboBox,MutableComboBoxModel接口由ComboBoxModel扩展而来。预定义的类不仅实现了ComboBoxModel接口,同时也实现了MutableComboBoxModel接口。

组件 数据模型接口 实现
AbstractButton ButtonModel DefaultButtonModel
JColorChooser ColorSelectionModel DefaultColorSelectionModel
JComboBox
ComboBoxModel
MutableComboBoxModel
N/A
DefaultComboBoxModel
JFileChooser ListModel BasicDirectoryModel
JList
ListModel
ListSelectionModel
AbstractListModel
DefaultListModel
DefaultListSelectionModel
JMenuBar SingleSelectinModel DefaultSingleSelectionModel
JPopupMenu SingleSelectionModel DefaultSingleSelectionModel
JProgressBar BoundedRangeModel DefaultBoundedRangeModel
JScrollbar BoundedRangeModel DefaultBoundedRangeModel
JSlider BoundedRangeModel DefaultBoundedRangeModel
JSpiner SpinnerModel
AbstractionSpinnerModel
SpinnerDateModel
SpinnerListModel
SpinnerNumberModel
JTabbedPane SingleSelectionModel DefaultSingleSelectionModel
JTable
TableModel
TableColumnModel
ListSelectionModel
AbstractTableModel
DefaultTableModel
DefaultTableColumnModel
DefaultListSelectionModel
JTextComponent Document
AbstractDocument
PlainDocument
StyledDocument
DefaultStyleDocument
HTMLDocument
JToggleButton ButtonModel
JToggleButton
ToggleButtonModel
JTree
TreeModel
TreeSelectionModel
DefaultTreeModel
DefaultTreeSelectionModel
JTree.EmptySelectionModel

Table: Swing组件模型

当直接访问一个组件的模型时,如果我们修改模型,所有注册的视图都会被自动通知。相应的,这会使得视图重新验证自身来保证组件显示他们的正确的当前状态。这种状态修改自动传播的特性也是为什么MVC如此流行的原因之一。另外,使用MVC体系结构有助于程序在随着时间与复杂性增长修改时变得更容易维护。如果我们改变可视组件库也不再需要担心丢失状态信息。

小结

本章提供了关于Swing组件如何使用修改的MVC体系结构的一个快速浏览。我们探讨了修改的MVC体系结构的构成以及一个特定的组件,JTextArea如何映射到这个体系结构。另外,本章讨论了在组件之间数据模型的共享并且列出了所有用于不同Swing组件的数据模型。

在第4章,我们将会开始了解构成Swing库的单个组件。另外,当我们检测Swing库中的基本JComponet组件时我们会探讨Swing组件类层次结构。

Core Swing Components

在第3章,我们简要介绍了JFC/Swing工程组件所用的Model-View-Controller(MVC)模式。在本章中,我们将会开始探讨如何使用许多可用组件中的关键部分。

所有的Swing组件都是以JComponent类为起点的。尽管Swing库的某些部分并不以JComponent类为根,但所有的组件在其继承的某些级别上共享JComponent类作为通用父类。JComponent类定义通用的行为与属性。在本章中,我们将会了解一些通用功能,例如组件绘制,自定义义,工具提示以及变化大小。

随着特定JComponent子孙类被关注 ,我们将会特别了解JLabel,JButton以及JPanel,三个更为广泛使用的Swing组件类。为了组件内显示图像,我们需要理解Icon接口,以及当使用预定义图像时的ImageIcon类与GrayFilter类的支持。另外,我们将会了解AbstractButton类,他是JButton类的父类。所有的AbstractButton的子类所共享的数据模型是ButtonModel接口;我们将会探讨这个接口及其特定实现,DefaultButtonModel。

JComponent类

JComponent类是所有的Swing组件继承的抽象基类。JComponent类有42个派生子类,每一个都继承了JComponent的功能。图4-1显示了继承层次结构。

尽管JComponent类是所有Swing组件的共同基类,但是Swing工程库中的许多类并不是由JComponent类派生类。这包括所有的高层窗口对象,例如JFrame,JApplet以及JInternalFrame;所有的MVC相关的类;事件处理相关的接口与类;等。所有这些类将会后面的章节中进行讨论。

尽管所有的Swing组件扩展JComponent,JComponent类扩展AWT的Container类,相应的,其扩展AWT的Component类。这就意味着许多的JComponent方面都是由AWT的Component与Container类所共享的。

Swing_4_1.png

Swing_4_1.png

组件分片

JComponent类定义了许多超出原始的AWT组件集合功能的AWT组件面。这包括自定义绘制行为以及自定义显示设置的不同方法,例如颜色,字体以及其他的客户端设置。

绘制JComponent对象

因为Swing的JComponent类是由Container类扩展而来的,因而会遵循基本的AWT绘制模型:所有的绘制都是通过paint()方法来完成的,而repaint()方法则用来触发更新。然而,许多任务的完成是不同的。JComponent类优化了绘制的许多方面从而改进性能与可扩展性。另外,RepaintManager类可以用来自定义绘制行为。

为了改进绘制性能与扩展性,JComponent将绘制操作分为三个任务。public void paint(Graphics g)方法被分为三个独立的protected方法调用。由调用的顺序,他们依次为paintComponent(g), paintBorder(g)以及paintChildren(g),通过原始的paint()调用传递Graphics参数。组件本身首先通过paintComponent(g)进行绘制。如果我们希望自定义Swing组件的绘制,我们可以重写paintComponent()方法而不是paint()方法。除非我们希望完全替换所有的绘制,我们需要首先调用super.paintComponent(),正如下面所示的,来获得默认的paintComponent()行为。

public class MyComponent extends JPanel {
  protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    // Customize after calling super.paintComponent(g)
  }
  ...
}

paintBorder()与paintChildren()方法是不可重写的。paintBorder()方法绘制组件周围的边框,第7章会对其概念进行更为完全的描述。如果在Swing容器对象内存在组件,则paintChildren()方法会绘制这些组件。

为了优化绘制,JComponent类提供了三个额外的绘制属性:opaque, optimizedDrawingEnabled以及doubleBuffered。其作用如下:

  • Optacity:JComponent的opaque属性定义了一个组件是否透明。当透明时,JComponent容器必须在组件之后绘制背景。为了改进性能,我们可以保留JComponent的不透明物并使得JComponent绘制其背景,而不要依赖于容器来绘制被覆盖的背景。
  • Optimization:optimizedDrawingEanbled属性紧邻的子元素是否可以重叠。如果子元素不可以重叠,可以极大的减少重绘时间。在默认情况下,优化绘制对于绝大多数的Swing组件是允许的,除了JDesktopPane,JLayeredPane以及JViewport。
  • Double buffering:在默认情况下,所有的Swing组件会将他们的绘制操作重复缓存到一个完整的容器层次结构所共享的缓冲区中;也就是,在一个窗体内的所有组件。这极大的改善了绘制性能,因为当允许双缓冲时(通过doubleBuffered属性),只有一个屏幕更新绘制。

JComponent的public void revalidate()方法也提供绘制支持。当这个方法被调用时,组件的高级容器会验证其本身。这与AWT的直接调用高级组件的revalidate()方法不同。

Swing组件绘制加强的最后一个方面就是RepaintManager类。

RepaintManager类

RepaintManager类负责保证当前显示的Swing组件之上的重绘请求的高效,确保当一个区域无效时只更新屏幕的最小“脏”区域。

尽管不能进行自定义,RepaintManager是公开并且提供了一个静态的安装例程来使用自定义管理器:public static void setCurrentManager(RepaintManager manager)。要获得当前的管理器,只需要调用public static void currentmanager(JComponent)方法。参数通常为null,除非我们已经自定义了管理器来提供组件级别的支持。一旦我们拥有管理器在,我们可以做的一件事就是将屏幕缓冲区获取为图像。因为缓冲区就是实际显示在屏幕上的内容,这可以使得我们高效的实现窗体内部的屏幕复制。

Component comp = ...
RepaintManager manager = RepaintManager.currentManager(null);
Image htmlImage = manager.getOffscreenBuffer(comp, comp.getWidth(),
  comp.getHeight());
// or
Image volatileImage = manager.getVolatileOffscreenBuffer(comp, comp.getWidth(),
  comp.getHeight());

表4-1显示了RepaintManager的两个属性。他可以使得我们禁止一个组件(层次结构)的所有绘制操作的双缓冲,并且设置最大的双缓冲尺寸,默认为终端用户的屏幕尺寸。

属性名 数据类型 可访问性
doubleBufferingEnabled boolean 读写
doubleBufferMaximumSize Dimension 读写

Table: RepaintManager属性

尽管很少实现,提供我们自己的RepaintManager子类确实允许我们自定义屏幕脏区域的绘制机制,或者是当绘制完成时的最少跟踪。重写下面四个方法的一个可以允许我们自定义机制:

public synchronized void addDirtyRegion(JComponent component, int x, int y,
  int width, int height)
public Rectangle getDirtyRegion(JComponent component)
public void markCompletelyClean(JComponent component)
public void markCompletelyDirty(JComponent component)

UIDefaults类

UIDefaults类表示为当前的观感所安装的包含显示设置的查询表,例如JList中所用的字体,在JTree节中所显示的颜色或图标。UIDefaults的使用将会在第20章探讨Java可插拨的观感体系结构时进行详细讨论。在这里,我们只是简要介绍UIDefaults表。

当我们创建一个组件时,组件会自动的请求UIManager在UIDefaults表中查找组件所用的当前设置。大多数的颜色,字体相关的组件设置,以及其他的一些与颜色与字体无关的设置,都是可配置的。如果你不喜欢一个特定的设置,我们可以简单的通过更新UIDefaults查询表中的相应项目进行修改。

首先我们需要知道我们希望修改的UIDefaults设置的名字。我们可以在本书的附录A中找到这些设置名字,在这个附录中包含J2SE 5.0中预定义观感的所有已知设置的完整列表。(由于发行版本的不同会略有不同。)另外,包含有每个组件描述的是一个包含UIResource相关属性元素的表。(要查找本书中特定组件部分,请查看内容表或是索引。)

一旦我们知道了设置的名字,我们可以使用UImanager的public static void put(Object key, Object value)方法来存放一个新的设置,其中key是键值字符串。例如,下面的代码会将新创建的按钮的背景颜色改变黑色,而前景色改变红色:

UIManager.put("Button.background", Color.BLACK);
UIManager.put("Button.foreground", Color.RED);

获取UIResource属性

如果我们正在创建自己的组件,或者是我们只需要查看当前的设置值,我们可以请求UIManager。尽管public static Object get(Object key)方法是最为通用的,却需要我们将返回值转换为合适的类型。相对应的,我们可以下列更为特定的getXXX()方法,这些方法会为我们执行錾,从而返回合适的类型:

public static boolean getBoolean(Object key)
public static Border getBorder(Object key)
public static Color getColor(Object key)
public static Dimension getDimension(Object key)
public static Font getFont(Object key)
public static Icon getIcon(Object key)
public static Insets getInsets(Object key)
public static int getInt(Object key)
public static String getString(Object key)
public static ComponentUI getUI(JComponent target)

这是一组接受Locale参数的重载的方法集合。

客户属性

除了UIManager维护一个key/value对设置以外,每一个组件实例还维护一个自己的key/value对集合。这对于维护一个不同于一定观感的组件或者是维护与一个组件关联的数据而不需要新类或是方法来存储这些数据的情况十分有用。

public final void putClientProperty(Object key, Object value)
public final Object getClientProperty(Object key)

例如,JTree类具有一个属性通过Metal观感来连接线风格或是显示JTree中的节点。因为设置特定于一个观感,因而向树API中添加一些内容并不合理。相反,我们可以通过下面的代码在一个特定的树实例上设置属性:

tree.putClientProperty("JTree.lineStyle", "None")

然后,当观感是默认的Metal时,树的节点将会用线连接。如果安装了其他的观感,客户属性就会被忽略。图4-2显示具有与不具有线的树。

Swing_4_2.png

Swing_4_2.png

JComopnent属性

我们已经了解了一些不同的JComponent子类所共享的属性。现在是了解JavaBean属性的时候了。表4-2显示了JComponent所定义的完整属性列表,包括由AWT Container为戌Component类所继承的属性。

属性名 数据类型 组件访问 容器访问 JComponent访问
accessibleContext AccessibleContext 只读 N/A 只读
actionMap ActionMap N/A N/A 读写
alignmentX float 只读 只读 读写
alignmentY float 只读 只读 读写
ancestorListeners AncestorListener[] N/A N/A 只读
autoscrolls boolean N/A N/A 读写
background Color 读写绑定 N/A 只写
backgroundSet boolean 只读 N/A N/A
border Border N/A N/A 读写绑定
bounds Rectangle 读写 N/A N/A
colorModel ColorModel 只读 N/A N/A
componentCount int N/A 只读 N/A
componentListeners ComponentListener[] 只读 N/A N/A
componentOrientation ComponentOrientation 读写绑定 N/A N/A
componentPopupMenu JPopupMenu N/A N/A 读写
components Component[] N/A 只读 N/A
containerListeners ContainerListener[] N/A 只读 N/A
cursor Cursor 读写 N/A N/A
cursorSet boolean 只读 N/A N/A
debugGraphicsOptions int N/A N/A 读写
displayable boolean 只读 N/A N/A
doubleBuffered boolean 只读 N/A 读写
dropTarget DropTarget 读写 N/A N/A
enabled boolean 读写 N/A 只写绑定
focusable boolean 读写绑定 N/A N/A
focusCycleRoot boolean N/A 读写绑定 N/A
focusCycleRootAncestor Container 只读 N/A N/A
focusListeners FocusListener[] 只读 N/A N/A
focusOwner boolean 只读 N/A N/A
focusTraversalKeyEnabled boolean 读写 N/A N/A
focusTraversalPolicy FocusTraversalPolicy N/A 读写绑定 N/A
focusTraversalPolicyProvider boolean N/A 读写绑定 N/A
focusTraversalPolicySet boolean N/A 只读 N/A
font Font 读写绑定 只写 只写
fontSet boolean 只读 N/A N/A
foreground Color 读写绑定 N/A 只写
foregroundSet boolean 只读 N/A N/A
graphics Graphics 只读 N/A 只读
graphicsConfiguration GraphicsConfiguration 只读 N/A N/A
height int 只读 N/A N/A
hierarchyBoundsLIsteners HierarchyBoundsListener[] 只读 N/A N/A
hierarchyListeners HierarchyListener[] 只读 N/A N/A
ignoreRepaint boolean 读写 N/A N/A
inheritsPopupMenu boolean N/A N/A 读写
inputContext InputContext 只读 N/A N/A
inputMap InputMap N/A N/A 只读
inputMethodListeners InputMethodListener[] 只读 N/A N/A
inputMethodRequests InputMethodRequests 只读 N/A N/A
inputVerifiers InputVerifier N/A N/A 读写绑定
insets Insets N/A 只读 只读
keyListeners KeyListener[] 只读 N/A N/A
layout LayoutManager N/A 读写 N/A
lightweight boolean 只读 N/A N/A
locale Locale 读写绑定 N/A N/A
location Point 读写 N/A N/A
locationOnScreen Point 只读 N/A N/A
maximumSize Dimension 读写绑定 只读 读写
maximumSizeSet boolean 只读 N/A N/A
minimumSize Dimension 读写绑定 只读 读写
minimumSizeSet boolean 只读 N/A N/A
mouseListeners MouseListener[] 只读 N/A N/A
mouseMotionListeners MouseMotionListener[] 只读 N/A N/A
mousePosition Point 只读 N/A N/A
mousWheelListeners MouseWheelListener 只读 N/A N/A
name String 读写 N/A N/A
opaque boolean 只读 N/A 读写绑定
optimizedDrawingEnabled boolean N/A N/A 只读
paintingTile boolean N/A N/A 只读
parent Container 只读 N/A N/A
preferredSize Dimension 读写绑定 只读 读写
preferredSizeSet boolean 只读 N/A N/A
propertyChangeListeners PropertyChangeListener[] 只读 N/A N/A
registeredKeyStrokes KeyStroke[] N/A N/A 只读
requestFocusEnabled boolean N/A N/A 读写
rootPane JRootPane N/A N/A 只读
showing boolean 只读 N/A N/A
size Dimension 读写 N/A N/A
toolkit Toolkit 只读 N/A N/A
tooltipText String N/A N/A 读写
topLevelAncestor Container N/A N/A 只读
transferHandler TransferHandler N/A N/A 读写绑定
treeLock Object 只读 N/A N/A
uiClassID String N/A N/A 只读
valid boolean 只读 N/A N/A
validateRoot boolean 只读 N/A N/A
verifyInputWhenFocusTarget boolean N/A N/A 只读
vetoableChangeListeners vetoableChangeListener[] N/A N/A 只读
visible boolean 读写 N/A 只写
visibleRect Rectangle N/A N/A 只读
width int 只读 N/A 只读
x int 只读 N/A 只读
y int 只读 N/A 只读

Table: JComponent属性

包括由父结构继承的属性,JComponent共有92个属性。正如这个数字所表明的,JComponent类极为适用于可视化开发。JComponent属性可以分十类,如下面所述。

面向位置的属性

处理JComponnent事件

所有的JComponent子类共享许多不同类型的事件。大多数的事件类型来自于父类,例如Component与Container。首先我们将会探讨由Container继承而来的PropertyChangeListener。然后我们会了解一下所有的JComponent子类所共享的两种事件处理功能的使用:VetoableChangeListener与AncestorListener。最后,我们来了解一下由Component继承的完全监听器集合。

使用PropertyChangeListener监听组件事件

JComponent类具有一些直接或间接的组件绑定属性。通过将PropertyChangeListener绑定到组件,我们可以监听特定的JComponent属性变化,并进行相应的响应。

public interface PropertyChangeListener extends EventListener {
  public void propertyChange(PropertyChangeEvent propertyChangeEvent);
}

为了演示的目的,列表4-1中的PropertyChangeListener演示当监听JButton组件中Action类型属性的变化时我们所需要的行为。属性的变化可以决定执行哪一个if语句块。

package swingstudy.ch04;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JButton;

public class ActionChangedListener implements PropertyChangeListener {

    private JButton button;

    public ActionChangedListener(JButton button) {
        this.button = button;
    }

    @Override
    public void propertyChange(PropertyChangeEvent e) {
        // TODO Auto-generated method stub

        String propertyName = e.getPropertyName();
        if(e.getPropertyName().equals(Action.NAME)) {
            String text = (String)e.getNewValue();
            button.setText(text);
            button.repaint();
        }
        else if(propertyName.equals("enabled")) {
            Boolean enabledState = (Boolean)e.getNewValue();
            button.setEnabled(enabledState.booleanValue());
            button.repaint();
        }
        else if(propertyName.equals(Action.SMALL_ICON)) {
            Icon icon = (Icon)e.getNewValue();
            button.setIcon(icon);
            button.invalidate();
            button.repaint();
        }
    }

}

使用VetoableChangeListener监听组件事件

VetoableChangeListener是Swing组件所使用的另一个JavaBean监听器。他使用限制属性,而PropertyChangeListener只使用绑定属性。这两个监听器之间的一个关键区别就在于如果监听器不能处理所请求的变化,则public void vetoableChange(PropertyChangeEvent propertyChangeEvent)方法会抛出一个PerpotyVetoException异常。

public interface VetoableChangeListener extends EventListener {
  public void vetoableChange(PropertyChangeEvent propertyChangeEvent)
    throws PropertyVetoException;
}

使用AncestorListener监听JComponent事件

我们可以使用AncestorListener可以确定组件何时移动,何时可见,以及何时不可见。如果我们允许用户通过在屏幕上移动组件以及由屏幕中移除组件进行屏幕定制,则AncestorListener就十分有用。

public interface  AncestorListener extends EventListener {
  public void ancestorAdded(AncestorEvent ancestorEvent);
  public void ancestorMoved(AncestorEvent ancestorEvent);
  public void ancestorRemoved(AncestorEvent ancestorEvent);
}

为了演示,列表4-2将一个AncestorListener与JFrame的根面板相关联。当程序首次启动时我们会看到Removed, Added以及Move信息。另外,当我们拖动窗体是我们会看Moved消息。

package swingstudy.ch04;

import java.awt.EventQueue;

import javax.swing.JFrame;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;

public class AncestorSample {

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

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

                AncestorListener ancestorListener = new AncestorListener() {
                    public void ancestorAdded(AncestorEvent event) {
                        System.out.println("Added");
                    }
                    public void ancestorMoved(AncestorEvent event) {
                        System.out.println("Moved");
                    }
                    public void ancestorRemoved(AncestorEvent event) {
                        System.out.println("Removed");
                    }
                };

                frame.getRootPane().addAncestorListener(ancestorListener);
                frame.setSize(300, 200);
                frame.setVisible(true);
                frame.getRootPane().setVisible(false);
                frame.getRootPane().setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

监听JComponent的继承事件

除了监听JComponent的AncestorEvent或是PropertyChangeEvent实际的功能,JComponent由其父类Container与Component继承了监听其他事件的能力。

表4-4列出了十个事件监听器。我们也许我们使用了相当多的JComponent监听器,但是旧版本也可以工作。使用最合适的来解决我们的任务。

事件监听器 事件对象
Component ComponentListener
componentHidden(ComponentEvent)
componentMoved(ComponenetEvent)
componentResized(ComponentEvent)
componentShow(ComponentEvent)
Component FocusListener
focusGained(FocusEvent)
focusLost(FocusEvent)
Component HierarchyBoundsListener
ancestorMoved(HierarchyEvent)
ancestorResized(HierarchyEvent)
Component HierarchyListener hierarchyChanged(HierarchyEvent)
Component InputMethodListener
carePositionChanged(InputMethodEvent)
inputMethodTextChanged(InputMethodEvent)
Component KeyListener
keyPressed(KeyEvent)
keyReleased(KeyEvent)
keyTyped(KeyEvent)
Component MouseListener
mouseClicked(MouseEvent)
mouseEntered(MouseEvent)
mouseExited(MouseEvent)
mousePressed(MouseEvent)
mouseReleased(MouseEvent)
Component MouseMotionListener
mouseDragged(MouseEvent)
mouseMoved(MouseEvent)
Component MouseWheelListener mouseWheelMoved(MouseWheelEvent)
Container ContainerListener
componentAdd(ContainerEvent)
componentRemoved(ContainerEvent)

Table: JComponent继承的事件监听器

JToolTip类

Swing组件支持当光标停留在其上时显示简短的弹出信息的功能。用来显示弹出信息的类就是JToolTip。

创建JToolTip

调用JComponent的public void setToolTipText(String text)方法可以使得当鼠标停留在一个安装了弹出信息的组件上时自动创建JToolTip实例。我们通常并不直接调用JToolTip构造函数。只有一个构造器,而他是无参数的变体。

工具提示的文本通常只是一行的长度。然而,如果文本字符串以

开头(在许多情况下如此),那么文本的内容可以任意的HTML 3.2格式化文本。例如,下面的代码使得弹出信息如图4-3所示:

component.setToolTipText("<html>Tooltip<br>Message");
Swing_4_3.png

Swing_4_3.png

创建自定义的JToolTip对象

正如在本章稍后的“自定义JToolTip观感”一节中所讨论的,我们可以通过为JToolTip设置UIResource元素很容易的自定义所有的弹出信息的显示特点。

JComponent类定义了一种简单的方法可以使得我们自定义当光标停留在某一个特定的组件上时工具提示的显示特点。我们只需要简单的继承我们要自定义的组件类并重写继承的public JToolTip createToolTip()方法。当ToolTipManager决定需要显示弹出信息的时候会调用createToolTip()方法。

要自定义弹出工具提示的外观,只需要重写这个方法并自定义由继承的方法所返回的JToolTip。例如,下面的代码显示了JButton工具提示的颜色的自定义,如图4-4所示。

JButton b = new JButton("Hello, World") {
  public JToolTip createToolTip() {
    JToolTip tip = super.createToolTip();
    tip.setBackground(Color.YELLOW);
    tip.setForeground(Color.RED);
    return tip;
  }
};
Swing_4_4.png

Swing_4_4.png

在创建了JToolTip之后,我们可以配置继承的JComponent属性或是任何特定的JToolTip属性,如表4-5所示。

属性名 数据类型 访问性
accessibleContext AccessibleContext 只读
component JComponent 读写
tipText String 读写
UI ToolTipUI 只读
UIClassID String 只读

Table: JToolTip属性

显示位置工具提示文本

Swing组件甚至支持基于鼠标位置的不同工具提示文本的显示。这需要重写public boolean contains(int x, int y)方法,这是由Component类继承的。

例如,在实现了前一节自定义的JButton的创建之后,工具提示文本可以依据鼠标点是否位距离组件左边50像素之内而显示不同的文本。

JButton button = new JButton("Hello, World") {
  public JToolTip createToolTip() {
    JToolTip tip = super.createToolTip();
    tip.setBackground(Color.YELLOW);
    tip.setForeground(Color.RED);
    return tip;
  }
  public boolean contains(int x, int y) {
    if (x < 50) {
      setToolTipText("Got Green Eggs?");
    }  else {
      setToolTipText("Got Ham?");
    }
    return super.contains(x, y);
  }
};

自定义义JToolTip观感

每一个已安装的Swing观感都会提供一个不同的JToolTip外观以及一个默认的UIResource值设置集合。图4-5显示了预安装的观感类型的JToolTip组件:Motif, Widnows与Ocean。

Swing_4_5.png

Swing_4_5.png

用于JToolTip的UIResource相关的属性显示在表4-6中。对于JToolTip组件,有9个不同的属性。

属性字符串 对象类型
ToolTip.background Color
ToolTip.backgroundInactive Color
ToolTip.border Border
ToolTip.borderInactive Color
ToolTip.font Font
ToolTip.foreground Color
ToolTip.foregroundInactive Color
ToolTip.hideAccelerator Boolean\
ToolTipUI String

Table: JToolTip UIResource元素

正如在本章的前面所注意到的,JToolTip类支持HTML内容的显示。这可以实现多列与多行输入的显示。

ToolTipManager类

尽管由于JComponent创建并创建并显示其自己的JToolTip,JToolTip在某种程度上是一个被动对象,其使用也有许多可以配置的方面。然而,这些配置是由管理工具提示的类来负责的,而不是由JToolTip本身负责。管理工具提示使用的类被称之为ToolTipManager类。由于使用了单例设计模式,ToolTipManager类并不存在构造函数。相反,我们可以通过ToolTipManager的静态sharedInstance()方法获得当前的管理器。

ToolTipManager属性

一旦我们获得了ToolTipManager的共享实例,我们就可以定制工具提示文本何时以及是否显示。如表4-7所示,有五个可配置的属性。

属性名 数据类型 访问性
dimissDelay int 读写
enabled boolean 读写
initialDelay int 读写
lightWeightPopupEnabled boolean 读写
reshowDelay int 只读

Table: ToolTipManager属性

初始时,工具提示是允许的,我们可通过ToolTipManager.shareInstance().setEnabled(false)方法来禁止。这使得我们可以将工具提示与组件相关联,并允许终端在需要允许或禁止工具提示。

有三个面向时间的属性:initialDelay, dimissDelay与reshowDelay。他们均以毫秒计数。initialDelay属性是合适的工具提示出现之前用户必须将鼠标停留在组件内部的毫秒数。dismissDelay指定当鼠标停止运动时文本显示的时间长度;如果用户移动鼠标,也会使得文本消失。reshowDelay决定用户重新进入组件并且使得弹出文本显示时在组件外部必须停留的时间。

lightWeightPopupEnabled属性用来决定存储工具提示文本的弹出窗口类型。如果这个属性为true,则弹出文本适应顶级窗口的边界之内,文本出现在一个Swing JPanel内部。如果这个属性为false,则弹出文本适应顶级窗口的边界之内,文本出现在一个AWT Panel之内。如果文本的部分内容不能出现在顶级窗口之内,无论属性设置为何值,弹出文本将会出现在Window内。

尽管不是ToolTipManager的属性,ToolTipManager的两个方法值得一提:

public void registerComponent(JComponent component)
public void unregisterComponent(JComponent component)

当我们调用JComponent的setToolTipText()方法时,这会使得组件将其自身注册到ToolTipManager。然而,有时我们需要直接注册一个组件。当组件部分的显示是由其他渲染器完成时必须如此。例如,对于JTree而言,TreeCellRenderer显示树的所有节点。当渲染器显示工具提示时,我们注册JTree并通知渲染器显示什么文本。

JTree tree = new JTree(...);
ToolTipManager.sharedInstance().registerComponent(tree);
TreeCellRenderer renderer = new ATreeCellRenderer(...);
tree.setCellRenderer(renderer);
...
public class ATreeCellRenderer implements TreeCellRenderer {
...
  public Component getTreeCellRendererComponent(JTree tree, Object value,
    boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
  ...
    renderer.setToolTipText("Some Tip");
    return renderer;
  }
}

JLabel类

我们要近距离查看的第一个真正的Swing组件就是最简单的JLabel。JLabel用作AWT Label的替换组件,但是所能做的事情更多。AWT Label仅限制为单行文本,JLabel可以是文本,图片,或者是两者都有。文本可以是单行文本也可以是HTML文本。另外,JLabel可以支持不同的允许与禁止的图片。图4-6显示了一些示例JLabel组件。

Swing_4_6.png

Swing_4_6.png

创建JLabel

JLabel有6个构造函数:

public JLabel()
JLabel label = new JLabel();

public JLabel(Icon image)
Icon icon = new ImageIcon("dog.jpg");
JLabel label = new JLabel(icon);

public JLabel(Icon image, int horizontalAlignment)
Icon icon = new ImageIcon("dog.jpg");
JLabel label = new JLabel(icon, JLabel.RIGHT);

public JLabel(String text)
JLabel label = new JLabel("Dog");

public JLabel(String text, int horizontalAlignment)
JLabel label = new JLabel("Dog", JLabel.RIGHT);

public JLabel(String text, Icon icon, int horizontalAlignment)
Icon icon = new ImageIcon("dog.jpg");
JLabel label = new JLabel("Dog", icon, JLabel.RIGHT);

通过JLabel的构造函数,我们可以自定义JLabel的三个属性:text, icon或是horizontalAlignment。默认情况下,text与icon属性是空的,而初始的horizontalAlignment属性设置依赖于构造函数的参数。这些设置可以是JLabel.LEFT,JLabel.CENTER或是JLabel.RIGHT。在大多数情况下,没有指定horizontalAlignment会导致左对齐标签。然而,如果仅指定了初始图标,则默认的对齐方式为居中对齐。

JLabel属性

表4-8显示了JLabel的14个属性。这些属性允许我们定制JLabel的内容,位置以及行为。

属性名 数据类型 访问性
accessibleContext AccessibleContext 只读
disabledIcon Icon 读写绑定
displayedMnemonic char 读写绑定
displaydMnemonicIndex int 读写绑定
horizontalAlignment int 读写绑定
horizontalTextPosition int 读写绑定
icon Icon 读写绑定
iconTextGap int 读写绑定
labelFor Component 读写绑定
text String 读写绑定
UI LabelUI 读写
UIClassID String 只读
verticalAlignment int 读写绑定
verticalTextPosition int 读写绑定

Table: JLabel属性

JLabel的内容是文本以及相关联的图片。在一个JLabel内显示图片将会在本章稍后的“接口Icon”一节中进行讨论。然而,我们可以依据于JLabel是允许或是禁止的而显示不同的图标。在默认情况下,如果允许的图标来自一个Image对象(ImageIcon,在本章稍后进行讨论),则允许的图标是灰度平衡的。如果允许图标并不是来自于一个Image,当Jlabel被禁止时则没有图标,除非我们手动指定。

JLabel内容的位置是由四个不同的属性来描述的:horizontalAlignment, horizontalTextPosition, verticalAlignment以及verticalTextPosition。horizontalAlignment与verticalAlignment属性描述了JLabel的内容在其所在的窗口的位置。

水平位置可以是JLabel的LEFT, RIGHT或是CENTER常量。垂直位置可以在TOP, BOTTOM或是CENTER。图4-7显示了各种对齐设置,通过图标显示对齐。

当同时指定了文本与图标时,文本位置属性反应了文本相对于图标的位置。这些属性可以设置为与对齐属性相同的常量。图4-8显示了各种文本属性设置,通过图标反应这些设置。

Swing_4_7.png

Swing_4_7.png

Swing_4_8.png

Swing_4_8.png

JLabel事件处理

JLabel并没有特定的事件处理功能。除了通过JComponent继承的事件处理功能以外,JLabel最接近于事件处理的就是displaydMnemonic, displayedMnemonicIndex与labelFor属性的组合使用。

当设置了displayedMnemonic与labelFor属性时,通过配合平台相关的热键按下指定的键时,会使得输入焦点移动到与labelFor属性相关联的组件上。当一个组件并没有自己的方式来显示记忆键设置时,例如所有的输入文本组件,这种用法就十分用。下面是一个演示示例,其运行结果如图4-9所示:

JLabel label = new JLabel("Username");
JTextField textField = new JTextField();
label.setDisplayedMnemonic(KeyEvent.VK_U);
label.setLabelFor(textField);
Swing_4_9.png

Swing_4_9.png

displayedMnemonicIndex属性可以使得所强调的记忆键并不一定是标签文本中的第一个记忆键实例。我们所指定的索引表示文本中的位置,而不是记忆键的实例。要强调Username中的第二个e,我们需要指定索引7:label.setDisplayedMnemonicIndex(7)。

自定义JLabel观感

每一个安装的Swing观感都会提供一个同的JLabel外观以及默认的UIResource值设置集合。尽管外观会依据当前的观感而不同,但是在预安装的观感类型集合中区别很小。表4-9显示了JLabel的UIResource相关的属性集合。对于JLabel组件有八个不同的属性。

属性字符串 对象类型
Label.actionMap ActionMap
Label.background Color
Label.border Border
Label.disableForeground Color
Label.disableShadow Color
Label.font Font
Lable.foreground Color
LabelUI String

Table: JLabel UIResource元素

Icon接口

Icon接口用来将图标与各种组件相关联。一个图标可以是简单的绘画或者是使用ImageIcon类由磁盘所载入的GIF图像。这个接口包含描述尺寸的两个属性以及一个用来绘制图标的方法。

public interface Icon {
  // Properties
  public int getIconHeight();
  public int getIconWidth();
  // Other methods
  public void paintIcon(Component c, Graphics g, int x, int y);
}

创建图标

图标的创建非常简单,只需要简单的实现接口。我们所需要做的就是指定图标的尺寸以及要绘制的内容。列表4-3演示了一个Icon的实现。这个图标是一个菱形图标,其尺寸,颜色以及填充状态都是可以配置的。

package swingstudy.ch04;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Polygon;

import javax.swing.Icon;

public class DiamondIcon implements Icon {

    private Color color;
    private boolean selected;
    private int width;
    private int height;
    private Polygon polygon;
    private static final int DEFAULT_WIDTH = 10;
    private static final int DEFAULT_HEIGHT = 10;

    public DiamondIcon(Color color) {
        this(color, true, DEFAULT_WIDTH, DEFAULT_HEIGHT);
    }

    public DiamondIcon(Color color, boolean selected) {
        this(color, selected, DEFAULT_WIDTH, DEFAULT_HEIGHT);
    }

    public DiamondIcon(Color color, boolean selected, int width, int height) {
        this.color = color;
        this.selected = selected;
        this.width = width;
        this.height = height;
        initPolygon();
    }

    private void initPolygon() {
        polygon = new Polygon();
        int halfWidth = width/2;
        int halfHeight = height/2;
        polygon.addPoint(0, halfHeight);
        polygon.addPoint(halfWidth, 0);
        polygon.addPoint(width, halfHeight);
        polygon.addPoint(halfWidth, height);
    }
    @Override
    public int getIconHeight() {
        // TODO Auto-generated method stub
        return height;
    }

    @Override
    public int getIconWidth() {
        // TODO Auto-generated method stub
        return width;
    }

    @Override
    public void paintIcon(Component c, Graphics g, int x, int y) {
        // TODO Auto-generated method stub
        g.setColor(color);
        g.translate(x, y);
        if(selected) {
            g.fillPolygon(polygon);
        }
        else {
            g.drawPolygon(polygon);
        }
        g.translate(-x, -y);
    }

}

使用图标

一旦我们有了Icon的实现,使用Icon就如何查看一个组件具有相应的属性一样简单。例如,下面的代码创建了一个具有图标的标签:

Icon icon = new DiamondIcon(Color.RED, true, 25, 25); JLabel label =

new JLabel(icon);

图4-10显这个标签的运行结果。

Swing_4_10.png

Swing_4_10.png

ImageIcon类

ImageIcon类提供了由AWT Image对象创建图标的Icon接口实现,Image对象可以来自内存(byte[]),来自磁盘(文件名)或是来自网络(URL)。与普通的Image对象不同,ImageIcon的载入是当ImageIcon被创建时立即启动的,尽管当使用时他也许还没有完全载入。另外,与Image对象不同,ImageIcon对象是可序列化的,所以他们可以很容易为JavaBean组件所使用。

创建ImageIcon

有九个构造函数可以用于创建ImageIcon:

public ImageIcon()
Icon icon = new ImageIcon();
icon.setImage(anImage);

public ImageIcon(Image image)
Icon icon = new ImageIcon(anImage);

public ImageIcon(String filename)
Icon icon = new ImageIcon(filename);

public ImageIcon(URL location)
Icon icon = new ImageIcon(url);

public ImageIcon(byte imageData[])
Icon icon = new ImageIcon(aByteArray);

public ImageIcon(Image image, String description)
Icon icon = new ImageIcon(anImage, "Duke");

public ImageIcon(String filename, String description)
Icon icon = new ImageIcon(filename, filename);public ImageIcon(URL location, String description)
Icon icon = new ImageIcon(url, location.getFile());

public ImageIcon(URL location, String description)
Icon icon = new ImageIcon(url, location.getFile());

public ImageIcon(byte imageData[], String description)
Icon icon = new ImageIcon(aByteArray, "Duke");

无参数的构造函数创建一个未初始化的版本。其余的八个构造函数提供了由Image,byte数组,文件名String或是URL,带有或是不带有描述来创建ImageIcon的功能。

使用ImageIcon

使用ImageIcon就如同使用Icon一样简单:仅需要创建ImageIcon并将其组件相关联。

Icon icon = new ImageIcon("Warn.gif");
JLabel label3 = new JLabel("Warning", icon, JLabel.CENTER)

ImageIcon属性

表4-10显示了ImageIcon的六个属性。ImageIcon的高与宽是实际的Image对象的高与宽。imageLoadStatus属性表示由隐藏MediaTracker载入ImageIcon的结果,或者是MediaTracker.ABORTED,MediaTracker.ERRORED,MediaTracker.COMPLETE。

属性名 数据类型 访问性
description String 读写
iconHeight int 只读
iconWidth int 只读
image Image 读写
imageLoadStatus int 只读
imageObserver ImageObserver 读写

Table: ImageIcon属性

有时使用ImageIcon来载入一个Image,然后由Image对象获取Icon是十分有用的。

ImageIcon imageIcon = new ImageIcon(...);
Image image = imageIcon.getImage();

使用ImageIcon对象时有一个主要问题:使用图标的图像与类文件都是由JAR文件载入时,他们不能工作,除非我们为JAR中的文件指定了完全的URL。我们不能仅仅指定文件名为一个String并使得ImageIcon查找这个文件。我们必须首先手动获取图像数据,然后将这些数据传递给ImageIcon构造函数。

为了解决在JAR文件外部载入图像,列表4-4显示了一个ImageLoader类,这个类提供了一个public static Image getImage(Class relativeClass, String filename)方法。我们同时指定图像文件相对的基类以及图像文件的名字。然后我们只需要将返回的Image对象传递给ImageIcon的构造函数。

package swingstudy.ch04;

import java.awt.Image;
import java.awt.Toolkit;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class ImageLoader {

    private ImageLoader() {

    }

    public static Image getImage(Class relativeClass, String filename) {
        Image returnValue = null;
        InputStream is = relativeClass.getResourceAsStream(filename);
        if(is != null) {
            BufferedInputStream bis = new BufferedInputStream(is);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            try {
                int ch;
                while ((ch = bis.read()) != -1) {
                    baos.write(ch);
                }
                returnValue = Toolkit.getDefaultToolkit().createImage(baos.toByteArray());
            }
            catch(IOException e) {
                System.err.println("Error loading: "+filename);
            }
        }
        return returnValue;
    }
}

下面的代码显示如何使用这个帮助类:

Image warnImage = ImageLoader.getImage(LabelJarSample.class, "Warn.gif");
Icon warnIcon = new ImageIcon(warnImage);
JLabel label2 = new JLabel(warnIcon);

GrayFilter类

另一个值得一提的类就是GrayFilter类。许多Swing组件依赖这个类来创建一个禁止的Image版本用作Icon。组件自动使用这个类,但是有时我们需要使用AWT的ImageFilter类实现灰度平衡。我们可以通过调用类的一个方法将一个Image由普通形式转换为灰度形式:public static Image crateDisabledImage(Image image)。

Image normalImage = ...
Image grayImage = GrayFilter.createDisabledImage(normalImage)

现在我们可以使用一个灰色的图像作为组件的Icon:

Icon warningIcon = new ImageIcon(grayImage);
JLabel warningLabel = new JLabel(warningIcon);

AbstractionButton类

如图4-1所示,AbstractButton类是作用在幕后作为所用 的Swing按钮组件的一个重要Swing类。在本章稍后的JButton类中所描述的JButton是最简单的子类。其余的子类将会在后续的章节中进行描述。

所有的AbstractButton子类使用ButtonModel接口来存储数据模型。DefaultButtonModel类是所使用的默认实现。另外,我们可以将任意的AbstractButton对象组合为一个ButtonGroup。尽管这种组合对于JRadioButton与JRadioButtonMenuItem组件最为自然,然而任意的AbstractButton子类都会起作用。

AbstractButton属性

表4-11列出了AbstractButton子类所共享的32个属性。这些属性可以使得我们自定义所有按钮的外观。

属性名 数据类型 访问性
action Action 读写绑定
actionCommand String 读写
actionListeners ActionListener[] 只读
borderPainted boolean 读写绑定
changeListeners ChangeListener[] 只读
contentAreaFilled boolean 读写绑定
disabledIcon Icon 读写绑定
disabledSelectedIcon Icon 读写绑定
disabledMnemonicIndex int 读写绑定
enabled boolean 只写
focusPainted boolean 读写绑定
horizontalAlignment int 读写绑定
horizontalTextPosition int 读写绑定
icon Icon 读写绑定
iconTextGap int 读写绑定
itemListeners ItemListener[] 只读
layout LayoutManager 只写
margin Insets 读写绑定
mnemonic char 读写绑定
mnemonic int 只写
model ButtonModel 读写绑定
multiClickThreshhold long 读写
pressedIcon Icon 读写绑定
rolloverEnabled boolean 读写绑定
rolloverIcon Icon 读写绑定
rolloverSelectedIcon Icon 读写绑定
selected boolean 读写
selectedIcon Icon 读写绑定
selectedObjects Object[] 只读
text String 读写绑定
UI ButtonUI 读写
verticalAlignment int 读写绑定
verticalTextPosition int 读写绑定

Table: AbstractButton属性

在这里值得一提的就是multiClickThreshhold。这个属性表示以毫秒计数的时间。如果一个按钮在这段时间间隔被鼠标多次选中,并不会产生额外的动作事件。默认情况下这个属性值为0,意味着每一次点击都会产生一个事件。为了避免在重要的对话框中偶然重复提交动作的发生,应将这个属性值设置0以上的合理值。

ButtonModel/Class DefaultButtonModel接口

ButtonModel接口被用来描述AbstractButton组件的当前状态。另外,他描述了为所有不同的AbstractButton子类所支持的事件监听器对象的集合。其定义如下:

public interface ButtonModel extends ItemSelectable {
  // Properties
  public String getActionCommand();
  public void setActionCommand(String newValue);
  public boolean isArmed();
  public void setArmed(boolean newValue);
  public boolean isEnabled();
  public void setEnabled(boolean newValue);
  public void setGroup(ButtonGroup newValue);
  public int getMnemonic();
  public void setMnemonic(int newValue);
  public boolean isPressed();
  public void setPressed(boolean newValue);
  public boolean isRollover();
  public void setRollover(boolean newValue);
  public boolean isSelected();
  public void setSelected(boolean newValue);
  // Listeners
  public void addActionListener(ActionListener listener);
  public void removeActionListener(ActionListener listener);
  public void addChangeListener(ChangeListener listener);
  public void removeChangeListener(ChangeListener listener);
  public void addItemListener(ItemListener listener);
  public void removeItemListener(ItemListener listener);
}

我们将要使用的特定的ButtonModel实现是DefaultButtonModel类,除非我们定义了自己的类。DefaultButtonModel类定义了不同的事件监听器的所有事件注册方法并且管理按钮状态并组织在ButtonGroup中。表4-12显示其9个属性。除了selectedObjects以外,他们均来自ButtonGroup接口,selectedObjects属性是DefaultButtonModel类的新成员,但是对于JToggleButton十分有用。在第5章中将会讨论ToggleButtonModel。

属性名 数据类型 访问性
actionCommand String 读写
armed boolean 读写
enabled boolean 读写
group ButtonGroup 读写
mnemonic int 读写
pressed boolean 读写
rollover boolean 读写
selected boolean 读写
selectedObjects Object[] 只读

Table: DefaultButtonModel属性

大多数情况下,我们并不直接访问ButtonModel。相反,使用ButtonModel的组件封装他们属性调用来更新模型属性。

理解AbstractButton热键

热键是一种特殊的键盘快捷键,当按下时会使用一个特定的动作发生。在前的JLable类一节中讨论JLablel时,按下所显示的热键会使得相关联的组件获得输入焦点。在AbstractButton的情况下,按下按键的热键会使得按钮被选中。

热键的实际点击需要一个观感特定的热键的点击(这个键通常是Alt键)。所以,如果一个按钮的热键是B键,我们需要按下Alt-B来激活具有B热键的按钮。当按钮被激活时,所注册的监听器会被通知相应的状态变化。例如,对于JButton,所有的ActionListener对象都会被通知。

如果热键是按钮文本标签的一部分,我们会看到这个字符以下划线表示。这会由于当前的观感不同而有不同的显示。另外,如果热键并不是文本标签的一部分,对于特定的热键的选中并不有可见的指示符,除非观感在工具提示文本中进行显示。

图4-11显示了两个按钮:一个具有W热键,而另一个具有H热键。左边的按钮在其内容具有W的标签,所以第一个W会以下划线显示。第二个组件并没有由按钮上的这种行为获益,但是在Ocean观感中,如果工具提示文本进行了设置则会时行标识。

Swing_4_11.png

Swing_4_11.png

要为抽象按钮赋予一个热键,我们可以使用任意一个setMnemonic()方法。其中一个接受char参数,而另一个则接受int参数。就个人而言,我比较喜欢int版本,其参数值是KeyEvent类中众多VK_*常量的一个。我们也可以通过displayedMnemonicIndex属性来指定热键。

AbstractButton button1 = new JButton("Warning");
button1.setMnemonic(KeyEvent.VK_W);
content.add(button1);

理解AbstractButton图标

AbstractButton具有七个特定的图标属性。默认的图标是icon属性。这个属性用于所有的情况,除非指定了一个不同的图标或是组件提供了默认的行为。selectedIcon属性是按钮被选中时所使用的图标。pressedIcon是按钮被按下时所用的图标。使用这两种图标中的哪一种依赖于组件,因为JButton被按下但是并没有被选中,而JCheckBox被选中却没有被按下。

当按钮通过setEnabled(false)被禁止时要使用disabledIcon与disabledSelectedIcon属性。默认情况下,如果图标是一个ImageIcon,将会使用图标的一个灰度平衡版本。

其他的两个属性,rolloverIcon与rolloverSelectedIcon允许我们当鼠标划过按钮时(rolloverEnabled为true)显示不同的图标。

理解内部的AbstractButton位置

horizontalAlignment, horizontalTextPosition, verticalAlignment与verticalTextPostion属性与JLabel类共享相同的设置与行为。表4-13列出这些属性。

位置属性 可用调用
horizontalAlignment LEFT, CENTER, RIGHT
horizontalTextPosition LEFT, CENTER, RIGHT
verticalAlignment TOP, CENTER, BOTTOM
verticalTextPosition TOP, CENTER, BOTTOM

Table: AbstractButton位置常量

处理AbstractButton事件

尽管我们并不会直接创建一个AbstractButton实例,但是我们会创建其子类。所有子类共享一个共同的事件处理功能集合。我们可以向抽象按钮注册PropertyChangeListener,ActionListener,ItemListener以及ChangeListener对象。在这里将会讨论PropertyChangeListener对象,其余的对象将会在后续的章节中进行讨论。

与JComponent类类似,AbstractButton组件支持当类的实例的绑定属性变化时支持PropertyChangeListener对象注册的检测。与JComponent类不同的是,AbstractButton组件提供了下列的类常量集合来表示不同的属性变化:

•BORDER_PAINTED_CHANGED_PROPERTY •CONTENT_AREA_FILLED_CHANGED_PROPERTY •DISABLED_ICON_CHANGED_PROPERTY •DISABLED_SELECTED_ICON_CHANGED_PROPERTY •FOCUS_PAINTED_CHANGED_PROPERTY •HORIZONTAL_ALIGNMENT_CHANGED_PROPERTY •HORIZONTAL_TEXT_POSITION_CHANGED_PROPERTY •ICON_CHANGED_PROPERTY •MARGIN_CHANGED_PROPERTY •MNEMONIC_CHANGED_PROPERTY •MODEL_CHANGED_PROPERTY •PRESSED_ICON_CHANGED_PROPERTY •ROLLOVER_ENABLED_CHANGED_PROPERTY •ROLLOVER_ICON_CHANGED_PROPERTY •ROLLOVER_SELECTED_ICON_CHANGED_PROPERTY •SELECTED_ICON_CHANGED_PROPERTY •TEXT_CHANGED_PROPERTY •VERTICAL_ALIGNMENT_CHANGED_PROPERTY •VERTICAL_TEXT_POSITION_CHANGED_PROPERTY

所以,我们可以创建一个使用这些常量的PropertyChangeListener,而不需要硬编码特定的文本字符串,如列表4-5所示。

import javax.swing.*;
import java.beans.*;
public class AbstractButtonPropertyChangeListener
    implements PropertyChangeListener {
  public void propertyChange(PropertyChangeEvent e) {
    String propertyName = e.getPropertyName();
    if (e.getPropertyName().equals(AbstractButton.TEXT_CHANGED_PROPERTY)) {
      String newText = (String) e.getNewValue();
      String oldText = (String) e.getOldValue();
      System.out.println(oldText + " changed to " + newText);
    }  else if (e.getPropertyName().equals(AbstractButton.ICON_CHANGED_PROPERTY)) {
      Icon icon = (Icon) e.getNewValue();
      if (icon instanceof ImageIcon) {
        System.out.println("New icon is an image");
      }
    }
  }
}

JButton类

JButton组件是可以被选中的最基本的AbstractButton组件。他支持文本,图像以及基于HTML的标签,如图4-12所示。

Swing_4_12.png

Swing_4_12.png

创建JButton

JButton类具有5个构造函数:

public JButton()
JButton button = new JButton();

public JButton(Icon image)
Icon icon = new ImageIcon("dog.jpg");
JButton button = new JButton(icon);

public JButton(String text)
JButton button = new JButton("Dog");

public JButton(String text, Icon icon)
Icon icon = new ImageIcon("dog.jpg");
JButton button = new JButton("Dog", icon);

public JButton(Action action)
Action action = ...;
JButton button = new JButton(action);

我们可以创建带有或是不带有文本标签或图标的按钮。图标表示AbstractButton中的默认或是selected图标属性。

JButton属性

JButton组件并没有为AbstactButton添加更多的内容。如表4-14所示,JButton的四个属性,唯一新添加的行为就是使用按钮成为默认。

属性名 数据类型 访问性
accessiableContext AccessiableContext 只读
defaultButton boolean 只读
defaultCapable boolean 读写绑定
UIClassID String 只读

Table: JButton属性

默认按钮使用与其他按钮不同的深色边框进行绘制。当一个按钮是默认按钮时,当在顶级窗口内按下回车键时会使得按钮被选中。这只有具有输入焦点的组件,例如文本组件或是其他的按钮,并不捕捉回车键的情况下才会起作用。因为defaultButton属性是只读的,(也许我们会问)我们如何将一个按钮设置为默认按钮呢?正如在第8章所描述的,所有的顶级窗口都包含一个JRootPane。我们通过设置JRootPane的defaultButton属性来告诉JRootPane哪一个按钮是默认按钮。只有defaultCapable属性为true的按钮才可以被设置为默认按钮。图4-13显示了右上解的按钮设置为默认按钮。

Swing_4_13.png

Swing_4_13.png

列表4-6演示了设置默认按钮以及基本JButton的使用。如果默认按钮的外观并没有如图4-13所示的那样明显,在第9章中会介绍JOptionPane,此时外观的区别将会更为明显。图4-13使用了一个2X2的GirdLayout布局。构造函数的另外两个参数表示间距,从而有助于使用默认按钮的外观更为明显。

package swingstudy.ch04;

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

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

public class DefaultButton {

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

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

                frame.setLayout(new GridLayout(2,2,10,10));

                JButton button1 = new JButton("Text Button");
                button1.setMnemonic(KeyEvent.VK_B);
                frame.add(button1);

                JButton button2 = new JButton("WarnIcon");
                frame.add(button2);

                JButton button3 = new JButton("Warn");
                frame.add(button3);

                String htmlButton = "<html><sup>HTML</sup><sub><em>Button</em></sub><br>"+
                    "<font color\"#FF0080\"><u>Multi-line</u></font>";
                JButton button4 = new JButton(htmlButton);
                frame.add(button4);

                JRootPane rootPane = frame.getRootPane();
                rootPane.setDefaultButton(button2);

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

}

处理JButton事件

JButton组件本身并没有特定的事件处理功能。他们都是由AbstractButton继承来的。尽管我们可以监听Change事件,Item事件以及PropertyChange事件,但是JButton最有用的监听器是ActionListener。

当JButton组件被选中时,所有注册的ActionListener对象都会被通知到。当按钮被选中时,ActionEvent会被传递到每一个监听器。当在多个组件之间使用共享监听器时,这个事件会传递按钮的actionCommand属性从而助于标识哪一个按钮被选中。如果actionCommand属性并没有被显示设置,则会传递当前的text属性。actionCommand属性的显式应用有助于本地化。因为JButton的text属性是用户所看到的,作为按钮被选中事件监听器的我们不能依赖于本地化文本标签来确定哪一个按钮被选中。所以由于text属性可以被本地化,因而在英语为Yes的按钮而在西班牙语中则是 Sí 按钮。如果我们显式的设置actionCommand属性为Yes字符串,那么无论用户正在使用哪一种语言 ,actionCommand会保持Yes不变,而并不会使用本地化的text属性字符串。

列表4-7在为列表4-6中的默认按钮添加了事件处理功能。注意,默认的行为可以正确工作:由任何组件按下回车键,按钮2(默认按钮)都会被激活。

package swingstudy.ch04;

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

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

public class ActionButtonSample {

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

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

                frame.setLayout(new GridLayout(2,2,10,10));

                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        String command = event.getActionCommand();
                        System.out.println("Selected: "+command);
                    }
                };

                JButton button1 = new JButton("Text Button");
                button1.setMnemonic(KeyEvent.VK_B);
                button1.setActionCommand("First");
                button1.addActionListener(actionListener);
                frame.add(button1);

                JButton button2 = new JButton("WarnIcon");
                button2.setActionCommand("Second");
                button2.addActionListener(actionListener);
                frame.add(button2);

                JButton button3 = new JButton("Warn");
                button3.setActionCommand("Third");
                button3.addActionListener(actionListener);
                frame.add(button3);

                String htmlButton = "<html><sup>HTML</sup><sub><em>Button</em></sub><br>"+
                    "<font color\"#FF0080\"><u>Multi-line</u></font>";
                JButton button4 = new JButton(htmlButton);
                button4.setActionCommand("Fourth");
                button4.addActionListener(actionListener);
                frame.add(button4);

                JRootPane rootPane = frame.getRootPane();
                rootPane.setDefaultButton(button2);

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

}

自定义JButton观感

每一个已安装的Swing观感都会提供一个不同的JButton外观与默认的UIResource值设置集合。图4-14显示了预安装的观感类型集合的JButton组件的外观:Motif,Windows以及Ocean。

Swing_4_14.png

Swing_4_14.png

表4-15显示了JButton的与UIResource相关的属性集合。对于JButton组件,共有34个不同的属性。

属性字符串 对象类型
Button.actionMap ActionMap
Button.background Color
Button.border Border
Button.contentAreaFilled Boolean
Button.darkShadow Color
Button.dashedRectGapHeight Integer
Button.dashedRectGapWidth Integer
Button.dashedRectGapX Integer
Button.dashedRectGapY Integer
Button.defaultButtonFollowsFocus Boolean
Button.disabledForeground Color
Button.disabledGrayRang Integer[]
Button.disabledShadow Color
Button.disabledText Color
Button.disabledToolBarBorderBackground Color
Button.focus Color
Button.focusInputMap InputMap
Button.font Font
Button.foreground Color
Button.gradient List
Button.highlight Color
Button.icon Icon
Button.iconTextGap Integer
Button.light Color
Button.margin Insets
Button.rollover Boolean
Button.rolloverIconType String
Button.select Color
Button.shadow Color
Button.showMnemonics Boolean
Button.textIconGap Integer
Button.textShiftOffset Integer
Button.toolBarBorderBackground Color
ButtonUI String

Table: JButton UIResource元素

JPanel类

最后一个基本的Swing组件是JPanel组件。JPanel组件可以作为一个通常目的的窗口对象,替换了AWT的Panel窗口,而当我们需要一个可绘制的Swing组件区域时,JPanel替换了Canvas组件。

创建JPanel

JPanel有四个构造函数:

public JPanel()
JPanel panel = new JPanel();

public JPanel(boolean isDoubleBuffered)
JPanel panel = new JPanel(false);

public JPanel(LayoutManager manager)
JPanel panel = new JPanel(new GridLayout(2,2));

public JPanel(LayoutManager manager, boolean isDoubleBuffered)
JPanel panel = new JPanel(new GridLayout(2,2), false);

使用这些构造函数,我们可以修改FlowLayout中的默认布局管理器,或是通过执行true或false修改默认的双缓冲。

使用JPanel

我们可以将JPanel用我们通常目的的容器,或者是用作新组件的基类。对于通常目的容器,其过程很简单:创建面析,如果需要设置其布局管理器,并且使用add()方法添加组件。

JPanel panel = new JPanel();
JButton okButton = new JButton("OK");
panel.add(okButton);
JButton cancelButton = new JButton("Cancel");
panel.add(cancelButton);

当我们需要创建一个新的组件时,派生JPanel并且重写public void paintComponent(Graphics g)方法。尽管我们可以直接派生JComponent,但派生JPanel修改更为合理。列表4-8演示了一个组件绘制适应组件尺寸的椭圆的简单组件,同时包含一个测试驱动。

package swingstudy.ch04;

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.GridLayout;

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

public class OvalPanel extends JPanel {

    Color color;

    public OvalPanel() {
        this(Color.black);
    }

    public OvalPanel(Color color) {
        this.color = color;
    }

    public void paintComponent(Graphics g) {
        int width = getWidth();
        int height = getHeight();
        g.setColor(color);
        g.drawOval(0, 0, width, height);
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

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

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

                Color colors[] = {Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW };

                for (int i=0;i<4;i++) {
                    OvalPanel panel = new OvalPanel(colors[i]);
                    frame.add(panel);
                }

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

        EventQueue.invokeLater(runner);
    }

}

图4-15显示了测试驱动的运行结果。

Swing_4_15.png

Swing_4_15.png

自定义JPanel观感

表4-16显示了JPanelUIResource相关的属性集合。对于JPanel组件,有五个不同的属性。这些设置也许会影响到面板内的组件。

属性字符串 对象类型
Panel.background Color
Panel.border Border
Panel.font Font
Panel.foreground Color
PanelUI String

Table: JPanel UIResource元素

小结

在本章中,我们探讨了所有Swing组件的基类:JComponent类。由讨论我们了解了所有组件的共同元素,例如工具提示,以及特定的组件,例如JLabel。同时我们了解了如何使用Icon接口以及ImageIcon类为组件添加图标,而GrayFilter图像过滤器用于禁止图标。

我们同时了解了AbstractButton组件,他是所有Swing按钮对象的根对象。我们了解了其数据模型接口,ButtonModel,以及这个接口的默认实现,DefalutButtonModel。接着,我们了解了JButton类,他是最简单的AbstractButton实现。最后,我们了解了作为基本Swing容器对象的JPanel。

在第5章中,我们将会深入一些复杂的AbstractButton实现:转换按钮。

Toggle Buttons

现在我们已经了解了相对简单的Swing组件JLabel与JButton的功能,现在我们来了解一些更为活跃的组件,特别是这些可以切换的组件。这些称之为可切换的组件-JToggleButton,JCheckBox与JRadioButton-为我们的用户提供了由一个选项集合中进行选择的方法。这些选项或者是打开,或者是关闭的,或者是允许,或者是禁止的。当表示在一个ButtonGroup中时,每次组中只有一个选项可以被选中。为了处理选中状态,组件与ToogleButtonModel共享一个共同的数据模型。下面我们来了解一下数据模型,使用ButtonGroup的组件组合机制,以及单个的组件。

ToggleButtonModel类

JToggleButton.ToggleButtonMode类是JToggleButton的一个公开内联类。这个类自定义了DefaultButtonModel类的行为,实现了ButtonModel接口。

自定义行为影响了ButtonGroup组件中所有AbstractButto的数据模型,ButtonGroup会在稍后的进行探讨。简单来说,一个ButtonGroup是AbstractButton组件的逻辑组合。在任意时刻,ButtonGroup中只有一个AbstractButton组件的selected属性被设置true,其他的必须为false。这并不意味着在组合中任意时刻只存在一个被选中的组件。如果ButtonGroup中的多个组件共享一个ButtonModel,那么在组合中就可以存在多个被选中的组件。如果没有组件共享模型,那么在组合中用户至多可以选中一个组件。一旦用户已经选择了一个组件,用户并不能交互的取消选择。然而,通过编程我们可以取消选中所有的组合元素。

JToggleButton.ToggleButtonModel定义如下:

public class ToggleButtonModel extends DefaultButtonModel {
  // Constructors
  public ToggleButtonModel();
  // Properties
  public boolean isSelected();
  public void setPressed(boolean newValue);
  public void setSelected(boolean newvalue);
}

ToggleButtonModel类为JToogleButton以及其后面章节中所描述的子类JCheckBox与JRadioButton,以及将在第6章进行描述的JCheckBoxMenuItem与JRadioButtonMenuItem类定义了默认的数据模型。

ButtonGroup类

在描述ButtonGroup类之前,我们先来演示其用法。列表5-1中的程序创建了使用ToggleButtonModel的对象并将其放在一个组合中。正如程序所演示的,除了向屏幕容器添加组件之外,我们必须将每一个组件添加到相同的ButtonGroup中。这导致了对于每一个组件的一对add()方法调用。而且,按钮组合的容器会将组件放在一列中,并且使用一个带有标题的边框为用户标识组合,尽管这些并不是必须的。图5-1显示了程序的输出。

package swingstudy.ch04;

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

import javax.swing.AbstractButton;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.JCheckBox;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JToggleButton;
import javax.swing.border.Border;

public class AButtonGroup {

    /**
     * @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 Group");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                JPanel panel = new JPanel(new GridLayout(0,1));
                Border border = BorderFactory.createTitledBorder("Examples");
                panel.setBorder(border);

                ButtonGroup group = new ButtonGroup();
                AbstractButton abstract1 = new JToggleButton("Toggle Button");
                panel.add(abstract1);
                group.add(abstract1);

                AbstractButton abstract2 = new JRadioButton("Radio Button");
                panel.add(abstract2);
                group.add(abstract2);

                AbstractButton abstract3 = new JCheckBox("Check Box");
                panel.add(abstract3);
                group.add(abstract3);

                AbstractButton abstract4 = new JRadioButtonMenuItem("Radio Button Menu Item");
                panel.add(abstract4);
                group.add(abstract4);

                AbstractButton abstract5 = new JCheckBoxMenuItem("Check Box Menu Item");
                panel.add(abstract5);
                group.add(abstract5);

                frame.add(panel, BorderLayout.CENTER);
                frame.setSize(300, 200);
                frame.setVisible(true);

            }
        };
        EventQueue.invokeLater(runner);
    }

}
Swing_5_1.png

Swing_5_1.png

正如前面所说的,ButtonGroup类表示AbstractButton组件的逻辑组合。ButtonGroup并不是一个可视化组件;所以,当使用ButtonGroup时在屏幕上并没有任何可见的内容。任何的AbstractButton组件可以通过public void add(AbstractButton abstractButton)方法添加到组合中。尽管任意的AbstractButton组件都可以属于一个ButtonGroup,只有当组件的数据模型是ToggleButtonModel时组合才会起作用。在ButtonGroup中具有一个模型为ToggleButtonModel的组件的结果是在组件被选中之后,ButtonGroup会出取消选中组合中当前被选中的组件。

尽管add()方法通常是我们唯一需要的方法,下面的类定义显示了其并不是ButtonGroup中唯一的方法:

public class ButtonGroup implements Serializable {
  // Constructor
  public ButtonGroup();
  // Properties
  public int getButtonCount();
  public Enumeration getElements();
  public ButtonModel getSelection();
  // Other methods
  public void add(AbstractButton aButton);
  public boolean isSelected(ButtonModel theModel) ;
  public void remove(AbstractButton aButton);
  public void setSelected(ButtonModel theModel, boolean newValue);
}

如上的类定义所显示的一件有趣的事就是给定一个ButtonGroup,我们并不能直接确定被选中的AbstractButton。我们只可以直接查询哪一个ButtonModel被选中。然而,getElements()可以返回组合中所有AbstractButton元素的Enumeration。然后我们可以使用类似如下的代码在所有的按钮中进行遍历来确定被选中的按钮:

Enumeration elements = group.getElements();
while (elements.hasMoreElements()) {
  AbstractButton button = (AbstractButton)elements.nextElement();
  if (button.isSelected()) {
    System.out.println("The winner is: " + button.getText());
    break; // Don't break if sharing models -- could show multiple buttons selected
  }
}

ButtonGroup另一个有趣的方法就是setSelected()。这个方法的两个参数是ButtonModel与boolean。如果boolean的值为false,则选中的请求会被忽略。如果ButtonModel并不是ButtonGroup中的按钮的模型,那么ButtonGroup会取消选中当前被选中的模型,从而使得组合中没有按钮被选中。这个方法的正确使用是使用组合中组件的模型以及一个true的新状态进行方法调用。例如,如果aButton是一个AbstractButton而aGroup是ButtonGroup,那么方法的调用类似于aGroup.setSelected(aButton.getModel(), true)。

下面我们来了解一下数据模型为ToggleButtonModel的各种组件。

JToggleButton类

JToggleButton是第一个可切换的组件。首先讨论JToggleButton类是因为他是其他们非面向菜单的组件,JCheckBox与JRadioButton,的父类。JToggleButton类似于JButton,当被选中时处理按下状态,相反则会返回到未选中状态。要取消被选中的组件,我们必须重新选择该组件。JToggleButton并不是一个被广泛使用的组件,但是我们会发现在工具栏上这个组件会非常有用,例如在Microsoft Word中或是在一个文件对话奇巧事,如图5-2所示。

Swing_5_2.png

Swing_5_2.png

定义JToggleButton结构是两个自定义AbstractButton父类的对象:ToggleButonModel与ToggleButtonUI。ToggleButtonModel类表示组件的自定义的ButtonModel数据模型,而ToggleButtonUI则是用户接口委托。

下面我们已经了解了JToggleButton的不同方面,现在我们来了解一下如何使用。

创建JToggleButton组件

对于JToggleButton有八个构造函数:

public JToggleButton()
JToggleButton aToggleButton = new JToggleButton();

public JToggleButton(Icon icon)
JToggleButton aToggleButton = new JToggleButton(new DiamondIcon(Color.PINK))

public JToggleButton(Icon icon, boolean selected)
JToggleButton aToggleButton = new JToggleButton(new DiamondIcon(Color.PINK), true);

public JToggleButton(String text)
JToggleButton aToggleButton = new JToggleButton("Sicilian");

public JToggleButton(String text, boolean selected)
JToggleButton aToggleButton = new JToggleButton("Thin Crust", true);

public JToggleButton(String text, Icon icon)
JToggleButton aToggleButton = new JToggleButton("Thick Crust",
  new DiamondIcon(Color.PINK));

public JToggleButton(String text, Icon icon, boolean selected)
JToggleButton aToggleButton = new JToggleButton("Stuffed Crust",
  new DiamondIcon(Color.PINK), true);

public JToggleButton(Action action)
Action action = ...;
JToggleButton aToggleButton = new JToggleButton(action);

每一个都允许我们自定义一个或是多个标签,图标,或是初始选中状态。除非指定,标签是空的,没有文本或是图标,而按钮初始时未被选中。

JToggleButton属性

在创建了JToggleButton之后,我们就可以修改其属性。尽管JToggleButton有近100个继承的属性,表5-1只显示了JToggleButton所引入的两个属性。其余的属性来自于AbstractButton,JComponent,Container以及Component。

属性名 数据类型 访问性
accessibleContext AccessibleContext 只读
UIClassID String 只读

Table: JToggleButton属性

我们可以在构造函数中修改一个或是多个text, icon或是selected属性,以及第4章所描述的其他的AbstractButton属性。我们可以通过getter与setter方法配置基本的三个属性:get/setText(), get/setIcon()以及is/setSelected()或setAction(action)。其他的属性也具有相应的getter与setter方法。

JToggleButton的更多的可视化配置选项包括按钮不同状态的各种图标。除了标准图标以外,当按钮被选中时,我们可以显示一个不同的图标。然而,如果我们正基于当前的选中状态修改图标,那么JToggleButton也许并不是最合适的组件。我们可以修改其子类,JCheckBox或是JRadioButton,我们会在本章稍后进行讨论。

处理JToggleButton选中事件

在配置了JToggleButton之后,我们可以使用三种方法来处理选中事件:使用ActionListener,ItemListener或是ChangeListener。除了向构造函数提供Action之外,被通知的方式类似于ActionListener。

使用ActionListener监听JToggleButton事件

如果我们只对当用户选中或是取消选中JToggleButton时所发生事件感兴趣,我们将ActionListener与组件相关联。在用户选中按钮之后,组件会通知任何已注册的ActionListener对象。不幸的是,这并不是所需要的行为,因为我们必须主动确定按钮的状态,从而我们能够对选中或是取消选中进行正确的响应。要确定选中状态,我们必须获取事件源的模型,然后查询其选中状态,如下面的ActionListener示例源码所示:

ActionListener actionListener = new ActionListener() {
  public void actionPerformed(ActionEvent actionEvent) {
    AbstractButton abstractButton = (AbstractButton)actionEvent.getSource();
    boolean selected = abstractButton.getModel().isSelected();
    System.out.println("Action - selected=" + selected + "\ n");
  }
};

使用ItemListener监听JToggleButton事件

关联到JToggleButton更好的监听器是ItemListener。ItemEvent会被传递到ItemListener的itemStateChanged()方法,包括按钮当前的选中状态。这使得我们可以进行正确的响应,而不需要查询当前的按钮状态。

为了演示,下面的ItemListener报告被选中的ItemEvent生成组件的状态:

ItemListener itemListener = new ItemListener() {
  public void itemStateChanged(ItemEvent itemEvent) {
    int state = itemEvent.getStateChange();
    if (state == ItemEvent.SELECTED) {
      System.out.println("Selected");
    }  else {
      System.out.println("Deselected");
    }
  }
};

使用ChangeListener监听JToggleButton事件

将ChangeListener关联到JToggleButton提供更多的灵活性。任意关联的监听器都会得到按钮数据模型变化的通知,响应armed, pressed以及selected属性的变化。由三个监听器监听通知-ActionListener, ItemListener以及ChangeListener-使得我们有七次不同的反应。

图5-3显示了ButtonModel属性变化序列,以及模型何时通知每个监听器。

Swing_5_3.png

Swing_5_3.png

为了演示ChangeListener通知,下面的代码片段定义了一个报告按钮模型三个属性状态变化的ChangeListener:

ChangeListener changeListener = new ChangeListener() {
  public void stateChanged(ChangeEvent changeEvent) {
    AbstractButton abstractButton = (AbstractButton)changeEvent.getSource();
    ButtonModel buttonModel = abstractButton.getModel();
    boolean armed = buttonModel.isArmed();
    boolean pressed = buttonModel.isPressed();
    boolean selected = buttonModel.isSelected();
    System.out.println("Changed: " + armed + "/" + pressed + "/" + selected);
  }
};

在我们将ChangeListener关联到JToggleButton之后并且通过组件之上的鼠标按下与释放选中组件时,输出结果如下:

Changed: true/false/false Changed: true/true/false Changed: true/true/true Changed: true/false/true Changed: false/false/true

将所有三个监听器关联到相同的按钮,已注册的ItemListener对象的通知将会在选中属性变化之后发生,换句说,在第3行与第4行之间。列表5-2演示了叛逆到相同的JToggleButton的所有三个监听器。考虑到已注册的ActionListener对象,通知发生在释放按钮之后,但是却在armed状态变为false之前,在第4行与第5行之间。

package swingstudy.ch04;

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

import javax.swing.AbstractButton;
import javax.swing.ButtonModel;
import javax.swing.JFrame;
import javax.swing.JToggleButton;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class SelectingToggle {

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

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

                JToggleButton toggleButton = new JToggleButton("Toggle Button");

                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        AbstractButton abstractButton = (AbstractButton)event.getSource();
                        boolean selected = abstractButton.getModel().isSelected();
                        System.out.println("Action - selected="+selected+"\n");
                    }
                };

                ChangeListener changeListener = new ChangeListener() {
                    public void stateChanged(ChangeEvent event) {
                        AbstractButton abstractButton = (AbstractButton)event.getSource();
                        ButtonModel buttonModel = abstractButton.getModel();
                        boolean armed = buttonModel.isArmed();
                        boolean pressed = buttonModel.isPressed();
                        boolean selected = buttonModel.isSelected();
                        System.out.println("Changed: "+armed +"/"+pressed+"/"+selected);
                    }
                };

                ItemListener itemListener = new ItemListener() {
                    public void itemStateChanged(ItemEvent event) {
                        int state = event.getStateChange();
                        if(state == ItemEvent.SELECTED) {
                            System.out.println("Selected");
                        }
                        else {
                            System.out.println("Deselected");
                        }
                    }
                };

                toggleButton.addActionListener(actionListener);
                toggleButton.addChangeListener(changeListener);
                toggleButton.addItemListener(itemListener);

                frame.add(toggleButton, BorderLayout.NORTH);
                frame.setSize(300, 125);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

自定义JToggleButton观感

每一个已安装的Swing观感都提供了不同的JToggleButton外观以及默认的UIResource值集合。图5-4显示了预安装的观感类型集合的JToggleButton的外观:Motif,Windows以及Ocean。正如按钮标签所指示的,第一个按钮被选中,第二个具有输入焦点(没有选中),第三个没有选中。

Swing_5_4.png

Swing_5_4.png

表5-2显示了JToggleButtonUIResource相关属性的集合。JToggleButton组件具有17个不同的属性。

属性字符串 对象类型
ToggleButton.background Color
ToggleButton.border Border
Toggle.darkShadow Color
ToggleButton.disabledText Color
ToggleButton.focus Color
ToggleButton.focusInputMap Object[]
ToggleButton.font Font
ToggleButton.foreground Color
ToggleButton.gradient List
ToggleButton.highlight Color
ToggleButton.light Color
ToggleButton.margin Insets
ToggleButton.select Color
ToggleButton.shadow Color
ToggleButton.textIconGap Integer
ToggleButton.textShiftOffset Integer
ToggleButtonUI String

Table: JToggleButton UIResource元素

JCheckBox类

JCheckBox类表示切换组件,在默认情况下,这个组件在接近文本标签处显示了一个复选框图标,用于两状态选项选择。复选框使用一个可选的复选标记来显示对象的当前状态,而不是如JToggleButton保持按钮按下状态。对于JCheckBox,图标显示了对象的状态,而对于JToggleButton,图标则是标签的一部分,通常并不用于显示状态信息。JCheckBox与JToggleButton之间除了UI相关部分不同外,这两个组件是相同的。图5-5演示了在一个匹萨预定程序中复选框的样子。

Swing_5_5.png

Swing_5_5.png

JCheckBox是由几部分构成的。与JToggleButton类似,JCheckBox使用一个ToggleButtonModel来表示其数据模型。用户界面委托是CheckBoxUI。尽管ButtonGroup可以用来组合复选框,但是通常这并不合适。当多个JCheckBox组件位于一个ButtonGroup中时,他们的行为类似于JRadioButton组件,但是看上去是JCheckBox组件。由于可视化的原因,我们不应将JCheckBox组件放在ButtonGroup中。

现在我们已经了解了JCheckBox的不同部分,下面我们来了解一下如何来使用。

创建JCheckBox组件

JCheckBox有八个构造函数:

public JCheckBox()
JCheckBox aCheckBox = new JCheckBox();

public JCheckBox(Icon icon)
JCheckBox aCheckBox = new JCheckBox(new DiamondIcon(Color.RED, false));
aCheckBox.setSelectedIcon(new DiamondIcon(Color.PINK, true));

public JCheckBox(Icon icon, boolean selected)
JCheckBox aCheckBox = new JCheckBox(new DiamondIcon(Color.RED, false), true);
aCheckBox.setSelectedIcon(new DiamondIcon(Color.PINK, true));

public JCheckBox(String text)
JCheckBox aCheckBox = new JCheckBox("Spinach");

public JCheckBox(String text, boolean selected)
JCheckBox aCheckBox = new JCheckBox("Onions", true);

public JCheckBox(String text, Icon icon)
JCheckBox aCheckBox = new JCheckBox("Garlic", new DiamondIcon(Color.RED, false));
aCheckBox.setSelectedIcon(new DiamondIcon(Color.PINK, true));

public JCheckBox(String text, Icon icon, boolean selected)
JCheckBox aCheckBox = new JCheckBox("Anchovies", new DiamondIcon(Color.RED,
  false), true);
aCheckBox.setSelectedIcon(new DiamondIcon(Color.PINK, true));

public JCheckBox(Action action)
Action action = ...;
JCheckBox aCheckBox = new JCheckBox(action);

每一个构造函数都允许我们定制零个或是至多三个属性:标签,图标或是初始选中状态。除非特别指明,默认情况下并没有标签,而复选框的默认选中/未选中图标表现为未选中。

如果我们在构造函数中初始化图标,图标用于复选框未选中状态,而复选框选中时也使用相同的图标。我们必须或者是通过setSelectedIcon(Icon newValue)方法来初始化选中图标,或者是确保图标是状态相关的并更新自身。如果我们没有配置选中图标,也没有使用状态相关图标,则相同的图标会出现在选中与未选中状态。通常而言,不在选中与未选中状态之间变化其可视外观的图标并不是JCheckBox所要求的。

JCheckBox属性

在创建了JCheckBox之后,我们可以修改其属性。JCheckBox特定的两个属性覆盖了其父类JToggleButton的行为。第三个borderPaintedFlat属性是在JDK 1.3版本中引入的。其余的属性都是通过其父类JToggleButton继承而来的。

属性名 数据类型 访问性
accessibleContext AccessibleContext 只读
borderPaintedFlat boolean 读写绑定
UIClassID String 只读

Table: JCheckBox属性

borderPaintedFlat属性可以将复选图标的边框的观感显示为两维而不是三维。在默认情况下,borderPaintedFlat属性为false,意味着边框将是三维的。图5-6显示了平坦边框的样子,其中第一个,第三个,第五个的边框是平坦的,而第二个与第四个不是。观感可以选择忽略这些属性。然而,对于组件的渲染者,例如表格与树,这个属性是十分用的,因为他们只显示状态而不显示是否可以选中。Windows与Motif观感类型使用这个属性,而Metal(以及Ocean)则不使用这个属性。

Swing_5_6.png

Swing_5_6.png

正如所列出的构造函数所显示的,如果我们选择通过构造函数设置图标,则构造函数只为未选中的状态设置一个图标。如果我们希望复选框图标显示实际的正确状态,我们必须使用一个状态感知图标,或者是通过setSelectedIcon()为选中状态关联一个不同的图标。具有两个不同的可视状态表示是大多数用户希望JCheckBox所应用的,所有除非我们有特殊的理由,最好是遵循普通用户界面的设计约定。

图5-7所显示的界面底部的第四个按钮演示了JCheckBox的用法。复选框总是显示了选中状态。下图显示了选中Pizza,未选中Calzone,未选中Anchovies以及未选中Crust时的状态。

Swing_5_7.png

Swing_5_7.png

列表5-3演示了创建具有不同图标的JCheckBox组件的三种可用方法,其中一个使用状态感知图标。最后一个复选框显示了坏图标的用法。

package net.ariel.ch05;

import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.Image;

import javax.swing.AbstractButton;
import javax.swing.ButtonModel;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JCheckBox;
import javax.swing.JFrame;

import net.ariel.ch04.DiamondIcon;

public class IconCheckBoxSample {

    private static class CheckBoxIcon implements Icon {
        private ImageIcon checkedIcon = new ImageIcon("plus.png");
        private ImageIcon uncheckedIcon = new ImageIcon("minus.png");

        public void paintIcon(Component component, Graphics g, int x, int y) {
            AbstractButton abstractButton = (AbstractButton)component;
            ButtonModel buttonModel = abstractButton.getModel();
            g.translate(x, y);
            ImageIcon imageIcon = buttonModel.isSelected() ? checkedIcon : uncheckedIcon;
            Image image = imageIcon.getImage();
            g.drawImage(image, 0, 0, component);
            g.translate(-x, -y);
        }

        public int getIconWidth() {
            return 20;
        }

        public int getIconHeight() {
            return 20;
        }
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

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

                Icon checked = new DiamondIcon(Color.BLACK, true);
                Icon unchecked = new DiamondIcon(Color.BLACK, false);

                JCheckBox aCheckBox1 = new JCheckBox("Pizza", unchecked);
                JCheckBox aCheckBox2 = new JCheckBox("Calzone");
                aCheckBox2.setIcon(unchecked);
                aCheckBox2.setSelectedIcon(checked);

                Icon checkBoxIcon = new CheckBoxIcon();
                JCheckBox aCheckBox3 = new JCheckBox("Anchovies", checkBoxIcon);
                JCheckBox aCheckBox4 = new JCheckBox("Stuffed Crust", checked);

                frame.setLayout(new GridLayout(0, 1));
                frame.add(aCheckBox1);
                frame.add(aCheckBox2);
                frame.add(aCheckBox3);
                frame.add(aCheckBox4);

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

        EventQueue.invokeLater(runner);
    }

}

处理JCheckBox选中事件

与JToggleButton类似,我们也可以使用三种方法来处理JCheckBox事件:使用ActionListener,ItemListener或是ChangeListener。接受Action的构造函数只是添加一个参数作为ActionListener。

使用ActionListener监听JCheckBox事件

使用ActionListener订阅ActionEvent事件可以使得我们确定用户何时切换JCheckBox状态。与JToggleButton类似,所订阅的监听器会被通知选中,但并不会通知新状态。要确定选中状态,我们必须获取事件源的模型并进行查询,如下面的ActionListener源码所示。这个监听器修改了复选框的标签来反映选中状态。

ActionListener actionListener = new ActionListener() {
   public void actionPerformed(ActionEvent actionEvent) {
     AbstractButton abstractButton = (AbstractButton)actionEvent.getSource();
     boolean selected = abstractButton.getModel().isSelected();
     String newLabel = (selected ? SELECTED_LABEL : DESELECTED_LABEL);
     abstractButton.setText(newLabel);
   }
};

使用ItemListener监听JCheckBox事件

与JToggleButton类似,对于JCheckBox而言,更为适合的监听器是ItemListener。传递给ItemListener的itemStateChanged()方法的ItemEvent事件包含复选框的当前状态。这使得我们可以进行正确的响应,而无需查询当前的按钮状态。

为了进行演示,下面的ItemListener依据选中组件的状态切换前景色与背景色。在这个ItemListener中,只有状态被选中时才会进行前景色与背景色的切换。

ItemListener itemListener = new ItemListener() {
   public void itemStateChanged(ItemEvent itemEvent) {
     AbstractButton abstractButton = (AbstractButton)itemEvent.getSource();
     Color foreground = abstractButton.getForeground();
     Color background = abstractButton.getBackground();
     int state = itemEvent.getStateChange();
     if (state == ItemEvent.SELECTED) {
       abstractButton.setForeground(background);
       abstractButton.setBackground(foreground);
     }
   }
};

使用ChangeListener监听JCheckBox事件

与JToggleButton类似,ChangeListener也可以响应JCheckBox事件。当按钮被armed, pressed, selected, released时所订阅的ChangeListener会得到通知。另外,ChangeListener也会得到ButtonModel变化的通知,但是复选框的键盘快捷键。因为在JToggleButton与JCheckBox之间并没有ChangeListener的区别,所以我们可以将JToggleButton中的监听器关联到JCheckBox,而我们会得到相同的选中响应。

列表5-4中的示例程序演示了监听一个JCheckBox事件的所有监听器。为了演示ChangeListener会得到其他按钮模型属性变化的通知,我们将一个执键与组件相关联。由于ChangeListener的注册发生在mnemonic属性变化之前,所以ChangeListener会得到这个属性变化的通知。因为前景色,背景色与文本标签并不是按钮模型属性,ChangeListener并不会得到其他监听器所引起的这些属性变化的通知。

package net.ariel.ch05;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyEvent;

import javax.swing.AbstractButton;
import javax.swing.ButtonModel;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class SelectingCheckBox {

    private static String DESELECTED_LABEL = "Deselected";
    private static String SELECTED_LABEL = "Selected";
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

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

                JCheckBox checkBox = new JCheckBox(DESELECTED_LABEL);

                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        AbstractButton abstractButton = (AbstractButton)event.getSource();
                        boolean selected = abstractButton.isSelected();
                        String newLabel = (selected ? SELECTED_LABEL : DESELECTED_LABEL);
                        abstractButton.setText(newLabel);
                    }
                };

                ChangeListener changeListener = new ChangeListener() {
                    public void stateChanged(ChangeEvent event) {
                        AbstractButton abstractButton = (AbstractButton)event.getSource();
                        ButtonModel buttonModel = abstractButton.getModel();
                        boolean armed = buttonModel.isArmed();
                        boolean pressed = buttonModel.isPressed();
                        boolean selected = buttonModel.isSelected();
                        System.out.println("Changed: "+armed+"/"+pressed+"/"+selected);
                    }
                };

                ItemListener itemListener = new ItemListener() {
                    public void itemStateChanged(ItemEvent event) {
                        AbstractButton abstractButton = (AbstractButton)event.getSource();
                        Color foreground = abstractButton.getForeground();
                        Color background = abstractButton.getBackground();
                        int state = event.getStateChange();
                        if(state == ItemEvent.SELECTED) {
                            abstractButton.setForeground(background);
                            abstractButton.setBackground(foreground);
                        }
                    }
                };

                checkBox.addActionListener(actionListener);
                checkBox.addChangeListener(changeListener);
                checkBox.addItemListener(itemListener);

                checkBox.setMnemonic(KeyEvent.VK_S);

                frame.add(checkBox, BorderLayout.NORTH);

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

}

SelectingCheckBox类在选中并取消选中后所产生的程序界面如图5-8所示。

Swing_5_8.png

Swing_5_8.png

自定义JCheckBox观感

每一个安装的Swing观感都会提供一个不同的JCheckBox外观与一个默认的UIResource值集合。图5-9显示了预安装的观感类型集合的JCheckBox组件外观:Motif,Windows,Ocean。第一,第三与第五个复选框为选中状态,而第三个具有输入焦点。

swing_5_9.png

swing_5_9.png

表5-4显示了JCheckBox的UIResource相关属性的集合。JCheckBox组件具有20个不同的属性。

属性字符串 对象类型
CheckBox.background Color
CheckBox.border Border
CheckBox.darkShadow Color
CheckBox.disabledText Color
CheckBox.focus Color
CheckBox.focusInputMap Object[]
CheckBox.font Font
CheckBox.foreground Color
CheckBox.gradient List
CheckBox.highlight Color
CheckBox.icon Icon
CheckBox.interiorBackground Color
CheckBox.light Color
CheckBox.margin Insets
CheckBox.rollover Boolean
CheckBox.select Color
CheckBox.shadow Color
CheckBox.textIconGap Integer
CheckBox.textShiftOffset Integer
CheckBoxUI String

Table: JCheckBox UIResource元素

JRadionButton类

当我们希望创建一个相斥的可切换组件组时我们可以使用JRadioButton。尽管由技术上来说,我们可以将一组JCheckBox组件放在一个ButtonGroup中,并且每次只有一个可以选中,但是他们看起来并不正确。至少由预定义的观感类型来看,JRadioButton与JCheckBox组件看起来是不同的,如图5-10所示。这种外观上的区别可以告诉终端用户可以期望组件的特定行为。

swing_5_10.png

swing_5_10.png

JRadioButton是由几方面构成的。类似于JToggleButton与JCheckBox,JRadioButton也使用一个ToggleButtonModel来表示其数据模型。他使用ButtonGroup通过AbstractButton来提供互斥的组合,并且用户界面委托是RadioButtonUI。

下面我们就来探讨如何使用JRadioButton的不同方同。

创建JRadioButton组件

与JCheckBox以及JToggleButton类似,JRadioButton有八个构造函数:

public JRadioButton()
JRadioButton aRadioButton = new JRadioButton();

public JRadioButton(Icon icon)
JRadioButton aRadioButton = new JRadioButton(new DiamondIcon(Color.CYAN, false));
aRadioButton.setSelectedIcon(new DiamondIcon(Color.BLUE, true));

public JRadioButton(Icon icon, boolean selected)
JRadioButton aRadioButton = new JRadioButton(new DiamondIcon(Color.CYAN, false),
  true);
aRadioButton.setSelectedIcon(new DiamondIcon(Color.BLUE, true));

public JRadioButton(String text)
JRadioButton aRadioButton = new JRadioButton("4 slices");

public JRadioButton(String text, boolean selected)
JRadioButton aRadioButton = new JRadioButton("8 slices", true);

public JRadioButton(String text, Icon icon)
JRadioButton aRadioButton = new JRadioButton("12 slices",
  new DiamondIcon(Color.CYAN, false));
aRadioButton.setSelectedIcon(new DiamondIcon(Color.BLUE, true));

public JRadioButton(String text, Icon icon, boolean selected)
JRadioButton aRadioButton = new JRadioButton("16 slices",
  new DiamondIcon(Color.CYAN, false), true);
aRadioButton.setSelectedIcon(new DiamondIcon(Color.BLUE, true));

public JRadioButton(Action action)
Action action = ...;
JRadioButton aRadioButton = new JRadioButton(action);

每一个都允许我们定制一个或是多个标签,图标或是初始选中状态属性。除非特别指定,在标签中并没有文本,而且复选框的默认选中/未选中状态图标为未选中。在创建一组单选按钮组件之后,我们需要将每一个放在一个ButtonGroup中,从而他们可以正常工作,在组合中每次只有一个按钮可以选中。如果我们在构造函数中初始化图标,则是复选框未选中状态的图标,当复选框被选中时也显示相同的图标。我们或者是使用JCheckBox中所描述的setSelectedIcon(Icon newValue)方法初始选中图标,或者是确保图标是状态感知的并进行自动更新。

JRadioButton属性

JRadioButton具有两个覆盖了父类JToggleButton的属性,如图表5-5所示。

属性名 数据类型 访问性
accessibleContext AccessibleContext 只读
UIClassID String 只读

Table: JRadioButton属性

将JRadioButton组件组合为一个ButtonGroup

JRadioButton是唯一一个为了正常作用需要放在ButtonGroup中的JToggleButton子类。仅仅是创建一组单选按钮并将其放置在屏幕中是不足够的。除了将每一个单选按钮放在一个容器中之外,我们需要创建一个ButtonGroup,并且将每一个单选按钮放在相同的ButtonGroup中。一旦所有的JRadioButton项目都放在一个组合中,当一个未选中的单选按钮被选中时,ButtonGroup会使得当前被选中的单选按钮取消选中。

将一个JRaidonButton组件集合放在一个ButtonGroup中是一个基本的四步过程: 1 为组合创建一个容器

JPanel aPanel = new JPanel(new GridLayout(0,1));

2 在窗口周围放置一个边框以标识组合。这是可选的一步,但是我们通常希望放置一个边框来为用户标识组合。我们将会在第7章中了解更多关于边框的内容。

Border border = BorderFactory.createTitledBorder("Slice Count");
aPanel.setBorder(border)

3 创建一个ButtonGroup

ButtonGroup aGroup = new ButtonGroup();

4 对于每一个可选择的选项,创建一个JRadioButton,将其添加到容器中,然后将其添加到组合中。

JRadioButton aRadioButton = new JRadioButton();
aPanel.add(aRadioButton);
aGroup.add(aRadioButton);

我们也许会发现整个过程,特殊是第四步,在一段时间之后会显得繁琐,特殊是当我们添加处理选中事件步骤时更是如此。列表5-5所示的助手类,具有一个静态的createRadioButtonGrouping(String elements[], String title)方法,证明是有用的。这个方法需要一个单选按钮的String的数组以及边框标题,然后在一个具有标题边框的JPanel内创建一个带有通常ButtonGroup的JRadioButton对象集合。

package net.ariel.ch05;

import java.awt.Container;
import java.awt.GridLayout;

import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.border.Border;

public class RadioButtonUtils {

    private RadioButtonUtils() {

    }

    public static Container createRadioButtonGrouping(String elements[], String title) {
        JPanel panel = new JPanel(new GridLayout(0,1));

        if(title != null) {
            Border border = BorderFactory.createTitledBorder(title);
            panel.setBorder(border);
        }

        ButtonGroup group = new ButtonGroup();
        JRadioButton aRadioButton;

        for(int i=0, n=elements.length; i<n; i++) {
            aRadioButton = new JRadioButton(elements[i]);
            panel.add(aRadioButton);
            group.add(aRadioButton);
        }

        return panel;
    }
}

现在我们可以更为简单的创建组合了,如列表5-6中的示例程序所示。

/**
 *
 */
package net.ariel.ch05;

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

import javax.swing.JFrame;

/**
 * @author mylxiaoyi
 *
 */
public class GroupRadio {

    private static final String sliceOptions[] = {
        "4 slices", "8 slices", "12 slices", "16 slices"
    };
    private static final String crustOptions[] = {
        "Sicilian", "Thin Crust", "Thick Crust", "Stuffed Crust"
    };
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

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

                Container sliceContainer = RadioButtonUtils.createRadioButtonGrouping(sliceOptions, "Slice Count");
                Container crustContainer = RadioButtonUtils.createRadioButtonGrouping(crustOptions, "Crust Type");

                frame.add(sliceContainer, BorderLayout.WEST);
                frame.add(crustContainer, BorderLayout.EAST);

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

}

当我们运行这个程序时,我们将会看到图5-11所示的结果。

swing_5_11.png

swing_5_11.png

处理JRadioButton选中事件

与JToggleButton以及JCheckBox类似,JRadioButton支持ActionListener,ItemListener以及ChangeListener的注册。而且,对于JRadioButton而言这些监听器的用法与其他组件的用法不同。

使用ActionListener监听JRadioButton事件

对于JRadioButton,通常是将相同的ActionListener注册到ButtonGroup中的所有单选按钮上。采用这种方法,当一个单选按钮被选中时,所订阅的ActionListener就会得到通知。通过覆盖前面的createRadioButtonGrouping()方法,这个方法就可以接受一个ActionListener作为参数,并将这个监听器对象关联到他们所创建的每一个按钮之上。

public static Container createRadionButtonGrouping(String elements[], String title, ActionListener actionListener) {
    JPanel panel = new JPanel(new GridLayout(0, 1));

    if(title != null) {
        Border border = BorderFactory.createTitledBorder(title);
        panel.setBorder(border);
    }

    ButtonGroup group = new ButtonGroup();
    JRadioButton aRadioButton;

    for(int i=0, n=elements.length; i<n; i++) {
        aRadioButton = new JRadioButton(elements[i]);
        panel.add(aRadioButton);
        group.add(aRadioButton);
        if(actionListener != null) {
            aRadioButton.addActionListener(actionListener);
        }
    }

    return panel;
}

现在,如果使用下面的代码创建一个组合,则所创建的每一个JRadioButton组件的AcitonListener都会得到通知。这里,监听器只是输出当前选中的值。我们所选择的响应方式会有所不同。

ActionListener sliceActionListener = new ActionListener() {
   public void actionPerformed(ActionEvent actionEvent) {
     AbstractButton aButton = (AbstractButton)actionEvent.getSource();
     System.out.println("Selected: " + aButton.getText());
   }
};
Container sliceContainer =
   RadioButtonUtils.createRadioButtonGrouping(sliceOptions, "Slice Count",
     sliceActionListener);

然而我们需要注意,这种方法有两个问题。首先,如果一个JRadioBtton已经处理选中状态,并且被再次选中时,任何已关联的ActionListener对象仍然会再次得到通知。尽管通过少量的工作我们并不能阻止所订阅的ActionListener的再次通知,但是我们依然可以进行正常的处理。我们需要重新获取到最后一个选中项目的引用,并且检测是否重新选中。下面修改的ActionListener进行这种检测:

ActionListener crustActionListener = new ActionListener() {
   String lastSelected;
   public void actionPerformed(ActionEvent actionEvent) {
     AbstractButton aButton = (AbstractButton)actionEvent.getSource();
     String label = aButton.getText();
     String msgStart;
     if (label.equals(lastSelected)) {
       msgStart = "Reselected: ";
     } else {
       msgStart = "Selected: ";
     }
     lastSelected = label;
     System.out.println(msgStart + label);
   }
};

第二个需要处理的问题就是确定在任意时刻哪一个JRadioButton被选中。通过重写RadioButtonUtils.createRadioButtonGrouping()助手方法,在方法外部ButtonGroup与JRadioButton组件都是不可见的。所以,并没有直接的方法在返回容器的ButtonGroup内部确定哪一个JRadioButton对象被选中。这也许是必须的,例如,如果在屏幕上有一个Order Pizza按钮,而我们希望在用户点击这个按钮之后我们可以确定哪一个匹萨预订选项被选中。

下面的助手方法,public Enumeration getSelectedElements(Container container),当添加到前面的RadioButtonUtils类中时将会提供这种必须的答案。助手方法只在传递给方法的容器装满AbstractButton对象时才会起作用。在前面所描述的createRadioButtonGrouping()方法所创建的容器正适合这种情况,尽管getSelectedElements()方法可以单独使用。

public static Enumeration<String> getSelectedEllements(Container container) {
    Vector<String> selections = new Vector<String>();
    Component components[] = container.getComponents();
    for(int i=0, n=components.length; i<n; i++) {
        if(components[i] instanceof AbstractButton) {
            AbstractButton button = (AbstractButton)components[i];
            if(button.isSelected()) {
                selections.addElement(button.getText());
            }
        }
    }

    return selections.elements();
}

为了使用getSelectedElements()方法,我们只需要将createRadionButtonGrouping()方法中返回的容器传递经getSelectedElements()方法来获得选中项目的String对象的Enumeration。下面的示例演示了使用方法。

final Container crustContainer =
     RadioButtonUtils.createRadioButtonGrouping(crustOptions, "Crust Type");
ActionListener buttonActionListener = new ActionListener() {
   public void actionPerformed(ActionEvent actionEvent) {
     Enumeration selected = RadioButtonUtils.getSelectedElements(crustContainer);
     while (selected.hasMoreElements()) {
       System.out.println ("Selected -> " + selected.nextElement());
     }
   }
};
JButton button = new JButton ("Order Pizza");
button.addActionListener(buttonActionListener);

对于getSelectedElements()方法返回多个值也许是必要的,因为如果在容器中多个按钮之间共享ButtonModel时,ButtonGroup的多个组件将会被选中。在组件之间共享ButtonModel并不是通常用法。如果我们确定我们的按钮模型并不会被共享,那么我们也许需要提供一个返回String的类似方法。

使用ItemListener监听JRadioButton事件

依赖于我们正在尝试作的事情,对于JRadionButton使用ItemListener通常并不是所希望的事件监听方法。当注册一个ItemListener时,一个新的JRadionButton选中会通知这个监听器两次:一次用于取消旧值,一次用于选中新值。对于重新选中(选中同一选项两次),监听器并不会被通知两次。

为了进行演示,下面的监听器会检测重新选中,正如前面的AcitonListener所做的,这个监听器会报告选中(或是取消选中)的元素。

ItemListener itemListener = new ItemListener() {
   String lastSelected;
   public void itemStateChanged(ItemEvent itemEvent) {
     AbstractButton aButton = (AbstractButton)itemEvent.getSource();
     int state = itemEvent.getStateChange();
     String label = aButton.getText();
     String msgStart;
     if (state == ItemEvent.SELECTED) {
       if (label.equals(lastSelected)) {
         msgStart = "Reselected -> ";
       } else {
         msgStart = "Selected -> ";
       }
       lastSelected = label;
     } else {
       msgStart = "Deselected -> ";
     }
     System.out.println(msgStart + label);
   }
};

为了正确的作用,对于RadioButtonUtils类需一些新的方法来允许我们将ItemListener关联到ButtonGroup中的每一个JRadioButton上。相应的代码会出现在后面的完整示例代码中。

使用ChangeListener监听JRadioButton事件

对于JRadioButton而言,ChangeListener的响应类似于JToggleButton与JCheckBox中的响应。当选中的单选按钮被armed,pressed,selected,released以及按钮模型的各种其他属性变化时所订阅的监听器都会得到通知。JRadioButton的唯一区别就在于ChangeListener也会被通知关于单选按钮正被取消选中的状态变化。前面例子中的ChangeListener也可以关联到JRadioButton。他也会被经常通知。

列表5-7中的相同程序演示了注册到两种不同的JRadioButton对象事件的所有监听器。另外,JButton报告了单选按钮被选中的元素。图5-12显示了程序运行时的主窗体。

/**
 *
 */
package net.ariel.ch05;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.Enumeration;

import javax.swing.AbstractButton;
import javax.swing.ButtonModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

/**
 * @author mylxiaoyi
 *
 */
public class GroupActionRadio {

    private static final String sliceOptions[] = {
        "4 slices", "8 slices", "12 slices", "16 slices"
    };
    private static final String crustOptions[] = {
        "Sicilian", "Thin Crust", "Thick Crust", "Stuffed Crust"
    };
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

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

                ActionListener sliceActionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        AbstractButton aButton = (AbstractButton)event.getSource();
                        System.out.println("Selected: "+aButton.getText());
                    }
                };
                Container sliceContainer = RadioButtonUtils.createRadioButtonGrouping(sliceOptions, "Slice Count", sliceActionListener);

                ActionListener crustActionListener = new ActionListener() {
                    String lastSelected;
                    public void actionPerformed(ActionEvent event) {
                        AbstractButton aButton = (AbstractButton)event.getSource();
                        String label = aButton.getText();
                        String msgStart;
                        if(label.equals(lastSelected)) {
                            msgStart = "Reselected: ";
                        }
                        else {
                            msgStart = "Selected: ";
                        }
                        lastSelected = label;
                        System.out.println(msgStart + label);
                    }
                };

                ItemListener itemListener = new ItemListener() {
                    String lastSelected;
                    public void itemStateChanged(ItemEvent event) {
                        AbstractButton aButton = (AbstractButton)event.getSource();
                        int state = event.getStateChange();
                        String label = aButton.getText();
                        String msgStart;
                        if(state == ItemEvent.SELECTED) {
                            if(label.equals(lastSelected)) {
                                msgStart = "Reselected -> ";
                            }
                            else {
                                msgStart = "Selected -> ";
                            }
                            lastSelected = label;
                        }
                        else {
                            msgStart = "Deselected -> ";
                        }
                        System.out.println(msgStart + label);
                    }
                };

                ChangeListener changeListener = new ChangeListener() {
                    public void stateChanged(ChangeEvent event) {
                        AbstractButton aButton = (AbstractButton)event.getSource();
                        ButtonModel aModel = aButton.getModel();
                        boolean armed = aModel.isArmed();
                        boolean pressed = aModel.isPressed();
                        boolean selected = aModel.isSelected();
                        System.out.println("Changed: "+armed+"/"+pressed+"/"+selected);
                    }
                };

                final Container crustContainer = RadioButtonUtils.createRadioButtonGrouping(crustOptions, "Crust Type", crustActionListener, itemListener, changeListener);

                ActionListener buttonActionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        Enumeration<String> selected = RadioButtonUtils.getSelectedElements(crustContainer);
                        while(selected.hasMoreElements()) {
                            System.out.println("Selected -> "+selected.nextElement());
                        }
                    }
                };

                JButton button = new JButton("Order Pizza");
                button.addActionListener(buttonActionListener);

                frame.add(sliceContainer, BorderLayout.WEST);
                frame.add(crustContainer, BorderLayout.EAST);
                frame.add(button, BorderLayout.SOUTH);
                frame.setSize(300, 200);
                frame.setVisible(true);

            }
        };
        EventQueue.invokeLater(runner);
    }

}
swing_5_12.png

swing_5_12.png

为了处理向ButtonGroup中所有的单选按钮注册ChangeListener对象,我们对RadioButtonUtils类进行了一些修改。完整的最终类定义如下列表5-8所示。

/**
 *
 */
package net.ariel.ch05;

import java.awt.Component;
import java.awt.Container;
import java.awt.event.ActionListener;
import java.awt.event.ItemListener;
import java.util.Enumeration;
import java.util.Vector;

import javax.swing.AbstractButton;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.border.Border;
import javax.swing.event.ChangeListener;

/**
 * @author mylxiaoyi
 *
 */
public class RadioButtonUtils2 {

    private RadioButtonUtils2() {

    }

    public static Enumeration<String> getSelectedElements(Container container) {
        Vector<String> selections = new Vector<String>();
        Component components[] = container.getComponents();
        for(int i=0, n=components.length; i<n; i++) {
            if(components[i] instanceof AbstractButton) {
                AbstractButton button = (AbstractButton)components[i];
                if(button.isSelected()) {
                    selections.addElement(button.getText());
                }
            }
        }

        return selections.elements();
    }

    public static Container createRadioButtonGrouping(String elements[]) {
        return createRadioButtonGrouping(elements, null, null, null, null);
    }

    public static Container createRadioButtonGrouping(String elements[], String title) {
        return createRadioButtonGrouping(elements, title, null, null, null);
    }

    public static Container createRadioButtonGrouping(String elements[], String title, ItemListener itemListener) {
        return createRadioButtonGrouping(elements, title, null, itemListener, null);
    }

    public static Container createRadioButtonGrouping(String elements[], String title, ActionListener actionListener) {
        return createRadioButtonGrouping(elements, title, actionListener, null, null);
    }

    public static Container createRadioButtonGrouping(String elements[], String title, ActionListener actionListener, ItemListener itemListener) {
        return createRadioButtonGrouping(elements, title, actionListener, itemListener, null);
    }

    public static Container createRadioButtonGrouping(String elements[], String title, ActionListener actionListener, ItemListener itemListener, ChangeListener changeListener) {
        JPanel panel = new JPanel();

        if(title != null) {
            Border border = BorderFactory.createTitledBorder(title);
            panel.setBorder(border);
        }

        ButtonGroup group =  new ButtonGroup();
        JRadioButton aRadioButton;
        for(int i=0, n=elements.length; i<n; i++) {
            aRadioButton = new JRadioButton(elements[i]);
            panel.add(aRadioButton);
            group.add(aRadioButton);
            if(actionListener != null) {
                aRadioButton.addActionListener(actionListener);
            }
            if(itemListener != null) {
                aRadioButton.addItemListener(itemListener);
            }
            if(changeListener != null) {
                aRadioButton.addChangeListener(changeListener);
            }
        }

        return panel;
    }
}

自定义JRadioButton观感

每一个已安装的Swing观感都会提供一个不同的JRadioButton外观以及默认的UIResource值集合。图5-13显示了预安装的观感类型集合的JRadioButton组件的外观:Motif,Windows,Ocean。下图显示了Thin Crust pizza预定程序的界面。另外,Thick Crust选项具有输入焦点。

swing_5_13.png

swing_5_13.png

表5-6显示了JRadioButton的UIResource相关的属性集合。JRadioButton组件具有20个不同的属性。

属性字符串 对象类型
RadioButton.background Color
RadioButton.border Border
RadioButton.darkShadow Color
RadioButton.disabledText Color
RadioButton.focus Color
RadioButton.focusInputMap Object[]
RadioButton.font Font
RadioButton.foreground Color
RadioButton.gradient List
RadioButton.highlight Color
RadioButton.icon Icon
RadioButton.interiorBackground Color
RadioButton.light Color
RadioButton.margin Insets
RadioButton.rollover Boolean
RadioButton.select Color
RadioButton.shadow Color
RadioButton.textIconGap Integer
RadioButton.textShiftOffset Integer
RadioButtonUI String

Table: JRadioButton UIResource元素

小结

本章描述了可切换的组件:JToggleButton,JCheckBox与JRadioButton。我们已经了解了每一个组件如何使用JToggleButton。其数据模型ToggleButtonModel类以及如何将组件组合在一个ButtonGroup中。另外,我们同时了解了如何处理每一个组件的选中事件。

第6章将会解释如何使用各种面向菜单的Swing组件。

wing Menus and Toolbars

本书的前面两章描述了一些低级的Swing组件。本章将会深入Swing面向菜单的组件。菜单与工具栏通过提供一些可视化的命令选项可以使得我们的程序更为友好。尽管Swing组件可以支持多个按键的命令序列,菜单被设计用来提供使用鼠标的图形化选择,而不是通过键盘。

本章将要讨论的菜单组件的使用如下:

  • 对于级联菜单,我们可以创建一个JMenu组件,并将其添加到JMenuBar。
  • 对于JMenu中的可选菜单,我们可以创建一个JMenuItem组件,并将其添加到JMenu。
  • 要创建子菜单,我们可以向JMenu添加一个新的JMenu,并向新菜单添加JMenuItem选项。
  • 然后,当一个JMenu被选中时,系统会在JPopupMenu中显示其当前的组件集合。

除了基本的JMenuItem元素,本章将还会讨论其他的菜单项目,例如JCheckBoxMenuItem以及JRadioButtonMenuItem,我们可以将这两个菜单项目放在JMenu中。同时我们还会探讨JSeparator类,他可以将菜单项目进行逻辑分组。我们将会了解如何通过使用JPopupMenu类来为JMenu被选中后出现的弹出菜单,或是任何组件的环境中提供支持。与抽象按钮类似,每一个菜单元素也有一个用于键盘选中的热键与其相关联。我们同进也会了解键盘快捷键支持,从而可以使得用记避免在多级菜单间进行遍历。

除了单个的菜单相关的组件之外,在本章中我们会了解JMenuBar选中模型以及菜单特定的事件相关类。我们要了解的选中模型接口是SingleSelectionModel接口,以及其默认实现DefaultSingleSelectionModel。我们将会探讨菜单特定的监听器以及事件MenuListener/MenuEvent,MenuKeyListener/MenuKeyEvent以及MenuDragMouseListener/MenuDragMouseEvent。另外,我们还会了解使用Popup与PopupFactory创建其他的弹出组件,以及通过JToolBar类使用工具栏。

使用菜单

我们先来了解一个演示菜单组件是如何组合在一起的示例。要开始我们的学习,创建一个具有菜单栏的窗体,如图6-1所示。

swing_6_1.png

swing_6_1.png

这个简单的菜单示例具有下列特性:

  • 在菜单栏上是两个普通的菜单:File与Edit。在File菜单下,是我们较为熟悉的New,Open,Close与Exit。在Edit菜单下则是Cut,Copy,Paste与Find以及一个Find选项的子菜单。选项子菜单将包含查找方向子菜单–向前与向后–以及一个大小写敏感的开关。
  • 在不同菜单的各种位置,菜单分隔符将选项分逻辑集合。
  • 每一个菜单选项都具有一个相关联的热键,通过热键可以进行键盘浏览与选中。热键可以使得用户通过键盘进行菜单选择,例如,在Windows平台下通过按下Alt-F可以打开File菜单。
  • 除了键盘热键,与多个选项相关联的击键可以作为键盘快捷键。与热键不同,快捷键可以直接激活一个菜单选项,甚至菜单选项并不可见时也是如此。
  • 选项子菜单具有一个与其相关联的图标。尽管在图6-1中只显示了一个图标,所有的菜单组件都可以具有一个图标,除了JSpearator与JPopupMenu组件。

注意,对于这个示例,这些菜单选项并不会完成任何有意义的事情,仅是输出哪一个菜单选项被选中。例如,由Edit菜单中选中Copy选项会显示Selected: Copy。

列表6-1显示了图6-1中生成示例类的完整代码。

/**
 *
 */
package net.ariel.ch06;

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

import javax.swing.ButtonGroup;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.KeyStroke;

/**
 * @author mylxiaoyi
 *
 */
public class MenuSample {

    static class MenuActionListener implements ActionListener {
        public void actionPerformed(ActionEvent event ) {
            System.out.println("Selected: "+event.getActionCommand());
        }
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Runnable runner = new Runnable() {
            public void run() {
                MenuActionListener menuListener = new MenuActionListener();
                JFrame frame  = new JFrame("Menu Sample");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                JMenuBar menuBar = new JMenuBar();

                JMenu fileMenu = new JMenu("File");
                fileMenu.setMnemonic(KeyEvent.VK_F);
                menuBar.add(fileMenu);

                JMenuItem newMenuItem = new JMenuItem("New", KeyEvent.VK_N);
                newMenuItem.addActionListener(menuListener);
                fileMenu.add(newMenuItem);

                JMenuItem openMenuItem = new JMenuItem("Open", KeyEvent.VK_O);
                openMenuItem.addActionListener(menuListener);
                fileMenu.add(openMenuItem);

                JMenuItem closeMenuItem = new JMenuItem("Close", KeyEvent.VK_C);
                closeMenuItem.addActionListener(menuListener);
                fileMenu.add(closeMenuItem);

                fileMenu.addSeparator();

                JMenuItem saveMenuItem = new JMenuItem("Save", KeyEvent.VK_S);
                saveMenuItem.addActionListener(menuListener);
                fileMenu.add(saveMenuItem);

                fileMenu.addSeparator();

                JMenuItem exitMenuItem = new JMenuItem("Exit", KeyEvent.VK_X);
                exitMenuItem.addActionListener(menuListener);
                fileMenu.add(exitMenuItem);

                JMenu editMenu  = new JMenu("Edit");
                editMenu.setMnemonic(KeyEvent.VK_E);
                menuBar.add(editMenu);

                JMenuItem cutMenuItem = new JMenuItem("Cut", KeyEvent.VK_T);
                cutMenuItem.addActionListener(menuListener);
                KeyStroke ctrlXKeyStroke = KeyStroke.getKeyStroke("control X");
                cutMenuItem.setAccelerator(ctrlXKeyStroke);
                editMenu.add(cutMenuItem);

                JMenuItem copyMenuItem = new JMenuItem("Copy", KeyEvent.VK_C);
                copyMenuItem.addActionListener(menuListener);
                KeyStroke ctrlCKeyStroke = KeyStroke.getKeyStroke("control C");
                copyMenuItem.setAccelerator(ctrlCKeyStroke);
                editMenu.add(copyMenuItem);

                JMenuItem pasteMenuItem = new JMenuItem("Paste", KeyEvent.VK_P);
                pasteMenuItem.addActionListener(menuListener);
                KeyStroke ctrlVKeyStroke = KeyStroke.getKeyStroke("control V");
                pasteMenuItem.setAccelerator(ctrlVKeyStroke);
                editMenu.add(pasteMenuItem);

                editMenu.addSeparator();

                JMenuItem findMenuItem = new JMenuItem("Find", KeyEvent.VK_F);
                findMenuItem.addActionListener(menuListener);
                KeyStroke f3KeyStroke = KeyStroke.getKeyStroke("F3");
                findMenuItem.setAccelerator(f3KeyStroke);
                editMenu.add(findMenuItem);

                JMenu findOptionsMenu = new JMenu("Options");
                Icon atIcon = new ImageIcon("at.gif");
                findOptionsMenu.setIcon(atIcon);
                findOptionsMenu.setMnemonic(KeyEvent.VK_O);

                ButtonGroup directionGroup = new ButtonGroup();

                JRadioButtonMenuItem forwardMenuItem = new JRadioButtonMenuItem("Forward", true);
                forwardMenuItem.addActionListener(menuListener);
                forwardMenuItem.setMnemonic(KeyEvent.VK_F);
                findOptionsMenu.add(forwardMenuItem);
                directionGroup.add(forwardMenuItem);

                JRadioButtonMenuItem backMenuItem = new JRadioButtonMenuItem("Back");
                backMenuItem.addActionListener(menuListener);
                backMenuItem.setMnemonic(KeyEvent.VK_B);
                findOptionsMenu.add(backMenuItem);
                directionGroup.add(backMenuItem);

                findOptionsMenu.addSeparator();

                JCheckBoxMenuItem caseMenuItem = new JCheckBoxMenuItem("Case Sensitive");
                caseMenuItem.addActionListener(menuListener);
                caseMenuItem.setMnemonic(KeyEvent.VK_C);
                findOptionsMenu.add(caseMenuItem);
                editMenu.add(findOptionsMenu);

                frame.setJMenuBar(menuBar);
                frame.setSize(350, 250);
                frame.setVisible(true);

            }
        };
        EventQueue.invokeLater(runner);
    }

}

菜单类层次结构

现在我们已经了解如何为程序创建级联菜单,我们应该已经了解了使用Swing菜单组件所涉及到的内容。为了表达更为清晰,图6-2显示了所有的Swing菜单组件内部是如何关联的。

swing_6_2.png

swing_6_2.png

图6-2所显示的最重要的概念就是作为JComponent的子类的所有Swing菜单元素都是AWT组件。我们可以将JMenuItem,JMenu以及JMenuBar组件放在AWT组件可以放置的位置,而仅不是在窗体上。另外,因为JMenuItem是由AbstractButton继承而来的,JMenuItem及其子类继承了各种图标以及HTML文本标签的支持,正如第5章所述。

除了是基本的类层次结构的一部分以外,每一个可选择的菜单组件都实现了MenuElement接口。这个接口描述了支持键盘与鼠标浏览所必须的菜单行为。预定义的菜单组件已经实现了这种行为,所以我们不必自己实现。但是如我们对这个接口是如何工作的比较感兴趣,可以查看本章中的“MenuElement接口”一节。

下面我们来了解一下不同的Swing菜单组件。

JMenuBar类

Swing的菜单栏组件是JMenuBar。其操作要求我们使用具有JMenuItem元素的JMenu元素来填充菜单栏。然后我们将菜单栏添加到JFrame或是其他的需要菜单栏的用户界面组件上。菜单然后会依赖于SingleSelectionModel的帮助来确定在其选中之后显示或是发送哪一个JMenu。

创建JMenuBar组件

JMenuBar具有一个无参数的构造函数:public JMenuBar()。一旦我们创建了菜单栏,我们就可以使用JApplet,JDialog,JFrame,JInternalFrame或是JRootPane的setJMenuBar()方法将其添加到一个窗口。

JMenuBar menuBar = new JMenuBar();
// Add items to it
...
JFrame frame = new JFrame("MenuSample Example");
frame.setJMenuBar(menuBar);

通过系统提供的观感类型,通过setJMenuBar()方法,菜单栏显示在窗体的上部,窗体标题的下部(如果有)。其他的观感类型,例如Macintosh的Aqua,会将菜单栏放在其他的位置。

我们也可以使用Container的add()方法将JMenuBar添加到窗口。当通过add()方法添加时,JMenuBar会通过Container的布局管理器进行管理。

在我们拥有一个JMenuBar之后,要使用其余的菜单类来填充菜单栏。

向菜单栏添加与移除菜单

我们需要将JMenu对象添加到JMenuBar。否则,所显示只是没有任何内容的边框。向JMenuBar添加菜单只有一个方法:

public JMenu add(JMenu menu)

在默认情况下,连续添加的菜单会由左向右显示。这会使得第一个添加到的菜单会是最左边的菜单,而最后添加的菜单则是最右边的菜单。在这两者之间添加的菜单则会以其添加的顺序进行显示。例如,列表6-1中的示例程序,菜单的添加顺序如下:

JMenu fileMenu = new JMenu("File");
menuBar.add(fileMenu);
JMenu editMenu = new JMenu("Edit");
menuBar.add(editMenu);

除了JMenuBar的add()方法以外,由Container继承的多个重载的add()方法可以菜单的位置进行更多的控制。其中最有趣的就是add(Component component, int index)方法,这个方法可以使得我们指定新的JMenu的显示位置。使用第二个add()方法可以使得我们以不同的顺序将File与Edit的JMenu组件放置在JMenuBar中,但是会得到相同的结果:

menuBar.add(editMenu);
menuBar.add(fileMenu, 0);

如果我们已经向JMenuBar添加了一个JMenu组件,我们可以使用remove(Component component)或是由Container继承的remove(int index)方法来移除菜单:

bar.remove(edit);
bar.remove(0);

JMenuBar属性

表6-1显示了JMenuBar的11个属性。其中的半数属性是只读的,只允许我们查询当前的菜单栏状态。其余的属性允许我们通过确定菜单栏边框是否绘制以及选择菜单元素之间的空白尺寸来修改菜单栏的外观。selected属性与selectionModel可以控制菜单栏上当前被选中的菜单是哪一个。当被选中的组件设置为菜单栏上的一个菜单,菜单组件会以弹出菜单的方式显示在窗口中。

属性名 数据类型 可访问性
accessibleContext AccessibleContext 只读
borderPainted boolean 读写
component Component 只读
helpMenu JMenu 只读
margin Insets 读写
menuCount int 只读
selected boolean/Component 读写
selectionModel SingleSelectionModel 读写
subElements MenuElement[] 只读
UI MenuBarUI 读写
UIClassID String 只读

Table: JMenuBar属性

自定义JMenuBar观感

每一个预定义的Swing观感都为JMenuBar以及菜单组件提供了一个不同的外观以及一个默认的UIResource值集合。图6-3显示了预安装的观感类型集合的菜单组件外观:Motif,Windows以及Ocean。

swing_6_3.png

swing_6_3.png

考虑JMenuBar的特定外观,表6-2显示了UIResource相关属性的集合。JMenuBar组件有12个属性。

属性字符串 对象类型
MenuBar.actionMap ActionMap
MenuBar.background Color
MenuBar.border Border
MenuBar.borderColor Color
MenuBar.darkShadow Color
MenuBar.font Font
MenuBar.foreground Color
MenuBar.gradient List
MenuBar.highlight Color
MenuBar.shadow Color
MenuBar.windowBindings Object[]
MenuBarUI String

Table: JMenuBar UIResource元素

如果我们需要一个垂直菜单栏,而不是一个水平菜单栏,我们只需要简单的改变菜单栏组件的LayoutManager。例如0行1列的GridLayout可以完成这个工作,如下面的示例所示,因为由于JMenu的添加,行数会无限增长:

import java.awt.*;
import javax.swing.*;
public class VerticalMenuBar extends JMenuBar {
  private static final LayoutManager grid = new GridLayout(0,1);
  public VerticalMenuBar() {
    setLayout(grid);
  }
}

将图6-1所示的菜单栏移动到BorderLayout的东侧,并使用VerticalMenuBar来替换JMenuBar所产生的结果如图6-4所示。尽管垂直菜单栏在这里看起来并不舒服,但是在窗口的右侧(或左侧)更需要使得菜单项目垂直堆放而不是水平堆放。然而,我们也许会需要修改MenuBar.border属性来修改边框。

swing_6_4.png

swing_6_4.png

SingleSelectionModel接口

SingleSelectionModel将索引描述为一个整数索引的数据结构,其中的元素可以被选中。接口后面的数据结构类似于数据或是向量,其中重复访问相同位置可以获得相同的对象。SingleSelectionModel接口是JMenuBar与JPopupMenu的选择模型。在JMenuBar中,接口描述了当前被选中的需要绘制的JMenu。在JPopupMenu中,接口描述了当前被选中的JMenuItem。

SingleSelectionModel的接口定义如下:

public interface SingleSelectionModel {
  // Listeners
  public void addChangeListener(ChangeListener listener);
  public void removeChangeListener(ChangeListener listener);
  // Properties
  public int getSelectedIndex();
  public void setSelectedIndex(int index);
  public boolean isSelected();
  // Other Methods
  public void clearSelection();
}

正如我们所看到的,除了选择索引外,接口需要维护一个当选择索引变化时需要通知的ChangeListener列表。

默认的Swing提供的SingleSelectionModel实现是DefaultSingleSelectionModel类。对于JMenuBar与JPopupMenu,我们通常并不需要修改由其默认实现所获得的选择模型。

DefaultSingleSelectionModel实现管理一个ChangeListener对象列表。另外,模型使用-1来标识当前并没有任何内容被选中。当选中的索引为-1时,isSelected()会返回false;否则,此方法会返回true。当选中索引变化时,所注册的ChangeListener对象会得到通知。

JMenuItem类

JMenuItem组件是用户可以在菜单栏上选择的预定义组件。作为AbstractButton的子类,JMenuItem是一个特殊的按钮组件,其行为类似于JButton。除了作为AbstractButton的子类,JMenuItem类共享JButton的数据模型(ButtonModel接口与DefaultButtonModel实现)。

创建JMenuItem组件

JMenuItem有六个构造函数。这些构造函数可以使得我们初始化菜单项的字符串或是图标以及菜单项的热键。并不存在显式的构造函数允许我们在创建时设置所有三个选项,除非我们将其作为Action的一部分。

public JMenuItem()
JMenuItem jMenuItem = new JMenuItem();

public JMenuItem(Icon icon)
Icon atIcon = new ImageIcon("at.gif");
JMenuItem jMenuItem = new JMenuItem(atIcon);

public JMenuItem(String text)
JMenuItem jMenuItem = new JMenuItem("Cut");

public JMenuItem(String text, Icon icon)
Icon atIcon = new ImageIcon("at.gif");
JMenuItem jMenuItem = new JMenuItem("Options", atIcon);

public JMenuItem(String text, int mnemonic)
JMenuItem jMenuItem = new JMenuItem("Cut", KeyEvent.VK_T);

public JMenuItem(Action action)
Action action = ...;
JMenuItem jMenuItem = new JMenuItem(action);

热键可以使得我们通过键盘浏览选择菜单。例如,在Windows平台上,如果菜单项出现在已打开的Edit菜单中,我们可以通过简单的按下Alt-T来选中Cut菜单。菜单项的热键通常以菜单文本标签中的下划线形式出现。然而,如果字符并没有出现在文本标签中,或者是没有文本标签,用户通常并不会得到明显的提示。字符是通过java.awt.event.KeyEvent类中的不同常量来标识的。

其他的平台也许会提供其他的选中热键。在Unix平台下,通常是Alt键;而在Macintosh平台下则是Command键。

JMenuItem属性

JMenuItem有多个属性。大约有100个属性是通过各种超类来继承的。表6-3显示了JMenuItem特定的10个属性。

属性名 数据类型 访问性
accelerator KeyStroke 读写绑定
accessibleContext AccessibleContext 只读
armed boolean 读写
component Component 只读
enabled boolean 只写绑定
menuDragMouseListeners MenuDragMouseListener[] 只读
menuKeyListeners MenuKeyListener[] 只读
subElements MenuElement[] 只读
UI MenuElementUI 只写绑定
UIClassID String 只读

Table: JMenuItem属性

其中比较有趣的一个属性就是accelerator。正如第2章所解释的,KeyStroke是一个工厂类,可以使得我们基于按键与标识符组合创建实例。例如,下面的代码语句来自于本章列表6-1中的示例,将Ctrl-X作为快捷键与一个特定的菜单项相关联:

KeyStroke ctrlXKeyStroke = KeyStroke.getKeyStroke("control X");
cutMenuItem.setAccelerator(ctrlXKeyStroke);

只读的component与subElement属性是JMenuItem所实现的MenuElement接口的一部分。component属性是菜单项的渲染器(JMenuItem本身)。subElement属性是空的(也就是一个空的数组,而不是null),因为JMenuItem并没有子类。

处理JMenuItem事件

我们可以在JMenuItem内部使用至少五种不同的方法来处理事件。组件继承了允许我们通过AbstractButton的ChangeListener与ActionListener注册的方法来触发ChangeEvent与ActionEvent的能力。中软皮,JMenuItem组件支持当MenuKeyEvent与MenuDragMouseEvent事件发生时注册MenuKeyListener与MenuDragMouseListener对象。这些技术会在后面的章节中进行讨论。第五种方法是向JMenuItem的构造函数传递Action,其作用类似于一种特殊的使用ActionListener监听的方法。要了解更多的关于使用Action的内容,可以查看本章稍后的“JMenu类”一节中关于在菜单中使用Action对象的讨论。

使用ChangeListener监听JMenuItem事件

通常我们并不会向JMenuItem注册ChangeListener。然而,演示一个理想的例子助于更为清晰的解释JMenuItem关于其ButtonModel数据模型的变化。所考虑的事件变化是与JButton相同的arm,press与select。然而,他们的名字会有一些迷惑,因为所选择的模型属性并没有进行设置。

当鼠标略过菜单选项并且菜单变为选中时,JMenuItem是armed。当用户释放其上的鼠标按钮时,JMenuItem是pressed。紧随按下之后,菜单项会变为未按下与unarmed。在菜单项变为按下与未按下之间,AbstractButton会得到模型变化的通知,从而使得菜单项所注册的ActionListener对象得到通知。一个普通JMenuItem的按钮模型不会报告被选中。如果我们没有选择而将鼠标移动到另一个菜单项上,则第一个菜单项会自动变化unarmed。为了有助于我们更好的理解不同的变化,图6-5显示了一个序列图。

swing_6_5.png

swing_6_5.png

使用ActionListener监听JMenuItem事件

关联到JMenuItem更好的监听器是ActionListener,或者是向构造函数传递一个Action。他可以使得我们确定哪一个菜单项被选中。当用户在作为打开菜单一部分的JMenuItem上释放鼠标按钮时,所注册的ActionListener对象会得到通知。如果用户通过键盘(箭头键或是热键)或是按下菜单快捷键来选中菜单时,所注册的监听器也会得到通知。

当我们希望菜单被选中时发生某个动作,我们必须为每一个JMenuItem添加一个ActionListener。并不存在一个自动的方法使得我们可以为JMenu或是JMenuBar注册一个ActionListener从而使得其所包含的JMenuItem对象通知一个ActionListener。

列表6-1中的示例程序为每一个JMenuItem关联了一个相同的ActionListener:

class MenuActionListener implements ActionListener {
  public void actionPerformed(ActionEvent e) {
    System.out.println("Selected: " + e.getActionCommand());
  }
}

然而更为通常的是,我们为每一个菜单项关联一个不同的动作,从而每一个菜单项可以进行不同的响应。

提示:我们并不需要为组件创建一个自定义的ActionListener并进行注册,我们可以创建一个自定义的Action,并且在组件上调用setAction()方法。

使用MenuKeyListener监听JMenuItem事件

MenuKeyEvent是用户界面类内部为JMenu与JMenuItem所用的特殊的KeyEvent,使得组件可以监听何时其键盘热键被按下。要监听这种键盘输入,每一个菜单组件注册一个MenuKeyListener,从而监听相应的输入。如果键盘热键被按下,事件就会被处理,从而不会再被传送到所注册的监听器。如果键盘热键没有被按下,所注册的键盘监听器(而不是菜单键监听器)就会得到通知。

MenuKeyListener接口定义如下:

public interface MenuKeyListener extends EventListener {
  public void menuKeyPressed(MenuKeyEvent e);
  public void menuKeyReleased(MenuKeyEvent e);
  public void menuKeyTyped(MenuKeyEvent e);
}

通常我们并不需要自己注册这种类型的监听器对象,尽管如果我们希望我们仍可以这样做。如果我们确定这样做,并且如果MenuKeyEvent发生(也就是一个键被按下/释放),JMenuBar中的每一个JMenu都会得到通知,就如同打开菜单中的每一个JMenuItem(或是子类)都有一个注册的MenuKeyListener。这包括禁止的菜单项,从而他们可以处理按下的热键。MenuKeyEvent类的定义如下:

public class MenuKeyEvent extends KeyEvent {
  public MenuKeyEvent(Component source, int id, long when, int modifiers,
    int keyCode, char keyChar, MenuElement path[], MenuSelectionManager mgr);
  public MenuSelectionManager getMenuSelectionManager();
  public MenuElement[] getPath();
}

确定当前选择路径是MenuSelectionManager的工作。选择路径是由顶层的JMenuBar上的JMenu到所选中的组件的菜单元素集合。对于大多数情况而言,管理器在幕后工作,而我们无需担心。

使用MenuDragMouseListener监听JMenuItem事件

与MenuKeyEvent类似,MenuDragMouseEvent也是用户界面类为JMenu与JMenuBar在内部所用的特殊的事件类型。正如其名字所显示的,MenuDragMouseEvent是一种特殊类型的MouseEvent。通过监听鼠标何时在打开的菜单中移动,用户界面类使用监听器来维护选择路径,从而确定当前选中的菜单项。其定义如下:

public interface MenuDragMouseListener extends EventListener {
  public void menuDragMouseDragged(MenuDragMouseEvent e);
  public void menuDragMouseEntered(MenuDragMouseEvent e);
  public void menuDragMouseExited(MenuDragMouseEvent e);
  public void menuDragMouseReleased(MenuDragMouseEvent e);
}

与MenuKeyListener类似,通常我们并不需要亲自监听这一事件。如果我们比较感兴趣一个菜单或是子菜单何时显示,要注册的更好的监听器是MenuListener,这个监听器可以注册到JMenu,但是并不可以注册到单个的JMenuItem。我们将会在描述JMenu的下一节了解到这一点。

MenuDragMouseEvent类的定义,MenuDragMouseListener方法的参数如下:

public class MenuDragMouseEvent extends MouseEvent {
  public MenuDragMouseEvent(Component source, int id, long when, int modifiers,
    int x, int y, int clickCount, boolean popupTrigger, MenuElement path[],
    MenuSelectionManager mgr);
  public MenuSelectionManager getMenuSelectionManager();
  public MenuElement[] getPath();
}

自定义JMenuItem观感

与JMenuBar类似,预定义的观感类型提供了不同的JMenuItem外观以及默认的UIResource值集合。图6-3显示了预安装集合的JMenuItem的外观:Motif,Windows与Ocean。

表6-4显示了JMenuItem的UIResource相关属性集合。JMenuItem组件提供了20个不同的属性。

属性字符串 对象类型
MenuItem.acceleratorDelimiter String
MenuItem.acceleratorFont Font
MenuItem.acceleratorForeground Color
MenuItem.acceleratorSelectionForeground Color
MenuItem.actionMap ActionMap
MenuItem.arrowIcon Icon
MenuItem.background Color
MenuItem.border Border
MenuItem.borderPainted Boolean
MenuItem.checkIcon Icon
MenuItem.commandSound String
MenuItem.disabledForeground Color
MenuItem.font Font
MenuItem.foreground Color
MenuItem.margin Insets
MenuItem.opaque Boolean
MenuItem.selectionBackground Color
MenuItem.selectionForeground Color
MenuItem.textIconGap Integer
MenuItemUI String

Table: JMenuItem UIResource元素

JMenu类

JMenu组件是放置在JMenuBar上的基本菜单项。当一个JMenu被选中时,菜单在JPopupMenu内显示所包含的菜单项。对于JMenuItem,JMenu的数据模型则是一个ButtonModel实现,或者更为特定的,DefaultButonModel。

创建JMenu组件

JMenu有四个构造函数可以使得我们初始化菜单的字符串标签:

public JMenu()
JMenu jMenu = new JMenu();
public JMenu(String label)
JMenu jMenu = new JMenu("File");
public JMenu(String label, boolean useTearOffs)
public JMenu(Action action)
Action action = ...;
JMenu jMenu = new JMenu(action);

其中一个构造函数用于tear-off菜单。然而,tear-off菜单当前并不被支持;所以其参数会被忽略。第四个构造函数使用Action中的属性来填充菜单。

注意:所谓tear-off菜单是显示在一个窗口中并且在选择之后仍然保持打开,而不是自动关闭。

向JMenu添加菜单项

一旦我们有了JMenu,我们需要向其添加JMenuItem对象;否则,菜单不会显示任何选择。有五个方法可以向JMenu添加所定义的菜单项,一个用于添加分隔符:

public JMenuItem add(JMenuItem menuItem);
public JMenuItem add(String label);
public Component add(Component component);
public Component add(Component component, int index);
public JMenuItem add(Action action);
public void addSeparator();

在本章前面的列表6-1中,所有的JMenuItem组件是通过第一个add()方法添加到JMenu组件的。为了简便起见,我们可以将JMenuItem的文本标签传递给JMenu的add()方法。这个方法会创建菜单项,设置其标签,并且传回新菜单项组件。然后我们可以将菜单项事件处理器绑定到这个新获得的菜单项。第三个add()方法表明我们可以在JMenu上放置任意的Component的,而不仅是JMenuItem。第四个add()方法允许我们按位置放置组件。最后一个add()方法变体,带有一个Action参数,将会在下一节进行讨论。

我们可以使用JMenu的addSeparator()方法添加分隔栏。例如,在列表6-1中,File菜单是使用类似下面的代码来创建的:

JMenu fileMenu = new JMenu("File");
JMenuItem newMenuItem = new JMenuItem("New");
fileMenu.add(newMenuItem);
JMenuItem openMenuItem = new JMenuItem("Open");
fileMenu.add(openMenuItem);
JMenuItem closeMenuItem = new JMenuItem("Close");
fileMenu.add(closeMenuItem);
fileMenu.addSeparator();
JMenuItem saveMenuItem = new JMenuItem("Save");
fileMenu.add(saveMenuItem);
fileMenu.addSeparator();
JMenuItem exitMenuItem = new JMenuItem("Exit");
fileMenu.add(exitMenuItem);

注意,addSpeparator()调用包围了添加Save菜单项的调用。

除了在菜单的结束处添加菜单项以外,我们可以将菜单项插入在指定的位置或是将分隔符插入在指定的位置,如下所示:

public JMenuItem insert(JMenuItem menuItem, int pos);
public JMenuItem insert(Action a, int pos);
public void insertSeparator(int pos);

当一个菜单被添加到JMenu后,他就被加入了内部的JPopupMenu。

菜单中使用Action对象

在第2章中描述了Aciton接口及其相关联的类。Action是ActionListener接口的扩展,并且包含用于自定义与其实现相关联的组件的一些特殊属性。

借助于AbstractAction这现,我们可以很容易的定义文本标签,图标,热键,工具提示文本,允许状态,以及一个与组件相分离的ActionListener。然后我们可以使用相关联的Action创建组件,并且不必为组件指定文本标签,图标,热键,工具提示文本,允许状态,或是ActionListener,因为这些属性来自Action。要了解详细的描述,可以参考第2章。

为了演示的需要,列表6-2创建了AbstractAction的一个特定实现,并且将其多次添加到JMenu中。一旦Action被添加到JMenu,选择JMenuItem将会借助JOptionPane类显示一个弹出对话框,我们会在第9章讨论这一主题 。

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class ShowAction extends AbstractAction {
  Component parentComponent;
  public ShowAction(Component parentComponent) {
    super("About");
    putValue(Action.MNEMONIC_KEY, new Integer(KeyEvent.VK_A));
    this.parentComponent = parentComponent;
  }
  public void actionPerformed(ActionEvent actionEvent) {
    Runnable runnable = new Runnable() {
       public void run() {
         JOptionPane.showMessageDialog(
           parentComponent, "About Swing",
           "About Box V2.0", JOptionPane.INFORMATION_MESSAGE);
       }
    };
    EventQueue.invokeLater(runnable);
  }
}

下面的代码为列表6-1中的示例程序中的File与Edit菜单创建了一个ShowAction与一个JMenuItem。无需显示的设置菜单项属性,他会具有一个About文本标签与一个热键,并且会执行所定义的actionPerformed()方法作为其ActionListener。事实上,我们可以创建Action一次,然后可以将其关联到所需要的多个地方(或者是其他支持添加Action对象的组件)。

Action showAction = new ShowAction(aComponent);
JMenuItem fileAbout = new JMenuItem(showAction);
fileMenu.add(fileAbout);
JMenuItem editAbout = new JMenuItem(showAction);
editMenu.add(editAbout);

使用AbstractAction的副作用就是通过setEnabled(false)方法禁止Action时,相应的,会禁止所有由其创建的组件。

JMenu属性

除了由JMenu继承的100多个属性以外,表6-5显示了16个JMenu特定的属性。其中的一些属性覆盖了继承属性的行为。例如,accelerator属性的设置方法会在我们尝试为这个属性赋值时抛出错误。换句话说,快捷键并不为JMenu对象所支持。其余的属性描述了JMenu对象的当前状态及其所包含的菜单组件。

Table: JMenu属性

delay属性表示在选择JMenu与发出JPopupMenu之间所逝去的时间。默认情况这个值为0,表示会立即显示子菜单。尝试将这个值设置负值会抛出IllegalArgumentException。

选择菜单组件

通常我们并不需要监听JMenu组件的选择。我们只需要监听单个的JMenuItem组件的选择。然而,与JMenuItem相比,我们会对JMenu所用的ChangeEvent的不同方法感兴趣。另外,当一个菜单被弹出或是关闭时,MenuEvent会通知我们。

使用ChangeListener监听JMenu事件

与JMenuItem类似,如果我们对于修改底层的ButtonModel比较感兴趣,我们可以向JMenu注册ChangeListener。奇怪的是,JMenu的ButtonModel的唯一的状态改变就是selected属性。当被选中时,JMenu显示其菜单项。当没有被选中时,弹出菜单会消失。

使用MenuListener监听JMenu事件

监听弹出菜单何时显示或是隐藏的更好的方法是就是向JMenu对象注册MenuListener对象。其定义如下:

public interface MenuListener extends EventListener {
  public void menuCanceled(MenuEvent e);
  public void menuDeselected(MenuEvent e);
  public void menuSelected(MenuEvent e);
}

通过注册MenuListener,当JMenu在弹出菜单打开之前选中时,我们会得到通知。这可以使得我们自定义其菜单选项。除了得到相关联的弹出菜单何时被弹出的通知,当菜单被取消选中以及菜单被关闭时我们也会得到通知。正如下面的MenuEvent类定义所显示的,事件所提供的唯一信息就是源(菜单):

public class MenuEvent extends EventObject {
  public MenuEvent(Object source);
}

提示:如果我们选择动态自定义JMenu上的项,在确保调用revalidate(),因为组件会在我们更新显示之前一直等待。

自定义JMenu观感

与JMenuBar和JMenuItem类似,预定义的观感提供了不同的JMenu外观以及默认的UIResource值集合。图6-3显示了预安装的观感类型集合的JMenu对象外观。

表6-6显示了JMenu的UIResource相关属性的集合。对于JMenu组件,有30个不同的属性。

属性字符串 对象类型
menu Color
Menu.acceleratorDelimiter String
Menu.acceleratorFont Font
Menu.acceleratorForeground Color
Menu.acceleratorSelectionForeground Color
Menu.ActionMap ActionMap
Menu.arrowIcon Icon
Menu.background Color
Menu.border Border
Menu.borderPainted Boolean
Menu.checkIcon Icon
Menu.delay Integer
Menu.disabledForeground Color
Menu.font Font
Menu.foreground Color
Menu.margin Insets
Menu.menuPopupOffsetX Integer
Menu.menuPopupOffsetY Integer
Menu.opaque Boolean
Menu.selectionBackground Color
Menu.selectionForeground Color
Menu.shortcutKeys int[]
Menu.submenuPopupOffsetX Integer
Menu.submenuPopupOffsetY Integer
Menu.textIconGap Integer
Menu.useMenuBarBackgroundForTopLevel Boolean
menuPressedItemB Color
menuPressedItemF Color
menuText Color
MenuUI String

Table: JMenu UIResource元素

JSeparator类

JSeparator类是一种特殊的组件,他在JMenu上提供分隔符。JPopupMenu与JToolBar类也支持分隔,但是每一个都使用JSeparator类的相应子类。除了可以放置在菜单上以外,JSeparator类也可以放置在任何我们希望使用水平或是垂直线来分隔屏幕不同区域的地方。

JSeparator是一个严格的可视化组件,所以,他没有数据模型。

创建JSeparator组件

要为菜单创建一个分隔,我们并不直接创建一个JSeparator,尽管我们可以这样做。相反,我们调用JMenu的addSeparator()方法,而菜单会创建分隔符并将其添加为下一个菜单项。他是一个JSeparator(不是JMenuItem子类)的事实是隐藏的。JMenu还有一个insertSeparator(int index)方法,这个方法可以使得我们在菜单上指定的位置添加分隔,这并不必须是下一个位置。

如果我们希望在菜单以外使用JSeparator(例如,在布局中分隔两个面板),我们应该使用JSeparator的两个构造函数:

public JSeparator()
JSeparator jSeparator = new JSeparator();
public JSeparator(int orientation)
JSeparator jSeparator = new JSeparator(JSeparator.VERTICAL);

这两个构造函数使得我们可以创建一个水平或是垂直分隔。如果没有指定方向,则为水平方向。如果我们希望显示式指定方向,我们可以使用JSeparator的常量HORIZONTAL或是VERTICAL。

JSeparator属性

在我们拥有JSeparator以外,我们就可以像其他的组件一样将其添加到屏幕中。组件的初始维度是空的(宽度与高度均为0),所以如果屏幕的布局管理器询问组件的尺寸应是多少,分隔符将会回复他不需要空间。另一方面,如果布局管理器提供一定量的空间,如果方向合适则分隔就会使用这个空间。例如,将一个水平JSeparator添加到BorderLayout面板的北侧则会在屏幕上绘制一个分隔线。然而,如果将水平JSeparator添加到相同面板的东侧则不会绘制任何内容。对于垂直JSeparator,其行为则是相反的:北侧将是空的,而在东侧则会出现垂直线。

表6-7显示了JSeparator的四个属性。

属性名 数据类型 访问性
accessibleContext AccessibleContext 只读
orientation int 读写绑定
UI SeparatorUI 读写绑定
UIClassID String 只读

Table: JSeparator属性

自定义JSeparator观感

预安装的观感类型集合下的JSeparator外观以及其他的菜单组件显示在图6-3中。

表6-8列出了JSeparator的UIResource相关属性集合。对于JSeparator组件,有五个不同的属性。

属性字符串 对象类型
Separator.background Color
Separator.foreground Color
Separator.insets Insets
Separator.thickness Integer
SeparatorUI String

Table: JSeparator UIResource元素

JPopupMenu类

JPopupMenu组件是弹出菜单组件的容器,可以显示在任何地方并且为JMenu所支持。当一个编程者定义的触发事件发生时,我们显示JPopupMenu,并且菜单显示所包含的菜单组件。与JMenuBar类似,JpopupMenu使用SingleSelectionModel来管理当前被选中的元素。

创建JpopupMenu组件

JPopupMenu有两个构造函数:

public JPopupMenu()
JPopupMenu jPopupMenu = new JPopupMenu();
public JPopupMenu(String title)
JPopupMenu jPopupMenu = new JPopupMenu("Welcome");

如果需要,只有一个函数允许我们初始化菜单标题。标题的处理方式会依赖于所安装的观感。当前安装的观感会忽略标题。

向JPopupMenu添加菜项

与JMenu类似,一旦我们有了JPopupMenu,我们需要向其添加菜单项;否则,菜单将会是空的。有三个JPopupMenu方法可以添加菜单项,一个用于添加分隔符。

public JMenuItem add(JMenuItem menuItem);
public JMenuItem add(String label);
public JMenuItem add(Action action);
public void addSeparator();

另外还有一个由Container所继承的add()方法可以用于添加通常的AWT组件:

public Component add(Component component);

添加菜单项的通常方法是使用第一个add()方法。我们独立于弹出菜单创建菜单项,包含其行为行定,然后将其关联到菜单。使用第二个add()方法,我们必须将事件处理器关联到由方法返回的菜单;否则,当被选中时菜单并不会响应。下面的代码显示了两种方法。我们使用哪一种方法完全依赖于我们的喜好。可视化编程环境,例如JBuilder,会使用第一种。因为第一种方法并不是十分复杂,如果不是全部,绝大多数的程序员应该使用第一种方法。

JPopupMenu popupenu = new JPopupMenu();
ActionListener anActionListener = ...;
// The first way
JMenuItem firstItem = new JMenuItem("Hello");
firstItem.addActionListener(anActionListener);
popupMenu.add(firstItem);
// The second way
JMenuItem secondItem = popupMenu.add("World");
secondItem.addActionListener(anActionListener);

使用Action来创建与JPopupMenu结合使用的菜单项的方式类似于JMenu。然而,依据JPopupMenu类的Javadoc,并不鼓励使用add()方法的Action变体。相反,可以将Action传递给JMenuItem的构造函数,或者是使用setAction()方法进行配置,然后将其添加到JPopupMenu。为什么这个方法没有被deprecated并不清楚。

最后,我们可以使用addSeparator()方法添加分隔。

除了在菜单尾部添加菜单项,我们可以在指定的位置添加菜单项,或者是在指定的位置添加分隔。

public JMenuItem insert(Component component, int position);
public JMenuItem insert(Action action, int position);

与JMenu不同,并不存在insertSeparator()方法。但是我们可以使用由Container继承的add(Component component, int position)方法。如果我们希望移除组件,可以使用JPopupMenu特定的remove(Component component)方法。

显示JPopupMenu

与JMenu不同,简单的组装弹出菜单并不够。我们需要将弹出菜单与合适的组件相关联。在Swing 5.0版本之前,我们需要添加事件处理代码来触发弹出菜单的显示。现在,我们所需要做的就是为我们希望关联弹出菜单的组件调用setComponentPopupMenu()方法。当平台特定的触发事件发生时,弹出菜单会自动显示。

我们只需要简单的创建JPopupMenu的实例,并将其关联到我们希望显示弹出菜单的组件,如下所示:

JPopupMenu popupMenu = ...;
aComponent.setComponentPopupMenu(popupMenu);

对于弹出菜单比较重要的JComponent方法主要有getComponentPopupMenu(), setComponentPopupMenu(), getInheritsPopupMenu(), setInheritsPopupMenu()以及getPopupLocation()方法。setInheritsPopupMenu()方法会接受一个boolean参数。当为true时,并没有直接为组件设置组件弹出菜单,则会查找父容器用于弹出菜单。

JPopupMenu属性

表6-9列出了JPopupMenu的16个属性。更多的属性是由JComponent,Container与Component继承的。

属性名 数据类型 访问性
accessibleContext AccessibleContext 只读
borderPainted boolean 读写
component Component 只读
invoker Component 只读
label String 读写绑定
lightWeightPopupEnabled boolean 读写
margin Insets 只读
menuKeyListeners MenuKeyListener[] 只读
popupMenuListeners PopupMenuListener[] 只读
popupSize Dimension 只写
selected Component 只写
selectionModel SingleSelectionModel 只写
subElements MenuElement[] 只读
UI PopupMenuUI 读写绑定
UIClassID String 只读
visible boolean 读写

Table: JPopupMenu属性

JPopupMenu最有趣的属性就是lightWeightPopupEnabled。通常来说,JPopupMenu会尝试避免为显示其菜单项而创建新的重量级组件。相反,当JPopupMenu可以完整的显示在最外层的窗体框架内时弹出菜单使用JPanel。否则,如果菜单项不适合时,JPopupMenu使用JWindow。然而,如果我们在不同的窗体层上混合使用轻量级与重量级组件,在一个JPanel内显示弹出菜单并不会起作用,因为在菜单层显示的一个重量级组件会在JPanel之前出现。要纠正这种行为,弹出菜单使用Panel用来显示菜单选项。默认情况下,JPopupMenu绝不使用Panel。

如果我们需要允许Panel显示,我们可以在单个的JPopupMenu级别或是整个的Applet或是程序进行配置。在单独的弹出级别,只需要将lightWeightPopupEnable属性设置为false。在系统级别,可以通过如下的代码进行设置:

// From now on, all JPopupMenus will be heavyweight
JPopupMenu.setDefaultLightWeightPopupEnabled(false);

这个方法必须在创建弹出菜单之前调用。JPopupMenu对象会在修改具有原始值(默认为true)之前创建。

监视弹出菜单可见性

类似于JMenu,JPopupMenu具有一个特殊的事件/监听器组合来监听弹出菜单何时可见,何时不可见或是何时关闭。这个组合中的事件就是PopupMenuEvent,而监听器就是PopupMenuListener。事件类只是简单的引用事件的源弹出菜单。

public class PopupMenuEvent extends EventObject {
  public PopupMenuEvent(Object source);
}

当JPopupMenu触发事件时,所注册的PopupMenuListener对象会通过他的三个接口方法得到通知。这可以使得我们依据系统状态或是弹出菜单的调用是谁来自定义当前的菜单项。PopupMenuListener接口定义如下:

public interface PopupMenuListener extends EventListener {
  public void popupMenuCanceled(PopupMenuEvent e);
  public void popupMenuWillBecomeInvisible(PopupMenuEvent e);
  public void popupMenuWillBecomeVisible(PopupMenuEvent e);
}

自定义JPopupMenu观感

每一个所安装的Swing观感都会提供不同的JPopupMenu外观与一个默认的UIResource值集合。图6-6显示了预安装的观感类型集合:Motif,Windows,Ocean的JPopupMenu组件外观。注意,在预安装的观感类中,只有Motif使用JPopupMenu的title属性。

表6-10显示了JPopupMenu相关的UIResource属性。对于JPopupMenu组件,有五个不同的属性。

属性字符串 对象类型
PopupMenu.actionMap ActionMap
PopupMenu.background Color
PopuMenu.border Border
PopupMenu.consumeEventOnClose Boolean
PopupMenu.font Font
PopupMenu.foreground Color
PopupMenu.popupSound String
PopupMenu.selectedWindowInputMapBindings Object[]
PopupMenu.selectedWindowInputMapBindings.RightToLeft Object[]
PopupMenuSeparatorUI String
PopupMenuUI String

Table: JPopupMenu UIResource元素

JPopupMenu.Separator类

JPopupMenu类维护了其自己的分隔符从而允许自定义JPopupMenu上的分隔符的观感。这个自定义的分隔符是JPopupMenu的内联类。

当我们调用JPopupMenu的addSeparator()方法时,则会自动生成这个类的一个实例并添加到弹出菜单中。另外,我们也可以通过调用无参数的构造函数来创建这个分隔符:

JSeparator popupSeparator =new JPopupMenu.Separator();

这两种都会创建水平分隔符。

注意:如果我们要修改分隔符的方向,我们必须使用JPopupMenu.Separator.VERTICAL作为参数调用由JSeparator所继承的setOrientation()方法。然而在弹出菜单中具有一个垂直分隔符并不合适。

一个完整的弹出菜单使用示例

列表6-3中的程序将JPopupMenu使用的所有方面组合在一起,包括监听所有菜单项的选中,同时监听菜单何时显示。程序的输出如图6-7所示。

Swing_6_7.png

Swing_6_7.png

/**
 *
 */
package net.ariel.ch06;

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

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;

/**
 * @author mylxiaoyi
 *
 */
public class PopupSample {

    // Define ActionListener
    static class PopupActionListener implements ActionListener {
        public void actionPerformed(ActionEvent event) {
            System.out.println("Selected: "+event.getActionCommand());
        }
    }

    // Define PopupMenuListener
    static class MyPopupMenuListener implements PopupMenuListener {
        public void popupMenuCanceled(PopupMenuEvent event) {
            System.out.println("Canceled");
        }
        public void popupMenuWillBecomeInvisible(PopupMenuEvent event) {
            System.out.println("Becoming Invisible");
        }
        public void popupMenuWillBecomeVisible(PopupMenuEvent event) {
            System.out.println("Becoming Visible");
        }
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

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

                ActionListener acitonListener = new PopupActionListener();
                PopupMenuListener popupMenuListener = new MyPopupMenuListener();

                // Create popup menu, attach popup menu listener
                JPopupMenu popupMenu = new JPopupMenu("Title");
                popupMenu.addPopupMenuListener(popupMenuListener);

                // Cut
                JMenuItem cutMenuItem = new JMenuItem("Cut");
                cutMenuItem.addActionListener(acitonListener);
                popupMenu.add(cutMenuItem);

                // Copy
                JMenuItem copyMenuItem = new JMenuItem("Copy");
                copyMenuItem.addActionListener(acitonListener);
                popupMenu.add(copyMenuItem);

                // Paste
                JMenuItem pasteMenuItem = new JMenuItem("Paste");
                pasteMenuItem.addActionListener(acitonListener);
                popupMenu.add(pasteMenuItem);

                // Separator
                popupMenu.addSeparator();

                // Find
                JMenuItem findMenuItem = new JMenuItem("Find");
                findMenuItem.addActionListener(acitonListener);
                popupMenu.add(findMenuItem);

                JButton label = new JButton();
                frame.add(label);
                label.setComponentPopupMenu(popupMenu);

                frame.setSize(350, 250);
                frame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

JCheckBoxMenuItem类

Swing的JCheckBoxMenuItem组件的行为类似于我们将一个JCheckBox作为一个JMenuItem放置在菜单上。菜单项的数据模型是ToggleButtonModel,我们在第5章进行了描述。他可以使得菜单项具有选中或是未选中状态,同时显示合适的状态图标。因为数据模型是ToggleButtonModel,当JCheckBoxMenuItem位于一个ButtonGroup中时,该组中只有一个JCheckBoxMenuItem可以被选中。然而,这并不是JCheckBoxMenuItem的通常使用方法,并且很可能会迷惑用户。如果我们需要这种行为,我们可以使用JRadioButtonMenuItem,如本章稍后所述。

创建JCheckBoxMenuItem组件

JCheckBoxMenuItem有七个构造函数。这些构造函数可以允许我们初始化文本标签,图标以及初始状态。

public JCheckBoxMenuItem()
JCheckBoxMenuItem jCheckBoxMenuItem = new JCheckBoxMenuItem();

public JCheckBoxMenuItem(String text)
JCheckBoxMenuItem jCheckBoxMenuItem = new JCheckBoxMenuItem("Boy");

public JCheckBoxMenuItem(Icon icon)
Icon boyIcon = new ImageIcon("boy-r.jpg");
JCheckBoxMenuItem jCheckBoxMenuItem = new JCheckBoxMenuItem(boyIcon);

public JCheckBoxMenuItem(String text, Icon icon)
JCheckBoxMenuItem jCheckBoxMenuItem = new JCheckBoxMenuItem("Boy", boyIcon);

public JCheckBoxMenuItem(String text, boolean state)
JCheckBoxMenuItem jCheckBoxMenuItem = new JCheckBoxMenuItem("Girl", true);

public JCheckBoxMenuItem(String text, Icon icon, boolean state)
Icon girlIcon = new ImageIcon("girl-r.jpg");
JCheckBoxMenuItem jCheckBoxMenuItem = new JCheckBoxMenuItem("Girl", girlIcon, true);

public JCheckBoxMenuItem(Action action)
Action action = ...;
JCheckBoxMenuItem jCheckBoxMenuItem = new JCheckBoxMenuItem(action);

与JCheckBox不同,图标是标签的一部分,而并不是一个单独的设备来表明某项是否被选中。如果在其构造函数中并没有传递文本标签或是图标,菜单项标签部分则会被设置其空的默认值。默认情况下,JCheckBoxMenuItem初始时未选中。

JCheckBoxMenuItem属性

JCheckBoxMenuItem的大部分属性都是由JCheckBoxMenuItem的多个超类继承来的。表6-11列出了JCheckBoxMenuItem所列出的四个属性。

属性名 数据类型 访问性
accessibleContext AccessibleContext 只读
selectedObjects Object[] 只读
state boolean 读写
UIClassID String 只读

Table: JCheckBoxMenuItem属性

处理JCheckBoxMenuItem选中事件

对于JCheckBoxMenuItem而言,我们可以关联多个事件变体:

  1. JMenuItem中的MenuDragMouseListener与MenuKeyListener
  2. AbstractButton中的ActionListener,ChangeListener与ItemListener
  3. JComponent中的AncestorListener与VetoableChangeListener
  4. Container中的ContainerListener与PropertyChangeListener
  5. Component中的ComponentListener,FocusListener,HierarchyBoundsListener,HierarchyListener,InputMenthodListener,KeyListener,MouseListener,MouseMotionListener以及MouseWheelListener

尽管我们可以监听18种没的事件类型,但是最有趣的还是ActionEvent与ItemEvent,如下所述。

使用ActionListener监听JCheckBoxMenuItem事件

将ActionListener关联到JCheckBoxMenuItem可以使得我们确定菜单何时被选中。监听器会被通知选中事件,但是并不会得到新状态的通知。要确定选中状态,我们必须获取事件源模型并查询选中状态,如下面的示例ActionListener源码所示。这个监听器会依据当前的选中状态修改复选框的文本与图标标签。

ActionListener aListener = new ActionListener() {
   public void actionPerformed(ActionEvent event) {
     Icon girlIcon = new ImageIcon("girl-r.jpg");
     Icon boyIcon = new ImageIcon("boy-r.jpg");
     AbstractButton aButton = (AbstractButton)event.getSource();
     boolean selected = aButton.getModel().isSelected();
     String newLabel;
     Icon newIcon;
     if (selected) {
       newLabel = "Girl";
       newIcon = girlIcon;
     } else {
       newLabel = "Boy";
       newIcon = boyIcon;
     }
     aButton.setText(newLabel);
     aButton.setIcon(newIcon);
   }
};

使用ItemListener监听JCheckBoxMenuItem事件

如果我们使用ItemListener监听JCheckBoxMenuItem选中事件,我们并不需要查询事件源以确定选中状态,事件已经带有这些信息了。依据这个状态,我们可以进行正确的响应。使用ItemListener重新创建ActionListener的行为只需要对前面所列出的源代码进行简单的修改,如下所示:

ItemListener iListener = new ItemListener() {
   public void itemStateChanged(ItemEvent event) {
     Icon girlIcon = new ImageIcon("girl-r.jpg");
     Icon boyIcon = new ImageIcon("boy-r.jpg");
     AbstractButton aButton = (AbstractButton)event.getSource();
     int state = event.getStateChange();
     String newLabel;
     Icon newIcon;
     if (state == ItemEvent.SELECTED) {
       newLabel = "Girl";
       newIcon = girlIcon;
     } else {
       newLabel = "Boy";
       newIcon = boyIcon;
     }
     aButton.setText(newLabel);
     aButton.setIcon(newIcon);
   }
};

自定义JCheckBoxMenuItem观感

图6-3显示了预安装的观感类型集合下JCheckBoxMenuItem的外观。

表6-12列出了JCheckBoxMenuItem的UIResource相关的属性。JCheckBoxMenuItem组件具有19个不同的属性。

属性字符串 对象类型
CheckBoxMenuItem.acceleratorFont Font
CheckBoxMenuItem.acceleratorForeground Color
CheckBoxMenuItem.acceleratorSelectionForeground Color
CheckBoxMenuItem.actionMap ActionMap
CheckBoxMenuItem.arrowIcon Icon
CheckBoxMenuItem.background Color
CheckBoxMenuItem.border Border
CheckBoxMenuItem.borderPainted Boolean
CheckBoxMenuItem.checkIcon Icon
CheckBoxMenuItem.commandSound String
CheckBoxMenuItem.disabledForeground Color
CheckBoxMenuItem.font Font
CheckBoxMenuItem.foreground Color
CheckBoxMenuItem.gradient List
CheckBoxMenuItem.margin Insets
CheckBoxMenuItem.opaue Boolean
CheckBoxMenuItem.selectionBackground Color
CheckBoxMenuItem.selectionForeground Color
CheckBoxMenuItemUI String

Table: JCheckBoxMenuItem UIResource元素

与CheckboxMenuItem.checkIcon属性键值相关联的Icon是显示在JCheckBoxMenuItem上的图标。如果我们不喜欢默认图标,我们可以使用下面的代码行进行修改,在这里假定已经定义并创建了新图标:

UIManager.put("CheckBoxMenuItem.checkIcon", someIcon);

为了使得新图标可以显示合适的选中图像,Icon实现必须其paintIcon()方法内检测关联的菜单组件状态。第4章所创建的DiamondIcon对于这个图标并不起作用,因为他并不检测其状态组件。相反,状态是在构造是确定的。列表6-4列出了一个可以使用的图标类。

package net.ariel.ch06;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Polygon;

import javax.swing.AbstractButton;
import javax.swing.Icon;

public class DiamondAbstractButtonStateIcon implements Icon {

    private final int width = 10;
    private final int height = 10;
    private Color color;
    private Polygon polygon;

    public DiamondAbstractButtonStateIcon(Color color) {
        this.color = color;
        initPolygon();
    }

    private void initPolygon() {
        polygon = new Polygon();
        int halfWidth = width/2;
        int halfHeight = height/2;
        polygon.addPoint(0, halfHeight);
        polygon.addPoint(halfWidth, 0);
        polygon.addPoint(width, halfHeight);
        polygon.addPoint(halfWidth, height);
    }
    @Override
    public int getIconHeight() {
        // TODO Auto-generated method stub
        return height;
    }

    @Override
    public int getIconWidth() {
        // TODO Auto-generated method stub
        return height;
    }

    @Override
    public void paintIcon(Component component, Graphics g, int x, int y) {
        // TODO Auto-generated method stub

        boolean selected = false;
        g.setColor(color);
        g.translate(x, y);
        if(component instanceof AbstractButton) {
            AbstractButton abstractButton = (AbstractButton)component;
            selected = abstractButton.isSelected();
        }
        if(selected) {
            g.fillPolygon(polygon);
        }
        else {
            g.drawPolygon(polygon);
        }
        g.translate(-x, -y);
    }

}

JRadioButtonMenuItem类

JRadioButtonMenuItem组件具有所有的Swing组件中最长的名字。其作用类似于JRadioButton,但是位于菜单中。当与其他的JRadioButtonMenuItem组件共同放在一个ButtonGroup中时,每次只有一个组件可以被选中。与JRadioButton类似,JRadioButtonMenuItem的按钮模型是JToggleButton.ToggleButtonModel。

创建JRadioButtonMenuItem组件

JRadioButtonMenuItem具有七个构造函数。这些构造函数允许我们初始化文本标签,图标以及初始状态。

public JCheckBoxMenuItem()
JCheckBoxMenuItem jCheckBoxMenuItem = new JCheckBoxMenuItem();
public JCheckBoxMenuItem(String text)
JCheckBoxMenuItem jCheckBoxMenuItem = new JCheckBoxMenuItem("Boy");
public JCheckBoxMenuItem(Icon icon)
Icon boyIcon = new ImageIcon("boy-r.jpg");
JCheckBoxMenuItem jCheckBoxMenuItem = new JCheckBoxMenuItem(boyIcon);
public JCheckBoxMenuItem(String text, Icon icon)
JCheckBoxMenuItem jCheckBoxMenuItem = new JCheckBoxMenuItem("Boy", boyIcon);
public JCheckBoxMenuItem(String text, boolean state)
JCheckBoxMenuItem jCheckBoxMenuItem = new JCheckBoxMenuItem("Girl", true);
public JCheckBoxMenuItem(String text, Icon icon, boolean state)
Icon girlIcon = new ImageIcon("girl-r.jpg");
JCheckBoxMenuItem jCheckBoxMenuItem = new JCheckBoxMenuItem("Girl", girlIcon, true);
public JCheckBoxMenuItem(Action action)
Action action = ...;
JCheckBoxMenuItem jCheckBoxMenuItem = new JCheckBoxMenuItem(action);

与JCheckBoxMenuItem组件类似,JRadioButtonMenuItem的图标也是标签的一部分。这与JRadioButton不同,在JRadioButton中图标可以表明单选按钮是否被选中。如果在构造函数中并没有传递文本标签或是图标,则项目标签部分则为空。在默认情况下,JRadioButtonMenuItem初始时未选中。如果我们创建一个选中的JRadioButtonMenuItem并将其添加到ButtonGroup中,如果在按钮组中已有一个被选中的项目时,则按钮组会取消新创建的菜单项的选中状态。

处理JRadioButtonMenuItem的选中事件

JRadioButtonMenuItem共享与JCheckBoxMenuItem相同的18个不同的事件/监听器对。要监听选中事件,关联ActionListener是通常的方法。另外,我们也许希望将相同的监听器关联到ButtonGroup中所有的JRadioButtonMenuItem对象之上,毕竟他们由于某种原因分为一组。如果我们使用相同的监听器,监听器可以依据当前的选中而执行某些通常的操作。在其他情况下,如图6-1所示,JRadioButtonMenuItem选项的选中并不进行任何操作。

配置JRadioButtonMenuItem属性

与JCheckBoxMenuItem类似,大部分的JRadioButtonMenuItem属性都是继承的。表6-13中的两个属性覆盖了超类的行为。

属性名 数据类型 访问性
accessibleContext AccessibleContext 只读
UIClassID String 只读

Table: JRadioButtonMenuItem属性

自定义JRadioButtonMenuItem观感

图6-3显示了预安装的观感类型集合下的JRadioButtonMenuItem的外观。

表6-14显示了JRadioButtonMenuItem的UIResource相关的属性集合。对于JRadioButtonMenuItem组件而言,共有19个不同的属性。

属性字符串 对象类型
RadioButtonMenuItem.acceleratorFont Font
RadioButtonMenuItem.acceleratorForeground Color
RadioButtonMenuItem.acceleratorSelectionForeground Color
RadioButtonMenuItem.actionMap ActionMap
RadioButtonMenuItem.arrowIcon Icon
RadioButtonMenuItem.background Color
RadioButtonMenuItem.border Border
RadioButtonMenuItem.borderPainted Boolean
RadioButtonMenuItem.checkIcon Icon
RadioButtonMenuItem.commandSound String
RadioButtonMenuItem.disabledForeground Color
RadioButtonMenuItem.font Font
RadioButtonMenuItem.foreground Color
RadioButtonMenuItem.gradient List
RadioButtonMenuItem.margin Insets
RadioButtonMenuItem.opaque Boolean
RadioButtonMenuItem.selectionBackground Color
RadioButtonMenuItem.selectionForeground Color
RadioButtonMenuItemUI String

Table: JRadioButtonMenuItem UIResource元素

完整的JRadioButtonMenuItem使用示例

为了助于我们理解JRadioButtonMenuItem的使用,列表6-5中的程序演示了如何将所有的内容组合在一起,包括监听菜单上的所有菜单项的选中,使用ActionListener或是ItemListener。程序的输出如图6-8所示。

swing_6_8.png

swing_6_8.png

/**
 *
 */
package net.ariel.ch06;

import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyEvent;

import javax.swing.AbstractButton;
import javax.swing.ButtonGroup;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JRadioButtonMenuItem;

/**
 * @author mylxiaoyi
 *
 */
public class RadioButtonSample {

    static Icon threeIcon = new ImageIcon("3.gif");
    static Icon fourIcon = new ImageIcon("4.gif");
    static Icon fiveIcon = new ImageIcon("5.gif");
    static Icon sixIcon = new ImageIcon("6.gif");

    public static class ButtonActionListener implements ActionListener {
        public void actionPerformed(ActionEvent event) {
            AbstractButton aButton = (AbstractButton)event.getSource();
            boolean selected = aButton.getModel().isSelected();
            System.out.println(event.getActionCommand()+" - selected? "+selected);
        }
    }

    public static class ButtonItemListener implements ItemListener {
        public void itemStateChanged(ItemEvent event) {
            AbstractButton aButton = (AbstractButton)event.getSource();
            int state = event.getStateChange();
            String selected = ((state == event.SELECTED)?"selected":"not selected");
            System.out.println(aButton.getText()+" - selected? "+selected);
        }
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Runnable runner = new Runnable() {
            public void run() {
                final ActionListener actionListener = new ButtonActionListener();
                final ItemListener itemListener = new ButtonItemListener();

                JFrame frame = new JFrame("Radio Menu Example");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                JMenuBar menuBar = new JMenuBar();
                JMenu menu = new JMenu("Menu");
                ButtonGroup buttonGroup = new ButtonGroup();
                menu.setMnemonic(KeyEvent.VK_M);

                JRadioButtonMenuItem emptyMenuItem = new JRadioButtonMenuItem();
                emptyMenuItem.setActionCommand("Empty");
                emptyMenuItem.addActionListener(actionListener);
                buttonGroup.add(emptyMenuItem);
                menu.add(emptyMenuItem);

                JRadioButtonMenuItem oneMenuItem = new JRadioButtonMenuItem("Partridge");
                oneMenuItem.addActionListener(actionListener);
                buttonGroup.add(oneMenuItem);
                menu.add(oneMenuItem);

                JRadioButtonMenuItem twoMenuItem = new JRadioButtonMenuItem("Turtle Dove", true);
                twoMenuItem.addActionListener(actionListener);
                buttonGroup.add(twoMenuItem);
                menu.add(twoMenuItem);

                JRadioButtonMenuItem threeMenuItem = new JRadioButtonMenuItem("French Hens", threeIcon);
                threeMenuItem.addItemListener(itemListener);
                buttonGroup.add(threeMenuItem);
                menu.add(threeMenuItem);

                JRadioButtonMenuItem fourMenuItem = new JRadioButtonMenuItem("Calling Birds", fourIcon, true);
                fourMenuItem.addActionListener(actionListener);
                buttonGroup.add(fourMenuItem);
                menu.add(fourMenuItem);

                JRadioButtonMenuItem fiveMenuItem = new JRadioButtonMenuItem(fiveIcon);
                fiveMenuItem.addActionListener(actionListener);
                fiveMenuItem.setActionCommand("Rings");
                buttonGroup.add(fiveMenuItem);
                menu.add(fiveMenuItem);

                JRadioButtonMenuItem sixMenuItem = new JRadioButtonMenuItem(sixIcon, true);
                sixMenuItem.addActionListener(actionListener);
                sixMenuItem.setActionCommand("Geese");
                buttonGroup.add(sixMenuItem);
                menu.add(sixMenuItem);

                menuBar.add(menu);

                frame.setJMenuBar(menuBar);
                frame.setSize(350, 250);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

使用工具栏:JToolBar类

工具栏是现代用户界面中主程序窗口的主要部分。工具栏向用户提供了对于常用命令的简单访问,这通常构建为层次结构的菜单结构。支持这种功能的Swing组件就是JToolBar。

JToolBar是一个种存放组件的特殊Swing容器。这个容器可以在我们的Java Applet或是程序中用作工具栏,而且可以在程序的主窗口之外浮动或是托拽。JToolBar是一个非常容易使用与理解的简单组件。

创建JToolBar组件

有四个构造函数可以用来创建JToolBar组件:

public JToolBar()
JToolBar jToolBar = new JToolBar();
public JToolBar(int orientation)
JToolBar jToolBar = new JToolBar(JToolBar.VERTICAL);
public JToolBar(String name)
JToolBar jToolBar = new JToolBar("Window Title");
public JToolBar(String name,int orientation)
JToolBar jToolBar = new JToolBar("Window Title", ToolBar.VERTICAL);

在默认情况下,工具栏是以水平方向进行创建的。然而,我们可以通过JToolBar的常量HORIZONTAL与VERTICAL显示指定方向。

而且在默认情况下,工具栏是可以浮动的。所以,如果我们使用水平方向创建一个工具栏,用户可以在窗口周围拖动工具栏来改变工具栏的方向。

向JToolBar添加组件

一旦我们拥有一个JToolBar,我们需要向其中添加组件。任意的Component都可以添加到工具栏。当处理水平工具栏时,由于美观的原因,如果工具栏的组件是大致相同的高度时是最好的。对于垂直工具栏,如果工具栏组件具有大致相同的宽度则是最好的。JToolBar类只定义了一个方法用于添加工具栏项目;其他的方法,例如add(Component)是由Container继承而来的。另外,我们可以向工具栏添加分隔符。

public JButton add(Action action);
public void addSeparator();
public void addSeparator(Dimension size);

当使用JToolBar的add(Action)方法时,所添加的Action被封闭在一个JButton对象中。这与向JMenu或是JPopupMenu组件添加Action不同,在后一种情况中,所添加的是JMenuItem对象。对于JMenu与JPopupMenu,以这种方式添加Action是类的Javadoc中所不推荐的。对于分隔符,如果我们没有指定尺寸,所安装的观感会强制默认的尺寸设置。

由工具栏移除组件可以使用下面的方法:

public void remove(Component component)

JToolBar属性

表6-15列出了JToolBar所定义的9个属性。

属性名 数据类型 访问性
accessibleContext AccessibleContext 只读
borderPainted boolean 读写绑定
floatable boolean 读写绑定
layout LayoutManager 只写
margin Insets 读写绑定
orientation int 读写绑定
rollover boolean 读写绑定
UI ToolBarUI 读写
UIClassID String 只读

Table: JToolBar属性

在默认情况下绘制JToolBar的边框。如果我们不希望绘制边框,我们可以将borderPainted属性设置为false。如果不使用borderPainted属性,我们需要修改border属性(由超类JComponent继承的属性)。

orientation属性只可以设置为JToolBar的HORIZONTAL或是VERTICAL常量。如果使用其他的值,则会抛出IllegalArgumentException。修改方向会改变工具栏的布局管理器。如果我们通过setLayout()方法直接修改布局管理器,改变方向会撤销我们的布局管理器。

正如前面所提到的,默认情况下工具栏是可浮动的。这就意味着用户可以拖动工具栏并放置在其他位置。要拖动工具栏,用户选择工具栏的空白部分。然后工具栏可以停留在原始的程序窗口,在主窗口内部浮动,或者是拖动到原始程序窗口的其他部分。如果原始窗口的布局管理器是BorderLayout,可拖动的部分是布局管理器无组件的边。(我们不能将工具栏放在窗口的中央。)否则,工具栏会被拖动到容器的最后一个点上。图6-10显示了拖动与停放过程的不同阶段。

swing_6_10.png

swing_6_10.png

rollover属性定义了当用户在工具栏的不同组件上移动时与观感特定的行为。这种行为涉及到颜色与边框的不同。

处理JToolBar事件

并没有特定于JToolBar的事件。我们需要将监听器关联到我们需要响应用户交互的JToolBar上的每一项上。当然,JToolBar是一个Container,所以我们也可以监听其事件。

自定义JToolBar观感

每一个可安装的Swing观感都提供了其自己的JToolBar外观以及默认的UIResource值集合。外观的大部分是由工具栏中的实际组件控制的。图6-11显示了预安装的观感类型集合Motif,Windows以及Ocean的JToolBar组件外观。每一个工具栏都有五个JButton组件,在第四个与第五个组件之间有一个分隔符。

swing_6_11.png

swing_6_11.png

表6-16中列出了JToolBar的UIResource相关的属性。对于JToolBar组件,有22个不同的属性。

属性字符串 对象类型
ToolBar.actionMap ActionMap
ToolBar.ancestorInputMap InputMap
ToolBar.background Color
ToolBar.border Border
ToolBar.borderColor Color
ToolBar.darkShadow Color
ToolBar.dockingBackground Color
ToolBar.docingForeground Color
ToolBar.floatingBackground Color
ToolBar.floatingForeground Color
ToolBar.font Font
ToolBar.foreground Color
ToolBar.handleIcon Icon
ToolBar.highlight Color
ToolBar.isRollover Boolean
ToolBar.light Color
ToolBar.nonrolloverBorder Border
ToolBar.rolloverBorder Border
ToolBar.separatorSize Dimension
ToolBar.shadow Color
ToolBarSeparatorUI String
ToolBarUI String

Table: JToolBar UIResource元素

完整的JToolBar使用示例

列表6-8演示了一个完整的JToolBar示例,这个程序生成了一个带有多个菱形按钮的工具栏。这个程序同时重用了本章前面列表6-2中为菜单示例所定义的ShowAction。

在这个示例中允许了rollover属性以演示当前观感的不同。图6-12是我们在不同的按钮上移动鼠标时的输出结果。

swing_6_12.png

swing_6_12.png

package net.ariel.ch06;

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

import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JToolBar;

import net.ariel.ch04.DiamondIcon;

public class ToolBarSample {

    private static final int COLOR_POSITION = 0;
    private static final int STRING_POSITION = 1;
    static Object buttonColors[][] = {
        {Color.RED, "RED"},
        {Color.BLUE, "BLUE"},
        {Color.GREEN, "GREEN"},
        {Color.BLACK, "BLACK"},
        null, // separator
        {Color.CYAN, "CYAN"}
    };

    public static class TheActionListener implements ActionListener {
        public void actionPerformed(ActionEvent event) {
            System.out.println(event.getActionCommand());
        }
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("JToolBar Example");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                ActionListener actionListener = new TheActionListener();

                JToolBar toolbar = new JToolBar();
                toolbar.setRollover(true);

                for(Object[] color: buttonColors) {
                    if(color == null) {
                        toolbar.addSeparator();
                    }
                    else {
                        Icon icon = new DiamondIcon((Color)color[COLOR_POSITION], true, 20, 20);
                        JButton button = new JButton(icon);
                        button.setActionCommand((String)color[STRING_POSITION]);
                        button.addActionListener(actionListener);
                        toolbar.add(button);
                    }
                }
                Action action = new ShowAction(frame);
                JButton button = new JButton(action);
                toolbar.add(button);

                Container contentPane = frame.getContentPane();
                contentPane.add(toolbar, BorderLayout.NORTH);
                JTextArea textArea = new JTextArea();
                JScrollPane pane = new JScrollPane(textArea);
                contentPane.add(pane, BorderLayout.CENTER);
                frame.setSize(350, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

JToolBar.Separator类

JToolBar维护其自己的分隔符从而允许自定义JToolBar上的分隔符的观感。

这个分隔符是我们调用JToolBar的addSeparator()方法时自动创建的。另外,如果我们希望手动创建这个组件,则有两个方法可以创建JToolBar.Separator。

public JToolBar.Separator()
JSeparator toolBarSeparator = new JToolBar.Separator();
public JToolBar.Separator(Dimension size)
Dimension dimension = new Dimension(10, 10);
JSeparator toolBarSeparator = new JToolBar.Separator(dimension);

这两个构造函数都创建一个水平分隔符。我们可以配置其尺寸。如果我们没有指定尺寸,观感类型会决定分隔符的尺寸。

与JPopupMenu.Separator类似,如果我们希望修改分隔符的方向,我们必须调用由JSeparator所继承的setOrientation()方法,此时的参数为JToolBar.Separator.VERTICAL。

小结

本章介绍了许多Swing菜单相关的类及其内部关系,以及Swing工具栏类。首先,我们了解了JMenuBar及其选择模型,并且了解了菜单栏如何应用在Applets以及程序中。

接下来,我们探讨了JMenuItem,这是用户选择的菜单元素,以及系统用来处理事件的两个新的事件/监听器对,MenuKeyEvent/MenuKeyListener以及MenuDragMouseEvent/MenuDragMouseListener。然后,我们探讨了JMenu组件,这是JMenuItem实放置的地方,及其新的事件/监听器对,MenuEvent/MenuListener,这可以用来确定菜单何时将会发送。

接下来,我们了解了JSeparator组件以及我们如何可以将其作为一个菜单分隔符或是菜单外的可视分隔符。

然后我们探讨了JPopupMenu,JMenu用其来显示其JMenuItem组件集合。对于JPopupMenu,我们了解了弹出菜单自己的事件/监听器对,PopupMenuEvent/PopupMenuListener。

然后我们探讨了JCheckBoxMenuItem与JRadioButtonMenuItem中的可选择的菜单元素,以及MenuElement接口,同时我们了解了如何创建自定义的菜单组件。

菜单并不是唯一可以弹出的内容,所以我们探讨了Popup与PopupFactory。最后,本章探讨了JToolBar类。

在第7章中,我们将会了解Swing提供的用来自定义Swing组件周围边框的不同类。

Borders

Swing组件提供了对组件周围的边框区域进行定制的功能。为了简单,我们可以使用预定义的八个边框,或者是我们可以创建自己的边框。在本章中,我们将会了解如何最好的使用已存在边框以及如何创建我们自己的边框。

Some Basics on Woring with Borders

边框是带有标准的setBorder()与getBorder()属性方法的JComponent属性。所以,所有的JComponent子类的Swing组件都具有边框。默认情况下,一个组件并没有与其相关联的自定义边框。(JComponent的getBorder()方法返回null。)相反,组件显示的默认边框是依据当前的观感对于其状态最为合适的边框。例如,对于JButton,对于每一个观感特定不同的边框,边框可以表现为按下,未按下或是禁止。

尽管对于所有的组件初始的边框属性设置为null,我们可以通过调用JComponent的setBorder(Border newValue)方法来修改组件的边框。一旦设置,修改的值就会覆盖当前观感的边框,并且在组件的区域内绘制新边框。如果在稍后的时候,我们希望将边框重新设置为对于状态与观感合适的边框,我们可以将边框属性修改为null,使用setBorder(null)并且调用组件的updateUI()方法。updateUI()方法会通知观感重新设置边框。如果我们没有调用updateUI()方法,则组件将没有边框。

图7-1显示了一个JLabel周围的各种边框设置,通过文本标签来标明边框类型。如何创建不同的边框将会在本章的稍后部分进行讨论。

swing_7_1.png

swing_7_1.png

Exploring the Border Inteface

我们可以在javax.swing.border包中找到Border接口。这个接口构成了所有边框类的基础。这个接口直接由AbstractBorder类实现,这个类是所有预定义的Swing边框类的父类:BevelBorder,CompoundBorder,EmptyBorder,EtchedBorder,LineBorder,MatteBorder,SoftBevelBorder以及TitledBorder。另外一个有趣的类就是BorderFactory类,我们可以在javax.swing包中找到这个类。这个类使用工厂设计模式来创建边框,隐藏了实现细节,并且可以缓存各种选项来优化共同使用。

在这里显示的Border接口由三个方法构成:paintBorder(),getBordernsets()以及isBorderOpaque()。这些方法会在后面的章节中进行描述。

paintBorder()

paintBorder()方法是这个接口的关键方法。其定义如下:

public void paintBorder(Component c, Graphics g, int x, int y, int
  width, int height)

边框实现的绘制是由这个方法完成的。通常,Border实现首先会询问Insets维度,然后在外层区域的四个边进行绘制,如图7-2所示。如果边框是不透明的,paintBorder()实现必须填充整个内部区域。如果一个边框是不透明的,并没有填充区域,那么这是一个bug并且需要修正。

列表7-1显示了一个简单的paintBorder()实现,这个实现使用比上部与下部略浅的颜色填充左边与右边。

public void paintBorder(Component c, Graphics g, int x, int y, int width,
    int height) {
  Insets insets = getBorderInsets(c);
  Color color = c.getForeground();
  Color brighterColor = color.brighter();
// Translate coordinate space
  g.translate(x, y);
// Top
  g.setColor(color);
  g.fillRect(0, 0, width, insets.top);
// Left
  g.setColor(brighterColor);
  g.fillRect(0, insets.top, insets.left, height-insets.top-insets.bottom);
// Bottom
  g.setColor(color);
  g.fillRect(0, height-insets.bottom, width, insets.bottom);
// Right
  g.setColor(brighterColor);
  g.fillRect(width-insets.right, insets.top, insets.right,
    height-insets.top-insets.bottom);
// Translate coordinate space back
  g.translate(-x, -y);
}

当创建我们自己的边框时,我们将会经常发现我们自己在填充相同的非重叠矩形区域。Graphics的translate()方法简化了绘制坐标的指定。无需转换坐标,我们需要通过原始的(x,y)来偏移绘制。

注意:我们不能通过插入g.fillRect(x,y,width,height)来简化,因为这会填充整个组件区域,而不是边框区域。

getBorderInsets()

getBorderInsets()方法会返回在指定的组件c作为Insets对象的周围绘制边框所必须的空间。其定义如下:

public Insets getBorderInsets(Component c)

如图7-2所示,这些内部区域定义了可以绘制边框的合法区域。Component参数可以使得我们使用他的一些属性来决定内部区域的尺寸。

isBorderOpaque()

边框可以是不透明的或是透明的。isBorderOpaque()方法可以返回true或是false来表明边框是哪种形式。其定义如下:

public boolean isBorderOpaque()

当这个方法返回true时,边框需要是非透明的,填充其整个内部区域。当其返回false时,没有绘制的区域将会保持边框所在的组件的背景颜色。

Introducing BorderFactory

现在我们已经基本了解了Border接口是如何工作的,现在我们来了解一下作为简单创建边框方法的BorderFactory类。我们可以在javax.swing包中找到这个类,BorderFactory类提供了一系列的static方法来创建预定义的边框。无需调用不同的边框类的特定构造函数,通过这个工厂类我们几乎可以创建所有的边框。这个工厂类同时可以缓存一些边框的创建从而避免多次重新创建经常使用的边框。这个类的定义如下:

public class BorderFactory {
  public static Border createBevelBorder(int type);
  public static Border createBevelBorder(int type, Color highlight,
    Color shadow);
  public static Border createBevelBorder(int type, Color highlightOuter,
    Color highlightInner, Color shadowOuter, Color shadowInner);
  public static CompoundBorder createCompoundBorder();
  public static CompoundBorder createCompoundBorder(Border outside,
    Border inside);
  public static Border createEmptyBorder();
  public static Border createEmptyBorder(int top, int left, int bottom,
    int right);
  public static Border createEtchedBorder();
  public static Border createEtchedBorder(Color highlight, Color shadow);
  public static Border createEtchedBorder(int type);
  public static Border createEtchedBorder(int type, Color highlight,
    Color shadow);
  public static Border createLineBorder(Color color);
  public static Border createLineBorder(Color color, int thickness);
  public static Border createLoweredBevelBorder();
  public static MatteBorder createMatteBorder(int top, int left, int bottom,
    int right, Color color);
  public static MatteBorder createMatteBorder(int top, int left, int bottom,
    int right, Icon icon);
  public static Border createRaisedBevelBorder();
  public static TitledBorder createTitledBorder(Border border);
  public static TitledBorder createTitledBorder(Border border, String title);
  public static TitledBorder createTitledBorder(Border border, String title,
    int justification, int position);
  public static TitledBorder createTitledBorder(Border border, String title,
    int justification, int position, Font font);
  public static TitledBorder createTitledBorder(Border border, String title,
    int justification, int position, Font font, Color color);
  public static TitledBorder createTitledBorder(String title);
}

我们将会在描述特定的边框类型的过程中描述这个类的不同方法。例如,要创建一个具有红线的边框,我们可以使用下面的语句,然后将这个边框关联到一个组件。

Border lineBorder = BorderFactory.createLineBorder(Color.RED);

Starting with AbstractBorder

在我们了解javax.swing.border包中单个的边框之前,一个系统边框需要获得特别的关注:AbstractBorder。正如前面所提到的,AbstractBorder类是其他的预定义边框的父类。

创建AbstractBorder

AbstractBorder有一个构造函数:

public AbstractBorder()

因为AbstractBorder是其他标准边框的父类,这个构造函数实际是为其他边框类自动调用的。

检测AbstractBorder方法

AbstractBorder类提供了Border接口的三个方法实现。

public Insets getBorderInsets(Component c)

AbstractBorder的内部区域是零。每一个预定义的子类要重写getBorderInsets()方法。

public boolean isBorderOpaque()

AbstractBorder的默认非透明属性设置为false。这就意味着如果我们需要绘制类似点划线的边框,组件的背景将会是透明的。许多预定义的子类重写了isBorderOpaque()方法。

public void paintBorder(Component c, Graphics g, int x, int y,
  int width, int height)

AbstractBorder的绘制边框是空的。所有的子类应该重写这个方法来实际绘制一个边框,也许除了EmptyBorder。

除了提供了Border方法的默认实现以外,AbstractBorder提供了我们可以利用的其他两个功能,或者仅是允许系统使用。首先,还有另外一个需要两个参数Component与Insets的getBorderInsets()方法:

public Insets getBorderInsets(Component c, Insets insets)

在这个方法版本中,并没有创建并返回一个新的Insets对象,所传递的Insets对象首先被修改然后返回。使用这个方法可以避免每次需要查询边框内部区域时创建然后销毁额外的Insets对象。

第二个可用的新方法是getInteriorRectangle(),这个方法有静态与非静态版本。指定了Component,Border,以及四个整数参数,这个方法将会返回一个内部的Rectangle,从而组件可以在边框内部区域内绘制其自身。

Examining the Predefined Borders

现在我们已经描述了边框基础,现在我们来了解一下每一个预定义的特定边框,在某种程度上以复杂性的顺序进行描述。

EmptyBorder Class

由逻辑上来说,空边框就是在其内部不进行任何绘制的边框。当我们在使用一个通常的AWT容器并且需要覆盖insets()或是getInsets()方法时我们可以使用EmptyBorder。他可以使得我们保留组件周围的额外空间从而略微向外一点扩展屏幕组件或是修改居中或是调整某些方面。图7-3显示了一个空边框以及一个非空边框。

swing_7_3.png

swing_7_3.png

EmptyBorder有两个构造函数以及两个BorderFactory的工厂方法:

public static Border createEmptyBorder()
Border emptyBorder = BorderFactory.createEmptyBorder();
public static Border createEmptyBorder(int top, int left, int bottom, int right)
Border emptyBorder = BorderFactory.createEmptyBorder(5, 10, 5, 10);
public EmptyBorder(Insets insets)
Insets insets = new Insets(5, 10, 5, 10);
Border EmptyBorder = new EmptyBorder(insets);
public EmptyBorder(int top, int left, int bottom, int right)
Border EmptyBorder = new EmptyBorder(5, 10, 5, 10);

每一个都允许我们以方法特定的方式来自定义边框的insets。无参数的版本会使用零insets创建一个空的边框;否则,我们可以使用AWT Insets实例或是insets片段来指定insets。在默认情况下,EmptyBorder是透明的。

注意:当我们使用零insets创建一个空边框时,我们应该使用工厂方法来创建边框,而避免直接使用构造函数。这可以使用工厂创建一个共享的空边框。如果我们所希望做的是隐藏边框,而且组件是一个AbstractButton子类,则只需要调用setBorderPainted(false)。

LineBorder Class

LineBorder是围绕组件周围用户义定宽度的单色行边框。他可以具有方角或是圆角。如果我们希望修改不同边的粗细,我们需要使用MatteBorder,我们会在本章稍后进行讨论。图7-4显示了一个青筋LineBorder的示例,在这个例子中两个边框分别为1像素与12像素宽,带圆角以及不带圆角。

创建LineBorder

LineBorder有三个构造函数,两个工厂方法以及两个BorderFactory工厂方法:

public LineBorder(Color color)
Border lineBorder = new LineBorder (Color.RED);

public LineBorder(Color color, int thickness)
Border lineBorder = new LineBorder (Color.RED, 5);

public LineBorder (Color color, int thickness, boolean roundedCorners)
Border lineBorder = new LineBorder (Color.RED, 5, true);

public static Border createBlackLineBorder()
Border blackLine = LineBorder.createBlackLineBorder();

public static Border createGrayLineBorder()
Border grayLine = LineBorder.createGrayLineBorder();

public static Border createLineBorder(Color color)
Border lineBorder = BorderFactory.createLineBorder(Color.RED);

public static Border createLineBorder(Color color, int thickness)
Border lineBorder = BorderFactory.createLineBorder(Color.RED, 5);

注意:LineBorder工厂方法工作如下:如果我们两次创建相同的边框,则会返回相同的LineBorder对象。然而,如同所有的对象对比,我们应总是使用equals()方法来检测对象相同。

每一个方法允许我们自定义边框的颜色与线的粗细。如果没有指定粗细,则默认值为1。LineBorder的两个工厂方法可以用于通常使用的黑色与灰色。因为边框填充整个insets区域,所以LineBorder是不透明的,除非他们是圆角。所以,边框的透明性是圆角设置相反的。

设置LineBorder属性

表7-1列出了由AbstractBorder继承的borderOpaque属性以及LineBorder的特定属性。

属性名 数据类型 访问性
borderOpaque Boolean 只读
lineColor Color 只读
roundedCorners boolean 只读
thickness int 只读

Table: LineBorder属性

BevelBorder Class

BevelBorder以三维外观绘制边框,其可以表现为升起或是降低。当边框升起时,在边框的底部与右边会出现阴影效果。当降低时,阴影的位置会相反。图7-5显示了带有默认与自定义颜色的升起与降低BevelBorder。

swing_7_5.png

swing_7_5.png

在组件周围绘制一对一像素宽的线可以产生三维外观的模拟效果。非阴影的边框侧边以被称为highlight颜色进行绘制,而其他两边以shadow颜色进行绘制。highlight颜色与shadow颜色对于BevelBorder的外边与内边使用不同的阴影进行绘制。所以,一个BevelBorder总共使用四种不同的颜色。图7-6显示这四种颜色是如何组合在一起的。

swing_7_6.png

swing_7_6.png

BevelBorder有三个构造函数以及一个工厂方法,同时还有BorderFactory创建BevelBorder对象的五个工厂方法:

public BevelBorder(int bevelType)
Border bevelBorder = new BevelBorder(BevelBorder.RAISED);

public static Border createBevelBorder(int bevelType)
Border bevelBorder = BorderFactory.createBevelBorder(BevelBorder.RAISED);

public static Border createLoweredBevelBorder()
Border bevelBorder = BorderFactory.createLoweredBevelBorder();

public static Border createRaisedBevelBorder()
Border bevelBorder = BorderFactory.createRaisedBevelBorder();

public BevelBorder(int bevelType, Color highlight, Color shadow)
Border bevelBorder = new BevelBorder(BevelBorder.RAISED, Color.PINK, Color.RED);

public static Border createBevelBorder(int bevelType, Color highlight, Color shadow)
Border bevelBorder = BorderFactory.createBevelBorder(BevelBorder.RAISED,
  Color.PINK, Color.RED);

public BevelBorder(int bevelType, Color highlightOuter, Color highlightInner,
  Color shadowOuter, Color shadowInner)
Border bevelBorder = new BevelBorder(BevelBorder.RAISED, Color.PINK,
  Color.PINK.brighter(), Color.RED, Color.RED.darker());

public static Border createBevelBorder(int bevelType, Color highlightOuter,
  Color highlightInner, Color shadowOuter, Color shadowInner)
Border bevelBorder = BorderFactory.createBevelBorder(BevelBorder.RAISED,
  Color.PINK, Color.PINK.brighter(), Color.RED, Color.RED.darker());

每一个方法都可以使得我们自定义斜面类型以及边框中明亮与阴影的颜色。斜面类型是通过下面两个值来指定的:BevelBorder.RAISED或是BevelBorder.LOWERED。如果没有指定明亮与阴影颜色,则会通过检测边框组件的背景颜色来生成合适的颜色。如果我们指定了相应的颜色,记住明亮颜色应亮一些,通常可以通过调用theColor.brighter()方法来实现。在默认情况下,BevelBorder是不透明的。

SoftBevelBorder Class

SoftBevelBorder是BevelBorder的近亲。这个组件会包围四角,所以他们的边并不尖利,而他使用下边与右边的相应外边颜色只绘制一条线。如图7-7所示,升起与落下的SoftBevelBorder与BevelBorder基本相同。

swing_7_7.png

swing_7_7.png

SoftBevelBorder有三个构造函数:

public SoftBevelBorder(int bevelType)
Border softBevelBorder = new SoftBevelBorder(SoftBevelBorder.RAISED);
public SoftBevelBorder(int bevelType, Color highlight, Color shadow)
Border softBevelBorder = new SoftBevelBorder(SoftBevelBorder.RAISED, Color.RED,
  Color.PINK);
public SoftBevelBorder(int bevelType, Color highlightOuter, Color highlightInner,
  Color shadowOuter, Color shadowInner)
Border softBevelBorder = new SoftBevelBorder(SoftBevelBorder.RAISED, Color.RED,
  Color.RED.darker(), Color.PINK, Color.PINK.brighter());

每一个方法都允许我们指定斜面类型以及边框内的明亮与阴影颜色。斜面类型是通过下面的值来指定的:SoftBevelBorder.RAISED或是SoftBevelBorder.LOWEERED。与BevelBorder类似,默认的颜色是由背景色得来的。一个SoftBevelBorder并不完全适应所指定的insets区域,所以SoftBevelBorder通常创建为透明的。

并没有静态的BorderFactory方法来创建这种边框。

EtchedBorder Class

EtchedBorder是BevelBorder的一种特殊情况,但是并不其子类。当BevelBorder的外层明亮颜色与内层阴影颜色相同,并且外层阴影颜色与内层明亮颜色相同,则我们就得到了EtchedBorder。图7-8显示了一个升起来落下的EtchedBorder的样子。

swing_7_8.png

swing_7_8.png

EtchedBorder有四个构造函数,同时有四个用于创建EtchedBorder对象的BorderFactory工厂方法:

public EtchedBorder()
Border etchedBorder = new EtchedBorder();

public EtchedBorder(int etchType)
Border etchedBorder = new EtchedBorder(EtchedBorder.RAISED);

public EtchedBorder(Color highlight, Color shadow)
Border etchedBorder = new EtchedBorder(Color.RED, Color.PINK);

public EtchedBorder(int etchType, Color highlight, Color shadow)
Border etchedBorder = new EtchedBorder(EtchedBorder.RAISED, Color.RED,
  Color.PINK);

public static Border createEtchedBorder()
Border etchedBorder = BorderFactory.createEtchedBorder();

public static Border createEtchedBorder(Color highlight, Color shadow)
Border etchedBorder = BorderFactory.createEtchedBorder(Color.RED, Color.PINK);

public static Border createEtchedBorder(EtchedBorder.RAISED)
Border etchedBorder = BorderFactory.createEtchedBorder(Color.RED, Color.PINK);

public static Border createEtchedBorder(int type, Color highlight, Color shadow)
Border etchedBorder = BorderFactory.createEtchedBorder(EtchedBorder.RAISED,
  Color.RED, Color.PINK);

每一个方法都允许我们指定Etch类型以及边框中的明亮与阴影颜色。如果没有指定Etch类型,则边框是落下的。与BevelBorder与SoftBevelBorder类似,我们可以通过两个常量来指定Etch类型:EtchedBorder.RAISED或是EtchedBorder.LOWERED。如果没有指定颜色,则由传递给paintBorder()的组件的背景颜色得到合适的颜色。默认情况下,所有的EtchedBorder对象是不透明的。

MatteBorder Class

MatteBorder是最通用的边框之一。他有两种形式。第一种形式如图7-9所示,在图中显示了一个MatteBorder,以与LineBorder类似的方式使用特定的颜色填充边框,但是在每一边有不同的粗细(有时一个普通的LineBorder并不能处理)。

Swing_7_9.png

Swing_7_9.png

第二种形式在边框区域内使用一个Icon进行连接。如果我们由一个Image对象创建,则这个Icon可以是一个ImageIcon,或者是我们通过实现Icon接口自己创建。图7-10显示了两种实现。

Swing_7_10.png

Swing_7_10.png

有七个构造函数以及两个BorderFactory工厂方法可以用来创建MatteBorder对象:

public MatteBorder(int top, int left, int bottom, int right, Color color)
Border matteBorder = new MatteBorder(5, 10, 5, 10, Color.GREEN);

public MatteBorder(int top, int left, int bottom, int right, Icon icon)
Icon diamondIcon = new DiamondIcon(Color.RED);
Border matteBorder = new MatteBorder(5, 10, 5, 10, diamondIcon);

public MatteBorder(Icon icon)
Icon diamondIcon = new DiamondIcon(Color.RED);
Border matteBorder = new MatteBorder(diamondIcon);

public MatteBorder(Insets insets, Color color)
Insets insets = new Insets(5, 10, 5, 10);
Border matteBorder = new MatteBorder(insets,  Color.RED);

public MatteBorder(Insets insets, Icon icon)
Insets insets = new Insets(5, 10, 5, 10);
Icon diamondIcon = new DiamondIcon(Color.RED);
Border matteBorder = new MatteBorder(insets, diamondIcon);

public static MatteBorder createMatteBorder(int top, int left, int bottom,
  int right, Color color)
Border matteBorder = BorderFactory.createMatteBorder(5, 10, 5, 10, Color.GREEN);

public static MatteBorder createMatteBorder(int top, int left, int bottom,
  int right, Icon icon)
Icon diamondIcon = new DiamondIcon(Color.RED);
Border matteBorder = BorderFactory.createMatteBorder(5, 10, 5, 10, diamondIcon);

每一个方法都允许我们自定义在边框区域内映射的内容。当连接Icon时,如果我们没有指定边框insets的尺寸,则使用实现的图标维度。

CompoundBorder Class

在EmptyBorder之后,CompundBorder也许是使用最简单的预定义边框之一了。他使用两个已存在的边框,使用组合设计模式将其组合在一个边框中。一个Swing组件只有一个与其相关联的边框,所以,CompundBorder允许我们在将边框关联到一个组件之前组合边框。图7-11显示了应用CompundBorder的两个例子。左边的边框是斜面线边框。右边是一个六个线边框,多个边框组合在一起。

Swing_7_11.png

Swing_7_11.png

创建CompundBorder

ComoundBorder有两个构造函数,以及用于创建CompondBorder对象的两个BorderFactory工厂方法(在这里无参数构造函数以及工厂方法是完全没用的,因为并没有setter方法用于稍后修改组合边框,所以在这里并没有显示示例源码):

public CompoundBorder()
public static CompoundBorder createCompoundBorder()
public CompoundBorder(Border outside, Border inside)
Border compoundBorder = new CompoundBorder(lineBorder, matteBorder);
public static CompoundBorder createCompoundBorder(Border outside, Border inside)
Border compoundBorder = BorderFactory.createCompoundBorder(lineBorder,
  matteBorder);

组合边框的透明性依赖于所包含的边框的透明性。如果所包含的两个边框都是不透明的,那么组合边框也是不透明的。否则,组合边框就是透明的。

配置属性

除了由AbstractBorder继承的borderOpaque属性以外,表7-2列出了CompoundBorder添加的两个只读属性。

属性名 数据类型 访问性
borderOpaque boolean 只读
insideBorder Border 只读
outsideBorder Border 只读

Table: CompoundBorder属性

TitledBorder Class

TitledBorder也许是最有趣,用起来最复杂的边框。TitledBorder允许我们在组件周围放置一个文本字符串。除了可以包围单一的组件,我们在还可以在一个组件组周围放置一个TitledBorder,例如JRadioButton对象,只要他们位于一个容器内,例如JPanel。TitledBorder的使用比较困难,但是有许多方法可以简化其使用。图7-12显示了一个简单的TitledBorder以及一个略为复杂一些的TitledBorder。

Swing_7_12.png

Swing_7_12.png

创建TitledBorder

有六个构造函数以及六个BorderFactory工厂方法可以用来创建TitledBorder对象。每一个方法都允许我们自定义文本,位置,以及在一个特定的边框内的标题外观。当没有特别指定时,当前的观感控制边框,标题颜色,以及标题字体。默认的标题位置位于左上角,而默认的标题是空字符串。标题边框至少总是部分透明的,因为标题下部的区域是可以看过的。所以,isBorderOpaque()报告false。

如果我们查看下面的方法,则这些方法会很容易理解。首先显示的是构造方法;然后显示的是等同的BorderFactory方法。

public TitledBorder(Border border)
Border titledBorder = new TitledBorder(lineBorder);

public static TitledBorder createTitledBorder(Border border)
Border titledBorder = BorderFactory.createTitledBorder(lineBorder);

public TitledBorder(String title)
Border titledBorder = new TitledBorder("Hello");

public static TitledBorder createTitledBorder(String title)
Border titledBorder = BorderFactory.createTitledBorder("Hello");

public TitledBorder(Border border, String title)
Border titledBorder = new TitledBorder(lineBorder, "Hello");

public static TitledBorder createTitledBorder(Border border, String title)
Border titledBorder = BorderFactory.createTitledBorder(lineBorder, "Hello");

public TitledBorder(Border border, String title, int justification, int position)
Border titledBorder = new TitledBorder(lineBorder, "Hello", TitledBorder.LEFT,
  TitledBorder.BELOW_BOTTOM);

public static TitledBorder createTitledBorder(Border border, String title,
  int justification, int position)
Border titledBorder = BorderFactory.createTitledBorder(lineBorder, "Hello",
  TitledBorder.LEFT, TitledBorder.BELOW_BOTTOM);

public TitledBorder(Border border, String title, int justification, int position,
  Font font)
Font font = new Font("Serif", Font.ITALIC, 12);
Border titledBorder = new TitledBorder(lineBorder, "Hello", TitledBorder.LEFT,
  TitledBorder.BELOW_BOTTOM, font);

public static TitledBorder createTitledBorder(Border border, String title,
  int justification, int position, Font font)
Font font = new Font("Serif", Font.ITALIC, 12);
Border titledBorder = BorderFactory.createTitledBorder(lineBorder, "Hello",
  TitledBorder.LEFT, TitledBorder.BELOW_BOTTOM, font);

public TitledBorder(Border border, String title, int justification, int position,
  Font font, Color color)
Font font = new Font("Serif", Font.ITALIC, 12);
Border titledBorder = new TitledBorder(lineBorder, "Hello", TitledBorder.LEFT,
  TitledBorder.BELOW_BOTTOM, font, Color.RED);

public static TitledBorder createTitledBorder(Border border, String title,
  int justification, int position, Font font, Color color)
Font font = new Font("Serif", Font.ITALIC, 12);
Border titledBorder = BorderFactory.createTitledBorder(lineBorder, "Hello",
  TitledBorder.LEFT, TitledBorder.BELOW_BOTTOM, font, Color.RED);

配置属性

与其他的预定义边框不同,标题边框有六个setter方法在边框创建之后修改其属性。如表7-3所示,我们可以修改一个标题的底部边框,标题,绘制颜色,字体,文本适应,以及文本位置。

属性名 数据类型 访问性
border Border 读写
borderOpaque boolean 只读
title String 读写
titleColor Color 读写
titleFont Font 读写
titleJustification int 读写
titlePosition int 读写

Table: TitledBorder属性

TitledBorder中的标题字符串的文本适应是通过四个类常量来指定的:

  • CENTER:标题放在中间。
  • DEFAULT_JUSTIFICATION:使用默认设置来放置文本。该值等同于LEFT。
  • LEFT:将标题放在左边。
  • RIGHT:将标题放在右边。

图7-13显示了具有不同文本适应的相同的TitledBorder。

Swing_7_13.png

Swing_7_13.png

我们可以将标题字符串放在由下面的七个类常量指定的六个不同的位置:

  • ABOVE_BOTTOM:将标题放在底线上部。
  • ABOVE_TOP:将标题放在顶线上部。
  • BELOW_BOTTOM:将标题放在底线下部。
  • BELOW_TOP:将标题放在顶线下部。
  • BOTTOM:将标题放在底线上。
  • DEFAULT_POSITION:使用默认设置放置文本。这个值等同于TOP。
  • TOP:将标题放在顶线上。

图7-14显示了TitledBorder的标题可以放置的六个不同位置。

Swing_7_14.png

Swing_7_14.png

因为TitledBorder包含另一个Border,我们可以组合多个边框来在一个边框中放置多个标题。例如,图7-15显示了在边框的顶部与底部显示标题。

Swing_7_15.png

Swing_7_15.png

用来生成图7-15的程序源码显示在列表7-2中。

package swingstudy.ch07;

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

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.border.TitledBorder;

public class DoubleTitle {

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

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

                TitledBorder topBorder = BorderFactory.createTitledBorder("Top");
                topBorder.setTitlePosition(TitledBorder.TOP);

                TitledBorder doubleBorder = new TitledBorder(topBorder, "Bottom", TitledBorder.RIGHT, TitledBorder.BOTTOM);

                JButton doubleButton = new JButton();
                doubleButton.setBorder(doubleBorder);

                frame.add(doubleButton, BorderLayout.CENTER);
                frame.setSize(300, 100);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

自定义TitledBorder观感

表7-4显示了TitledBorder的UIResource相关属性的集合。他有三个不同的属性。

属性字符串 对象类型
TitledBorder.font font
TitledBorder.titleColor Color
TitledBorder.border Border

Table: TitledBorder UIResource元素

Creating Your Own Borders

当我们想要创建我们自己的独特的边框时,我们或者可以直接实现Border接口创建新类,或者是我们可以扩展AbstractBorder类。正如前面所提到的,扩展AbstractBorder是更好的方法,因为其中进行优化,从而特定的Swing类可以千年虫AbstractBorder的特定方法。例如,如果一个边框是一个AbstractBorder,当获取边框的Insets时,JComponent可以重用Insets对象。所以,当获取insets时只有少量的对象需要创建与销毁。

除了考虑继承AbstractBorder与自己实现Border接口以外,我们需要考虑我们是否需要一个静态边框。如果我们将一个边框关联到一个按钮,我们希望这个按钮能够进行信息选择。我们必须检测传递给paintBorder()方法的组件,并进行相应的响应。另外,我们应该在组件不可以选择时绘制一个禁止的边框。尽管setEnabled(false)可以禁止组件的选择,如果组件有一个与其相关联的边框,边框仍然进行绘制,尽管他已经被禁止。图7-6实际显示了一个考虑了传递给边框的paintBorder()方法的组件选项的边框。

Swing_7_16.png

Swing_7_16.png

列表7-3显示了自定义边框与示例程序的源码。

package swingstudy.ch07;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Insets;

import javax.swing.AbstractButton;
import javax.swing.ButtonModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.border.AbstractBorder;
import javax.swing.border.Border;

public class RedGreenBorder extends AbstractBorder {

    public boolean isBorderOpaque() {
        return true;
    }

    public Insets getBorderInsets(Component c) {
        return new Insets(3, 3, 3, 3);
    }

    public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
        Insets insets = getBorderInsets(c);
        Color horizontalColor;
        Color verticalColor;
        if(c.isEnabled()) {
            boolean pressed = false;
            if(c instanceof AbstractButton) {
                ButtonModel model = ((AbstractButton)c).getModel();
                pressed = model.isPressed();
            }
            if(pressed) {
                horizontalColor = Color.RED;
                verticalColor = Color.GREEN;
            }
            else {
                horizontalColor = Color.GREEN;
                verticalColor = Color.RED;
            }
        }
        else {
            horizontalColor = Color.LIGHT_GRAY;
            verticalColor = Color.LIGHT_GRAY;
        }
        g.setColor(horizontalColor);

        g.translate(x, y);

        // Top
        g.fillRect(0, 0, width, insets.top);

        // Bottom
        g.fillRect(0, height-insets.bottom, width, insets.bottom);

        g.setColor(verticalColor);

        // Left
        g.fillRect(0, insets.top, insets.left, height-insets.top-insets.bottom);

        // Right
        g.fillRect(width-insets.right, insets.top, insets.right, height-insets.top-insets.bottom);

        g.translate(-x, -y);
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

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

                Border border = new RedGreenBorder();

                JButton helloButton = new JButton("Hello");
                helloButton.setBorder(border);

                JButton braveButton = new JButton("Brave New");
                braveButton.setBorder(border);
                braveButton.setEnabled(false);

                JButton wordButton =  new JButton("World");
                wordButton.setBorder(border);

                frame.add(helloButton, BorderLayout.NORTH);
                frame.add(braveButton, BorderLayout.CENTER);
                frame.add(wordButton, BorderLayout.SOUTH);

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

        EventQueue.invokeLater(runner);
    }

}

Summary

在本章中,我们了解了Border接口及其预定义实现的使用。同时我们了解了如何使用由BorderFactory类所提供的工厂设计模式来创建预定义的边框。最后,我们了解了如何定义我们自己的边框以及为什么继承AbstractBorder是有益的。

在第8章中,我们将会了解底层的组件,并且检测Swing中可用的类似于窗口的容器。

Root Pane Containers

在第7章中,我们了解使用Swing组件周围的边框。在本章中,我们将会探讨高层Swing容器,并且将会发现与相对应的AWT容器的不同。

使用Swing中的高层容器与使用高层AWT容器不同。对于AWT容器,Frame,Window,Dialog以及Applet,我们可以将组件直接添加到容器,并且我们只有一个位置来放置这些组件。在Swing世界中,高层容器,JFrame,JWindow,JDialog以及JApplet,加上JInternalFrame容器,依赖JRootPane。我们并不能将组件直接添加到容器,而只能将这些组件添加到root pane(根面板)的一部分。然后由根面板来管理这些组件。

为什么添加这个间接层呢?无论我们是否相信,这样做是为了事情的简化。根面板在层中管理其组件,从而如工具提示文本这样的元素总是显示在组件上面,而且我们不必担心拖拽某个组件在其他组件周围运动。

JInternalFrame并没有相对应的AWT组件,他也提供了一些额外的功能用于处理被放置在桌面(在JDesktopPane中)中的情况。JInternalFrame可以用作在Swing程序创建多文档界面(MDI)程序的基础。在我们的程序中我们可以管理一系列的内部框架,并且他们绝不会超出我们的主程序容器。

下面我们开始探讨新的JRootPane类,他管理所有的高层容器。

JRootPane类

JRootPane担当高层Swing容器的容器代理。因为容器只存放一个JRootPane,当我们由高层容器中添加或是移除组件时,我们并没有直接修改容器中的组件,而是间接的由JRootPane实例添加或是移除组件。事实上,高层容器担当代理的角色,由JRootPane完成所有的工作。

JRootPane容器依赖其内联类RootLayout进行布局管理,并且管理存储JRootPane的高层容器的所有空间。在JRootPane中只有两个组件:一个JLayeredPane以及一个玻璃嵌板(Component)。前面的玻璃嵌板可以是任意组件,而且是不可见的。玻璃嵌板保证类似工具提示文本这样的元素显示在其他的Swing之前。后面是JLayeredPane,在其上部包含一个可的选的JMenuBar,在其下面的另一层中包含一个内容面析(Container)。通常我们将组件放在JRootPane中就是放置在内容面板中。图8-1有助于我们理解RootLayout是如何布局组件的。

Swing_8_1.png

Swing_8_1.png

注意:JLayerPane也仅是一个Swing容器。他可以包含任意的组件并且具有一些特定的布局特性。JRootPane面板中所用的JlayeredPane只包含一个JMenuBar以及一个Container作为其内容面板。内容面板有其自己的布局管理器,默认情况下为BorderLayout。

创建JRootPane

尽管JRootPane具有一个公开的无参数的构造函数,但是通常我们并不会亲自创建JRootPane。相反,实现了RootPaneContainer接口的类创建JRootPane。然后,我们由该组件通过RootPaneContainer接口来获取根面板,我们会在稍后进行描述。

JRootPane属性

如表8-1所示,JRootPane有11个属性。大多数情况下,当我们为高层容器获取或是设置一个这样的属性时,例如JFrame,容器只是简单的将请求传递给其JRootPane。

JRootPane的玻璃嵌板必须是透明的。因为玻璃嵌板会占据JLayeredPane前面的整个区域,一个不透明的玻璃嵌板会将其菜单栏与内容面板渲染为不可见。而且,因为玻璃嵌板与内容面板共享相同的边界,当设置optimizedDrawingEnabled属性时会返回玻璃嵌板的可见性。

属性名 数据类型 访问性
accessibleContext AccessibleContext 只读
contentPane Container 读写
defaultButton JButton | 读写绑定
glassPane Component 读写
jMenuBar JMenuBar 读写
layeredPane JLayeredPane 读写
optimizedDrawingEnabled boolean 只读
UI RootPaneUI 读写
UIClassID String 只读
validateRoot boolean 只读
windowDecorationStyle int 读写绑定

Table: Table 8-1. JRootPane属性

windowDecorationStyle属性用来描述包含JRootPane窗口的窗口装饰(边框,标题,关闭窗口的按钮)。他可以设置为下列的JRootPane类常量:

  • COLOR_CHOOSER_DIALOG
  • ERROR_DIALOG
  • FILE_CHOOSER_DIALOG
  • FRAME
  • INFORMATION_DIALOG
  • NONE
  • PLAIN_DIALOG
  • QUESTION_DIALOG
  • WARNING_DIALOG

使用windowDecorationStyle设置后的实际效果要依据于当前的观感。这只是一个小提示。默认情况,这个设置为NONE。如果这个设置不为NONE,使用true值来调用JDialog或JFrame的setUndecorated()方法,并且当前观感的getSupportsWindowDecorations()方法报告true,那么则由观感,而不是窗口管理器,来提供窗口装饰。这可以使得使用高层窗口的程序看起来并不是来自于用户所用的工作平台,而是来自于我们自己的一半,但是仍然可以提供通知,最大化,最小化以及关闭按钮。

对于Metal观感(以及Ocean主题),getSupportsWindowDecorations()报告true。其他系统提供的观感类型报告false。图8-2演示了由Metal观感所提供的带有窗口装饰的框架样子。

Swing_8_2.png

Swing_8_2.png

生成图8-2的程序源码显示在列表8-1中。

package swingstudy.ch08;

import java.awt.EventQueue;

import javax.swing.JFrame;
import javax.swing.JRootPane;

public class AdornSample {

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

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Adornment Example");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setUndecorated(true);
                frame.getRootPane().setWindowDecorationStyle(JRootPane.FRAME);
                frame.setSize(300, 100);
                frame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

自定义JRootPane观感

表8-2显示了JRootPane的12UIResource相关的属性。这些中的大多数属性与配置窗体装饰风格时所用的默认边框有关。

属性字符串 对象类型
RootPane.actionMap ActionMap
RootPane.ancestroInputMap InputMap
RootPane.colorChooserDialogBorder Border
RootPane.defaultButtonWindowKeyBindings Object[]
RootPane.errorDialogBorder Border
RootPane.fileChooserDialogBorder Border
RootPane.frameBorder Border
RootPane.informationDialogBorder Border
RootPane.plainDialogBorder Border
RootPane.questionDialogBorder Border
RootPane.warningDialogBorder Border
RootPnaeUI String

Table: JRootPane UIResource元素

RootPaneContainer接口

RootPaneContainer接口定义了用于访问JRootPane中的各种面板以及访问JRootPane本身的setter/getter方法。

public interface RootPaneContainer {
  // Properties
  public Container getContentPane();
  public void setContentPane(Container contentPane);
  public Component getGlassPane();
  public void setGlassPane(Component glassPane);
  public JLayeredPane getLayeredPane();
  public void setLayeredPane(JLayeredPane layeredPane);
  public JRootPane getRootPane();
}

在预定义的Swing组件之中,JFrame, JWindow, JDialog,JApplet以及JInternalFrame类实现了RootPaneContainer接口。对于大部分来说,这些实现简单的将请求传递给高层容器的JRootPane实现。下面的代码是RootPaneContainer的玻璃嵌板实现:

public Component getGlassPane() {
  return getRootPane().getGlassPane();
}
public void setGlassPane(Component glassPane) {
  getRootPane().setGlassPane(glassPane);
}

JLayeredPane类

JLayeredPane是JRootPane的主要组件容器。JLayeredPane管理其内部的组件的Z顺序或层。这可以保证在某些任务的情况下,例如创建工具提示文本,弹出菜单与拖拽,正确的组件可以创建在其他的组件之上。我们可以使用系统定义的层次,或者是我们可以创建自己的层次。

尽管JLayeredPane容器并没有布局管理器,但是并没有什么可以阻止我们设置容器的layout属性。

创建JLayeredPane

与JRootPane类似,我们从不亲自创建JLayeredPane类的实例。当为实现了RootPaneContainer的预定义类创建一个默认的JRootPane时,JRootPane为其主要的组件区域创建一个JLayeredPane,并添加一个初始化的内容面板。

在层中添加组件

每一个所添加的组件的层设置管理JLayeredPane中组件的Z顺序。层设置越高,则组件绘制离顶层组件就越近。当我们向JLayeredPane中添加组件时我们可以使用布局管理的限制来设置层。

Integer layer = new Integer(20);
aLayeredPane.add(aComponent, layer);

我们也可以在向JLayeredPane添加组件之前调用public void setLayer(Component comp, int layer)或public void setLayer(Component comp, int layer, int position)方法。

aLayeredPane.setLayer(aComponent, 10);
aLayeredPane.add(aComponent);

JLayeredPane类预定义了六个特殊值常量。另外,我们还可以使用public int currentLayer()方法来获得最顶部的当前层,使用public int lowestLayer()方法获得最底层。表8-3列出六个预定义的层常量。

常量 描述
FRAME_CONTEND_LAYER 层-30000用于存储菜单栏以及内容面板;通常并不为开发者所用。
DEFAULT_LAYER 零层用于通常的组件层。
PALETTE_LAYER 层100用于存储浮动工具栏以及类似的组件
MODAL_LAYER 层200用于存储显示在默认层,调色板之上以及弹出菜单之下的弹出对话框
POPUP_LAYER 层300用于存储弹出菜单以及工具提示文本
DRAG_LAYER 层400用于存储保持在顶部的拖动对象

Table: JLayeredPane层常量

尽管我们可以为层次使用自己的常量,但是使用时要小心,因为系统会在需要时使用预定义的常量。如果我们的常量不正确,组件就不会如我们希望的那样工作。

图8-3可视化的显示了不同层是如何放置的。

Swing_8_3.png

Swing_8_3.png

使用内容层与位置

JLayeredPane中的组件同时具有层与位置。当某一层只有一个组件时,其位于位置零。当在相同的层有多个组件时,后添加的组件具有更高的位置数字。位置设置越低,显示距离顶部组件越近。(这与层的行为相反。)图8-4显示在相同层上四个组件的位置。

要重新安排一层上的组件,我们可以使用public void moveToBack(Component component)或是public void moveToFront(Component component)方法。当我们将一个组件移到前面时,他到达该层的位置0。当我们一个组件移动到后时,他到达该层的最大位置处。我们也可以使用public void setPosition(Component component, int position)方法来手动设置位置。位置-1自动为具有最高位置的底层(如图8-4)。

Swing_8_4.png

Swing_8_4.png

JLayeredPane属性

表8-4显示了JLayeredPane的两个属性。optimizedDrawingEnabled属性决定了JlayeredPane中的组件是否可以重叠。默认情况下,这个设置为true,在JRootPane的标准用法中,JMenuBar与内容面板不可以重叠。然而,JLayeredPane自动验证属性设置来反映面板内容的当前状态。

属性名 数据类型 访问性
accessibleContext AccessibleContext 只读
optimizedDrawingEnabled boolean 只读

Table: JLayeredPane属性

JFrame类

JFrame类是使用JRootPane并且实现了RootPaneContainer接口的Swing高层容器。另外,他使用WindowConstants接口来帮助管理相关操作。

创建JFrame

JFrame类提供了两个基本构造函数:一个用于不带标题的框架,而另一个用来创建带标题的框架。还有另外两个构造函数使用特定的GraphicsConfiguration来创建框架。

public JFrame()
JFrame frame = new JFrame();

public JFrame(String title)
JFrame frame = new JFrame("Title Bar");

public JFrame(GraphicsConfiguration config)
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice gsd[] = ge.getScreenDevices();
GraphicsConfiguration gc[] = gsd[0].getConfigurations();
JFrame frame = new JFrame(gc[0]);

public JFrame(String title, GraphicsConfiguration config)
GraphicsConfiguration gc = ...;
JFrame frame = new JFrame("Title Bar", gc);

JFrame属性

表8-5显示了JFrame的九个属性。

属性名 数据类型 访问性
accessibleContext AccessibleContext 只读
contentPane Container 读写
defaultCloseOperation int 读写
glassPane Component 读写
iconImage Image 只写
jMenuBar JMenuBar 读写
layeredPane JLayeredPane 读写
layout LayoutManager 只写
rootPane JRootPane 只读

Table: JFrame属性

尽管大多数的属性都是实现RootPaneContainer接口的结果,但是有两个特殊的属性:defaultCloseOperation以及layout。(我们首先在第2章看到了defaultCloseOperation。)默认情况下,当用户关闭容器时,JFrame会隐藏自己。要修改这种设置,当设置默认关闭行为时我们可以表8-6中所列表出的常量来作为参数。第一个直接来自于JFrame;其他的则是WindowConstants接口的一部分。

aFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
常量 描述
EXIT_ON_CLOSE 调用System.exit(0)
DISPOSE_ON_CLOSE 在窗体上调用dispose()
ON_NOTHING_ON_CLOSE 忽略请求
HIDE_ON_CLOSE 在窗体上调用setVisible(false);这是默认行为

Table: 关闭操作常量

layout属性是比较奇特的。默认情况下,设置JFrame的布局管理器会将调用传递给内容面板。我们不可以修改JFrame的默认布局管理器。

JFrame还有另外一个静态属性:defaultLookAndFeelDecorated。这个属性与JRootPane的windowDecorationStyle属性结合使用。当设置为true时,新创建的窗体会使用观感中的装饰而不是窗口管理中的装饰进行装饰。当然,只有当前的观感支持窗口装饰时才会发生这种情况。列表8-2显示了另一种创建与图8-2相同的屏幕的方法(通过使用Metal观感所提供的窗口装饰)。

package swingstudy.ch08;

import java.awt.EventQueue;

import javax.swing.JFrame;

public class AdornSample2 {

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

        Runnable runner = new Runnable() {
            public void run() {
                JFrame.setDefaultLookAndFeelDecorated(true);
                JFrame frame = new JFrame("Adornment Sample");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setSize(300,200);
                frame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

向JFrame添加组件

因为JFrame实现了RootPaneContainer接口并且使用JRootPane,我们不能直接向JFrame添加组件。相反,我们将组件添加到JFrame所包含的JRootPane。在J2SE 5.0之前,我们需要使用下面的方法来添加组件:

JRootPane rootPane = aJFrame.getRootPane();
Container contentPane = rootPane.getContentPane();
contentPane.add(...);

这可以用下面的语句进行简化:

aJFrame.getContentPane().add(...);
</syntaxhighlihgt>

如果我们尝试直接向JFrame添加组件,则会抛出运行时错误。

由于许多建议(或是抱怨?),Sun最终决定将add()方法修改为代理:

<syntaxhighlight lang="java">
// J2SE 5.0
aJFrame.add(...);

使用J2SE 5.0时,当我们向JFrame添加组件时,他们实际上被添加到了RootPaneContainer的内容面板。

处理JFrame事件

JFrame类支持11种不同的监听器的注册:

  • ComponentListener:确定窗体何时移动或修改尺寸
  • ContainerListener:通常并不添加到JFrame,因为我们将组件添加到其JRootPane的内容面板。
  • FocusListener:确定窗体何时获得或是失去输入焦点。
  • HierarchyBoundsListener:确定窗体何时移动或是修改尺寸。其作用与ComponentListener类似,因为窗体是组件的顶层容器。
  • HierarchyListener:确定窗体何时显示或隐藏。
  • InputMethodListener:用于国际化时与输入法结合使用。
  • KeyListener:通常并不添加到JFrame。相反,我们为其内容面板注册一个键盘动作,如下所示:
JPanel content = (JPanel)frame.getContentPane();
KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
content.registerKeyboardAction(actionListener, stroke,
  JComponent.WHEN_IN_FOCUSED_WINDOW);
  • MouseListener与MouseMotionListener:用于监听鼠标以及鼠标动作事件。
  • PropertyChangeListener:用来监听绑定属性的改变。
  • WindowListener:来确定窗口何时被图标化或是取消图标化或是用户正在尝试打开或关闭窗口。

通过defaultCloseOperation属性,我们通常并不需要添加WindowListener来帮助处理关闭窗体或是停止程序。

扩展JFrame

如果我们需要扩展JFrame,这个类有两个重要的protected方法:

protected void frameInit()
protected JRootPane createRootPane()
</syntaxhighlihgt>

通过在子类中重写这些方法,我们可以自定义初始外观以及窗体或是其JRootPane的行为。例如,在列表8-3中所示的ExitableJFrame类的例子中,默认的关闭操作被初始化EXIT_ON_CLOSE状态。无需要为每一个创建的窗体调用setDefaultCloseOperation()方法,我们可以使用这个类进行替换。因为JFrame被继承,我们并不需要在其构造函数中添加frameInit()方法的调用。其父类自动调用这个方法。

<syntaxhighlight lang="java">

package swingstudy.ch08;

import javax.swing.JFrame;

public class ExitableFrame extends JFrame {

    public ExitableFrame() {

    }

    public ExitableFrame(String title) {
        super(title);
    }

    protected void frameInit() {
        super.frameInit();
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }
}

注意:如果我们重写JFrame的frameInit()方法,记住首先调用super.frameInit()来初始化其默认行为。如果我们忘记了并没有自己重新实现所有的默认行为,我们的新窗体的外观与行为就会不同。

JWindow类

JWindow类与JFrame类类似。他使用JRootPane用于组件管理并且实现了RootPaneContainer接口。他是一个无装饰的顶层窗口。

创建JWindow

JWindow类有五个构造函数:

public JWindow()
JWindow window = new JWindow();
public JWindow(Frame owner)
JWindow window = new JWindow(aFrame);
public JWindow(GraphicsConfiguration config)
GraphicsConfiguration gc = ...;
JWindow window = new JWindow(gc);
public JWindow(Window owner)
JWindow window = new JWindow(anotherWindow);
public JWindow(Window owner, GraphicsConfiguration config)
GraphicsConfiguration gc = ...;
JWindow window = new JWindow(anotherWindow, gc);

我们可以不指定父类或是将父类指定为Frame或Window。如果没有指定父类,则会一个不可见的。

JWindow属性

表8-7列出了JWindow的六个属性。这些属性与JFrame属性类似,所不同的是JWindow没有用于默认关闭操作或是菜单栏的属性。

属性名 数据类型 访问性
accessibleContext AccessibleContext 只读
contentPane Container 读写
glassPane Component 读写
layeredPane JLayeredPane 读写
layout LayoutManager 只写
rootPane JRootPane 只读

Table: JWindow属性

处理JWindow事件

JWindow类在JFrame以及Window类之外并没有添加额外的事件处理功能。查看本章前面的“处理JFrame事件”一节可以了解我们可以关联到JWidnow的监听器列表。

扩展JWindow

如果我们需要扩展JWindow,这个类具有两个重要的protected方法:

protected void windowInit()
protected JRootPane createRootPane()

JDialog类

JDialog类表示用于显示与Frame相关信息的标准弹出窗口。其作用类似于JFrame,其JRootPane包含一个内容面板以及一个可选的JMenuBar,而且他实现了RootPaneContainer与WidnowConstants接口。

创建JDialog

有11个构造函数可以用来创建JDialog窗口:

public JDialog()
JDialog dialog = new JDialog();

public JDialog(Dialog owner)
JDialog dialog = new JDialog(anotherDialog);

public JDialog(Dialog owner, boolean modal)
JDialog dialog = new JDialog(anotherDialog, true);

public JDialog(Dialog owner, String title)
JDialog dialog = new JDialog(anotherDialog, "Hello");

public JDialog(Dialog owner, String title, boolean modal)
JDialog dialog = new JDialog(anotherDialog, "Hello", true);

public JDialog(Dialog owner, String title, boolean modal, GraphicsConfiguration gc)
GraphicsConfiguration gc = ...;
JDialog dialog = new JDialog(anotherDialog, "Hello", true, gc);

public JDialog(Frame owner)
JDialog dialog = new JDialog(aFrame);

public JDialog(Frame owner, String windowTitle)
JDialog dialog = new JDialog(aFrame, "Hello");

public JDialog(Frame owner, boolean modal)
JDialog dialog = new JDialog(aFrame, false);

public JDialog(Frame owner, String title, boolean modal)
JDialog dialog = new JDialog(aFrame, "Hello", true);

public JDialog(Frame owner, String title, boolean modal, GraphicsConfiguration gc)
GraphicsConfiguration gc = ...;
JDialog dialog = new JDialog(aFrame, "Hello", true, gc);

注意,我们并不需要手动创建JDialog并进行装配,我们将会发现JOptionPane可以为我们自动创建并填充JDialog。我们将会在第9间探讨JOptionPane组件。

每一个构造函数都允许我们自定义对象拥有者,窗口标题以及弹出模式。当JDialog为模态时,他会阻止到其拥有者及程序其余部分的输入。当JDialog为非模态时,他会允许用户与JDialog以及程序的其余部分进行交互。

小心,为了使得对话框模式在不同的Java版本之间正常工作,我们要避免在JDialog中混合使用重量级的AWT组件以及轻量级的Swing组件。

JDialog属性

除了可以设置的图标,JDialog类具有与JFrame类相同的属性。表8-8中列出了这些八个属性。

属性名 数据类型 访问性
accessibleContext AccessibleContext 只读
contentPane Container 读写
defaultCloseOperation int 读写
glassPane Component 读写
jMenuBar JMenuBar 读写
layeredPane JLayeredPane 读写
layout LayoutManager 只写
rootPane JRootPane 只读

Table: JDialog属性

用于指定所使用的默认关闭操作的常量是在前面的表8-6中所显示的WidnowConstants(除了EXIT_ON_CLOSE基本相同)。默认情况下,defaultCloseOperation属性设置为HIDE_ON_CLOSE,这是弹出对话框所要求的默认行为。

与JFrame类似,JDialog也有一个静态的defaultLookAndFeelDecorated属性。这可以控制默认情况下对话框是否由观感进行装饰。

处理JDialog事件

并没有需要我们特殊处理的JDialog事件;其事件处理与JFrame类相同。也许我们需要处理的一件与JDialog相关的事情就是指定当按下Escape按键时关闭对话框。处理这一事件的最简单的方法就是向对话框内的JRootPane里的键盘动作注册一个Escape按键,从而可以使得当按下Escape时JDialog变得不可见。列表8-4演示了这一行为。源码中的大部分重复了JDialog的构造函数。createRootPane()方法将Escape按键映射到自定义的Action。

package swingstudy.ch08;

import java.awt.Dialog;
import java.awt.Frame;
import java.awt.GraphicsConfiguration;
import java.awt.event.ActionEvent;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JRootPane;
import javax.swing.KeyStroke;

public class EscapeDialog extends JDialog {

    public EscapeDialog() {
        this((Frame)null, false);
    }

    public EscapeDialog(Frame owner) {
        this(owner, false);
    }

    public EscapeDialog(Frame owner, boolean modal) {
        this(owner, null, modal);
    }

    public EscapeDialog(Frame owner, String title) {
        this(owner, title, false);
    }

    public EscapeDialog(Frame owner, String title, boolean modal) {
        super(owner, title, modal);
    }

    public EscapeDialog(Frame owner, String title, boolean modal, GraphicsConfiguration gc) {
        super(owner, title, modal, gc);
    }

    public EscapeDialog(Dialog owner) {
        this(owner, false);
    }

    public EscapeDialog(Dialog owner, boolean modal) {
        this(owner, null, modal);
    }

    public EscapeDialog(Dialog owner, String title) {
        this(owner, title, false);
    }

    public EscapeDialog(Dialog owner, String title, boolean modal) {
        super(owner, title, modal);
    }

    public EscapeDialog(Dialog owner, String title, boolean modal, GraphicsConfiguration gc) {
        super(owner, title, modal, gc);
    }

    protected JRootPane createRootPane() {
        JRootPane rootPane = new JRootPane();
        KeyStroke stroke = KeyStroke.getKeyStroke("ESCAPE");
        Action actionListener = new AbstractAction() {
            public void actionPerformed(ActionEvent event) {
                setVisible(false);
            }
        };
        InputMap inputMap = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        inputMap.put(stroke, "ESCAPE");
        rootPane.getActionMap().put("ESCAPE", actionListener);

        return rootPane;
    }

}

注意,如果我们使用JOptionPane的静态创建方法,则其所创建的JDialog窗口会自动将Escape按键注册为关闭对话框。

扩展JDialog

如果我们需要扩展JDialog,该类具有两个重要的protected方法:

protected void dialogInit()
protected JRootPane createRootPane()

后面的方法在前面的列表8-4中进行了演示。

JApplet类

JApplet类是AWT Applet类的扩展。为了在使用Swing组件的applet中能正确的进行事件处理,我们applet必须继承JApplet,而不是Applet。

JApplet的作用与其他的实现了RootPaneContainer接口的高层窗口相同。JApplet与Applet之间一个重要的区别就是默认的布局管理器。因为我们向JApplet的内容面析添加组件,其默认的布局管理器为BorderLayout。这与Applet的默认布局管理器FlowLayout不同。另外,Swing applet还可以具有一个工具栏,或者更为特定的JMenuBar,这是applet的JRootPane的另一个属性。

如果我们计划部署一个使用Swing组件的applet,最好是使用Sun Microsystems所提供的Java插件,因为这会随运行时安装Swing库。

如查我们要扩展JApplet类,他只有一个重要的protected方法:

protected JRootPane createRootPane()

配合桌面使用

Spring提供了对一个通常窗口或是桌面内的窗体集合进行管理。正如我们在第1章所讨论的,这种管理通常被称之为MDI。窗体可以位于其他的窗体之上,或者是可以被拖动,而其外观适当当前的观感。这些窗体是JInternalFrame类的实例,而桌面是一个称之为JDesktopPane的特殊JLayeredPane。桌面内窗体的管理是DesktopManager的责任,其中所提供的默认实现是DefaultDesktopManager。桌面上的JInternalFrame的图标形式是通过JDesktopIcon的内联类JInternalFrame来表示的。同时有InternalFrmaeListener,InternalFrameAdapter以及InternalFrameEvent用于事件处理。

首先,我们来看一下构成桌面的部分,然后我们会看到使用这些部分的一个完整示例。

JInternalFrame类

JInternalFrame类与JFrame类类似。他是一个高层窗口,使用RootPaneContainer接口,但是并不是一个顶层窗口。我们必须将内部窗体放在另一个顶层窗口中。当我们拖动时,内部窗体会停留在其窗口的边界之内,这通常是一个JDesktopPane。另外,内部窗体是轻量级的,并且提供了一个UI委托从而使得内部窗体看起来类似当前配置的观感。

创建JInternalFrame

JInternalFrame有六个构造函数:

public JInternalFrame()
JInternalFrame frame = new JInternalFrame();

public JInternalFrame(String title)
JInternalFrame frame = new JInternalFrame("The Title");

public JInternalFrame(String title, boolean resizable)
JInternalFrame frame = new JInternalFrame("The Title", true);

public JInternalFrame(String title, boolean resizable, boolean closable)
JInternalFrame frame = new JInternalFrame("The Title", false, true);

public JInternalFrame(String title, boolean resizable, boolean
  closable, boolean maximizable)
JInternalFrame frame = new JInternalFrame("The Title", true, false, true);

public JInternalFrame(String title, boolean resizable, boolean
  closable, boolean maximizable, boolean iconifiable)
JInternalFrame frame = new JInternalFrame("The Title", false, true, false, true);

这些构造函数以一个向另一个添加参数的方式进行级联。无参数时,所创建的JInternalFrame没有标题,并且不能调整大小,关闭,最大化或是图标化。然而,内部窗体总是可以拖动的。

JInternalFrame属性

表8-9列出了JInternalFrame类的30个不同属性。layer属性列出了两次,因为他具有两个设置方法,其中一个用于int,而另一个用于Integer。

属性名 数据类型 访问性
accessibleContext AccessibleContext 只读
closable boolean 读写绑定
closed boolean 读写绑定
contentPane Container 读写绑定
defaultCloseOperation int 读写
desktopIcon JInternalFrame.JDesktopIcon 读写绑定
desktopPane JDesktopPane 只读
focusCycleRoot boolean 读写
focusCycleRootAncester Container 只读
focusOwner Component 只读
frameIcon Icon 读写绑定
glassPane Component 读写绑定
icon boolean 读写绑定
iconifiable boolean 读写
internalFrameListeners InternalFrameListener[] 只读
jMenuBar JMenuBar 读写绑定
layer int 读写
layer Integer 只写
layeredPane JLayeredPane 读写绑定
layout LayoutManager 只写
maximizable boolean 读写绑定
maximum boolean 读写
mostRecentFocusOwner Component 只读
normalBounds Rectangle 读写
resizable boolean 读写绑定
rootPane JRootPane 读写绑定
selected boolean 读写绑定
title String 读写绑定
UI InternalFrameUI 读写
UIClassID String 只读
warningString String 只读

Table: JInternalFrame属性

对于Java 1.3及以后的版本,JInternalFrame的初始defaultCloseOperation属性设置为DISPOSE_ON_CLOSE。以前版本的默认设置为HIDE_ON_CLOSE。我们可以将这个属性设置为前面的表8-6中列出的WindowConstants的值。

normalBounds属性描述了当一个图标化的内部窗体取消息图标化时应该在哪里显示。focusOwner属性在特定的JInternalFrame被激活时提供了一个实际带有输入焦点的Component。

在Swing类中,JInternalFrame只包含四个限制属性:closed, icon, maximum以及selected。他们与四个boolean构造函数参数直接相关。每一个都可以允许我们在改变其设置时检测当前的属性状态。然而,因为属性是受限制的,当我们要设置一个属性时,我们所做的尝试必须位于一个try-catch块中,捕捉PropertyVetoException:

try {
  // Try to iconify internal frame
  internalFrame.setIcon(false);
}  catch (PropertyVetoException propertyVetoException) {
  System.out.println("Rejected");
}

为了有助于我们使用这些绑定属性,JInternalFrame类定义了一个11个常量,如表8-10所示。他们表示在PropertyChangeListener中通过PropertyChangeEvent的getPropertyName()方法返回的字符串。

属性名常量 关联属性
CONTENT_PANE_PROPERTY contentPane
FRAME_ICON_PROPERTY frameIcon
GLASS_PANE_PROPERTY glassPane
IS_CLOSED_PROPERTY closed
IS_ICON_PROPERTY icon
IS_MAXIMUM_PROPERTY maximum
IS_SELECTED_PROPERTY selected
LAYERED_PANE_PROPERTY layeredPane
MENU_BAR_PROPERTY jMenuBar
ROOT_PANE_PROPERTY rootPane
TITLE_PROPERTY title

Table: JInternal属性常量

下面的类示例演示了在PropertyChangeListener中常量的使用。

package swingstudy.ch08;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.JInternalFrame;

public class InternalFramePropertyChangeHandler implements
        PropertyChangeListener {

    @Override
    public void propertyChange(PropertyChangeEvent event) {
        // TODO Auto-generated method stub

        String propertyName = event.getPropertyName();
        if(propertyName.equals(JInternalFrame.IS_ICON_PROPERTY)) {
            System.out.println("Icon property changed. React.");
        }
    }

}

处理JInternalFrame事件

为了帮助我们像使用JFrame一样来使用JInternalFrame,有一个额外的事件监听器来负责内部窗体的打开与关闭的相关事件。这个接口名为InternalFrameListener,其定义如下。其作用类似于AWT的WindowListener接口,但是所用的JInternalFrame类,而不是AWT的Window类。

  public interface InternalFrameListener extends EventListener {
  public void internalFrameActivated(InternalFrameEvent internalFrameEvent);
  public void internalFrameClosed(InternalFrameEvent internalFrameEvent);
  public void internalFrameClosing(InternalFrameEvent internalFrameEvent);
  public void internalFrameDeactivated(InternalFrameEvent internalFrameEvent);
  public void internalFrameDeiconified(InternalFrameEvent internalFrameEvent);
  public void internalFrameIconified(InternalFrameEvent internalFrameEvent);
  public void internalFrameOpened(InternalFrameEvent internalFrameEvent);
}

另外,与具有所有WindowListener方法桩的WindowApapter类类似,也有一个具有所有InternalFrameListener方法桩的InternalFrameAdapter类。如果我们并不是对JInternalFrame发生的所有事件感兴趣,我们可以继承InternalFrameAdapter类,并且只重写我们所感兴趣的方法。例如,列表8-5中所示的监听器只对图标化方法感兴趣。无需提供InternalFrameListener的其他五个方法的桩实现,我们只需要继承InternalFrameAdapter,并重写两个相关的方法。

package swingstudy.ch08;

import javax.swing.JInternalFrame;
import javax.swing.event.InternalFrameAdapter;
import javax.swing.event.InternalFrameEvent;

public class InternalFrameIconifyListener extends InternalFrameAdapter {

    public void internalFrameIconified(InternalFrameEvent event) {
        JInternalFrame source = (JInternalFrame)event.getSource();
        System.out.println("Iconified: "+source.getTitle());
    }

    public void internalFrameDeiconified(InternalFrameEvent event) {
        JInternalFrame source = (JInternalFrame)event.getSource();
        System.out.println("Deiconified: "+source.getTitle());
    }
}

InternalFrameEvent是AWTEvent的子类。为了定义由AWTEvent的public int getID()方法返回的值,InternalFrameEvent每一个可用的特定事件子类定义了一个常量。表8-11列出了九个常量。我们也可以通过事件的getInternalFrame()方法来获得实际的JInternalFrame。

事件子类型ID 关联的接口方法
INTERNAL_FRAME_ACTIVATED internalFrameActivated
INTERNAL_FRAME_CLOSED internalFrameClosed
INTERNAL_FRAME_CLOSING internalFrameClosing
INTERNAL_FRAME_DEACTIVATED internalFrameDeactivated
INTERNAL_FRAME_DEICONIFIED internalFrameDeiconified
INTERNAL_FRAME_FIRST N/A
INTERNAL_FRAME_ICONIFIED internalFrameIconified
INTERNAL_FRAME_LAST N/A
INTERNAL_FRAME_OPENED internalFrameOpened

Table: InternalFrameEvent事件子类型

自定义JInternalFrame观感

因为JInternalFrame是一个轻量级组件,他具有可安装的观感。每一个可安装的Swing观感提供了一个不同的JInternalFrame外观以及默认的UIResource值集合。图8-5预安装的观感类型集合的JWindow窗口外观。

Swing_8_5_motif.png

Swing_8_5_motif.png

Swing_8_5_windows.png

Swing_8_5_windows.png

Swing_8_5_ocean.png

Swing_8_5_ocean.png

表8-12中列出了JInternalFrame的可用UIResource相关属性的集合。对于JInternalFrame常量,有60个不同的属性,包括内部窗体的标题面板的属性。

属性字符串 对象类型
InternalFrame.actionMap ActionMap
InternalFrame.activeBroderColor Color
InternalFrame.activeTitleBackground Color
InternaleFrame.activeTitleForeground Color
InternalFrame.activeTitleGradient List
InternalFrame.border Border
InternalFrame.borderColor Color
InternalFrame.borderDarkShadow Color
InternalFrame.borderHighlight Color
InterenalFrame.borderLight Color
InternaleFrame.borderShadow Color
InternaleFrame.borderWidth Integer
InternalFrame.closeButtonToolTip String
InternalFrame.closeIcon Icon
InternalFrmae.closeSound String
InternalFrame.icon Icon
InternalFrame.iconButtonToolTip String
InternalFrame.iconifyIcon Icon
InternalFrame.inactiveBorderColor Color
InternalFrame.inactiveTitleBackground Color
InternalFrame.inactiveTitleForeground Color
InternalFrame.inactiveTitleGradient List
InternalFrame.layoutTitlePaneAtOrigin Boolean
InternalFrame.maxButtonToolTip String
InternalFrame.maximizeIcon Icon
InternalFrame.maximizeSound String
InternalFrame.minimizeIcon Icon
InternalFrame.minimizeIconBackground Color
InternalFrame.minimizeSound String
InternalFrame.optionDialogBorder Border
InternalFrame.paletteBorder Border
InternalFrame.paletteCloseIcon Icon
InternalFrame.paletteTitleHeight Integer
InternaleFrame.resizeIconHighlight Color
InternalFrame.resizeIconShadow Color
InternalFrame.restoreButtonToolTip String
InternalFrame.restoreDownSound String
InternalFrame.restoreUpSound String
InternalFrame.titlebuttonHeight Integer
InternalFrame.titleButtonWidth Integer
InternalFrame.titleFont Font
InternalFrame.titlePaneHeight Integer
InternalFrame.useTaskBar Boolean
InternalFrame.windowBindings Object[]
InternalFrameTitlePane.closebuttonAccessibleName String
InternalFrameTitlePane.closebuttonText String
InternalFrameTitlePane.closeIcon Icon
InternalFrameTitlePane.iconifyButtonAccessibleName String
InternalFrameTitlePane.iconifyIcon Icon
InternalFrameTitlePane.maximizeButtonAccessiblName String
InternalFrameTitlePane.maximizeButtonText String
InternalFrameTitlePane.minimizeIcon Icon
InternalFrameTitlePane.moveButtonText String
InternalFrameTitlePane.restoreButtonText String
InternalFrameTitlePane.sizeButtonText String
InternalFrameTitlePane.titlePaneLayout LayoutManager
InternalFrameTitlePaneUI String
InternalFrameUI String

Table: JInternalFrame UIResource元素

除了表8-12中许多可配置属性以外,对于Metal观感,我们还可以通过特殊的客户端属性JInternalFrame.isPalette来将内部窗体设计为一个palette。当设置为Boolean.TRUE时,内部窗体的外观会与其他窗体略微不同,并且具有较短的标题栏,如图8-6所示。

Swing_8_6.png

Swing_8_6.png

如果我们同时在桌面的PALETTE_LAYER上添加了一个内部窗体,则这个窗体会位于其他所有窗体之上(如图8-6所示):

JInternalFrame palette = new JInternalFrame("Palette", true, false, true, false);
palette.setBounds(150, 0, 100, 100);
palette.putClientProperty("JInternalFrame.isPalette", Boolean.TRUE);
desktop.add(palette, JDesktopPane.PALETTE_LAYER);

创建图8-6所示的程序的完整代码显示在本章稍后的列表8-6中。

修改JDesktopIcon

JInternalFrame依赖一个内联类JDesktopIcon来为JInternalFrame的图标化显示提供UI委托。这个类只是用来这种功能的一个特殊的JComponent,而不是如其名字暗示的一个特殊的Icon实现。事实上,JDesktopIcon类的注释表明这个类是临时的,所以我们不应直接对其进行自定义。(当然,这个类会存在一段时间。)

如果我们需要自定义JDesktopIcon,我们可以修改一些UIResource相关的属性。表8-13列出了JDesktopIcon组件的八个UIResource相关属性。

属性字符串 对象类型
DesktopIcon.background Color
DesktopIcon.border Border
DesktopIcon.font Font
DesktopIcon.foreground Color
DesktopIcon.icon Icon
DesktopIcon.width Integer
DesktopIcon.windowBindings Object[]
DesktopIconUI String

Table: JInternalFrame.JDesktopIcon UIResource元素

JDesktopPane类

与内部窗体组合配合使用的另一个类就是JDesktopPane类。桌面面板的目的就是包含内部窗体集合。当内部窗体被包含在一个桌面面板中时,他们将其行为的大部分委托给桌面面板的桌面管理器。我们将会在本章稍后详细了解DesktopManager接口。

创建JDesktopPane

JDesktopPane只有一个无参数的构造函数。一旦创建,我们通常将其放在由BorderLayout管理的容器的中部。这可以保证桌面占据容器的所有空间。

将内部窗体添加到JDesktopPane

JDesktopPane并没有实现RootPaneContainer。我们并不能直接将组件添加到JRootPane内的不同面板中,而是直接将其添加到JDesktopPane:

desktop.add(anInternalFrame);

JDesktopPane属性

如表8-14所示,JDesktopPane有八个属性。位于allFrames属性数组索引0外的JInternalFrame是位于桌面前面的内部窗体(JInternalFrame f = desktop.getAllFrames()[0])。除了获取JDesktopPane中的所有窗体以外,我们还可以仅获取特定层的窗体:public JInternalFrame[] getAllFramesInLayer(int layer)。

可用的dragMode属性设置可以为类的LIVE_DRAG_MODE与OUTLINE_DRAG_MODE常量。

属性名 数据类型 访问性
accessibleContext AccessibleContext 只读
allFrames JInternalFrame[] 只读
desktopManager DesktopManager 读写
dragMode int 读写绑定
opaque boolean 只读
selectedFrame JInternalFrame 读写
UI DesktopPaneUI 读写
UIClassID String 只读

Table: JDesktopPane属性

自定义JDesktopPane观感

回到图8-5,我们可以看到JDesktopPane中的JInternalFrame对象。JDesktopPane的基本观感与每一个观感相同。如表8-15所示,对JDesktopPane并没有太多可以配置的UIResource相关属性。

属性字符串 对象类型
desktop Color
Desktop.ancestorInputMap InputMap
Desktop.background Color
Desktop.windowBindings Object[]
DesktopPane.actionMap ActionMap
DesktopPaneUI String

Table: JDesktopPane UIResource元素

完整的桌面示例

现在我们已经了解了主要的桌面相关类,现在我们来看一下完整的示例。基本的过程包括创建一组JInternalFrame对象,然后将放在一个JDesktopPane中。如果需要,可以对每一个内部窗体的单个组件进行事件处理,也可以对单个窗体进行事件处理。在这个示例中简单的使用了前面的列表8-5中所给出的InternalFrameIconifyListener类来监听正在图标化和取消图标化的内容窗体。

图8-6显示了程序启动时的样子。一个特定的内部窗体被设计为palette,并且允许了拖放模式。

列表8-6显示了这个示例的完整代码。

package swingstudy.ch08;

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

import javax.swing.JDesktopPane;
import javax.swing.JFrame;
import javax.swing.JInternalFrame;
import javax.swing.JLabel;
import javax.swing.event.InternalFrameListener;

public class DesktopSample {

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

        Runnable runner = new Runnable() {
            public void run() {
                String title = (args.length==0 ? "Desktop Sample" : args[0]);
                JFrame frame = new JFrame(title);
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                JDesktopPane desktop = new JDesktopPane();
                JInternalFrame internalFrames[] = {
                        new JInternalFrame("Can Do All", true, true, true, true),
                        new JInternalFrame("Not Resizable", false, true, true, true),
                        new JInternalFrame("Not Closable", true, false, true, true),
                        new JInternalFrame("Not Maximizable", true, true, false, true),
                        new JInternalFrame("Not Iconifiable", true, true, true, false)
                };

                InternalFrameListener internalFrameListener = new InternalFrameIconifyListener();

                int pos = 0;
                for(JInternalFrame internalFrame: internalFrames) {
                    // Add to desktop
                    desktop.add(internalFrame);

                    // Position and size
                    internalFrame.setBounds(pos*25, pos*25, 200, 100);
                    pos++;

                    // Add listener for iconification events
                    internalFrame.addInternalFrameListener(internalFrameListener);

                    JLabel label = new JLabel(internalFrame.getTitle(), JLabel.CENTER);
                    internalFrame.add(label, BorderLayout.CENTER);

                    // Make visible
                    internalFrame.setVisible(true);

                }

                JInternalFrame palette = new JInternalFrame("Palette", true, false, true, false);
                palette.setBounds(350, 150, 100, 100);
                palette.putClientProperty("JInternalFrame.isPalette", Boolean.TRUE);
                desktop.add(palette, JDesktopPane.PALETTE_LAYER);
                palette.setVisible(true);

                desktop.setDragMode(JDesktopPane.OUTLINE_DRAG_MODE);

                frame.add(desktop, BorderLayout.CENTER);
                frame.setSize(500, 300);
                frame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

DesktopManager接口

使用桌面的最后一部分就是桌面管理器了,他是DesktopManager接口的实现,其定义如下:

public interface DesktopManager {
  public void activateFrame(JInternalFrame frame);
  public void beginDraggingFrame(JComponent frame);
  public void beginResizingFrame(JComponent frame, int direction);
  public void closeFrame(JInternalFrame frame);
  public void deactivateFrame(JInternalFrame frame);
  public void deiconifyFrame(JInternalFrame frame);
  public void dragFrame(JComponent frame, int newX, int newY);
  public void endDraggingFrame(JComponent frame);
  public void endResizingFrame(JComponent frame);
  public void iconifyFrame(JInternalFrame frame);
  public void maximizeFrame(JInternalFrame frame);
  public void minimizeFrame(JInternalFrame frame);
  public void openFrame(JInternalFrame frame);
  public void resizeFrame(JComponent frame, int newX, int newY, int newWidth,
    int newHeight);
  public void setBoundsForFrame(JComponent frame, int newX, int newY, int newWidth,
    int newHeight);
}

当JInternalFrame位于JDesktopPane中时,他们不应尝试例如图标化或是最大化的操作。相反,他们应该请求他们所安装在的桌面面板的桌面管理器来执行这些操作:

getDesktopPane().getDesktopManager().iconifyFrame(anInternalFrame);

DefaultDesktopManager类提供了DesktopManager的一个实现。如果默认实现还足够,观感会提供他们自己的DesktopManager实现类,例如Windows观感的WindowsDesktopManager。我们也可以定义自己的管理器,但是通常并不需要这样。

小结

在本章中,我们探讨了JRootPane类,以及如何实现依据JRootPane对内部组件进行管理的RootPaneContainer接口。我们同时了解了在Swing中我们如何使用JFrame, JDialog, JWindow, JApplet或是JInternalFrame类的JRootPane。根面板可以借助JLayeredPane来布局组件,其方式是工具提示文本以及弹出菜单总是显示在相关联组件的上面。

JInternalFrame同时也可以存在于桌面环境中,在这种情况下,JDesktopPane以及DesktopManager管理如何以及在哪里放置并显示内部窗体。我们还可以通过将InternalFrameListener实现也JInternalFrame关联来响应内部窗体事件。

在第9章中,我们将会探讨Swing库中的特定弹出组件:JColorChooser, JFileChooser, JOptionPane以及ProgressMonitor。

Pop-Ups and Choosers

在第8章中,我们了解了顶层容器,例如JFrame与JApplet。另外,我们探讨了用来创建弹出窗口来显示信息或是获取用户输入的JDialog类。尽管JDialog类可以工作得很好,Swing组件集合同时提供了一些更为简单的方法来由弹出窗口获取用户输入,我们将会在本章进行探讨。

JOptionPane类对于显示信息,获取文本用户输入,或是获取问题答案十分有用。ProgressMonitor与ProgressMonitorInputStream类可以使得我们监视长时间任务的过程。另外,JColorChooser与JFileChooser类提供了特性弹出窗口用来由用户获取颜色选择,或是获取文件或目录名。通过使用这些类,我们的界面开发任务可以更为快速与简单的实现。

JOptionPane Class

JOptionPane是一个可以用来创建放在弹出窗口中的面板的一个特殊类。面板的目的是向用户显示信息或是由用户获取响应。要实现这一任务,面板在四个区域显示内容(如图9-1):

  • Icon:图标区域用来显示一个图标,标识显示给用户的信息类型。为特定的消息类型提供默认的图标是所安装的观感的责任,但是如果我们希望显示其他的图标类型,我们可以提供我们自己的图标。
  • Message:这一区域的基本目的是显示一个文本信息。另外,这一区域还可以包含其他的可选对象集合来使得消息提供更多的信息。
  • Input:输入区域允许用户提供消息的响应。响应可以是自由格式,文本域,或组合框中选择列表或是列表控件。要显示是或否类型的问题,应该使用按钮区域。
  • Button:按钮区域也可以用来获取用户输入。在这个区域的按钮选择通知了JOptionPane使用的结束。可以使用按钮标签的默认集合,或者我们可以显示任意数量的按钮,也可以没有,并使用我们所希望的标签。
Swing_9_1.png

Swing_9_1.png

所有这些区域都是可选的(尽管没有消息与按钮的面板使得选项面板毫无用处)。

除了作为一个在弹出窗口中具有四部分的面板以外,JOptionPane还具有自动将其自身放在一个弹出窗口中并管理用户响应的获取的能力。依据我们所提供给用户的GUI类型,他也可以将自身放在一个JDialog或是一个JInternalFrame中。借助于Icon与JButton组件集合,JOptionPane可以很容易配置来显示多种消息与输入对话框。

注意,因为JOptionPane可以自动将其自身放在一个JDialog中,所以我们并不需要直接创建JDialog。

创建JOptionPane

我们可以使用JOptionPane的七个构造函数或是使用本章稍后将会讨论的25个工厂方法来创建一个JOptionPane。当手动创建JOptionPane时,我们拥有最大的控制。然而,然后我们必须将其放在一个弹出窗口中,显示窗口,最后管理响应。

由于自动完成所有事情所提供的方法的简便,我们也许会认为当使用JOptionPane时应只使用工厂方法。然而,通过本章,我们将会发现手动完成一些事情的其他原因。另外,当我们使用一个可视化编程环境时,环境会将JOptionPane看作一个JavaBean组件,并且会忽略其工厂方法。

对于七个构造函数,我们可以有6个不同参数的不同组合。参数可以使得我们配置图9-1中所显示的不同区域的一部分。六个参数是消息,消息类型,可选类型,图标,选项数组,以及初始的选项设置。参数的用法与工厂方法相同。

我们首先来看一下七个构造函数,然后探讨不同的参数。注意构造参数是级联的,并且只在前一个构造函数中添加额外的参数。

public JOptionPane()
JOptionPane optionPane = new JOptionPane();

public JOptionPane(Object message)
JOptionPane optionPane = new JOptionPane("Printing complete");

public JOptionPane(Object message, int messageType)
JOptionPane optionPane = new JOptionPane("Printer out of paper",
  JOptionPane.WARNING_MESSAGE);

public JOptionPane(Object message, int messageType, int optionType)
JOptionPane optionPane = new JOptionPane("Continue printing?",
  JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_NO_OPTION);

public JOptionPane(Object message, int messageType, int optionType,
  Icon icon)
Icon printerIcon = new ImageIcon("printer.jpg");
JOptionPane optionPane = new JOptionPane("Continue printing?",
  JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_NO_OPTION, printerIcon);

public JOptionPane(Object message, int messageType, int optionType, Icon icon,
  Object options[ ])
Icon greenIcon = new DiamondIcon(Color.GREEN);
Icon redIcon = new DiamondIcon(Color.RED);
Object optionArray[] = new Object[] { greenIcon, redIcon} ;
JOptionPane optionPane = new JOptionPane("Continue printing?",
  JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_NO_OPTION, printerIcon,
  optionArray);

public JOptionPane(Object message, int messageType, int optionType, Icon icon,
  Object options[], Object initialValue)
JOptionPane optionPane = new JOptionPane("Continue printing?",
  JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_NO_OPTION, printerIcon,
  optionArray, redIcon);

JOptionPane消息参数

message参数是一个Object,而不是一个String。当我们通常将一个引起来的字符串作为Object的参数时,我们仅仅可以在消息区域显示我们希望显示的任何内容。在本章稍后的“理解消息属性”一节中,我们将会了解这一参数的高级用法。然而,概括来说,有四个基本的规则可以解释Object类型消息参数意义。对于Object中的元素,依次遵循下列规则:

  • 如果消息是一个对象数组(Object[]),会使得JOptionPane在单独行放置每一项。
  • 如果消息是一个Component,则将该组件放置在消息区域。
  • 如果消息是一个Icon,会在JLable中放置Icon,并且在消息区域显示标签。
  • 如果消息是一个Object,使用toString()方法将其转换为String,将String放在JLabel中,并在消息区域显示标签。

JOptionPane消息类型与图标参数

messageType构造函数参数用来表示显示在JOptionPane中的消息类型。如果我们没有为JOptionPane指定自定义的图标,已安装的观感使用messageType参数设置来决定在图标区域使用哪个图标。JOptionPane提供了五个不同的消息类型常量:

  • ERROR_MESSAGE用来显示一个错误消息
  • INFORMATION_MESSAGE用来显示一个信息提示消息
  • QUESTION_MESSAGE用来显示一个查询消息
  • WARNING_MESSAGE用来显示一个警告消息
  • PLAIN_MESSAGE用来显示任何其他类型的消息

如果我们使用了同时带有messageType与icon作为参数的构造函数,并且希望JOptionPane为messageType使用默认图标,只需要将icon参数的值指定为null。如果icon参数不为null,则会使用所指定的图标,而不论是何种消息类型。

如果没有指定构造函数的messageType参数,则默认的消息类型为PLAIN_MESSAGE。

JOptionPane选项类型参数

optionType构造函数参数用来决定按钮区域的按钮集合配置。如果提供了一个下面所描述的options参数,则optionType参数会被忽略,而按钮集合配置则会由options获取。JOptionPane有四个不同的选项类型常量:

  • DEFAULT_OPTION用于一个OK按钮
  • OK_CANCEL_OPTION用来OK与Cancel按钮
  • YES_NO_CANCEL_OPTION用于Yes,No与Cancel按钮
  • YES_NO_OPTION用于Yes与No按钮

如果没有指定optionType构造函数参数,则默认的选项类型为DEFAULT_OPTION。

JOptionPane选项以及初始值参数

options参数是一个用来构建用在JOptionPane按钮区域的JButton对象集合的Object数组。如果这个参数为null(或者是使用了一个没有这个参数的构造函数),则按钮标签会由optionType参数来决定。否则,这个数组的作用类似于消息参数,但是并不支持迭代数组:

  • 如果options数组元素是一个Component,则会将这个组件放在按钮区域。
  • 如果options数组元素是一个Icon,则会将这个Icon放在一个JButton中,然后将按钮放在按钮区域。
  • 如果options数组元素是一个Object,则使用toString()方法将其转换为一个String,将这个String放在一个JButton中,然后将按钮放在按钮区域。

通常,options参数是一个String对象的数组。也许我们希望JButton上带有一个Icon,尽管最终的按钮不会带有标签。如果我们希望按钮上同时带有图标与文本标签,我们可以手动创建一个JButton,并将其放在一个数组中。相对的,我们可以在数组中直接包含其他任意的Component。然而对于后两种方法有一个小问题。我们要负责处理组件选中的响应,并且通知JOptionPane用户何时选择了这个组件。本章稍后的“向按钮区域添加组件”将会讨论如何正确处理这种行为。

当options参数不为null时,initialValue参数可以指定当面板初始显示时哪一个按钮是默认按钮。如果其为null,则按钮区域的第一个组件为默认按钮。在任何一种情况下,第一个按钮会获得输入焦点,除非在消息区域有一个输入组件,在这种情况下,输入组件会获得初始输入焦点。

显示JOptionPane

在我们使用一个构造函数创建了JOptionPane之后,我们所获得的是一个使用组件填充的面板。换句话说,所获得的JOptionPane还没有位于弹出窗口中。我们需要创建一个JDialog,一个JinternalFrame,或是其他的弹出窗口,然后将JOptionPane放在其中。另外,如果我们选择JOptionPane构造的这种手动风格,我们需要处理弹出窗口的关闭。我们必须监听按钮区域组件的选中,然后在选中之后隐藏弹出窗口。

因为在这里有如此多的工作要做,JOptionPane包含两个助手方法来将JOptionPane放在一个JDialog或是一个JInternalFrame之中,并且处理前面所描述的所有行为:

public JDialog createDialog(Component parentComponent, String title)
public JInternalFrame createInternalFrame(Component parentComponent, String title)

注意,当使用createDialog()与createInternalFrame()方法创建弹出窗口时,自动创建的按钮的选中会导致所创建的弹出窗口的关闭。然后我们需要使用getValue()方法来查询JOptionPane用户选中的选项,而且如果需要,可以使用getInputValue()方法获得输入值。

方法的第一个参数是弹出窗口所在的组件。第二个参数是弹出窗口的标题。一旦我们创建了弹出窗口,无论他是一个JDialog还是一个JInternalFrame,我们要显示他。然后弹出窗口会在按钮区域的一个组件被选中之后关闭,此时,我们的程序继续。下面的代码行演示了显示在一个JDialog中的JOptionPane的创建。所创建的弹出窗口显示在图9-2中。

JOptionPane optionPane = new JOptionPane("Continue printing?",
  JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_NO_OPTION);
JDialog dialog = optionPane.createDialog(source, "Manual Creation");
dialog.setVisible(true);
Swing_9_2.png

Swing_9_2.png

我们创建了JOptionPane,将其放在一个弹出窗口中,显示弹出窗口,并且用户响应之后,我们需要确定用户的选择是什么。选择是通过JOptionPane的public Object getValue()方法来提供的。getValue()方法返回的值是通过是否向JOptionPane构造函数提供了一个options数组来确定的。如果我们提供了这个数组,则会返回选中的参数。如果我们没有提供数组,则会返回一个Integer对象,而其值表示了选中的按钮在按钮区域中的位置。在另一种情况下,如果没有选择任何内容,getValue()方法会返回null,例如当通过选择弹出窗口标题的相应装饰关闭JDialog时。

为了更容易获取这种多面响应,列表9-1显示了一个OptionPaneUtils类,其中定义了方法public static int getSelection(JOptionPane optionPane)。指定一个选项面板,这个方法会将选中值的位置作为int返回,而不论是否提供了选项数组。为了标识没有选中任何内容,JOptionPane.CLOSED_OPTION(-1)会被返回。

package swingstudy.ch09;

import javax.swing.JOptionPane;

public final class OptionPaneUtils {

    private OptionPaneUtils() {

    }

    public static int getSelection(JOptionPane optionPane) {
        // Default return value, signals nothing selected
        int returnValue = JOptionPane.CLOSED_OPTION;

        // Get selected value
        Object selectedValue = optionPane.getValue();
        // if none, then nothing selected
        if(selectedValue != null) {
            Object options[] = optionPane.getOptions();
            if(options == null) {
                // default buttons, no array specified
                if(selectedValue instanceof Integer) {
                    returnValue = ((Integer)selectedValue).intValue();
                }
            }
            else {
                // array of option buttons specified
                for(int i=0, n=options.length; i<n; i++) {
                    if(options[i].equals(selectedValue)) {
                        returnValue = i;
                        break; // out of for loop
                    }
                }
            }
        }
        return returnValue;
    }
}

借助于这个新的OptionPaneUtils.getSelection(JOptionPane)助手方法,现在我们可以使用一行代码来确定选项面板的选择,并依据响应执行动作。

int selection = OptionPaneUtils.getSelection(optionPane);
switch (selection) {
  case ...: ...
    break;
  case ...: ...
    break;
  default: ...
}

如果我们使用一个null选项数组创建一个JOptionPane,我们可以使用JOptionPane类中的常量来标识默认按钮标签的位置以及由OptionPaneUtils.getSelection(JOptionPane)方法返回的值。这些常量列在表9-1中。使用这些常量可以使用我们避免硬编码常量,例如0,1,2或是-1。

Swing_table_9_1.png

Swing_table_9_1.png

在弹出窗口中自动创建JOptionPane

我们可以手动创建JOptionPane,将其放在一个JDialog或是JInternalFrame中,并获取响应。相对的,我们可以使用JOptionPane工厂方法在JDialog或是JInternalFrame中直接创建JOptionPane组件。使用工厂方法,我们可以使用一行代码创建一个选项面板,将其放在一个弹出窗口中,并且获取响应。

有25个工厂方法,可以分为两类:创建显示在JDialog中的JOptionPane或是创建显示在JInternalFrame中的JOptionPane。在JInternalFrame中显示JOptionPane的方法以showInternalXXXDialog()的方式命名,而创建显示在JDialog中的面板则以showXXXDialog()的方式命名。

JOptionPane的工厂方法的第二个分组是填充方法名字中的XXX部分。这表示了我们可以创建并显示的选项面板的各种消息类型。另外,消息类型定义了用户在选项面板中选择某个组件之后所返回的内容。四个不同的消息类型如下:

  • Message:对于消息弹出窗口,没有返回值。所以其方法定义为void show[Internal]MessageDialog(...)。
  • Input:对于输入弹出窗口,返回值或者是用户在文本域中所输入的内容(String),或者是用户在选项列表中的选择(Object)。所以,依据我们所使用的版本,show[Internal]InputDialog(...)方法或者返回一个String,或者返回一个Object。
  • Confirm:对于确认弹出窗口,返回值标识了用户在选项面板内选择的按钮。在一个按钮被选中后,弹出窗口消失,而返回值是显示在表9-1中的整数常量中的一个。所以,在这里方法定义为int show[Interal]ConfirmDialog(...)。
  • Option:对于选项弹出窗口,返回值是一个int,与确认弹出窗口的返回值类型相同,所以方法定义为itn show[Internal]OptionDialog(...)。如果按钮标签是通过一个非null的参数手动指定的,整数表示所选择的按钮的位置。

表9-2中的信息应该可以帮助我们理解25个方法及其参数。方法名(与返回类型)显示在表的左侧,而其参数列表(与数据类型)显示在右侧。对于每个方法名跨越各个列重复出现的数字标识了此方法的一个特定的参数集合。例如,showInputDialog行在父组件列,消息列,标题列以及消息类型列显示了一个3.所以,showInputDialog方法的一个版本定义如下:

public static String showInputDialog(Component parentComponent, Object message, String title, int messageType)

由于定义了不同的showXXXDialog()方法,我们不再需要亲自确定所选中的按钮或是用户的输入。依据所显示的对话框类型,各种方法的返回值是下列值中的一个:无返回值(void返回类型),表9-1中的int,String或是Object。

Swing_table_9_2.png

Swing_table_9_2.png

工厂方法的JOptionPanl参数

几乎工厂方法的所有参数都匹配JOptionPane的构造函数参数。本章前面的“创建JOptionPane”中的两个列表描述了消息类型与选项类型参数的可接受的值。另外,同时描述了消息,选项以及初值参数用法。父组件以及标题参数被传递给createDialog()或是createInternalFrame()方法,这依赖于JOptionPane所嵌入的弹出窗口的类型。

接下来我们需要考虑的是showInputDialog()方法的选择值参数以及初始选中值参数。对于输入对话框,我们可以向用户要求文本输入,并且允许用户输入任何内容,或者是我们可以向用户展示一个预定义的选项列表。showInputDialog()的选择值参数决定了我们如何提供该选项集合。初始选择值表示当JOptionPane首次显示时被选择的特定项。观感将会依据所展示的选项数目来决定义要使用的相应Swing组件。对于较小的列表,可以使用JComboBox。对于大的列表,对于Motif,Metal/Ocean以及Windows观感,多于20将会使用JList。

Swing_table_9_3.png

Swing_table_9_3.png

消息弹出窗口

sowMessageDialog()与showInternalDialog()方法使用弹出标题“Message”创建一个INFORMATION_MESSAGE弹出窗口,除非我们为消息类型与窗口标题指定了不同的参数设置。因为消息对话框的目的就是要显示一个信息,这些对话框只提供了OK按钮,并且没有返回值。图9-3显示了使用下面的代码所创建的示例消息弹出窗口:

JOptionPane.showMessageDialog(parent, “Printing complete”);

JOptionPane.showInternalMessageDialog(desktop, “Printing complete”);

Swing_9_3.png

Swing_9_3.png

确认弹出窗口

showConfirmDialg()与showInternalConfirmDialog()方法默认情况下使用QUESTION_MESSAGE类型以及“Select an Option”弹出标题创建一个确认弹出窗口。因为确认对话框询问一个问题,其默认选项类型为YES_NO_CANCEL_OPTION,为其指定Yes,No以及Cancel按钮。对这些方法的调用所获得的返回值是下列JOptionPane常量中的一个:YES_OPTION,NO_OPTION或是CANCEL_OPTION。我们可以很容易猜到哪一个常量对应哪一个按钮。图9-4显示了使用下面代码创建的确认弹出窗口:

JOptionPane.showConfirmDialog(parent, "Continue printing?");
JOptionPane.showInternalConfirmDialog(desktop, "Continue printing?");
Swing_9_4.png

Swing_9_4.png

输入弹出窗口

默认情况下,showInputDialog()与showInternalInputDialog()方法使用“Input”弹出标题创建一个QUESTION_MESASGE弹出窗口。输入对话框的选项类型为OK_CANCEL_OPTION,为其指定一个OK与一个Cancel按钮,而且选项类型是不可以改变的。这些方法的返回数据类型或者是一个String,或者是一个Object。如果我们没有指定选项值,弹出窗口会向用户展示一个文本域,并且将输入作为一个String返回。如果我们指定了选项值,我们会由选项值数组中获取一个Object。图9-5显示了使用下面代码所创建的输入弹出窗口:

JOptionPane.showInputDialog(parent, "Enter printer name:");
// Moons of Neptune
String smallList[] = {
  "Naiad", "Thalassa", "Despina", "Galatea", "Larissa", "Proteus",
  "Triton", "Nereid"} ;
JOptionPane.showInternalInputDialog(desktop, "Pick a printer", "Input",
  JOptionPane.QUESTION_MESSAGE, null, smallList, "Triton");
// Twenty of the moons of Saturn
String bigList[] = {"Pan", "Atlas", "Prometheus", "Pandora", "Epimetheus",
  "Janus", "Mimas", "Enceladus", "Telesto", "Tethys", "Calypso", "Dione",
  "Helene", "Rhea", "Titan", "Hyperion", "Iapetus", "Phoebe", "Skadi",
  "Mundilfari"};
JOptionPane.showInputDialog(parent, "Pick a printer", "Input",
  JOptionPane.QUESTION_MESSAGE, null, bigList, "Titan");
Swing_9_5.png

Swing_9_5.png

选项弹出窗口

showOptionDialg()与showInternalOptionDialog()方法提供了最大的灵活性,因为他们允许我们所有的参数。他们没有默认参数,并且返回值是一个int。如果没有指定options参数,则返回值为表9-1中所列出的常量之一。否则,返回值表示了所选择的选项在options参数中的组件位置。图9-6显示了使用下列代码创建的多个输入弹出窗口,其中在按钮上提供了图标(而不是文本):

Icon greenIcon = new DiamondIcon(Color.GREEN);
Icon redIcon = new DiamondIcon(Color.RED);
Object iconArray[] = { greenIcon, redIcon} ;
JOptionPane.showOptionDialog(source, "Continue printing?", "Select an Option",
  JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, iconArray,
  iconArray[1]);
Icon blueIcon = new DiamondIcon(Color.BLUE);
Object stringArray[] = { "Do It", "No Way"} ;
JOptionPane.showInternalOptionDialog(desktop, "Continue printing?",
  "Select an Option", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE,
blueIcon, stringArray, stringArray[0]);
Swing_9_6.png

Swing_9_6.png

JOptionPane属性

表9-3显示了JOptionPane的15个属性。这些属性只有在我们没有使用JOptionPane的工厂方法时才可以访问。对于大多数参数来说,其意义直接与一个构造函数参数相对应。

Swing_table_9_5.png

Swing_table_9_5.png

对于输入对话框或是当selectionValues属性不为null时,wantsInput属性会自动被设置为true。inputValue属性是由一个输入对话框中选择的项。value属性标识了由按钮区域所做的选项。

显示多行消息

默认情况下maxCharacterPerLineCount属性设置为一个极大的值,Integer.MAX_VALUE。由于某些奇怪的原因,Swing开发选择不为这个属性提供一个setter方法。如果我们需要修改这个属性,我们必须继承JOptionPane并且重写public int getMaxCharacterPerLineCount()方法。这使得一个长的文本被分为选项面板中的多行。另外,我们不能使用任何的工厂方法,因为他们并不知道我们的子类。

为了帮助我们创建短小的JOptionPane组件,我们可以将列表9-2中所示的代码添加到前面列表9-1中所示的OptionPaneUtils类义中。这个新方法提供了指定所需要的选项面板字符宽度的方法。

public static JOptionPane getNarrowOptionPane(int maxCharacterPerLineCount) {
    // our inner class definition
    class NarrowOptionPane extends JOptionPane {
        int maxCharacterPerLineCount;

        NarrowOptionPane(int maxCharacterPerLineCount) {
            this.maxCharacterPerLineCount = maxCharacterPerLineCount;
        }

        public int getMaxCharacterPerLineCount() {
            return maxCharacterPerLineCount;
        }
    }
    return new NarrowOptionPane(maxCharacterPerLineCount);
}

一旦定义了这个方法与新类,我们可以创建指定字符宽度的选项面板,手动配置所有的属性,将其放在一个弹出窗口中,显示弹出窗口,然后确定用户的响应。下面的代码演示了使用这个新的功能:

String msg = "this is a really long message ... this is a really long message";
JOptionPane optionPane = OptionPaneUtils.getNarrowOptionPane(72);
optionPane.setMessage(msg);
optionPane.setMessageType(JOptionPane.INFORMATION_MESSAGE);
JDialog dialog = optionPane.createDialog(source, "Width 72");
dialog.setVisible(true);

图9-7显示了我们没有修改maxCharacterPerLineCount属性时对话框的样子。图9-7同时显示新的短小JOptionPane的样子。

Swing_9_7.png

Swing_9_7.png

尽管这看起来需要大量的工作,这却是创建多行选项面板的最好方法,除非我们要手动将消息分析为单行。

理解消息属性

在本章前面所有使用JOptionPane构造函数的消息参数以及使用工厂方法的例子中,消息仅是一个字符串。正如在前面“JOptionPane消息参数”一节中所描述的,这个参数并不需要是一个字符串。例如,如果这个参数是一个字符串数组,每一个字符串就会在单独的行上显示。这就减少了使用短小JOptionPane的必要,但是需要我们自己计算字符。然而,因为我们是在分割消息,我们可以使用25个厂方法中的一个。例如,下面的代码创建了图9-8中显示的弹出窗口。

String multiLineMsg[] = { "Hello,", "World"} ;
JOptionPane.showMessageDialog(source, multiLineMsg);
Swing_9_8.png

Swing_9_8.png

消息参数不仅仅支持显示字符串数组,同时他还可以支持任意对象类型的数组。如果数组中的元素是一个Component,他会被直接放置在消息区域中。如果其元素是一个Icon,图标会被放置在一个JLabel中,然后JLabel被放置在消息区域。所有其他的对象会被转换为一个String,将其放在一个JLabel中,并且在消息区域显示,除非对象本身是一个数组;在这种情况下,规则会被迭代应用。

为了演示这种可能性,图9-9显示了JOptionPane的真正功能。实际的内容并不是要显示特定的内容,只是为了表明我们可以显示多种不同的内容。消息参数是由下面的数组构成的:

Object complexMsg[] = {
   "Above Message", new DiamondIcon(Color.RED), new JButton("Hello"),
  new JSlider(), new DiamondIcon(Color.BLUE), "Below Message"} ;
Swing_9_9.png

Swing_9_9.png

向消息区域添加组件

如果我们要显示图9-9中的弹出窗口,我们就会注意到一个小问题。选项面板并不了解所嵌入的JSlider的设置,这与他自动了解JTextField,JComboBox或是JList组件的输入不同。如果我们希望JOptionPane获取JSlider值,我们需要使得我们的输入组件修改JOptionPane的inputValue属性。当这个值被修改时,选项面板会通知弹出窗口关闭,因为JOptionPane已经获取其输入值。

将一个ChangeListener关联到JSlider组件可以使得我们确定其值何时发生变化。向前面的列表9-1中显示的OptionPaneUtils类中添加另一个方法可以使得我们更为容易的重用JOptionPane中的这个特殊的JSlider。在列表9-3中以粗体显示了重要的方法调用。相似的代码行需要添加到我们希望在JOptionPane中使用的任意输入组件上。这一行会在用户修改了输入组件的值时通知选项面板。

public static JSlider getSlider(final JOptionPane optionPane) {
    JSlider slider = new JSlider();
    slider.setMajorTickSpacing(10);
    slider.setPaintTicks(true);
    slider.setPaintLabels(true);
    ChangeListener chageListener =  new ChangeListener() {
        public void stateChanged(ChangeEvent event) {
            JSlider theSlider = (JSlider)event.getSource();
            if(!theSlider.getValueIsAdjusting()) {
                optionPane.setInputValue(new Integer(theSlider.getValue()));
            }
        }
    };
    slider.addChangeListener(chageListener);
    return slider;
}

现在创建了这个特殊的JSlider,我们需要将其放置在一个JOptionPane中。这需要我们后动创建JOptionPane组件,奇怪的是,并不要求wantsInput属性的设置。只有当我们希望JOptionPane来提供其自己的输入组件时,wantsInput属性才会被设置为true。因为我们正在提供这样的组件,所以就不需要这个属性。最终弹出窗口显示在图9-10中。

JOptionPane optionPane = new JOptionPane();
JSlider slider = OptionPaneUtils.getSlider(optionPane);
optionPane.setMessage(new Object[] { "Select a value: " , slider} );
optionPane.setMessageType(JOptionPane.QUESTION_MESSAGE);
optionPane.setOptionType(JOptionPane.OK_CANCEL_OPTION);
JDialog dialog = optionPane.createDialog(source, "My Slider");
dialog.setVisible(true);
System.out.println ("Input: " + optionPane.getInputValue());
Swing_9_10.png

Swing_9_10.png

注意,如果用户并没有移动滑块,JOptionPane.getInputValue()会返回JOptionPane.UNINITIALIZED_VALUE。

向按钮区域添加组件

在本章前面的“JOptionPane选项以及初始值参数”一节中,如果我们在JOptionPane的选项数组中有一个Component,我们必须自己配置组件来处理选中。对于我们通过options属性添加其他组件也是如此。当组件被配置为处理选中时,当组件被选中时,JOptionPane被嵌入的弹出窗口会显示。按钮的默认设置如此工作。当安装我们自己的组件时,我们必须通过设置选项面板的value属性在一个组件被选中时通知选项面板。

为了演示这种机制,创建一个可以放置在选项中具有图标与文本标签的JButton。如果我们没有自己定义这个组件,选项面板仅支持按钮上标签或是图标的显示。当按钮被选中时,按钮会通过将选项面板的value属性设置为按钮当前的文本标签来通知选项面板他被选中。在前面的列表9-1中添加另一个方法来使用我们可以创建一个这样的按钮。列表9-4源码中以粗体显示的代码行是我们需要添加到其他我们希望与组件数组结合作为JOptionPane的选项属性的类似组件上的重要方法调用。这一行代码会在这个组件被选中后调用。

public static JButton getButton(final JOptionPane optionPane, String text, Icon icon) {
    final JButton button = new JButton(text, icon);
    ActionListener actionListener = new ActionListener() {
        public void actionPerformed(ActionEvent event) {
            // return current text label, instead of argument to method
            optionPane.setValue(button.getText());
        }
    };
    button.addActionListener(actionListener);
    return button;
}

在创建了这个特殊的JButton之后,我们需要将其放在一个JOptionPane中。不幸的,这也需要较长的JOptionPane使用。最终的弹出窗口显示在图9-11中。

JOptionPane optionPane = new JOptionPane();
optionPane.setMessage("I got an icon and a text label");
optionPane.setMessageType(JOptionPane.INFORMATION_MESSAGE);
Icon icon = new DiamondIcon (Color.BLUE);
JButton jButton = OptionPaneUtils.getButton(optionPane, "OK", icon);
optionPane.setOptions(new Object[] {jButton} );
JDialog dialog = optionPane.createDialog(source, "Icon/Text Button");
dialog.setVisible(true);
Swing_9_11.png

Swing_9_11.png

监听属性变化

JOptionPane类定义了下列11个常量来辅助监听边界属性的变化:

  • ICON_PROPERTY
  • INITIAL_SELECTION_VALUE_PROPERTY
  • INITIAL_VALUE_PROPERTY
  • INPUT_VALUE_PROPERTY
  • MESSAGE_PROPERTY
  • MESSAGE_TYPE_PROPERTY
  • OPTION_TYPE_PROPERTY
  • OPTIONS_PROPERTY

SELECTION_VALUES_PROPERTY

  • VALUE_PROPERTY
  • WANTS_INTUT_PROPERTY

如果我们没有使用JOptionPane的工厂方法,我们可以使用PropertyChangeListener来监听边界属性的变化。这可以使得我们被动的监听边界属性的变化,而不是在变化后主动获取。

自定义JOptionPane观感

每一个可安装的Swing观感都提供了不同的JOptionPane外观以及默认的UIResource值集合。图9-12显示了预安装的观感类型Motif,Windows,以及Ocean的JOptionPane窗口的外观。

Swing_9_12.png

Swing_9_12.png

JOptionPane的消息类型帮助确定要在选项面板的图标区域显示的默认图标。对于普通的消息,并没图标。其余的四个图标-用于信息,问题,警告以及错误消息-显示在表9-4中。

Swing_table_9_6.png

Swing_table_9_6.png

表9-5中显示了JOptionPane可用的UIResource相关属性的集合。对于JOptionPane组件,有56种不同的属性。

Swing_table_9_7_1.png

Swing_table_9_7_1.png

Swing_table_9_7_2.png

Swing_table_9_7_2.png

Swing_table_9_7_3.png

Swing_table_9_7_3.png

Swing_table_9_7_4.png

Swing_table_9_7_4.png

表9-5中所列资源的一个很好的用法就是用来自定义默认的按钮标签从而来匹配用户的locale或是语言。例如,要将Cancel,No,OK以及Yes按钮的标签修改为法语,可以在我们的程序中添加下面的代码。(我们也可以由java.util.ResourceBundle中获取翻译的文本。)

// Set JOptionPane button labels to French
UIManager.put("OptionPane.cancelButtonText", "Annuler");
UIManager.put("OptionPane.noButtonText", "Non");
UIManager.put("OptionPane.okButtonText", "D'accord");
UIManager.put("OptionPane.yesButtonText", "Oui");

现在当我们显示在选项面板时,按钮将会具有本地化的按钮标签。当然,这需要为选项面板翻译消息。图9-13显示在下面的代码所创建的弹出窗口的样子。因为弹出窗口的标题并不是一个属性,我们必须将标题传递给每一个所创建的对话框。

int result = JOptionPane.showConfirmDialog(
  aFrame, "Est-ce que vous avez 18 ans ou plus?", "Choisisez une option",
  JOptionPane.YES_NO_CANCEL_OPTION);
Swing_9_13.png

Swing_9_13.png

JOptionPane组件支持本地化的JOptionPane按钮标签。JOptionPane可以为标准的Yes,No,Cancel与OK按钮显示相庆的中文或是日文按钮标签。例如,图9-14中左侧显示了带有日文标签的Yes,No与Cancel按钮标签,而右侧则显示了带有日文标签的OK与Cancel按钮标签。很明显,我们需要修改选项面板中的消息。

Swing_9_14.png

Swing_9_14.png

幸运的是,JDK 5.0版本包含了对于标签JOptionPane(同时还有JFileChooser与JColorChooser)标签的翻译。这可以用于德语(de),西班牙语(es),法语(fr),意大利语(it),日语(ja),韩语(ko),英语,瑞典语(sv),以及中文(简体/zh_CN与繁体/zh_TW)。

ProgressMonitor类

ProgressMonitor类用来报告需要一段时间完成的任务的状态。这个类是一个特殊的Swing类,他并不是一个GUI组件,也不是一个选项面板或是JavaBean组件。相反,当任务的每一部分完成时,我们通知ProgressMonitor。如果任务需要一段相当长的时间来完成,ProgressMonitor会显示一个类似图9-15所示的弹出窗口。

Swing_9_15.png

Swing_9_15.png

在ProgressMonitor显示弹出窗口以后,用户可以做下列两件事情。用户可以监视ProgressMontior显示来确认任务已经完成了多少;当任务完成时,ProgressMonitor显示会自动消失。或者,如果用户选择了关闭按钮,这会通知ProgressMonitor任务需要被结束。要检测关闭,任务需要定时查看ProgressMonitor来确认用户是否关闭了任务操作。否则,任务会继续。

ProgressMonitor类显示的弹出窗口是一个maxCharacterPerLineCount属性设置为60的JOptionPane,允许选项面板自动回行所显示的消息。选项面板会嵌入在一个其标题为“Progress...”的非模态JDialog中。为JDialog是非模态的,用户仍然可以与主程序进行交互。ProgressMonitor的JOptionPane总是可以在其图标区域显示一个信息图标。

另外,选项面板的消息区域由下面三个对象组件:

  • 在消息区域的顶部是在整个JOptionPane生命周期中保持不变的固定消息。与JOptionPane的message属性类似,这个消息可以是一个文本字符串,或者是一个对象数组。
  • 在消息区域的中部是会随着任务过程而变化的注释或是变化消息。
  • 在消息区域的底部是一个由已完成任务的增加百分比填充的过程栏(JProgressBar组件)。

选项面板的按钮区域显示一个关闭按钮。

创建ProgressMonitor

当我们创建一个ProgressMonitor时,其构造函数有五个参数:

public ProgressMonitor(Component parentComponent, Object message, String note,
  int minimum, int maximum)

第一个参数表示当ProgressMonitor需要显示时JOptionPane的父组件。父组件是弹出窗口显示在其上的绷脸的,并且其作用类似于JOptionPane的createDialog()方法中的parentComponent组件。然后我们为JOptionPane的消息区域提供静态或是变化的消息部分。这些消息部分的每一个可以为null,尽管null意味着消息区域的这一部分不会显示。最后,我们需要为过程栏提供minimum与maximum值作为其范围。这两个值之间的区别表示要执行的期望操作数目,例如要载入的文件数或是要读取的文件尺寸。通常,最小设置为零,但是并不做要求。完成操作数决定了过程栏要移动多远。

初始时,弹出窗口并不显示。默认情况下,过程监视器每半分钟(500毫秒)检测一次来确认正在进行的任务是否会在两秒内结束。如果任务已经显示了某些进程,并且他不会在两秒内结束,那么弹出窗口就会显示。结束时间可以通过修改ProgressMonitor的millisToDecideToPopup与millisToPopup属性来配置。

下面的代码演示了一个具有200步操作的ProgressMonitor的创建。应该保存到ProgressMonitor的引用,从而他可以在任务过程中得到通知。

ProgressMonitor monitor = new ProgressMonitor(
  parent, "Loading Progress", "Getting Started...", 0, 200);

使用PropressMonitor

一旦我们创建了ProgressMonitor,我们需要启动其过程已经被监视的任务。当任务完成了一步或是多步时,ProgressMonitor需要得到任务进程的通知。通知是通过public void setProgress(int newValue)方法调用来实现的,其中参数表示现在已经完成的进程,而newValue需要位于初始指定的minimum...maximum范围之间。这个进程值需要在ProgressMonitor之外进行维护,因为我们不能向监视器询问进程已经完成了多少(ProgressMonitor并没有public int getProgress()方法)。如果进程值在一个名为progress的变化中进行维护,下面两行代码可以更新进程并且通知ProgressMonitor。

progress += 5;
monitor.setProgress(progress);

progress设置表示到目前为止已经载入的文件数,或者是由文本读取的字节数。除了更新计数,我们还应该更新note来反映进程。如果ProgressMonitor构造函数之间中所用的minimum与maximum参数之间的差值为100,那么当前的进程可以被看作是当前任务的百分比。否则,progress属性仅表示到目前为止已经完成的进程。

monitor.setNote("Loaded " + progress + " files");

执行任务代码要负责检测用户是否按下了ProgressMonitor对话框中的Cancel按钮。如果任务被关闭,ProgressMonitor会自动关闭对话框,但是任务必须在代码中的合适位置添加一个简单的检测来主动检测变化:

if (monitor.isCanceled()) {
// Task canceled - cleanup
  ...
}  else {
// Continue doing task
  ...
}

大多数的任务要求ProgressMonitor使用单独的线程来实现,从而避免阻塞主程序的响应。

列表9-5显示了一个创建ProgressMonitor并且允许我们手动或自动增加其progress属性的程序。这些任务是由屏幕上的按钮来处理的(如图9-16)。选择Start按钮创建ProgressMonitor。选择Manual Increase按钮可以使得进程增加5.选择Automatic Increase按钮可以使得进程每250毫秒增加5.在自动增加的过程中按下弹出窗口的Cancel按钮演示了操作被关闭时发生的情况;计时器停止发送更新。

Swing_9_16.png

Swing_9_16.png

列表9-5中开始处的ProgressMonitorHandler内联类对于保证仅由事件线程访问ProgressMonitor是必须。否则,在某些随机的线程内,访问将不是线程安全的。

package swingstudy.ch09;

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

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.ProgressMonitor;
import javax.swing.Timer;

public class SampleProgress {

    static ProgressMonitor monitor;
    static int progress;
    static Timer timer;

    static class ProgressMonitorHandler implements ActionListener {
        // Called by Timer
        public void actionPerformed(ActionEvent event) {
            if(monitor == null) {
                return ;
            }
            if(monitor.isCanceled()) {
                System.out.println("Monitor canceled");
                timer.stop();
            }
            else {
                progress += 3;
                monitor.setProgress(progress);
                monitor.setNote("Load "+progress+" files");
            }
        }
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("ProgressMonitor Sample");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new GridLayout(0,1));

                // define start button
                JButton startButton = new JButton("Start");
                ActionListener startActionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        Component parent = (Component)event.getSource();
                        monitor = new ProgressMonitor(parent, "Loading Progress", "Getting Started...", 0, 200);
                        progress = 0;
                    }
                };
                startButton.addActionListener(startActionListener);
                frame.add(startButton);

                // define manual increase button
                // pressing this button increases progress by 5
                JButton increaseButton = new JButton("Manual Increase");
                ActionListener increaseActionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        if(monitor == null)
                            return ;
                        if(monitor.isCanceled()) {
                            System.out.println("Monitor cancled");
                        }
                        else {
                            progress += 5;
                            monitor.setProgress(progress);
                            monitor.setNote("Loaded "+progress+" files");
                        }
                    }
                };
                increaseButton.addActionListener(increaseActionListener);
                frame.add(increaseButton);

                // define automatic increase button
                // start timer to increase progress by 3 every 250 ms
                JButton autoIncreaseButton = new JButton("Automatic Increase");
                ActionListener autoIncreaseActionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        if(monitor != null) {
                            if(timer == null) {
                                timer = new Timer(250, new ProgressMonitorHandler());
                            }
                            timer.start();
                        }
                    }
                };
                autoIncreaseButton.addActionListener(autoIncreaseActionListener);
                frame.add(autoIncreaseButton);

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

}

ProgressMonitor属性

表9-6显示了ProgressMonitor的八个属性。

Swing_table_9_8.png

Swing_table_9_8.png

millisToDecideToPoppup属性表示监视器在决定是否需要显示弹出窗口前要等待的毫秒数。如果progress属性还没有变化,则监视器会在再一次检测之前等待另一个时间间隔。当ProgressMonitor检测并且发现progress属性已经变化时,他会估计任务是否会在millisToPopup属性的毫秒数之内完成。如果ProgressMonitor认为所的任务会及时完成,则不会显示弹出窗口。否则,弹出窗口会在任务开始时刻的millisToPopup毫秒之后显示。

自定义ProgressMonitor观感

修改ProgressMonitor的外观需要修改JProgressBar以及JLabel的外观,以及ProgressMonitor所用的JOptionPane。

ProgressMonitor只有一个UIResource相关的属性:

  • String类型的ProgressMonitor.progressText

ProgressMonitorInputStream类

ProgressMonitorInputStream类表示一个输入流过滤器,这个输入流过滤器使用ProgressMonitor来检测一个输入流的读取。如果读取需要较长的时间完成,则会显示ProgressMonitor,且用户可以选择弹出窗口中的Cancel按钮,从而使得读取被中断并且输入流会抛出一个InterruptedIOException。

创建ProgressMonitorInputStream

类似于其他的过滤器流,ProgressMonitorInputStream是使用一个到需要过滤的流的引用来创建的。除了到这个过滤器的引用,ProgressMonitorInputStream的构造函数还需要其ProgressMonitor的两个参数:父组件以及一个消息。正如在这里所看到的,构造函数首先需要ProgressMonitor参数:

public ProgressMonitorInputStream(
  Component parentComponent, Object message, InputStream inputStream)

与JOptionPane与ProgressMonitor类似,消息参数是一个Object,而不是一个String,所以我们可以在多行上显示一个组件数组或是字符串。下面的代码创建了一个ProgressMonitorInputStream。

FileInputStream fis = new FileInputStream(filename);
ProgressMonitorInputStream pmis =
  new ProgressMonitorInputStream(parent, "Reading " + filename, fis);

使用ProgressMonitorInputStream

与所有的输入流一样,一旦我们创建了ProgressMonitorInputStream,我们需要由其中进行读取。如果输入流的读取不够快,底层的ProgressMonitor会使得进程弹出窗口显示。一旦这个窗口显示,用户可以监视进程或是通过选择Cancel按钮关闭读取。如果Cancel按钮被选中,则InterruptedIOException会被抛出,而异常的bytesTransferred或会被设置为已经成功读取的字节数。

图9-7显示了一个ProgressMonitorInputStream弹出窗口的样子。略为不同的是,弹出窗口在消息区域使用两个JLabel组件,而不是一个。

Swing_9_17.png

Swing_9_17.png

列表9-6显示了完整的源代码示例。其中粗体显示的代码行是使用ProgressMonitorInputStream的关键。他们设置对话框的消息并且创建输入流。程序使用一个由命令行指定的文件名,读取文件,并且将文件拷贝到标准输出。如果文件足够大,进程监视器将会显示。如果我们按下Cancel按钮,读取停止,并且Canceled会被输出到标准错误。

package swingstudy.ch09;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;

import javax.swing.JLabel;
import javax.swing.ProgressMonitorInputStream;

public class ProgressInputSample {

    public static final int NORMAL = 0;
    public static final int BAD_FILE = 1;
    public static final int CANCELED = NORMAL;
    public static final int PROBLEM = 2;

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

        int returnValue = NORMAL;
        if(args.length != 1) {
            System.err.println("Usage:");
            System.err.println("java ProgressInputSample filename");
        }
        else {
            try {
                FileInputStream fis = new FileInputStream(args[0]);
                JLabel filenameLabel = new JLabel(args[0], JLabel.RIGHT);
                Object message[] = {"Reading:", filenameLabel};
                ProgressMonitorInputStream pmis = new ProgressMonitorInputStream(null, message, fis);
                InputStreamReader isr = new InputStreamReader(pmis);
                BufferedReader br = new BufferedReader(isr);
                String line;
                while((line = br.readLine()) != null) {
                    System.out.println(line);
                }
                br.close();
            }
            catch(FileNotFoundException exception) {
                System.err.println("Bad File "+exception);
                returnValue = BAD_FILE;
            }
            catch(InterruptedIOException exception) {
                System.err.println("Canceled");
                returnValue = CANCELED;
            }
            catch(IOException exception) {
                System.err.println("I/O Exception "+exception);
                returnValue = PROBLEM;
            }
        }
        System.exit(returnValue);
    }

}

ProgressMonitorInputStream属性

表9-7显示了ProgressMonitorInputStream的属性。ProgressMonitor在输入流创建时创建。我们不需要修改ProgressMonitor。然而我们也许需要在弹出窗口显示之前提供一个或长或短的时延(ProgressMonitor的millisToDecideToPopup属性)。

Swing_table_9_9.png

Swing_table_9_9.png

JColorChooser类

我们可以将JColorChooser认为是一个只可以输入的JOptionPane,其输入域要求我们选择一种颜色。与JOptionPane类似,JColorChooser也仅是位于窗口中的一堆组件,而并不是一准备好用来使用的弹出窗口。图9-18显示在了我们自己的程序窗口中JColorChooser的样子。在顶部是三个可选择的颜色选择面板;在底部是一个预览面板。其中“I Love Swing”并不是选择器的一部分,而包含选择器的程序所有的。

Swing_9_18.png

Swing_9_18.png

除了可以在我们的程序窗口显示以外,JColorChooser同时也为自动放置在JDialog的组件集合中提供了支持方法。图9-19显示了一个以这种方式自动创建的弹出窗口。

Swing_9_19.png

Swing_9_19.png

为了支持这种行为,JColorChooser需要位于javax.swing.colorchooser包中的一些支持类的帮助。JColorChooser的数据模型是ColorSelectionModel接口的一种实现。javax.swing.colorchooser包提供了DefaultColorSelectionModel类作为ColorSelectionModel接口的实现。对于用户界面,JColorChooser依赖ColorChooserComponentFactory来创建选择颜色的默认面板。这些面板是AbstractColorChooserPanel类的特殊子类,如果我们不喜欢默认的集合,我们也可以自己创建。

默认情况下,当在一个JColorChooser中有有多个选择器面板时,每一个面板显示在JTabbedPane的一个标签上。然而,ColorChooserUI可以以他要求的任何方式处理多个面板。

创建JColorChooser

如果我们希望创建一个JColorChooser,并且将其放在我们自己的窗口,我们可以使用下列JColorChooser类的三个构造函数中的一个:

public JColorChooser()
JColorChooser colorChooser = new JColorChooser();

public JColorChooser(Color initialColor)
JColorChooser colorChooser =
  new JColorChooser(aComponent.getBackground());

public JColorChooser(ColorSelectionModel model)
JColorChooser colorChooser = new JColorChooser(aColorSelectionModel);

默认情况下,选择器的初始颜色为白色。如果我们不希望白色作为默认颜色,我们可以使用Color对象或是ColorSelectionModel来提供初始颜色。

使用JColorChooser

一旦我们应用构造函数创建了一个JColorChooser,我们可以将其放在任何容器中,类似于其他的组件。例如,列表9-7所示的源码创建了一个前面图9-18所示的GUI。

package swingstudy.ch09;

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

import javax.swing.BorderFactory;
import javax.swing.JColorChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;

public class ColorSample {

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

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

                final JLabel label = new JLabel("I Love Swing", JLabel.CENTER);
                label.setFont(new Font("Serif", Font.BOLD | Font.ITALIC, 48));
                frame.add(label, BorderLayout.SOUTH);

                final JColorChooser colorChooser = new JColorChooser(label.getBackground());
                colorChooser.setBorder(BorderFactory.createTitledBorder("Pick Foreground Color"));

                frame.add(colorChooser, BorderLayout.CENTER);

                frame.pack();
                frame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

尽管上面的代码创建了GUI,但是在JColorChooser中选择不同的颜色并不会做任何事情。下面我们看一下使得颜色变化的代码。

监听颜色选择变化

JColorChooser使用ColorSelectionModel作为其数据模型。正如下面的接口定义所示,数据模型只包含一个属性,selectedColor,用来管理颜色选择器的状态。

public interface ColorSelectionModel {
  // Listeners
  public void addChangeListener(ChangeListener listener);
  public void removeChangeListener(ChangeListener listener);
  // Properties
  public Color getSelectedColor();
  public void setSelectedColor(Color newValue);
}

当用户改变了JColorChooser中的颜色,selectedColor属性发生变化,并且JColorChooser生成一个ChangeEvent来通知所注册的ChangeListener对象。

所以,要完成前一节中的ColorSample示例,并且当用户修改JColorChooser中的颜色选择时使得标签的前景色发生变化,我们需要向颜色选择器注册一个ChangeListener。这涉及到创建一个ChangeListener并将其添加到ColorSelectionModel中。将列表9-8中所示的代码添加到前面的9-7代码中的相应位置。

ColorSelectionModel model = colorChooser.getSelectionModel();
ChangeListener changeListener = new ChangeListener() {
  public void stateChanged(ChangeEvent changeEvent) {
    Color newForegroundColor = colorChooser.getColor();
    label.setForeground(newForegroundColor);
  }
};
model.addChangeListener(changeListener);

一旦添加了这段代码,这个示例就完成了。运行这个程序会出现图9-18所示的颜色选择器,并且选择一个新的地修改标签的前景色。

创建并显示一个JColorChooser弹出窗口

尽管前面的例子对于如果我们仅是希望在我们程序中包含一个JColorChooser的情况来说已经足够了,但是更多的时候,我们希望JColorChooser在一个单独的弹出窗口中显示。这个窗口看起来像是我们在屏幕上选择一个按钮或者是选择一个菜单项目的结果。为了支持这种行为,JColorChooser包含下列的工厂方法:

public static Color showDialog(Component parentComponent,
  String title, Color initialColor)

当调用这个方法时,showDialog()会使用指定的父组件与标题创建一个模态对话框。在这个对话框中是一个给定了初始颜色值的JColorChooser。正如我们可以在图9-18中所看到的,在底部是三个按钮:OK,Cancel与Rest。当OK按钮被按下时,弹出窗口会消失,而showDialog()方法会返回当前选中的颜色值。当Cancel按钮被按下时,此方法会返回null,而不会返回所选择的颜色值或是初始颜色值。Reset按钮的选择会使得JColorChooser将其所选中的颜色修改为在启动时所提供的初始颜色。

showDialog()方法的通常作用过程是其初始颜色参数是一个对象的某个颜色属性。然后方法调用的返回值变为相同颜色属性的新值。这种模式用法显示在下面的代码行中,其中颜色属性的变化是一个按钮的背景颜色属性值。类似于JOptionPane,null父组件参数会使得弹出窗口位于屏幕的中间,而不是重叠在某个特定的组件之上。

Color initialBackground = button.getBackground();
Color background = JColorChooser.showDialog(
  null, "Change Button Background", initialBackground);
if (background != null) {
  button.setBackground(background);
}

可以将这段代码放在完整的示例程序中,列表9-9显示了这样的一个示例程序,其中提供了一个按钮,当选中时会显示一个JColorChooser。在OK按钮被选中之后颜色选择器中选中的颜色变为按钮的背景色。

package swingstudy.ch09;

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

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

public class ColorSamplePopup {

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

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

                final JButton button = new JButton("Pick to Change Background");

                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        Color initialBackground = button.getBackground();
                        Color background = JColorChooser.showDialog(null, "Change Button Background", initialBackground);
                        if(background != null) {
                            button.setBackground(background);
                        }
                    }
                };

                button.addActionListener(actionListener);

                frame.add(button, BorderLayout.CENTER);

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

        EventQueue.invokeLater(runner);
    }

}

提供我们自己的OK/Cancel事件监听器

如果showDialog()方法提供了太多了自动行为,我们也许会更喜欢另一种JColorChooser方法 ,这种方法允许我们在显示之前自定义JColorChooser并且定义当选择OK与Cancel按钮时的所发生的事件。

public static JDialog createDialog(Component parentComponent, String title,
  boolean modal, JColorChooser chooserPane, ActionListener okListener,
  ActionListener cancelListener)

在createDialog()方法,父组件与标题参数与showDialog()方法相同。modal参数使得弹出窗口可以是非模态的,这与showDialog()不同,后者弹出窗口总是模态的。当弹出窗口是非模态时,用户仍然可以与程序的其他部分交互。弹出窗口中的OK与Cancel按钮有一个相关联的ActionListener,从而在选择之后可以隐藏弹出窗口。如果我们需要选择之后的额外响应,则我们要负责添加我们自己的监听器。

为了演示createDialog()的正确使用,列表9-10中的程序重复了列表9-9中的程序的功能。然而,如果新的背景色与前景色相同,则颜色修改会被拒绝,而不会自动接受新的颜色值。另外,如果用户选择Cancel按钮,按钮的背景色会被设置为红色。

package swingstudy.ch09;

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

import javax.swing.JButton;
import javax.swing.JColorChooser;
import javax.swing.JDialog;
import javax.swing.JFrame;

public class CreateColorSamplePopup {

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

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

                final JButton button = new JButton("Pick to Change Background");

                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        Color initialBackground = button.getBackground();

                        final JColorChooser colorChooser = new JColorChooser(initialBackground);

                        // for okay selection, change button background to selected color
                        ActionListener okActionListener = new ActionListener() {
                            public void actionPerformed(ActionEvent event) {
                                Color newColor = colorChooser.getColor();
                                if(newColor.equals(button.getForeground())) {
                                    System.out.println("Color change rejected");
                                }
                                else {
                                    button.setBackground(newColor);
                                }
                            }
                        };

                        // for cancel selection, change button background to red
                        ActionListener cancelActionListener = new ActionListener() {
                            public void actionPerformed(ActionEvent event) {
                                button.setBackground(Color.RED);
                            }
                        };

                        final JDialog dialog = JColorChooser.createDialog(null, "Change Button Background", true, colorChooser, okActionListener, cancelActionListener);

                        // wait for current event dispatching to complete before showing
                        Runnable showDialog = new Runnable() {
                            public void run() {
                                dialog.setVisible(true);
                            }
                        };

                        EventQueue.invokeLater(showDialog);
                    }
                };

                button.addActionListener(actionListener);
                frame.add(button, BorderLayout.CENTER);

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

        EventQueue.invokeLater(runner);
    }

}

注意,actionPerformed()方法使用EventQueue.invokeLate()方法来显示选择器。当前的事件处理需在显示选择器之前完成。否则,在选择器显示之前,前一个动作的事件处理还没有完成。

JColorChooser属性

表9-8列出了JColorChooser的八个属性的信息,包括color属性的三个数据类型。

Swing_table_9_10.png

Swing_table_9_10.png

color属性比较特殊,因为他有三种设置的方法:

  • 直接由Color设置
  • 由一个使用0xAARRGGBB分配形式将红绿蓝值组合在一起的int变量来设置,其中A表示alpha值(被忽略,使用255替换)
  • 由分别表示红,绿,蓝颜色组件的单独三个int变量设置

如果我们没有使用showDialog(),我们可以在JColorChooser显示之前进行自定义。除了自定义color属性以外,color属性可以在JColorChooser构造函数中设置,我们还可以自定义在预览区域显示的组件以及选择器面板。

修改预览面板

ColorChooserComponentFactory类负责为JColorChooser的预览区域提供默认的组件。对于标准的观感类型,预览面板位于颜色选择器的底部。

如果我们不需要颜色选择器中的预览面板,我们必须将previewPanel属性设置为一个不为null的组件值。当这个属性设置为null时,则会显示观感的默认预览面板。将这个属性设置为一个空的JPanel可以实现不显示预览面板的目的。

colorChooser.setPreviewPanel(new JPanel());

图9-20显示了一个不带预览面板的颜色选择器的样子。因为当JPanel内没有任何内容时,JPanel没有尺寸,这就有效的移除的面板。

Swing_9_20.png

Swing_9_20.png

如果我们希望显示预览面板,但是并不喜欢默认的外观,我们可以向这个区域添加我们自己的JComponent。配置要求我们将我们的新预览面板放在一个带有标题边框的容器内,并且当用户选择一个新的颜色时预览面板的前景色发生变化。

注意,ColorChooserUI实现类(BasicColorChooserUI)中的bug要求额外的步骤来安装预览面板。除了调用setPreviewPanel(newPanel)之外,我们必须设置面板的尺寸与边框,从而使得用户界面正确的配置新的预览面板。

下面的代码演示了使用JLabel作为自定义预览面板。图9-21演示了使用了这种预览面板后JColorChooser的样子。

final JLabel previewLabel = new JLabel("I Love Swing", JLabel.CENTER);
previewLabel.setFont(new Font("Serif", Font.BOLD | Font.ITALIC, 48));
previewLabel.setSize(previewLabel.getPreferredSize());
previewLabel.setBorder(BorderFactory.createEmptyBorder(0,0,1,0));
colorChooser.setPreviewPanel(previewLabel);
Swing_9_21.png

Swing_9_21.png

注意,因为预览面板的前景色的初始设置为其背景色,所以面板看起来是空的。这也就是默认预览面板使用限制的背景色显示文本的原因。

修改颜色选择器面板

JColorChooser上部的各种标签表示AbstractColorChooserPanel实现。每一个标签都允许用户以一种不同的方式选择颜色。默认情况下,ColorChooserComponentFactory提供具有三个面板的JColorChooser(如图9-22):

  • 样本面板允许用户由一个预定义的颜色样本集合中选择颜色,就如同在一个颜料店一样。
  • HSB面板允许用户使用色调饱和度明亮度的颜色模式选择颜色。
  • RGB面板允许用户使用红绿蓝颜色模式选择颜色。
Swing_9_22.png

Swing_9_22.png

如果我们不喜欢默认的选择器面板,或者是我们希望添加其他工作方式不同的颜色选择器面板,我们可以通过继承AbstractColorChooserPanel类来创建我们自己的面板。要将新面板添加到已存在集合中,我们可以调用下面的方法:

public void addChooserPanel(AbstractColorChooserPanel panel)

如果稍后我们决定不再使用新面板,我们可以使用下面的方法来移除:

public AbstractColorChooserPanel removeChooserPanel(AbstractColorChooserPanel panel)

要替换已存在的面板集合,可以调用下面的方法:

setChooserPanels(AbstractColorChooserPanel panels[ ])

创建一个新面板要求我们继承AbstractColorChooserPanel,并且为新面板填充颜色选择的细节。下面的代码行显示了这个类的定义,其中包括了五个抽象方法。这五个方法是我们必须重写的。

public abstract class AbstractColorChooserPanel extends JPanel {
  public AbstractColorChooserPanel();
  protected abstract void buildChooser();
  protected Color getColorFromModel();
  public ColorSelectionModel getColorSelectionModel();
  public int getDisplayMnemonicIndex();
  public abstract String getDisplayName();
  public abstract Icon getLargeDisplayIcon();
  public int getMnemonic();
  public abstract Icon getSmallDisplayIcon();
  public void installChooserPanel(JColorChooser);
  public void paint(Graphics);
  public void uninstallChooserPanel(JColorChooser);
  public abstract void updateChooser();
}

为了演示如何使用颜色选择器面板,下面我们来看一下如何来创建显示Color与SystemColor类中的颜色列表的颜色选择面板。由这个列表中,用户必须选择一个颜色。面板使用JComboBox来表示颜色列表。(JComboBox的使用会在第13章进行详细解释)图9-23显示了完成的面板。面板是使用下面的代码创建并添加的:

SystemColorChooserPanel newChooser = new SystemColorChooserPanel();
AbstractColorChooserPanel chooserPanels[] = {newChooser};
colorChooser.setChooserPanels(chooserPanels);
Swing_9_23.png

Swing_9_23.png

要定义的第一个方法是public String getDisplayName()。这个方法返回一个当存在多个选择器面板时在Tab上显示的文本标签。如果只有一个选择面板,这个名字不会显示。

public String getDisplayName() {
  return "SystemColor";
}

由两个Icon方法返回的值与系统的观感类型没有任何关系。我们可以由这两个方法中返回null,或者是返回与这两个方法无关的Icon来检测。自定义的ColorChooserUI需要使用这两个Icon方法,通常用于在选择器面板Tab页上的图标。

public Icon getSmallDisplayIcon() {
  return new DiamondIcon(Color.BLUE);
}
public Icon getLargeDisplayIcon() {
  return new DiamondIcon(Color.GREEN);
}

protected void buildChooser()方法是由AbstractColorChooserPanel的installChooserPanel()方法在面板被添加到选择器时调用的。我们使用这个方法来向容器添加必要的组件。在示例SystemColorChooserPanel选择器中,这个涉及到创建JComboBox并将其添加到面板。因为AbstractColorChooserPanel是一个JPanel子类,我们就可以使用add()方法添加组合框。组合框必须使用选项进行填充,并且必须安装一个事件处理器用于用户选择组件时的事件处理。事件处理的代码在下面的代码块之后描述。

protected void buildChooser() {
  comboBox = new JComboBox(labels);
  comboBox.addItemListener(this);
  add(comboBox);
}

注意,另外,如果我们选择重写uninstallChooserPanel(JColorChooser enclosingChooser),我们需要最后调用super.uninstallChooserPanel(JColorChooser enclosingChooser),而不是先调用。

当用户修改AbstractColorChooserPanel中的颜色时,面板必须通知颜色变化的ColorSelectionModel。在SystemColorChooserPanel面板中,这等同于用户在JComboxBox中选择一个新的选项。所以,当复选框的值发生变化时,确定与选项等同的Color,然后通知模型相应的变化。

public void itemStateChanged(ItemEvent itemEvent) {
  int state = itemEvent.getStateChange();
  if (state == ItemEvent.SELECTED) {
    int position = findColorLabel(itemEvent.getItem());
    // Last position is bad (not selectable)
    if ((position != NOT_FOUND) && (position != labels.length-1)) {
      ColorSelectionModel selectionModel = getColorSelectionModel();
      selectionModel.setSelectedColor(colors[position]);
    }
  }
}

最后要实现的AbstractColorChooserPanel方法是public void updateChooser()。这个方法也是在启动时由installChooserPanel()方法来调用的。另外,当JColorChooser的ColorSelectionModel发生变化时也会调用这个方法。当updateChooser()方法被调用时,选择器面板必须更新其显示来显示当前被选中的模型颜色。并不是所有的面板都显示当前被选中的是哪一个颜色,所以调用也许会不做任何事情。(系统提供的样品面板就是一个不显示当前颜色的面板。)另外,也有可能当前的颜色在面板上不能显示。例如,在SystemColorChooserPanel中,如果当前选择并不是一个SystemColor或是Color常量,我们可以选择不做任何事情或是显示一些内容来表示自定义的颜色。所以,在updateChooser()实现中,我们需要由ColorSelectionModel中获取当前的颜色,并且修改面板的颜色。实际的设置是通过一个为setColor(Color newValue)的助手方法来实现的。

public void updateChooser() {
  Color color = getColorFromModel();
  setColor(color);
}

setColor(Color newValue)方法简单使用由findColorPosition(Color newColor)返回的位置在一个查询表中查询颜色。

// Change combo box to match color, if possible
private void setColor(Color newColor) {
  int position = findColorPosition(newColor);
  comboBox.setSelectedIndex(position);
}

findColorLabel(Object label)与findColorPosition(Color newColor)的细节将会在稍后的列表9-11的完整源码中进行显示。

如果我们不使用显示选择器弹出容器的showDialog()方法,一旦选择器面板已经被定义,而且我们已经创建了一个选择器面板,他可以被通过addChooserPanel()方法放入JColorChooser中。

AbstractColorChooserPanel newChooser = new SystemColorChooserPanel();
colorChooser.addChooserPanel(newChooser);

在显示JColorChooser并且选择相应的Tab页之后,我们的新选择器就可以使用,如图9-24所示。

Swing_9_24.png

Swing_9_24.png

SystemColorChooserPanel的完整源码显示在列表9-11中。这个程序应该使用ComboBoxModel来将示例的labels与colors数组存储在一个数据模型中。然而,使用JComboBox的MVC功能的复杂性将会在第13章讨论。我们可以自由的修改示例从而为JComboBox或者是其他可用的集合API类使用合适的数据模型。

package swingstudy.ch09;

import java.awt.Color;
import java.awt.SystemColor;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;

import javax.swing.Icon;
import javax.swing.JComboBox;
import javax.swing.colorchooser.AbstractColorChooserPanel;
import javax.swing.colorchooser.ColorSelectionModel;

import swingstudy.ch04.DiamondIcon;

public class SystemColorChooserPanel extends AbstractColorChooserPanel implements ItemListener{

    private static int NOT_FOUND = -1;

    JComboBox comboBox;
    String labels[] = {
        "BLACK",
        "BLUE",
        "CYAN",
        "DARK_GRAY",
        "GRAY",
        "GREEN",
        "LIGHT_GRAY",
        "MAGENTA",
        "ORANGE",
        "PINK",
        "RED",
        "WHITE",
        "YELLOW",
        "activeCaption",
        "activeCaptionBorder",
        "activeCaptionText",
        "control",
        "controlDkShadow",
        "controlHighlight",
                "controlLtHighlight",
        "controlShadow",
        "controlText",
        "desktop",
        "inactiveCaption",
        "inactiveCaptionBorder",
        "inactiveCaptionText",
        "info",
        "infoText",
        "menu",
        "menuText",
        "scrollbar",
        "text",
        "textHighlight",
        "textHighlightText",
        "textInactiveText",
        "textText",
        "window",
        "windowBorder",
        "windowText",
        "<Custom>"
    };

    Color colors[] = {
        Color.BLACK,
        Color.BLUE,
        Color.CYAN,
        Color.DARK_GRAY,
        Color.GRAY,
        Color.GREEN,
        Color.LIGHT_GRAY,
        Color.MAGENTA,
        Color.ORANGE,
        Color.PINK,
        Color.RED,
        Color.WHITE,
        Color.YELLOW,
        SystemColor.activeCaption,
        SystemColor.activeCaptionBorder,
        SystemColor.activeCaptionText,
        SystemColor.control,
        SystemColor.controlDkShadow,
        SystemColor.controlHighlight,
        SystemColor.controlLtHighlight,
        SystemColor.controlShadow,
        SystemColor.controlText,
        SystemColor.desktop,
        SystemColor.inactiveCaption,
        SystemColor.inactiveCaptionBorder,
        SystemColor.inactiveCaptionText,
        SystemColor.info,
        SystemColor.infoText,
        SystemColor.menu,
        SystemColor.menuText,
        SystemColor.scrollbar,
        SystemColor.text,
        SystemColor.textHighlight,
        SystemColor.textHighlightText,
        SystemColor.textInactiveText,
        SystemColor.textText,
        SystemColor.window,
        SystemColor.windowBorder,
        SystemColor.windowText,
        null
    };

    // change combo box to match color, if possible
    private void setColor(Color newColor) {
        int position = findColorPosition(newColor);
        comboBox.setSelectedIndex(position);
    }

    // given a label, find the position of the label in the list
    private int findColorLabel(Object label) {
        String stringLabel = label.toString();
        int position = NOT_FOUND;
        for(int i=0, n=labels.length; i<n; i++) {
            if(stringLabel.equals(labels[i])) {
                position = i;
                break;
            }
        }
        return position;
    }

    // given a color, find the position whose color matches
    // this could result in a position different from original if two are equal
    // since actual color is same, this is considered to be okay
    private int findColorPosition(Color color) {
        int position =  colors.length-1;
        // cannot use equals() to compare Color and SystemColor
        int colorRGB = color.getRGB();
        for(int i=0, n=colors.length; i<n; i++) {
            if((colors[i] != null) && (colorRGB == colors[i].getRGB())) {
                position = i;
                break;
            }
        }
        return position;
    }

    public void itemStateChanged(ItemEvent event) {
        int state = event.getStateChange();
        if(state == event.SELECTED) {
            int position = findColorLabel(event.getItem());
            // last position is bad(not selectable)
            if((position != NOT_FOUND) && (position != labels.length-1)) {
                ColorSelectionModel selectionModel = getColorSelectionModel();
                selectionModel.setSelectedColor(colors[position]);
            }
        }
    }

    public String getDisplayName() {
        return "SystemColor";
    }

    public Icon getSmallDisplayIcon() {
        return new DiamondIcon(Color.BLUE);
    }

    public Icon getLargeDisplayIcon() {
        return new DiamondIcon(Color.GREEN);
    }

    protected void buildChooser() {
        comboBox =  new JComboBox(labels);
        comboBox.addItemListener(this);
        add(comboBox);
    }

    public void updateChooser() {
        Color color = getColorFromModel();
        setColor(color);
    }
}

列表9-12演示了新的选择器面板的使用。这只是前面9-10中所显示的CreateColorSamplePopup程序的简单修改版本。我们可以取消setChooserPanels()语句的注释,并且注释掉addChooserPanel()调用就可以实现由添加一个面板(如图9-23所示)到替换所有面板(如图9-24所示)的转变。

package swingstudy.ch09;

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

import javax.swing.JButton;
import javax.swing.JColorChooser;
import javax.swing.JDialog;
import javax.swing.JFrame;

public class CustomPanelPopup {

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

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

                final JButton button = new JButton("Pick to Change Background");

                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        Color initialBackground = button.getBackground();

                        final JColorChooser colorChooser = new JColorChooser(initialBackground);
                        SystemColorChooserPanel newChooser = new SystemColorChooserPanel();

                        // AbstractColorchooserPanel chooserPanels[] = {newChooser};
                        // colorChooser.setChooserPanels(chooserPanels);
                        colorChooser.addChooserPanel(newChooser);

                        // for okay button, change button background to selected color
                        ActionListener okActionListener = new ActionListener() {
                            public void actionPerformed(ActionEvent event) {
                                Color newColor = colorChooser.getColor();
                                if(newColor.equals(button.getForeground())) {
                                    System.out.println("Color change rejected");
                                }
                                else {
                                    button.setBackground(newColor);
                                }
                            }
                        };

                        // for cancel button, change button background to red
                        ActionListener cancelActionlistener = new ActionListener() {
                            public void actionPerformed(ActionEvent event) {
                                button.setBackground(Color.RED);
                            }
                        };

                        final JDialog dialog = JColorChooser.createDialog(null, "Change Button Background", true, colorChooser, okActionListener, cancelActionlistener);

                        // wait for current event dispatching to complete before showing
                        Runnable showDialog = new Runnable() {
                            public void run() {
                                dialog.setVisible(true);
                            }
                        };
                        EventQueue.invokeLater(showDialog);
                    }
                };

                button.addActionListener(actionListener);
                frame.add(button, BorderLayout.CENTER);

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

}

使用ColorChooserComponentFactory类

值得关注的一个类就是ColorChooserComponentFactory。通常这个类在幕后工作,而我们也不需要对其进行处理。

然而,如果我们希望移除一个默认的颜色选择器,我们不能使用JColorChooser的public AbstractColorChooserPanel removeChooserPanel(AbstractColorChooserPanel panel)。初始时,JColorChooser的chooserPanels属性为null。当这个属性为null时,默认的ColorChooserUI使用public static AbstractColorChooserPanel[] getDefaultChooserPanels()方法向ColorChooserComponentFactory查询默认面板。所以在我们修改这个属性之前,不会有面板显示。如果我们希望移除一个默认面板,我们必须获取默认数组,将我们希望保持的面板存入一个新数组,然后将选择器的chooserPanel属性修改为新数组。这是额外的工作,但是这可以使得工作完成。

ColorChooserComponentFactory类的另一个方法就是public static JComponent getPreviewPanel()方法,这个方法会在JColorChooser的previewPanel属性为null时获取默认的预览面板。这就是向JColorChooser的setPreviewPanel()方法提供null参数并不会移除预览面板的原因。对于空面板,我们必须提供一个没有尺寸的JComponent。

colorChooser.setPreviewPanel(new JPanel());

自定义JColorChooser观感

JColorChooser的外观几乎与所有已安装的观感类型相同。唯一的区别与每一个观感如何显示内部组件相关,例如JTabbedPane,JLabel,JButton,或是JSlider。修改这些组件的UIResource相关属性可以影响新创建的JColorChooser的外观。另外,表9-9中列出了JColorChooser类用于自定义的39个UIResource相关属性。这些属性中的大多数与显示在各种默认颜色选择面板上的文本标签有关。

Swing_table_9_11_1.png

Swing_table_9_11_1.png

Swing_table_9_11_2.png

Swing_table_9_11_2.png

Swing_table_9_11_3.png

Swing_table_9_11_3.png

Swing_table_9_11_4.png

Swing_table_9_11_4.png

JFileChooser类

Swing组件集合同时提供了用于选择文件名字与目录的选择器:JFileChooser类。这个选择器替换了原始AWT组件集合中使用FileDialog的需要。类似于其他的Swing选择器组件,JFileChooser并没有自动被放入一个弹出窗口中,但是他可以放在我们程序中用户界面的任何地方。图9-25显示了一个具有Metal观感,Ocean主题的JFileChooser,他被自动放在一个模态JDialog中。

Swing_9_25.png

Swing_9_25.png

对JFileChooser提供支持的是javax.swing.filechooser包中的大量类。这些支持类包括用于限制列出在JFileChooser的FileView的文件与目录。FileView控制目录与文件如何列在JFileChooser中。FileSystemView是一个尝试由选择器隐藏文件系统相关的操作系统细节的一个抽象类。Java 2平台提供者将会提供特定操作系统的版本,从而类似列出根分区这样的任务可以实现(使用100%纯Java代码)。

注意,不要混淆javax.swing.filechooser.FileFilter抽象类与java.io.FileFilter接口。尽管功能类似,但是他们是不同的。他们两个共存是因为java.io.FileFilter接口并不存在于Java 1.1运行时中。因为原始的Swing JFileChooser需要同时运行在Java 1.1与Java 2一部 ,选择器需要定义一个替换。除非特别指定,本部分的所有FileFilter引用位于javax.swing.filechooser包中的类。

创建JFileChooser

JFileChooser有六个构造函数:

public JFileChooser()
JFileChooser fileChooser = new JFileChooser();

public JFileChooser(File currentDirectory)
File currentDirectory = new File("."); // starting directory of program
JFileChooser fileChooser = new JFileChooser(currentDirectory);

public JFileChooser(File currentDirectory, FileSystemView fileSystemView)
FileSystemView fileSystemView = new SomeFileSystemView(...);
JFileChooser fileChooser = new JFileChooser(currentDirectory, fileSystemView);

public JFileChooser(FileSystemView fileSystemView)
JFileChooser fileChooser = new JFileChooser(fileSystemView);

public JFileChooser(String currentDirectoryPath)
String currentDirectoryPath = "."; // starting directory of program
JFileChooser fileChooser = new JFileChooser(currentDirectoryPath);

public JFileChooser(String currentDirectoryPath, FileSystemView fileSystemView)
JFileChooser fileChooser = new JFileChooser(currentDirectoryPath, fileSystemView);

默认情况下,显示的起始目录是用户主目录(系统属性user.home)。如果我们希望启动JFileChooser指向其他的目录,这个目录可以使用String或是File对象进行指定。

我们也可以指定一个FileSystemView来指定操作系统顶层目录结构的自定义表示。当没有指定FileSystemView参数时,JFileChooser使用适合于用户操作系统的FileSystemView。

使用JFileChooser

在由构造函数创建JFileChooser之后,我们可以将其放在任何的Container中,因为他是一个JComponent。位于非弹出窗口对象中的JFileChooser对象看起来有一些奇怪,但是这可以使得我们完成一些无需创建新文件选择器的任务。

列表9-13演示了带有两个标签以及一个JFileChooser的简单窗体。注意,这个窗体并没有Open或是Cancel按钮,但是位于FileSystemView区域的按钮是可选择的。

package swingstudy.ch09;

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

import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;

public class FileSamplePanel {

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

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

                final JLabel directoryLabel = new JLabel("");
                directoryLabel.setFont(new Font("Serif", Font.BOLD | Font.ITALIC, 36));
                frame.add(directoryLabel, BorderLayout.NORTH);

                final JLabel filenameLabel = new JLabel("");
                filenameLabel.setFont(new Font("Serif", Font.BOLD | Font.ITALIC, 36));
                frame.add(filenameLabel, BorderLayout.SOUTH);

                JFileChooser fileChooser = new JFileChooser(".");
                fileChooser.setControlButtonsAreShown(false);
                frame.add(fileChooser, BorderLayout.CENTER);

                frame.pack();
                frame.setVisible(true);

            }
        };

        EventQueue.invokeLater(runner);
    }

}

向JFileChooser添加ActionListener

JFileChooser允许我们添加ActionListener对象来监听确认或是关闭动作的选择。确认是双击一个文件;关闭是按下Escape按键。要检测激发了哪一个动作,我们可以检测我们的ActionLister所接收到的ActionEvent的动作命令。其动作命令设置可以为用于文件选择的JFileChooser.APPROVE_SELECTION或是用于按下Escape按键的JFileChooser.CANCEL_SELECTION。

为了完成前面列表9-13中的示例,添加一个ActionListener从而使得我们在用户选择文件时设置两个标签的文本。一旦选择,文本变为当前目录与文件名。一旦按下Escape按键,文本会被清除。列表9-14显示了新的ActionListener。

// create ActionListener
ActionListener actionListener = new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        JFileChooser theFileChooser = (JFileChooser)event.getSource();
        String command = event.getActionCommand();
        if(command.equals(JFileChooser.APPROVE_SELECTION)) {
            File selectedFile = theFileChooser.getSelectedFile();
            directoryLabel.setText(selectedFile.getParent());
            filenameLabel.setText(selectedFile.getName());
        }
        else if(command.equals(JFileChooser.CANCEL_SELECTION)) {
            directoryLabel.setText("");
            filenameLabel.setText("");
        }
    }
};

fileChooser.addActionListener(actionListener);

通过使用这个ActionListener,由选择被激活的角度来说程序是完整的了。图9-26显示了在选中了C:\jdb1.5.0目录下的COPYRIGHT文件之后窗口的样子。

Swing_9_26.png

Swing_9_26.png

在弹出窗口中显示JFileChooser

除了可以将JFileChooser放在我们自己的窗口以外,我们更为通常的是将其放在一个模态JDialog中。依据我们希望在确认按钮上所显示的文本,有三种方法可以实现:

  • public int showDialog(Component parentComponent, String approvalButtonText)
  • public int showOpenDialog(Component parentComponent)
  • public int showSaveDialog(Component parentComponent)

调用这些方法中的任何一个都可以将配置的JFileChooser放在一个模态JDialog中,并且在父组件的中间位置显示对话框。提供一个null父组件参数会将弹出窗口放在屏幕中间。这个方法只有当用户选择确认或是关闭按钮时才会返回。在选择这两个按钮中的一个之后,调用会依据哪一个按钮被选中而返回一个状态值。这个状态值可以是JFileChooser的三个常量之一:APPROVE_OPTION, CANCEL_OPTION或是ERROR_OPTION。

注意,如果用户点击了确认按钮而没有选择任何文件,则CANCEL_OPTION会返回。

为了执行与前面的例子相同的任务,其中是将一个ActionListener关联到JFileChooser,我们可以显示这个对话框并依据返回状态修改标签,而是不依赖于动作命令,如下所示:

JFileChooser fileChooser = new JFileChooser(".");
int status = fileChooser.showOpenDialog(null);
if (status == JFileChooser.APPROVE_OPTION) {
  File selectedFile = fileChooser.getSelectedFile();
  directoryLabel.setText(selectedFile.getParent());
  filenameLabel.setText(selectedFile.getName());
}  else if (status == JFileChooser.CANCEL_OPTION) {
  directoryLabel.setText(" ");
  filenameLabel.setText(" ");
}

使用这一技术,文件选择器将会在另一个窗口中显示,而不是在具有两个标签的窗口中显示。注意,这个版本是由检测前面示例中的String返回值切换到检测int返回值:[if (command.equals(JFileChooser.APPROVE_SELECTION)) versus if (status == JFileChooser.APPROVE_OPTION)].

JFileChooser属性

一旦我们理解了基本的JFileChooser的使用,我们可以通过修改其属性来自定义组件的行为与外观。表9-10显示了JFileChooser的26个属性。

Swing_table_9_12.png

Swing_table_9_12.png

Swing_table_9_12_1.png

Swing_table_9_12_1.png

当使用不同的showDialog()方法时,dialogType属性被自动设置为JOptionPane的三个常量之一:OPEN_DIALOG, SAVE_DIALOG, CUSTOM_DIALOG。如果我们没有使用showDialog(),我们应该依据我们计划使用的对话框类型来设置这个属性。controlButtonsAreShown属性可以使得我们隐藏Open, Save与Cancel按钮。

使用文件过滤器

JFileChooser支持三种过滤其文件与目录列表的方法。前两个涉及到使用FileFilter类,而最后一个涉及到隐藏文件。首先,我们来看一下FileFilter类。

FileFilter是一个抽象类,其工作方式类似于AWT中的FilenameFilter。然而,这个并不使用目录或是文件名的字符串,而是使用File对象。对于每一个要显示的File对象(文件与目录),过滤器决定File是否要显示在JFileChooser中。除了提供一个接受机制,当向用户显示描述时,过滤器同时提供了描述或名字。在下面的类定义的两个方法反映了这种功能:

public abstract class FileFilter {
  public FileFilter();
  public abstract String getDescription();
  public abstract boolean accept(File file);
}

注意,由于这个类的抽象特性,他本应是一个接口,但事实上不是。

为了演示文件过滤器,列表9-15创建了一个可以接受一个文件扩展名数组的过滤器。如果发送给accept()方法的文件是一个目录,则会被自动接受。否则,文件扩展名必须与所提供的数组中的扩展名匹配,而且扩展名前的字符必须是一个句点。对于这个特定的过滤器,比较是大小写敏感的。

package swingstudy.ch09;

import java.io.File;

import javax.swing.filechooser.FileFilter;

public class ExtensionFileFilter extends FileFilter {

    String description;
    String extensions[];

    public ExtensionFileFilter(String description, String extension) {
        this(description, new String[] {extension} );
    }

    public ExtensionFileFilter(String description, String extensions[]) {
        if(description == null) {
            // since no description, use first extension and # of extensions as description
            this.description = extensions[0]+"{ "+extensions.length+"} ";
        }
        else {
            this.description = description;
        }

        // convert array to lowercase
        // don't alter original entries
        this.extensions = (String[])extensions.clone();
        toLower(this.extensions);
    }

    private void toLower(String array[]) {
        for(int i=0, n=array.length; i<n; i++) {
            array[i] = array[i].toLowerCase();
        }
    }

    // ignore case, always accept directories
    // character before extension must be a period
    @Override
    public boolean accept(File file) {
        // TODO Auto-generated method stub
        if(file.isDirectory()) {
            return true;
        }
        else {
            String path = file.getAbsolutePath().toLowerCase();
            for(int i=0, n=extensions.length; i<n; i++) {
                String extension = extensions[i];
                if(path.endsWith(extension) && (path.charAt(path.length()-extension.length()-1)=='.')) {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public String getDescription() {
        // TODO Auto-generated method stub
        return description;
    }

}

使用这个文件过滤器可以使得我们创建并将其关联到JFileChooser。如果我们只是想使得过滤器可以为用户选择,但是并不是默认的初始选择,可以调用public void addChoosableFileFilter(FileFilter filter)。这可以使得默认的接受所有文件的过滤器被选中。相反,如果我们希望过滤参选择器第一次出现时设置,调用public void setFileFilter(FileFilter filter)方法,而文件选择器将会过滤所显示的初始文件集合。

例如,下面的源码将会向文件选择器添加两个过滤器:

FileFilter jpegFilter =
  new ExtensionFileFilter(null, new String[]{ "JPG", "JPEG"} );
fileChooser.addChoosableFileFilter(jpegFilter);
FileFilter gifFilter = new ExtensionFileFilter("gif", new String[]{ "gif"} );
fileChooser.addChoosableFileFilter(gifFilter);

当没有文件过滤器与JFileChooser相关联时,JFileChooser.getAcceptAllFileFilter()中的过滤器会被用来提供一个接受所有文件的过滤器,而这也同样适用于底层操作系统。

图9-27显示了Motif文件选择器中的一个打开的过滤器选择组合框。

Swing_9_27.png

Swing_9_27.png

提示,在使用addChoosableFileFilter()方法添加过滤器之前使用setFileFilter()方法设置FileFilter会使得接受所有文件的过滤器不可用。要恢复这个过滤器,调用setAcceptAllFileFilterUsed(true)方法。另外,我们可以使用resetChoosableFileFilters()方法重新设置过滤器列表。

内建的过滤器并不是FileFilter。他关注于隐藏文件,例如Unix文件系统上以句点(.)开始的文件。默认情况下,隐藏文件并不会显示在JFileChooser中。要允许显示隐藏文件,我们必须将fileHidingEnabled属性设置为false:

aFileChooser.setFileHidingEnabled(false);

提示,当创建javax.swing.filechooser.FileFilter子类时,我们也许会希望新类同时实现java.io.FileFilter接口。要实现这一目的,只需要简单的将implements java.io.FileFilter添加到类定义。这样做是因为javax.swing.filechooser类中的accept()方法的方法签名与接口定义相匹配:public boolean accept(File file)。

选择目录而不选择文件

JFileChooser支持三种选择模式:只选择文件,只选择目录,同时选择文件与目录。fileSelectionMode属性设置决定了选择器的模式。可用的设置是通过三个JFileChooser常量来指定的:FILES_ONLY, DIRECTORIES_ONLY以及FILES_AND_DIRECTORIES。初始时,文件选择器位于JFileChooser.FILES_ONLY模式。要修改模式,只需要调用public void setFileSelectionMode(int newMode)。

除了fileSelectionMode属性,我们可以使用只读的fileSelectionEnabled与directorySelectionEnabled属性来决定文件选择当前所支持的输入类型。

添加附加面板

JFileChooser支持附加组件的添加。这个组件可以加强选择器的功能,包括预览图片或文档,或是播放音频文件。要响应文件选择变化,附加组件应将其自己作为PropertyChangeListener关联到JFileChooser。当JFileChooser.SELECTED_FILE_CHANGED_PROPERTY属性变化时,附加组件发生变化来反映文件选择。图9-28显示了一个图片预览附加组件的样子。配置选择器的附加性就如同设置其他属性一样。

fileChooser.setAccessory(new LabelAccessory(fileChooser));
Swing_9_28.png

Swing_9_28.png

列表9-16显示了显示一个附加图标的Image组件的源码。被选中的图像文件变为JLabel组件的图标。组件执行两个缩放操作来保证图像的维度适合附加组件的尺寸。

package swingstudy.ch09;

import java.awt.Dimension;
import java.awt.Image;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;

import javax.swing.ImageIcon;
import javax.swing.JFileChooser;
import javax.swing.JLabel;

public class LabelAccessory extends JLabel implements PropertyChangeListener {

    private static final int PREFERRED_WIDTH = 125;
    private static final int PREFERRED_HEIGHT = 100;

    public LabelAccessory(JFileChooser chooser) {
        setVerticalAlignment(JLabel.CENTER);
        setHorizontalAlignment(JLabel.CENTER);
        chooser.addPropertyChangeListener(this);
        setPreferredSize(new Dimension(PREFERRED_WIDTH, PREFERRED_HEIGHT));
    }

    @Override
    public void propertyChange(PropertyChangeEvent event) {
        String changeName = event.getPropertyName();
        if(changeName.equals(JFileChooser.SELECTED_FILE_CHANGED_PROPERTY)){
            File file = (File)event.getNewValue();
            if(file != null) {
                ImageIcon icon = new ImageIcon(file.getPath());
                if(icon.getIconWidth() > PREFERRED_WIDTH) {
                    icon = new ImageIcon(icon.getImage().getScaledInstance(PREFERRED_WIDTH, -1, Image.SCALE_DEFAULT));
                    if(icon.getIconHeight() > PREFERRED_HEIGHT) {
                        icon = new ImageIcon(icon.getImage().getScaledInstance(-1, PREFERRED_HEIGHT, Image.SCALE_DEFAULT));
                    }
                }
                setIcon(icon);
            }
        }
    }
}

使用FileSystemView类

FileSystemView类可以访问平台相关的文件系统信息。java.io.File的JDK 1.1版本在这方面比较弱,FileSystemView的出现使得设计FileChooserUI对象更为容易。Swing的FileSystemView类以FileSystemView的包私有子类的方式提供了三个自定义的视图。他们包括对Unix,Windows以及一个通用处理器的支持。

尽管并没有必要来定义我们自己的FileSystemView,这个类提供了一些在JFileChooser环境之外十分有用的特性。要获取特定于用户运行时环境的视图,可以调用public static FileSystemView getFileSystemView()方法。这个类的定义如下:

public abstract class FileSystemView {
  // Constructors
  public FileSystemView();  // Properties
  // Properties
  public File getDefaultDirectory();
  public File getHomeDirectory();
  public File[] getRoots();
  // Class Methods
  public static FileSystemView getFileSystemView();
  // Other Methods
  public File createFileObject(File directory, String filename);
  public File createFileObject(String path);
  protected File createFileSystemRoot(File file);
  public abstract File createNewFolder(File containingDir) throws IOException;
  public File getChild(File parent, String filename);
  public File[] getFiles(File directory, boolean useFileHiding);
  public File getParentDirectory(File file);
  public String getSystemDisplayName(File file);
  public Icon getSystemIcon(File file);
  public String getSystemTypeDescription(File file);
  public boolean isComputerNode(File file);
  public boolean isDrive(File file);
  public boolean isFileSystem(File file);
  public boolean isFileSystemRoot(File file);
  public boolean isFloppyDrive(File file);
  public boolean isHiddenFile(File file);
  public boolean isParent(File folder, File file);
  public boolean isRoot(File file);
  public Boolean isTraversable(File file);
}

注意,isTraversable()方法返回Boolean,而不是boolean。

FileView类

我们将要探讨的JFileChooser的最后一部分就是FileView区域,其中列出了所有的文件名。每一个自定义的观感类型都有其自己的FileView区域类。另外,某些预定义的观感类型,例如Motif,是不可改变的。然而,至少在Metal与Windows文件选择器,我们可以为不同的文件类型自定义图标,或是修改文件的显示名字。

FileView类的五个方法允许我们修改视图中每一个File的名字,图标或是描述(两种形式)。另外,FileView实际上控制一个目录是否是可遍历的,从而使得我们可以以访问控制的弱级别进行编程。不可遍历的目录具有一个不同的默认图标,因为这些目录不能用于文件选择的浏览。

下面是抽象的FileView类的定义:

public abstract class FileView {
  public FileView();
  public String getDescription(File file);
  public Icon getIcon(File file);
  public String getName(File file);
  public String getTypeDescription(File file);
  public Boolean isTraversable(File file);
}

注意,类似FileSystemView,isTraversable()方法返回Boolean,而不是boolean。

自定义FileView需要创建一个子类并重写相应的方法。默认情况下,所有的方法返回null,表明我们并不希望为特定的方法定制自定义行为。

一旦我们定义了五个视图,简单的修改我们JFileChooser的fileView属性:

fileChooser.setFileView(new JavaFileView());

图9-29显示了一个Metal JFileChooser在安装了自定义的FileView之后的外观样式。

Swing_9_29.png

Swing_9_29.png

列表9-17中的JavaFileView类提供了一个FileView实现,这个实现自定义了与Java开发相关的文件的显示,特别是.java, .class, .jar以及.html或是.htm文件。对于这些文件类型中的每一种,一个特殊的图标替换了默认图标显示在文件名旁边。另外,对于Java源文件,显示文件长度。不幸的是,我们不可以修改FileView中的字体或颜色。

package swingstudy.ch09;

import java.awt.Color;
import java.io.File;

import javax.swing.Icon;
import javax.swing.filechooser.FileView;

import swingstudy.ch04.DiamondIcon;

public class JavaFileView extends FileView {

    Icon javaIcon =new DiamondIcon(Color.BLUE);
    Icon classIcon = new DiamondIcon(Color.GREEN);
    Icon htmlIcon = new DiamondIcon(Color.RED);
    Icon jarIcon = new DiamondIcon(Color.PINK);

    public String getName(File file) {
        String filename = file.getName();
        if(filename.endsWith(".java")) {
            String name = filename +" : "+file.length();
            return name;
        }
        return null;
    }

    public String getTypeDescription(File file) {
        String typeDescription = null;
        String filename = file.getName().toLowerCase();

        if(filename.endsWith(".java")) {
            typeDescription = "Java Source";
        }
        else if(filename.endsWith(".class")) {
            typeDescription = "Java Class File";
        }
        else if(filename.endsWith(".jar")) {
            typeDescription = "Java Archive";
        }
        else if(filename.endsWith(".html") || filename.endsWith(".htm")) {
            typeDescription = "Applet Loader";
        }

        return typeDescription;
    }

    public Icon getIcon(File file) {
        if(file.isDirectory()) {
            return null;
        }
        Icon icon = null;
        String filename = file.getName().toLowerCase();
        if(filename.endsWith(".java")) {
            icon = javaIcon;
        }
        else if(filename.endsWith(".class")) {
            icon = classIcon;
        }
        else if(filename.endsWith(".jar")) {
            icon = jarIcon;
        }
        else if(filename.endsWith(".html") || filename.endsWith(".htm")) {
            icon = htmlIcon;
        }

        return icon;
    }
}

自定义JFileChooser观感

每一个可安装的Swing观感提供了不同的JFileChooser外观以及默认的UIResource值集合。图9-30显示了预安装的观感集合,Motif,Windows,以及Ocean的JFileChooser外观。

|Swing\_9\_30\_motif.png| |Swing\_9\_30\_windows.png| |Swing\_9\_30\_ocean.png|

JFileChooser可用的UIResource相关属性集合显示在表9-11中。对于JFileChooser组件,有83个不同的属性。几乎所有的属性都与按钮标签,热键,图标与工具提示文本相关。

|Swing\_table\_9\_13\_1.png| |Swing\_table\_9\_13\_2.png| |Swing\_table\_9\_13\_3.png| |Swing\_table\_9\_13\_4.png| |Swing\_table\_9\_13\_5.png| |Swing\_table\_9\_13\_6.png|

除了80多个的JFileChooser资源以外,另外还有FileView的五个,显示在表9-12中。

Swing_table_9_14.png

Swing_table_9_14.png

小结

在本章中,我们探讨了Swing弹出窗口以及选择器类的细节。除了后动创建一个JDialog并且为其填充必要的部分外,Swing组件集合包含了对多个不同的弹出窗口与选择器类的支持。由JOptionPane开始,我们了解了如何创建信息,问题,以及输入弹出窗口。另外,我们探讨了如何通过使用ProgressMonitor与ProgressMonitorInputStream类来监视需要长时间完成的任务的进程。

在了解了通用的弹出类之后,我们探讨了特殊的Swing颜色以及文件选择器类:JColorChooser与JFileChooser。通过这两个类,我们可以提示用户用于请求输入以及以我们可以想像的更多的方式自定义显示。

现在我们已经对预定义的弹出窗口有一定的了解了,现在是开始第10章LayoutManager类的讨论的时候了。借助于系统布局管理器,我们可以创建更好的用户界面。

布局管理器

在第9章中,我们了解了Swing组件集合中的各种弹出窗口以及选择器类。在本章中,我们将会了解AWT与Swing布局管理器。

然而由于本书关注于Swing组件集合,我们不能仅是简单的使用。我们需要理解AWT与Swing布局管理器。事实上,比起五个Swing布局管理器中的三个,我们更经常使用的是五个AWT布局管理器中的四个。AWT布局管理器是FlowLayout,BorderLayout,GridLayout,CardLayout以及GridBagLayout。Swing布局管理器是BoxLayout,OverlayLayout,ScrollPaneLayout,ViewportLayout以及SpringLayout。还有一个管理器就是JRootPane.RootLayout,我们在第8章中进行描述。

除了布局管理器,我们还有了解一些助手类:GridBagLayout的限制类GridBagConstraints,BoxLayout与OverlayLayout管理器所用的SizeRequirements类,以及与SpringLayout管理器相关联的Spring与SpringLayout.Constraints类。

布局管理器职责

每一个容器,例如JPanel或是Container,都有一个布局管理器。布局管理器定位组件,无论平台或屏幕尺寸。

布局管理器避免了我们自己计算组件位置的需要,这几乎是一个不可完成的任务,因为每一个组件所需要的尺寸依据我们的程序所部署的平台以及当前的观感而不同。甚至对于一个简单的布局,确定组件尺寸并计算绝对位置所需要代码也要几百行,特别是如果我们关注于当用户调整窗口尺寸时所发生的情况,则所需要的代码会更多。布局管理器为我们处理这些事情。他会询问容器中的每一个组件需要多少空间,然后依据所用平台的组件尺寸,可用空间,以及布局管理器的规则在屏幕上尽最好可能来安排组件。

为了确定组件需要多少空间,布局管理器调用组件的getMinimumSize(),getPreferredSize()以及getMaximumSize()方法。这些方法报告一个组件要正确显示所需要的最小,适当,以及最大空间。所以每一个组件必须了解其空间需求。然后布局管理器使用组件的空间需求来调整组件尺寸并在屏幕上进行安排。除了布局管理器的设置之外,我们的Java程序不需要担心平台依赖的位置。

注意,布局管理器会忽略一些组件;并没有布局管理器显示所有内容的要求。例如,使用BorderLayout的Container也许会包含30或40个组件;然而,BorderLayout至多显示其中的五个。类似的,CardLayout也许会管理多个组件,但是每次只显示一个。

除了忽略组件,布局管理器会对组件的最小,适当以及最大尺寸进行所需要的处理。他可以忽略其中任意或是所有的尺寸。布局管理器忽略适当的尺寸也是有道理的,毕竟,更好的方法就是“如果合适,就给我这个尺寸”。然而,布局管理器也可以忽略最小尺寸。有时,并没有合理的选择,因为也许容器并没有足够的空间以组件的最小尺寸来显示。如何处理这种情况则留给布局管理者的判断力。

LayoutManager接口

LayoutManager接口定义了布局Container内的Component对象的管理器的职责。正如在前面所解释的,决定Container中每一个组件的位置与尺寸是布局管理器的职责。我们不要直接调用LayoutManager接口中的方法;对于大部分来说,布局管理器在幕后完成他们的工作。一旦我们创建了LayoutManager对象并且通知容器来使用(通过调用setLayout(manager)),我们就完成了相应的工作。系统会在需要的时候调用布局管理器的相应方法。类似于任意的接口,LayoutManager指定了布局管理器必须实现的方法,但是没有约束LayoutManager如何来完成这些工作。

如果我们要编写一个新的布局管理器,那么LayoutManager接口本身是最重要的。我们先来描述这个接口是因为他是所有的布局管理器所基于的基础。我们也会描述LayoutManager2接口,他会为某些布局管理器使用。

探讨LayoutManager接口

LayoutManager接口由五个方法组成:

public interface LayoutManager {
  public void addLayoutComponent(String name, Component comp);
  public void layoutContainer(Container parent);
  public Dimension minimumLayoutSize(Container parent);
  public Dimension preferredLayoutSize(Container parent);
  public void removeLayoutComponent(Component comp);
}

如果我们要创建我们自己的类来实现LayoutManager,我们必须定义所有的五个方法。正如我们将要看到的,一些方法并不需要做任何事情,但是我们必须包含一个具有相应签名的桩。

addLayoutComponent()方法只有当我们通过调用add(String, Component)或是add(Component, Object)方法添加组件时才会被调用,而不是普通的add(Component)方法。对于add(Component, Object)方法,Object必须是String类型,或者是其他不被调用的。

探讨LayoutManager2接口

对于要求每一个组件来实现其布局管理器限制的布局管理器,可以使用LayoutManager2接口。使用LayoutManager2的布局管理器包括BorderLayout,CardLayout,以及GridBagLayout等。

LayoutManager2具有五个方法:

public interface LayoutManager2 {
  public void addLayoutComponent(Component comp, Object constraints);
  public float getLayoutAlignmentX(Container target);
  public float getLayoutAlignmentY(Container target);
  public void invalidateLayout(Container target);
  public Dimension maximumLayoutSize(Container target);
}

addLayoutComponent()方法会在我们向添加到布局中的组件赋予限制时调用。实际上,这意味着我们通过调用add(Component component, Objectconstraints)或是add(String name, Component component)方法向容器添加组件,而不是通过add(Component component)方法。最终由布局管理器决定什么与限制有关。例如,GridBagLayout使用约束来将一个GridBagConstraints对象关联到所添加的组件,而BorderLayout使用约束将位置(类似于BorderLayout.CENTER)关联到组件。

FlowLayout类

FlowLayout是JPanel的默认布局管理器。FlowLayout以Component的getComponentOrientation()方法所定义的顺序,按行向容器添加组件,在美国以及西欧通常是由左到右。当在一行中不能适应更多的组件时,他开始新的一行,类似于开启文字环绕的字处理器。当容器调整大小时,其中的组件会依据容器的新尺寸重新定位。在FlowLayout管理的容器中的组件会给予其合适的尺寸。如果没有足够的空间,我们就不会看到所有的组件,如图10-1所示。

Swing_10_1.png

Swing_10_1.png

有三个方法用于创建FlowLayout布局管理器:

public FlowLayout()
public FlowLayout(int alignment)
public FlowLayout(int alignment, int hgap, int vgap)

如果没有指定alignment,FlowLayout管理的容器中的组件会位于中间。否则,通过下列的常量来控制设置:

  • CENTER
  • LEADING
  • LEFT
  • RIGHT
  • TRAILING

对于通常的由左到右的方向,LEADING与LEFT是相同的,同样TRAILING与RIGHT也是相同的。对于类似Hebrew的语言则正相反。图10-2显示了几个不同对齐的效果。

Swing_10_2.png

Swing_10_2.png

我们可以以像素为组件之间的水平(hgap)与垂直(vgap)间隔。间隔默认为五像素,除非我们特别指定。如果我们希望组件放置在另一个组件之上,我们也可以指定负间隔。

BorderLayout类

BorderLayout是JFrame,JWindow,JDialog,JInternalFrame以及JApplet内容面板的默认布局管理器。他提供了一种更为灵活的方法来将组件沿窗体边放置。图10-3显示了一个通常的BorderLayout。

Swing_10_3.png

Swing_10_3.png

当使用BorderLayout时,我们为组件添加约束来表明要将组件放在五个位置中的哪一个。如果我们没有指定约束,组件会被添加到中间位置。将多个组件添加到同一个区域只会显示最后一个组件,尽管由技术上来说,其他的组件仍然位于容器内;他们只是没有显示。

有两个构造函数可以用来创建BorderLayout布局管理器:

public BorderLayout()
public BorderLayout(int hgap, int vgap)

与FlowLayout不同,BorderLayout的默认间隔为零像素,意味着组件会紧临着其他组件放置。

当向BorderLayout管理的容器添加组件时所用的约束是BorderLayout类的常量:

  • AFTER_LAST_LINE
  • AFTER_LINE_ENDS
  • BEFORE_FIRST_LINE
  • BEFORE_LINE_BEGINS
  • CENTER
  • EAST
  • LINE_END
  • LINE_START
  • NORTH
  • PAGE_END
  • PAGE_START
  • SOUTH
  • WEST

因为只有五个区域可以添加组件,我们只期望五个常量。与FlowLayout类似,其他的常量用来处理当组件方向相反时的正确放置,或者是水平或者是垂直。对于通常的由左至右,由上到下的方向,通常的值集合如下:

  • AFTER_LAST_LINE, PAGE_END, SOUTH
  • AFTER_LINE_ENDS, LINE_END, EAST
  • BEFORE_FIRST_LINE, PAGE_START_NORTH
  • BEFORE_LINE_BEGINS, LINE_START, WEST
  • CENTER

使用BEFORE与AFTER常量,而不使用NORTH, SOUTH, EAST与WEST常量是推荐做法,尽管所有这些常量都被支持。

我们并不需要指定容器的所有五个区域。北边区域的组件会占据容器顶部的完整宽度。南边的组件同样会占据容器底部的完整宽度。北边与南边区域的高度是所添加组件的合适高度。东边与西边区域的宽度是所包含组件的宽度,而高度则是容器在满足了北边与南边区域的高度需求后剩下的高度。其余的空间指定给中间区域的组件。

将多个组件添加到BorderLayout管理的容器的一个区域的方法就是首先将其添加到不同的容器,然后将他们添加到BorderLayout管理的容器中。例如,如果我们希望将一个标签与文本区域放在Borderlayout管理的容器的北边区域,首先将他们放在另一个BorderLayout管理的容器的西边与中间区域,如下所示:

JPanel outerPanel = new JPanel(new BorderLayout());
JPanel topPanel = new JPanel(new BorderLayout());
JLabel label = new JLabel("Name:");
JTextField text = new JTextField();
topPanel.add(label, BorderLayout.BEFORE_LINE_BEGINS);
topPanel.add(text, BorderLayout.CENTER);
outerPanel.add(topPanel, BorderLayout.BEFORE_FIRST_LINE);

GridLayout类

GridLayout布局管理器是按行与列排列对象的理想选择,其中布局中的每一个单元具有相同的尺寸。组件的添加顺序是由左至右,由上到下。调用setLayout(new GridLayout(3,4))会将当前容器的布局管理器修改为具有三行四列的GridLayout,如图10-4所示。

Swing_10_4.png

Swing_10_4.png

有三个构造函数可以用来创建GridLayout布局管理器:

public GridLayout()
public GridLayout(int rows, int columns)
public GridLayout(int rows, int columns, int hgap, int vgap)

通常,我们可以指定GridLayout管理的容器的网格尺寸。然而,我们可以将行或列的数目设置为零,而布局就会在零设置的方向上无限增长。

注意,如果GridLayout构造函数的行与列都被指定为零,则会抛出运行异常IllegalArgumentException。

实际绘制的行与列的数目要依据容器内的组件数目而定。GridLayout会首先尝试观察所要求的行与列的数目。如果所要求的行数不为零,则列数通过(#组件数+行数-1)/行数来确定。如果我们的要求是零行,则所使用的行数由类似的公式确定:(#组件数+列数-1)/列数。表10-1演示了这种计算。表格中的最后一项特别有趣:如果我们请求一个3x3的网格,但是在布局中只放置4个组件,最终我们实际得到的是一个2x2的网格。如果我们不想要这种惊奇,我们要依据计划添加显示的实际对象数目来确定GridLayout尺寸。

Swing_table_10_1.png

Swing_table_10_1.png

GridBagLayout类

GridBagLayout是布局管理器中最复杂与最灵活的。尽管他看起来像是GridLayout的子类,然而他却是完全不同的一个类。对于GridLayout,元素在矩形网格内排列,并且容器中的每一个元素的尺寸相同。对于GridBagLayout,元素具有不同的尺寸,并且可以位于多行或是多列。

GridBagLayout只有一个无参数的构造函数:

public GridBagLayout()

每一个元素的位置与行为都是通过GridBagConstraints类来指定的。通过正确的约束元素,我们可以指定一个元素所占用的行数与列数,此元素会在额外的屏幕状态可用时增长,以及其他的各种约束。实际的网格尺寸是依据位于GridBagLayout以及GridBagConstraints中的组件数目而定的。例如,图10-5演示了具有七个组件,排为3x3网格的GridBagLayout。

Swing_10_5.png

Swing_10_5.png

注意,使用GridBagLayout的最大屏幕容量为512行x512列。这是通过布局管理器中受保护的MAXGRIDSIZE常量来指定的。

用来创建图10-5的代码显示在列表10-1中。

package swingstudy.ch10;

import java.awt.Component;
import java.awt.Container;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;

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

public class GridBagButtons {

    private static final Insets insets = new Insets(0,0,0,0);

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

        Runnable runner = new Runnable() {
            public void run() {
                final JFrame frame = new JFrame("GridBagLayout");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new GridBagLayout());

                JButton button;

                // row one - three buttons
                button = new JButton("One");
                addComponent(frame, button, 0, 0, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.BOTH);
                button = new JButton("Two");
                addComponent(frame, button, 1, 0, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.BOTH);
                button = new JButton("Three");
                addComponent(frame, button, 2, 0, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.BOTH);

                // row two - two buttons
                button = new JButton("Four");
                addComponent(frame, button, 0, 1, 2, 1, GridBagConstraints.CENTER, GridBagConstraints.BOTH);
                button = new JButton("Five");
                addComponent(frame, button, 2, 1, 1, 2, GridBagConstraints.CENTER, GridBagConstraints.BOTH);

                // row three - two buttons
                button = new JButton("Six");
                addComponent(frame, button, 0, 2, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.BOTH);
                button = new JButton("Seven");
                addComponent(frame, button, 1, 2, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.BOTH);

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

        EventQueue.invokeLater(runner);
    }

    private static void addComponent(Container container, Component component, int gridx, int gridy,
            int gridwidth, int gridheight, int anchor, int fill) {
        GridBagConstraints gbc = new GridBagConstraints(gridx, gridy, gridwidth, gridheight, 1.0, 1.0, anchor, fill, insets, 0, 0);
        container.add(component, gbc);
    }

}

列表10-1中的大部分工作都是通过助手方法addComponent()来完成的,他为添加到容器中的组件创建了约束集合。

GridBagLayout的行与列

为了帮助我们理解GridBagLayout中的组件的网格,图10-6显示了布局管理器如何计数网格单元。布局中左上角的单元位置为(0,0)。这样对于按钮1,2,3,6与7的位置就不觉得奇怪了。这些按钮中的每一个占据布局3x3网络听 一个区域。按钮4占据一个2x1的区域;其位置为(0,1),所以占据的网格单元还要加上(1,1)。类似的,按钮5占据1x2区域,因而占据(2,1)与(2,2)单元。布局的总尺寸是由位于其中的组件及其约束来决定的。

Swing_10_6.png

Swing_10_6.png

GridBagConstraints类

布局管理器的神奇之处是由传递给添加到容器的每一个组件的不同GridBagConstraints对象来控制的。每一个都指定如何显示一个特定的组件。与大多数其他的布局管理器具有内建的处理显示的方法不同,GridBagLayout是一个空白。关联到组件的约束通知布局管理器如何构建其显示。

每一个添加到GridBagLayout容器中的组件都应有一个与其相关联的GridBagConstraints对象。当对象首先被添加到布局时,会为其指定一个默认的约束集合(如表10-2)。调用container.add(Component, GridBagConstraints)或是gridBagLayout.setConstraints(GridBagConstraints)会为组件应用新的约束集合。

GridBagConstraints具有两个构造函数:

public GridBagConstraints()
public GridBagConstraints(int gridx, int gridy, int gridwidth, int gridheight,
  double weightx, double weighty, int anchor, int fill, Insets insets, int ipadx,
  int ipady)

使用GridBagConstraints无参数构造函数会应用表10-2中的默认设置。我们可以保持单个的设置不变并且只设置单个域。所有的属性都是公开的,没有gettter方法。尽管我们可以盲目的将所有的约束传递给GridBagConstraints构造函数,但是最好是单独描述不同的域。

组件停靠

anchor变量指定了如果组件比可用的空间小时组件偏移的方向。默认情况下为CENTER。绝对值可以为NORTH, SOUTH, EAST, WEST, NORTHEAST, NORTHWEST, SOUTHEAST以及SOUTHWEST。相对值可以为PAGE_START, PAGE_END, LINE_START, LINE_END, FIRST_LINE_START, FIRST_LINE_END, LAST_LINE_START以及LAST_LINE_END。

组件调整尺寸

fill值控制组件的尺寸调整策略。如果fill值为NONE(默认情况),布局管理器会尝试为组件指定最优的尺寸。如果fill为VERTICAL,如果有可用的空间则在高度上调整。如果fill为HORIZONTAL,则在宽度上调整。如果fill为BOTH,布局管理器就会利用两个方向上的可用空间。图10-7演示了VERTICAL,HORIZONTAL以及NONE值(通过修改列表10-1中的GridBagConstraints.BOTH设置来生成)。

|Swing\_10\_7.png| |Swing\_10\_7\_1.png|

网格定位

gridx与gridy变量指定了这个组件将要被放置的网格位置。(0,0)指定了屏幕原点的单元。gridwidth与gridheight变量指定了组件所占用的行数(gridwidth)以及列数(gridheight)。表10-3显示了前面图10-5中所示示例的gridx, gridy, gridwidth以及gridheight。

Swing_table_10_3.png

Swing_table_10_3.png

指定位置时并不是必须设置gridx与gridy。如果我们将这些域设置为RELATIVE(默认情况),系统会为我们计算位置。依据Javadoc注释,如果gridx为RELATIVE,则组件显示在最后一个添加到布局中的组件的右边。如果gridy为RELATIVE,组件会出现最后添加到布局中的组件的下边。然而,这是一种会给人造成误解的简单。如果我们是沿一行或一列添加组件,则RELATIVE工作得最好。在这种情况下,有四种可能的位置:

  • 如果gridx与gridy都是RELATIVE,则组件会被放置在一行。
  • 如果gridx是RELATIVE而gridy是常量,组件会被放置在一行,位于前一个组件的右边。
  • 如果grix是常量而gridy为RELATIVE,则组件会被放置在一列,位于前一个组件的下边。
  • 当设置其他域RELATIVE时变化gridx与gridy会开始新的一行显示,将组件作为新行的第一个元素。

如果gridwidth或是gridheight被设置为REMAINDER,组件将是行或是列中占用剩余空间的最后一个元素。例如,对于表10-3中最右边列的组件,gridwidth值可以为REMAINDER。类似的,对于位于底部行中的组件gridheight也可以设置为REMAINDER。

gridwidth与gridheight的值也可以是RELATIVE,这会强制组件成为行或是列中相邻最后一个组件的组件。我们回头看一下图10-5,如要按钮六的gridwidht为RELATIVE,则按钮七不会显示,因为按钮五是行中的最后一项,而按钮六已经紧邻最后一个了。如果按钮五的gridheight为RELATIVE,则布局管理器会保留其下面的空间,从而按钮可以紧邻列中的最后一项。

填充

insets值以像素指定了组件周围的外部填充(组件与单元格边或是分派给他的单元格之间的空间)。Insets对象可以为组件的顶部,底部,左边或是右边指定不同的填充。

ipadx与ipady指定了组件的内部填充。ipadx指定到组件右边与左边的额外空间(所以最小宽度增加了2xipadx像素)。ipady指定了组件上部与下部的额外空间(所以最小宽度增加了2xipady像素)。insets(外部填充)与ipadx/ipady(内部填充)之间的区别会令人疑惑。insets并没有为组件本身添加空间;他们是组件的外部。ipadx与ipady改变了组件的最小尺寸,所以他们向组件本身添加了空间。

weight

weightx与weighty描述了如何分布容器内的额外空间。他使得我们可以在用户调整容器尺寸或是容器略大时控制组件如何增长(或缩小)。

如果weightx为0.0,则该组件不会获得该行上的额外可用空间。如果一行中的一个或是多个组件具有一个正的weightx,则额外空间会在这些组件之间按比例分配。例如,如果一个组件的weightx值为1.0,而其他的组件全部为0.0,则该组件会获得全部的额外空间。如果一行中的四个组件每一个的weightx的值都为1.0,而其他组件的weightx的值为0.0,则四个组件中的每一个会获得额外空间的四分之一。weighty的行为与weightx类似,但是作用在另一个方向上。因为weightx与weighty控制一行或一列中额外空间的分配,为一个组件进行设置也许会影响其他组件的位置。

CardLayout类

CardLayout布局管理器与其他的布局管理器十分不同。其他的布局管理器尝试一次显示容器中的所有组件,而CardLayout一次只显示一个组件。这个组件可以是一个组件或是一个容器,而后者会让我们看到布局在基于嵌入容器的布局管理器之上的多个组件。

现在可以使用JTabbedPane组件了(会在下一章描述),CardLayout很少使用。

BoxLayout类

Swing的BoxLayout管理允许我们在我们自己的容器中在一个水平行或一个垂直列中布局组件。除了在我们自己的容器中使用BoxLayout,Box类(在下一章描述)提供了一个使用BoxLayout作为其默认布局管理器的容器。

相对于FlowLayout或GridLayout,使用BoxLayout的好处在于BoxLayout会考虑到每一个组件的x与y对齐属性及其最大尺寸。比起GridBagLayout,BoxLayout的使用要容易得多。图10-8显示了使用BoxLayout的样子。而在前面的GridBagLayout使用中,我们需要配置必须的布局约束来使得GridBagLayout达到类似的效果。

Swing_10_8.png

Swing_10_8.png

创建BoxLayout

BoxLayout只有一个构造函数:

public BoxLayout(Container target, int axis)

构造函数需要两个参数。第一个参数是布局管理器实例要关联到的容器,而第二个是布局方向。可用的方法有:用于由左到至右布局的BoxLayout.X_AXIS与由上到下布局的BoxLayout.Y_AXIS。

注意,尝试将坐标设置为其他的值会抛出AWTError。如果布局管理器关联的容器并不是传递给构造函数的容器,则在布局管理器尝试布局其他的容器时抛出AWTError。

一旦我们创建了BoxLayout实例,我们可以将这个布局管理器关联到容器,类似于我们使用其他的布局管理器。

JPanel panel = new JPanel();
LayoutManager layout = new BoxLayout (panel, BoxLayout.X_AXIS);
panel.setLayout(layout);

与所有其他的系统提供的布局管理器不同,BoxLayout与容器是双向绑定在一起的,由管理器到容器同时也由容器到管理。

提示,我们在第11章将要描述的Box类可以使得我们一步创建容器并设置其布局管理器。

布局组件

一旦我们将容器的布局管理器设置为BoxLayout,我们就完成了直接使用布局管理器所需要做的全部工作。向容器添加组件可以通过add(Component component)或是add(Component component, int index)方法来实现。尽管BoxLayout实现了LayoutManager2接口,意味着约束的使用,但是当前并没有使用。所以,并不是必须使用add(Component component, Object constraints)。

当需要布局容器时,BoxLayout会完成其他。BoxLayout管理会尝试满足容器中组件的最小与最大尺寸以及x坐标与y坐标对齐。对齐值的范围由0.0f到1.0f。(对齐设置是浮点常量,而不是双精度,所以需要使用f。)

默认情况下,所有的Component子类都有一个Component.CENTER_ALIGNMENT的x坐标对齐以及Component.CENTER_ALIGNMENT的y坐标对齐。然而,所有的AbstractButton子类与JLabel都有一个默认的Component.LEFT_ALIGNMENT的x坐标对齐。表10-4显示了Component中可用的组件属性常量,这些属性可以通过setAlignmentX(float newValue)或是setAlignmentY(float newValue)方法进行设置。不同的对齐工作方式相同,除非是在不同的方向上。在水平对齐的情况下,这类似于左对齐,中间对齐或右对齐调整段落的情况。

Swing_table_10_4.png

Swing_table_10_4.png

使用相同的对齐布局组件

BoxLayout管理器依据被管理的容器内的组件的对齐而进行不同的处理。如果所有的对齐都相同,则最大尺寸小于容器尺寸的组件会依据对齐设置进行对齐。例如,如果我们有一个使用垂直BoxLayout的宽区域,而在其内是小按钮,则水平对齐将会左对齐,中间对齐或是右对齐调整按钮。图10-9显示了调整的样子。

Swing_10_9.png

Swing_10_9.png

在这里演示的关键点在于如果所有的组件共享相同的对齐设置,则被管理的容器内的所有组件的实际对齐就是组件对齐设置。

列表10-2显示在生成图10-9的源码。

package swingstudy.ch10;

import java.awt.Component;
import java.awt.Container;
import java.awt.EventQueue;
import java.awt.FlowLayout;

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

public class YAxisAlignX {

    private static Container makeIt(String title, float alignment) {
        String labels[] = {"--", "----", "------", "--------"};

        JPanel container = new JPanel();
        container.setBorder(BorderFactory.createTitledBorder(title));
        BoxLayout layout = new BoxLayout(container, BoxLayout.Y_AXIS);
        container.setLayout(layout);

        for(int i=0, n=labels.length; i<n; i++) {
            JButton button = new JButton(labels[i]);
            button.setAlignmentX(alignment);
            container.add(button);
        }
        return container;
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

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

                Container panel1 = makeIt("Left", Component.LEFT_ALIGNMENT);
                Container panel2 = makeIt("Center", Component.CENTER_ALIGNMENT);
                Container panel3 = makeIt("Right", Component.RIGHT_ALIGNMENT);

                frame.setLayout(new FlowLayout());
                frame.add(panel1);
                frame.add(panel2);
                frame.add(panel3);

                frame.pack();
                frame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

当所有组件具有相同的垂直对齐时,x坐标的BoxLayout的作用类似。组件将会显示在容器的顶部,中部或是底,而不是左对齐,中间对齐或是右对齐调整。图10-10显示了这种外观。

Swing_10_10.png

Swing_10_10.png

显示在图10-10中的示例源码只需要对列表10-2的代码进行小量的修改即可。列表10-3提供了完整的代码。

package swingstudy.ch10;

import java.awt.Component;
import java.awt.Container;
import java.awt.EventQueue;
import java.awt.GridLayout;

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

public class XAxisAlignY {

    private static Container makeIt(String title, float alignment) {
        String labels[] = {"-", "-", "-"};

        JPanel container = new JPanel();
        container.setBorder(BorderFactory.createTitledBorder(title));
        BoxLayout layout = new BoxLayout(container, BoxLayout.X_AXIS);
        container.setLayout(layout);

        for(int i=0, n=labels.length; i<n; i++) {
            JButton button = new JButton(labels[i]);
            button.setAlignmentY(alignment);
            container.add(button);
        }
        return container;
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

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

                Container panel1 = makeIt("Top", Component.TOP_ALIGNMENT);
                Container panel2 = makeIt("Center", Component.CENTER_ALIGNMENT);
                Container panel3 = makeIt("Bottom", Component.BOTTOM_ALIGNMENT);

                frame.setLayout(new GridLayout(1,3));
                frame.add(panel1);
                frame.add(panel2);
                frame.add(panel3);

                frame.setSize(423, 171);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

使用不同的对齐布局组件

使用具有相同对齐的小组件是相对简单的。然而,如果BoxLayout管理的容器中的组件具有不同的对齐,事情就会变得更为复杂。另外,组件并不是必须按着我们所希望的样子进行显示。对于垂直box,组件的显示按如下方式显示:

  • 如果组件的x对齐设置为Component.LEFT_ALIGNMENT,组件的左边将与容器的中间对齐。
  • 如果组件的x对齐设置为Component.RIGHT_ALIGNMENT,组件的右将会与容器的中间对齐。
  • 如果组件的x对齐设置为Component.CENTER_ALIGNMENT,组件将会位于容器的中间。
  • 其他的设置值会使得组件被设置相对于容器中间的变化位置上(依据值而不同)。

为了帮助我们理解这种混合的对齐行为,图10-11显示了两个BoxLayout容器。左边的容器具有两个组件,一个为左对齐(标识为0.0的按钮),而另一个为右对齐(标识为1.0的按钮)。在这里我们可以看到右边组件的左边与左边组件的右边相对齐。右边的容器显示了在0.0与1.0对齐设置之间的额外组件放置。每一个按钮的标签表示了其对齐设置。

Swing_10_11.png

Swing_10_11.png

对于水平box,y对齐相对于x坐标上的组件顶部与底部的工作方式类似。

Swing_10_12.png

Swing_10_12.png

布局大组件

到目前为止的示例中,组件的尺寸总是小于可用空间的大小。这些示例演示了Swing组件与原始AWT组件之间的细微区别。Swing组件的默认最大尺寸为组件的最优尺寸。对于AWT组件,默认的最大尺寸为具有Short.MAX_VALUE宽与高的维度。如果前面的例子使用了AWT Button组件而不是Swing的JButton组件,我们就会看到不同的结果。如果我们手动将组件的最大尺寸属性设置为比BoxLayout的屏幕还要宽或高的值,我们也会看到不同的结果。使用AWT组件会使得事情的演示更为容易。

图10-9显示了三个y坐标BoxLayout,容器中的组件共享相同的水平对齐,而每个按钮的最大尺寸是有约束的。如果组件的最大尺寸没有约束,或者是比容器略大,我们就会看到图10-13的结果,其中y坐标BoxLayout容器中有四个具有相同水平对齐的Button组件。注意,组件并没有左对齐,中间对齐或是右对齐,组件会增长以适应可用的空间。

Swing_10_13.png

Swing_10_13.png

如果组件具有不同的对齐以及无限制的最大尺寸,我们就会得到另一种行为。对齐设置不为最小(0,0f)或是最大(1.0f)的组件将会增长以适应整个空间。如果同时指定了最小与最大对齐设置,这两个组件的中间边将会在中间对齐,如图10-14所示。

Swing_10_14.png

Swing_10_14.png

然而,如果只有一个组件的边设置为(0.0或1.0),并且位于一个具有其他对齐设置的容器中,则具有边设置的组件将背离容器中间增长。图10-15显示了这种行为。x坐标BoxLayout容器在不同的水平对齐的情况下作用类似。

Swing_10_15.png

Swing_10_15.png

OverlayLayout类

正如其名字所暗示的,OverlayLayout类用于位于其他组件之上的布局管理。当使用add(Component component)时,我们将组件添加到由OverlayLayout管理器管理的容器中的顺序决定了组件层次。相反,如果我们使用add(Component component, int index),我们可以以任意顺序添加组件。尽管OverlayLayout实现了LayoutManager2接口,类似于BoxLayout,OverlayLayout并没有使用任何约束。

确定组件的二维位置需要布局管理器检测所包含组件的x与y对齐属性。只要组件的x与y对齐属性定义了一个所有组件都可以共享的点,称之为布局管理器的坐标点,那么组件就可以进行放置。如果我们在相应的方向上将对齐值乘以组件的尺寸,我们就可以获得组件的坐标点的所有部分。

在为每一个组件确定了坐标点以后,OverylayLayout管理器计算容器内这个共享点的位置。为了计算这个位置,布局管理器平均组件的不同对齐属性,然后将每一个设置乘以容器的宽度或高度。这个位置就是布局管理器放置坐标点的位置,然后组件可以被放置在这个点上。

例如,假定我们有三个按钮:一个100x100的黑色按钮,其上是一个50x50的灰色按钮,其上是一个25x25的白色按钮。如果每一个按钮的x与y对齐是0.0f,则三个组件的共享坐标点是他们的左上角,而组件位于容器的左上角。如图10-16所示。

如果每个按钮的x与y对齐为1.0f,三个组件的坐标点是他们的右下解,而组件位于容器的右下角。如图10-17所示。

Swing_10_16.png

Swing_10_16.png

Swing_10_17.png

Swing_10_17.png

如果每个按钮的x与y对齐为0.5f,三个组件的坐标点为他们的中间,而且组件位于容器中间。如图10-18所示。

Swing_10_18.png

Swing_10_18.png

所有的组件具有相同的对齐要相对容易理解,但是如果组件具有不同的对齐时会怎么样呢?例如,如果小按钮的x与y对齐为0.0f,而中型按钮的x与y对齐为0.5f,大按钮的x与y对齐为1.0f,那么这些组件如何显示呢?那么首先,布局管理器要计算坐标点。基于每个按钮的特定对齐,坐标点将是小按钮的左上角。容器内的坐标点将会对齐值的平均乘以容器的维度。两个方向上的0,0.5与1的平均将坐标点放在容器的中间。然后由这个位置进行组件的放置与布局,如图10-19所示。

Swing_10_19.png

Swing_10_19.png

当我们设置重叠组件时,要保证组件容器的optimizedDrawingEnabled属性设置为false。这可以保证属性重绘与事件传播。

要尝试OverlayLayout管理器,可以使用列表10-4中的源码。他提供了可选中的按钮来演示变化对齐值的效果。初始时,程序会有将所有组件放在中间。

package swingstudy.ch10;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.LayoutManager;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.OverlayLayout;

public class OverlaySample {

    public static final String SET_MINIMUM = "Minimum";
    public static final String SET_MAXIMUM = "Maximum";
    public static final String SET_CENTRAL = "Central";
    public static final String SET_MIXED = "Mixed";

    static JButton smallButton = new JButton();
    static JButton mediumButton = new JButton();
    static JButton largeButton = new JButton();

    public static void setupButtons(String command) {
        if (SET_MINIMUM.equals(command)) {
            smallButton.setAlignmentX(0.0f);
            smallButton.setAlignmentY(0.0f);
            mediumButton.setAlignmentX(0.0f);
            mediumButton.setAlignmentY(0.0f);
            largeButton.setAlignmentX(0.0f);
            largeButton.setAlignmentY(0.0f);
        } else if (SET_MAXIMUM.equals(command)) {
            smallButton.setAlignmentX(1.0f);
            smallButton.setAlignmentY(1.0f);
            mediumButton.setAlignmentX(1.0f);
            mediumButton.setAlignmentY(1.0f);
            largeButton.setAlignmentX(1.0f);
            largeButton.setAlignmentY(1.0f);
        } else if (SET_CENTRAL.equals(command)) {
            smallButton.setAlignmentX(0.5f);
            smallButton.setAlignmentY(0.5f);
            mediumButton.setAlignmentX(0.5f);
            mediumButton.setAlignmentY(0.5f);
            largeButton.setAlignmentX(0.5f);
            largeButton.setAlignmentY(0.5f);
        } else if (SET_MIXED.equals(command)) {
            smallButton.setAlignmentX(0.0f);
            smallButton.setAlignmentY(0.0f);
            mediumButton.setAlignmentX(0.5f);
            mediumButton.setAlignmentY(0.5f);
            largeButton.setAlignmentX(1.0f);
            largeButton.setAlignmentY(1.0f);
        } else {
            throw new IllegalArgumentException("Illegal Command: " + command);
        }
        // redraw panel
        ((JPanel) largeButton.getParent()).revalidate();
    }

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

        final ActionListener generalActionListener = new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                JComponent comp = (JComponent) event.getSource();
                System.out.println(event.getActionCommand() + " : "
                        + comp.getBounds());
            }
        };

        final ActionListener sizingActionListener = new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                setupButtons(event.getActionCommand());
            }
        };

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

                JPanel panel = new JPanel() {
                    public boolean isOptimizedDrawingEnabled() {
                        return false;
                    }
                };
                LayoutManager overlay = new OverlayLayout(panel);
                panel.setLayout(overlay);

                Object settings[][] = {
                        {"Small", new Dimension(25,25), Color.white},
                        {"Medium", new Dimension(50,50), Color.gray},
                        {"Large", new Dimension(100,100), Color.black}
                };

                JButton buttons[] = {smallButton, mediumButton, largeButton};

                for(int i=0, n=settings.length; i<n; i++) {
                    JButton button = buttons[i];
                    button.addActionListener(generalActionListener);
                    button.setActionCommand((String)settings[i][0]);
                    button.setMaximumSize((Dimension)settings[i][1]);
                    button.setBackground((Color)settings[i][2]);
                    panel.add(button);
                }
                setupButtons(SET_CENTRAL);

                JPanel actionPanel = new JPanel();
                actionPanel.setBorder(BorderFactory.createTitledBorder("Change Alignment"));
                String actionSettings[] = {SET_MINIMUM, SET_MAXIMUM, SET_CENTRAL, SET_MIXED};

                for(int i=0, n=actionSettings.length; i<n; i++) {
                    JButton button = new JButton(actionSettings[i]);
                    button.addActionListener(sizingActionListener);
                    actionPanel.add(button);
                }

                frame.add(panel, BorderLayout.CENTER);
                frame.add(actionPanel, BorderLayout.SOUTH);

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

        EventQueue.invokeLater(runner);
    }

}

SizeRequirements类

BoxLayout与OverlayLayout管理器依赖于SizeRequirements类来决定所包含组件的确切位置。SizeRequirements类包含各种静态方法来协助将组件放在管理器中所需要的计算。布局管理器使用这个类来计算他们组件的x坐标与宽度以及y坐标与高度。每一对值都是单独计算的。如果相关联的布局管理器需要放置的所有属性集合,布局管理器会单独请求SizeRequirements类。

ScrollPanelLayout类

JScrollPane类,将会在第11章描述的一个容器类,使用ScrollPanelLayout管理器。试图在JScrollPane之外使用布局管理器是不可能的,因为布局管理器会检测与布局管理器相关联的容器对象是否是一个JScrollPane实例。查看第11章可以获得关于这个布局管理器的完整描述。

ViewportLayout类

ViewportLayout管理器为JViewport类所使用,JViewport类是我们将会在第11章描述的一个容器类。JViewport同时也可以使用ScrollPaneLayout/JScrollPane组合中。类似于ScrollPaneLayout,ViewportLayout管理器是与其组件紧密结合在一起的,在这种情况下是JViewport类,并且不可以在组件之外使用,除非是在子类中。另外,JViewport类很少用在JScrollPane之外。ViewportLayout管理器及其容器JViewport类,将会在第11章讨论。

SpringLayout类

最新添加到Java布局管理器前端的就是SpringLayout管理器,这是在J2SE 1.4版本中添加的。这个布局管理器可以允许我们将”springs”关联到组件,从而他们可以相对于其他的组件布局。例如,使用SrpingLayout,我们可以将一个按钮与右边框相关联,而不论用户如何调整屏幕尺寸。

SpringLayout管理器依赖于SpringLayout.Constraints进行组件约束。这类似于作为GridBagLayout管理器补充的GridBagConstraints类。添加到容器中的每一个组件都具有相关联的SpringLayout.Constraints。在这一点两种约束类型具有相似性。

我们通常并不需要添加带有约束的组件。相反,我们可以添加组件,然后单独关联约束。并没有什么可以阻止我们向组件添加约束,但是SpringLayout.Constraints并不是一个简单类。他是一个Spring对象集合,每一个Spring对象都是组件的不同约束。我们需要将每一个Spring约束单独添加到SpringLayout.Constraints。我们可以通过在每一个组件的边设置特定的约束来实现这一操作。使用SpringLayout的四个常量EAST, WEST, NORTH与SOUTH,我们可以调用SpringLayout.Constraints的setConstraints(String edge, Spring spring)方法,其中String是四个常量之一。

例如,如果我们希望将一个组件添加到容器的左上方,我们可以设置一个组件尺寸的两个Spring,将其组合在一起,然后通过组合的set方法将组件以容器,如下面的代码所示:

Component left = ...;
SpringLayout layout = new SpringLayout();
JPanel panel = new JPanel(layout);
Spring xPad = Spring.constant(5);
Spring yPad = Spring.constant(25);
SpringLayout.Constraints constraint = new SpringLayout.Constraints();
constraint.setConstraint(SpringLayout.WEST, xPad);
constraint.setConstraint(SpringLayout.NORTH, yPad);
frame.add(left, constraint);

这看起来并不是太复杂,但是当我们需要添加下一个组件,前一个组件的右边或是下边时,事情就会变得更为困难。我们不能仅是将组件添加额外的n个像素。我们必须实际的向前一个组件的边添加填充。要确定前一个组件的边,我们可以使用getConstraint()查询布局管理器,并传递我们所希望的边与组件,例如在layout.getConstraint(SpringLayout.EAST, left)来获得前一个组件右边的位置。由这个位置起,我们可以添加必须的填充,并将其关联到其他组件的边,如下面的代码所示:

Component right = ...;
Spring rightSideOfLeft = layout.getConstraint(SpringLayout.EAST, left);
Spring pad = Spring.constant(20);
Spring leftEdgeOfRight = Spring.sum(rightSideOfLeft, pad);
constraint = new SpringLayout.Constraints();
constraint.setConstraint(SpringLayout.WEST, leftEdgeOfRight);
constraint.setConstraint(SpringLayout.NORTH, yPad);
frame.add(right, constraint);

上面的方法可以工作得很好,但是随着组件数的增加,上面的方法就会变得十分繁琐。要减少其中的步骤,我们可以添加没有约束的组件,然后单独添加,通过SpringLayout的putConstraint()方法连接组件。

public void putConstraint(String e1, Component c1, int pad, String e2,
  Component c2)
public void putConstraint(String e1, Component c1, Spring s, String e2,
  Component c2)

这样,我们不需要查询边并亲自添加填充,putConstraint()调用可以为我们组合这些任务。为了演示,下面的代码片段为右边的组件添加了相同的组件约束,但是所用的是putConstraint()方法而是直接使用SpringLayout.Constraints:

Component left = ...;
Component right = ...;
SpringLayout layout = new SpringLayout();
JPanel panel = new JPanel(layout);
panel.add(left);
panel.add(right);
layout.putConstraint(SpringLayout.WEST, left, 5, SpringLayout.WEST, panel);
layout.putConstraint(SpringLayout.NORTH, left, 25, SpringLayout.NORTH, panal);
layout.putConstraint(SpringLayout.NORTH, right, 25, SpringLayout.NORTH, panel);
layout.putConstraint(SpringLayout.WEST, right, 20, SpringLayout.EAST, left);

为了有助于我们理解SpringLayout的使用,Sun有一个名为The Bean Builder的工作,https://bean-builder.dev.java.nt/。这个工具最初是要用于使用JavaBean组件的工作,但是也可以用于SpringLayout。图10-20显示了通过Java WebStart启动时的样子。

Swing_10_20.png

Swing_10_20.png

每一个组件边的周围有一个四个盒子的集合,分别用于NORTH, SOURTH, EAST与WEST。我们可以由一个盒子拖动箭头并将其连接到其他盒子。这个工具有一些复杂,可以允许我们为spring指定间隔距离,但是在屏幕设计时,屏幕看起来有一些像图20-21。所创建的每一个箭头都被映射到putConstraint()方法的一个特定调用。

Swing_10_21.png

Swing_10_21.png

列表10-5提供了生成图10-21的源码。注意,我们必须直接使用JFrame的内容面板,因为putConstraint()需要这个容器,而是窗体本身。

package swingstudy.ch10;

import java.awt.Component;
import java.awt.Container;
import java.awt.EventQueue;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
import javax.swing.SpringLayout;

public class SpringSample {

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

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("SpringLayout");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                Container contentPane = frame.getContentPane();

                SpringLayout layout = new SpringLayout();
                contentPane.setLayout(layout);

                Component left = new JLabel("Left");
                Component right = new JTextField(15);

                contentPane.add(left);
                contentPane.add(right);

                layout.putConstraint(SpringLayout.WEST, left, 10, SpringLayout.WEST, contentPane);
                layout.putConstraint(SpringLayout.NORTH, left, 25, SpringLayout.NORTH, contentPane);
                layout.putConstraint(SpringLayout.NORTH, right, 25, SpringLayout.NORTH, contentPane);
                layout.putConstraint(SpringLayout.WEST, right, 20, SpringLayout.EAST, left);

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

        EventQueue.invokeLater(runner);
    }

}

小结

本章介绍了AWT的预定义布局管理器FlowLayout,BorderLayout,GridLayout,GridBagLayout与CardLayout,以及Swing的预定义布局管理器BoxLayout,OverlayLayout,ScrollPaneLayout,ViewportLayout与SpringLayout。我们了解了当我们使用一个布局管理器时,例如BoxLayout或OverlayLayout,各种对齐设置如何影响容器内的组件。另外,我们还了解了SizeRequirements类,这是BoxLayout与OverlayLayout所用的类。

在第11章,我们将会了解使用ScrollPaneLayout与ViewportLayout管理器的JScrollPane与JViewport容器,以及其他一些高级Swing容器类。

高级Swing容器

第10章探讨了AWT与Swing中的布局管理器。在本章中,我们将会了解一些构建在这些布局管理器之上的容器以及其他的一些无需布局管理器的容器。

我们的探讨由Box类开始,我们将会发现使用BoxLayout管理器来创建一个单行或单列组件的最好方法。接下来我们会了解JSplitPane容器,他类似于其中只有两个组件的特殊的Box。JSplitPane提供了一个分隔栏,用户可以拖动这个分隔栏来调整组件的大小以满足各自的需求。

然后我们会探讨JTabbedPane容器,其工作方工式类似于一个由CardLayout布局管理器管理的容器,所不同的是容器内建的标签可以使得我们由一个卡片移动到一个卡片。我们也可以使用JTabbedPane创建多个屏幕,属性页对话框用于用户输入。

最后讨论的两个高级Swing容器是JScrollPane与JViewport。这两个组件都提供了在有限的屏幕真实状态之内显示大组件集合的能力。JScrollPane为显示区域添加滚动条,从而我们可以在一个小区域内在大组件周围移动。事实上,JScrollPane使用JViewport来分割本看不见的大组件部分。

下面我们就开始了解第一个容器,Box类。

Box类

作为JComponent类的子类,Box类是借助于BoxLayout管理器创建单行或单列组件的一个特殊Java Container。Box容器的作用类似于JPanel(或Panel),但是具有一个不同的默认布局管理器,BoxLayout。在Box之外使用BoxLayout有一些麻烦,而Box简化了BoxLayout的使用。我们只需三步就可以将BoxLayout管理器与容器相关联:手动创建容器,创建布局管理器,然后将管理器与容器相关联。当我们创建一个Box的实例时,我们一次就执行了这三个步骤。另外,我们可以使用Box的名为Box.Filler的内联类来更好的放置容器内的组件。

创建Box

我们有三种方法来创建Box,一个构造函数以及两个静态工厂方法:

public Box(int direction)
Box horizontalBox = new Box(BoxLayout.X_AXIS);
Box verticalBox   = new Box(BoxLayout.Y_AXIS);
public static Box createHorizontalBox()
Box horizontalBox = Box.createHorizontalBox();
public static Box createVerticalBox()
Box verticalBox   = Box.createVerticalBox();

注意 ,Box类并没有被设计用来作为JavaBean组件使用。在IDE中这个容器的使用十分笨拙。

不经常使用的构造函数需要布局管理器主坐标的方向。这个方向是通过BoxLayout的两个常量来指定的:X_AXIS或Y_AXIS,分别用来创建水平或垂直盒子。我们无需手动指定方向,我们可以简单的通过所提供的工厂方法来创建所需方向的盒子:createHorizontalBox()或createVerticalBox()。

使用JLabel,JTextField与JButton填充一个水平与垂直Box演示了BoxLayout的灵活性,如图11-1所示。

Swing_11_1.png

Swing_11_1.png

对于水平容器,标签与按钮以其最优的宽度显示,因为他们的最大尺寸与最优尺寸相同。文本域使用余下的空间。

在垂直容器中,标签与按钮的尺寸也是他们的最优尺寸,因为他们的最大尺寸依然与他们的最优尺寸相同。文本的高度填充了标签与按钮没有使用的高度,而其宽度与容器的宽度相同。

用于创建图11-1所示屏幕的源码显示在列表11-1中。

package swingstudy.ch11;

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

import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;

public class BoxSample {

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

        Runnable runner = new Runnable() {
            public void run() {
                JFrame verticalFrame = new JFrame("Vertical");
                verticalFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                Box verticalBox = Box.createVerticalBox();
                verticalBox.add(new JLabel("Top"));
                verticalBox.add(new JTextField("Middle"));
                verticalBox.add(new JButton("Bottom"));
                verticalFrame.add(verticalBox, BorderLayout.CENTER);
                verticalFrame.setSize(150, 150);
                verticalFrame.setVisible(true);

                JFrame horizontalFrame = new JFrame("Horizontal");
                horizontalFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                Box horizontalBox = Box.createHorizontalBox();
                horizontalBox.add(new JLabel("Left"));
                horizontalBox.add(new JTextField("Middle"));
                horizontalBox.add(new JButton("Right"));
                horizontalFrame.add(horizontalBox, BorderLayout.CENTER);
                horizontalFrame.setSize(150, 150);
                horizontalFrame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

Box属性

如表11-1所示,Box只有两个属性。尽管布局属性由其父类Container继承了setLayout(LayoutManager)方法,但是如果在Box对象上调用,这个类会抛出一个AWTError。一旦BoxLayout管理器在其构造函数中被设置,那么就能再改变,其方向也不能改变。

Swing_table_11_1.png

Swing_table_11_1.png

使用Box.Filer

Box类具有一个内联类Box.Filler,可以帮助我们创建不可见的组件从而更好的为采用BoxLayout布局管理器的容器内的组件进行位置布局。通过直接操作所创建组件的最小,最大与最优尺寸,我们可以创建可以增长来填充未用的空间或是保持固定尺寸的组件,从而屏幕更为用户所接受。

注意,由技术上来说,Box.Filler的使用并没有局限于使用BoxLayout布局管理器的容器。我们可以将其用在其他任何使用Component的地方。只是组件是不可见的。

我们无需直接使用Box.Filler类,Box类的一些静态方法可以帮助我们创建合适的填充器组件。工厂方法可以使得我们通过类型对组件进行分类,而不是通过最小值,最大值或是最优尺寸进行分类。我们将会在接下来的两节中了解这些方法。

如果我们对类定义感兴趣,Box.Filler的类定义显示如下。类似于Box类,Box.Filler本来也不是作为JavaBean组件来使用的。

public class Box.Filler extends Component implements Accessible {
  // Constructors
  public Filler(Dimension minSize, Dimension prefSize, Dimension maxSize);
  // Properties
  public AccessibleContext getAccessibleContext();
  public Dimension getMaximumSize();
  public Dimension getMinimumSize();
  public Dimension getPreferredSize();
  // Others
  protected AccessibleContext accessibleContext;
  public void changeShape(Dimension minSize, Dimension prefSize, Dimension maxSize);
}

创建扩展区域

如果一个组件具有较小的最小尺寸与最优尺寸,而最大尺寸要大于屏幕尺寸,组件可以在一个或是两个方向上进行扩展以占用容器中组件之间的未用空间。在Box的情况下,或者更确切的说,布局管理器为BoxLayout的容器,扩展出现在布局管理器初始选择的方向上(BoxLayout.X_AXIS或BoxLayout.Y_AXIS)。对于水平的盒子,扩展影响了组件的宽度。对于垂直的盒子,扩展反映在组件的高度上。

通常为这种扩展组件类型指定的名字为胶水(glue)。glue的两种类型为独立于方向的glue与方向相关的glue。下面的Box工厂方法用于创建胶合组件:

public static Component createGlue()
// Direction independent
Component glue = Box.createGlue();
aBox.add(glue);
public static Component createHorizontalGlue();
// Direction dependent: horizontal
Component horizontalGlue = Box.createHorizontalGlue();
aBox.add(horizontalGlue);
public static Component createVerticalGlue()
// Direction dependent: vertical
Component verticalGlue  = Box.createVerticalGlue();
aBox.add(verticalGlue);

一旦我们创建了glue,我们就可以像添加其他的组件一样将其添加到容器中,通过Container.add(Component)或是其他的add()方法。glue可以使得我们在容器内对齐组件,如图11-2所示。

Swing_11_2.png

Swing_11_2.png

我们可以将胶合组件添加到任何其布局管理器考虑到组件的最小尺寸,最大尺寸与最优尺寸的容器中,例如BoxLayout。例如,图11-3演示了当我们将一个胶合组件添加到JMenuBar而在最后一个JMenu之前的样子。因为JMenuBar的布局管理器为BoxLayout(实际上是子类javax.swing.plaf.basic.DefaultMenuLayout),这一操作可以将最后一个菜单推到工具栏的右边,类似于Motif/CDE风格的帮助菜单。

注意,我们推荐避免使用胶合组件的这种功能来设置菜单栏上的菜单。事实上JMenuBar的public void sethelpMenu(JMenu menu)将会实现这种行为而且不会抛出Error。当然,我们中的许多人仍在等待这种操作。

Swing_11_3.png

Swing_11_3.png

创建固定区域

因为胶合组件会扩展来填充可用的空间,如果我们希望组件之间有一段固定的距离,我们需要创建一个固定组件,或strut。当我们这样做时,我们需要指定strut的尺寸。strut可以是二维的,需要我们指定组件的宽度或调试;或者也可以是一维的,需要我们指定宽度或高度。

public static Component createRigidArea(Dimension dimension)
// Two-dimensional
Component rigidArea = Box. createRigidArea(new Dimension(10, 10));
aBox.add(rigidArea);
public static Component createHorizontalStrut(int width)
// One-dimensional: horizontal
Component horizontalStrut = Box. createHorizontalStrut(10);
aBox.add(horizontalStrut);
public static Component createVerticalStrut(int height)
// One-dimensional: vertical
Component verticalStrut   = Box. createVerticalStrut(10);
aBox.add(verticalStrut);

注意,尽管使用createGule()方法创建的方向无关的胶合组件在我们修改容器方向时并没有副作用,然而创建固定区域会在修改坐标时引起布局问题。(想像一下拖动菜单栏)这是因为组件具有一个最小尺寸。使用createRigidArea()方法并不推荐,除非我们确实需要一个二维的空组件。

图11-4显示了一些固定组件。注意,我们可以变化不同的组件之间的固定距离,而且容器最末的固定组件并没有效果。在用户调整屏幕之后,组件之间的固定距离会保持不变,如图11-4所示。

Swing_11_4.png

Swing_11_4.png

JSplitPane类

类似于Box容器,JSplitPane容器允许我们在单行或单列中显示组件。然而Box可以包含任意数量的组件,JSplitPane只可以用来显示两个组件。组件可以变化尺寸并通过一个可移动的分隔栏进行分隔。分隔栏可以使得用户可以通过拖拽分隔栏来调整所包含组件的尺寸。图11-5显示了垂直与水平分割面板,同时显示在移动分隔栏之前与之后的样子。

Swing_11_5.png

Swing_11_5.png

创建JSplitPane

JSplitPane有五个构造函数。通过这些构造函数,我们可以初始化所包含组件对的方向,设置continuousLayout属性或是为容器初始化组件对。

public JSplitPane()
JSplitPane splitPane = new JSplitPane();

public JSplitPane(int newOrientation)
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);

public JSplitPane(int newOrientation, boolean newContinuousLayout)
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true);

public JSplitPane(int newOrientation, Component newLeftComponent,
  Component newRightComponent)
JComponent topComponent = new JButton("Top Button");
JComponent bottomComponent = new JButton("Bottom Button");
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
  topComponent, bottomComponent);

public JSplitPane(int newOrientation, boolean newContinuousLayout,
  Component newLeftComponent, Component newRightComponent)
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true,
  topComponent, bottomComponent);

除非特别指定,默认方向为水平方向。方向可以通过JSplitPane的常量VERTICAL_SPLIT或HORIZONTAL_SPLIT来指定。continuousLayout属性设置瘊定了当用户拖动分隔栏时分隔面板如何响应。当设置为false(默认)时,在拖动时只有分隔符被重绘。当设置为true时,在用户拖拽分隔栏时,JSplitPane会调整尺寸并重绘分隔栏每一边的组件。

注意,如果方向为JSplitPane.VERTICAL_SPLIT,我们可以将上部的组件看作左侧组件,而将下部组件看作右侧组件。

如果我们使用无参数的构造函数,分隔面板内的初始组件集合由按钮组成(两个JButton组件)。其他的两个构造函数显示的设置了初始的两个组件。奇怪的是,其余的两个构造函数默认情况下并没有提供容器内的组件。要添加或修改JSplitPane内的组件,请参看稍后的“修改JSplitPane组件”一节。

JSplitPane属性

表11-2显示了JSplitPane的17个属性。

Swing_table_11_2_1.png

Swing_table_11_2_1.png

Swing_table_11_2_2.png

Swing_table_11_2_2.png

设置方向

除了在构造函数中初始化方向以外,我们可以通过将方向属性修改为JSplitPane.VERTICAL_SPLIT或是JSplitPane.HORIZONTAL_SPLIT来修改JSplitPane方向。如果我们试着将属性修改为非等同的设置,则会抛出IllegalArgumentException。

不推荐在运行时动态修改方向,因为这会使用户感到迷惑。然而,如果我们正在使用可视化开发工具,我们可以在创建JSplitPane之后显示设置方向属性。当没有进行可视化编程时,我们通常在创建JSplitPane时初始化方向。

修改JSplitPane组件

有四个读写属性可以用来处理JSplitPane内组件的不同位置:bottomComponent, leftComponent, rightComponent与topComponent。事实上,这四个属性表示两种内部组件:左边与上部组件是一种;右边与下部组件表示另一种。

我们应该使用与我们的JSplitPane的方向相适应的属性。使用不合适的属性方法会使得程序员的维护生命十分困难。想像一下,在创建用户界面之后,在六个月之后看到如下的代码:

JComponent leftButton = new JButton("Left");
JComponent rightButton = new JButton("Right");
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
splitPane.setLeftComponent(leftButton);
splitPane.setRightComponent(rightButton);

如果我们看一下代码,基于变量名以及setXXXComponent()方法的使用,我们也许会认为屏幕在左边包含一个按钮,而右边也是一个按钮。但是实例化的JSplitPane具有一个垂直方向,所创建的界面如图11-6所示。所用的变量是按钮的标签,而不是他们的位置。

Swing_11_6.png

Swing_11_6.png

如果setTopComponent()与setBottomComponent()方法使用更好的变量名,代码会更容易理解:

JComponent topButton = new JButton("Left");
JComponent bottomButton = new JButton("Right");
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
splitPane.setTopComponent(topButton);
splitPane.setBottomComponent(bottomButton);

移动JSplitPane分隔符

初始时,分隔符显示在上部组件的下面或是左边组件的右边合适尺寸处。任何时候,我们可以通过调用JSplitPane的restToPreferredSizes()方法来重新设置分隔位置。如果我们要编程来定位分隔符,我们可以通过setDividerLocation(newLocation)来修改dividerLocation属性。这个属性可以修改一个int位置,表示距离上部或左边的绝对距离,或者是设置为一个0.0与1.0之间的double值,表示JSplitPane容器宽度的百分比。

注意,如果将属性设置为0.0与1.0范围之外的double值则会抛出IllegalArgumentException。

如果我们要设置分隔符的位置,我们必须等到组件已经被实现。本质上,这就意味着组件必须可见。有多种方法可以实现这一操作,最直接的方法就是向JSplitPane关联一个HierarchyListener,并且监听HierarchyEvent何时变为SHOWING_CHANGED类型。下面的代码片段演示了这一操作,将分隔符位置修改为75%。

HierarchyListener hierarchyListener = new HierarchyListener() {
  public void hierarchyChanged(HierarchyEvent e) {
    long flags = e.getChangeFlags();
    if ((flags & HierarchyEvent.SHOWING_CHANGED) ==
         HierarchyEvent.SHOWING_CHANGED) {
      splitPane.setDividerLocation(.75);
    }
  }
};
splitPane.addHierarchyListener(hierarchyListener);

尽管我们可以使用double值设置dividerLocation属性,我们只会获得了一个标识绝对位置的int值。

调整组件尺寸与使用可扩展的分隔符

对于JSplitPane内的组件调整尺寸存在限制。JSplitPane会考虑到每一个所包含组件的最小尺寸。如果拖动分隔符使得一个组件缩小到小于其最小尺寸,则滚动面板不会允许用户拖动分隔符超过这个最小尺寸。

注意,我们可以编程实现将分隔符放在任意位置,甚至是使得组件小于其最小尺寸。然而这并不是一个好主意,因为组件最小尺寸的存在是有原因的。

如果组件的最小维度对于JSplitPane来说过大,我们需要修改组件的最小尺寸,从而分隔符可以使用组件的空间。对于AWT组件,修改一个标准组件的最小尺寸需要子类派生。对于Swing组件,我们可以简单的通过一个新的Dimension来调用JComponent的setMinimumSize()方法。然而,最小尺寸的设置要合理。如果我们显式的缩小其最小尺寸,组件就不会正常的工作。

有一个更好的方法可以使得一个组件比其他组件占用更多的空间:将JSplitPane的onTouchExpandable属性设置为true。当这个属性为真时,就会为分隔符添加一个图标,从而使得用户可以完全折叠起两个组件中的一个来为另一个组件指定全部的空间。在图11-7的盒子中,图标是一个上下箭头的组合。

图11-7显示了这个图标显示的样子(通过Ocean观感渲染)并且演示了在选择分隔符上的向上箭头来将下部的组件扩展为其全部尺寸时的样子。再一次点击分隔符上的图标会使得组件又回到其先前的位置。点击分隔符上图标以外的位置会将分隔符定位到使得折叠的组件位于其最优尺寸处。

Swing_11_7.png

Swing_11_7.png

注意,并没有较容易的方法来修改扩展分隔符的图标或是修改分隔符如何渲染。这两方面都是通过BasicSplitPaneDivider子类来定义并且在用于特定观感类型的BasicSplitPaneUI子类的createDefaultDivider()方法中创建的。我们可以简单修改分隔符周围的边框,这是一个自定义边框。

lastDividerLocation属性可以使得我们或是系统查询前一个分隔符位置。当用户选择maximizer图标来取消JSplitPane中的一个组件的最小化时,JSplitPane会使用这个属性。

小心,要小心其最小尺寸是基于容器尺寸或是其初始尺寸的组件。将这些属性放置在JSplitPane中也许会要求我们手动设置组件的minimum或是最优尺寸。当用在JSplitPane中时最常引起问题的组件就是JTextArea与JScrollPane。

调整JSplitPane尺寸

如果在JSplitPane中存在其所包含的组件的最优尺寸所不需要的额外空间时,这个空间会依据resizeWeight属性设置进行分配。这个属性的初始设置为0.0,意味着右边或是下边的组件会获得额外的空间。将这个设置修改为1.0会将所有的空间指定给左边或上部的组件。0.5则会在两个组件之间分隔面板。图11-8显示了这些变化的效果。

Swing_11_8.png

Swing_11_8.png

监听JSplitPane属性变化

JSplitPane类定义了下列的常量来帮助监听边界属性的变化:

  • CONTINUOUS_LAYOUT_PROPERTY
  • DIVIDER_LOCATION_PROPERTY
  • DIVIDER_SIZE_PROPERTY
  • LAST_DIVIDER_LOCATION_PROPERTY
  • ONE_TOUCH_EXPANDABLE_PROPERTY
  • ORIENTATION_PROPERTY
  • RESIZE_WEIGHT_PROPERTY

监听用户何时移动分隔符的一个方法就是监听lastDividerLocation属性的变化。列表11-2中的示例将一个PropertyChangeListener关联到JSplitPane,从而显示当前的分隔符位置,当前的最后位置以及前一个最后位置。分隔符上面与下面的组件是OvalPanel类(在第四章中讨论),绘制来填充组件的维度。这个组件有助于演示将continuousLayout属性设置true的效果状态。

package swingstudy.ch11;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JSplitPane;

import swingstudy.ch04.OvalPanel;

public class PropertySplit {

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

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

                // create/configure split pane
                JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
                splitPane.setContinuousLayout(true);
                splitPane.setOneTouchExpandable(true);

                // create top component
                JComponent topComponent = new OvalPanel();
                splitPane.setTopComponent(topComponent);

                // create bottom component
                JComponent bottomComponent = new OvalPanel();
                splitPane.setBottomComponent(bottomComponent);

                // create PropertyChangeListener
                PropertyChangeListener propertyChangeListener = new PropertyChangeListener() {
                    public void propertyChange(PropertyChangeEvent event) {
                        JSplitPane sourceSplitPane = (JSplitPane)event.getSource();
                        String propertyName = event.getPropertyName();
                        if(propertyName.equals(JSplitPane.LAST_DIVIDER_LOCATION_PROPERTY)){
                            int current = sourceSplitPane.getDividerLocation();
                            System.out.println("Current: "+current);
                            Integer last = (Integer)event.getNewValue();
                            System.out.println("Last: "+last);
                            Integer priorLast = (Integer)event.getOldValue();
                            System.out.println("Prior last: "+priorLast);
                        }
                    }
                };
                // attach listener
                splitPane.addPropertyChangeListener(propertyChangeListener);

                frame.add(splitPane, BorderLayout.CENTER);
                frame.setSize(300, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

如下面的示例输出所示,当我们运行前面的程序时,我们会注意到lastDividerLocation属性的变化来反映分隔符的拖动。当用户停止拖动分隔符时,最后设置被设置为dividerLocation属性的前一个设置,而不是用户开始拖动时的初始设置值。当用户拖动分隔符时,当前值变为最后一个值然后变为前一个最后值。

Current: 11 Last: -1 Prior last: 0 Current: 12 Last: 11 Prior last: -1 Current: 12 Last: 12 Prior last: 11 Current: 12 Last: 11 Prior last: 12 Current: 15 Last: 12 Prior last: 11 Current: 15 Last: 15 Prior last: 12 Current: 15 Last: 12 Prior last: 15 Current: 112 Last: 15 Prior last: 12 Current: 112 Last: 112 Prior last: 15 Current: 112 Last: 15 Prior last: 112

注意,PropertyChangeListener并不支持JSplitPane类的BOTTOM, DIVIDER, LEFT, RIGHT与TOP常量。相反,他们是为add(Component component, Object constraints)方法所用的内部约束。

自定义JSplitPane类型

每一个可安装的Swing观感提供了不同的JSplitPane外观以及组件的默认UIResource值集合。图11-9显示了预安装的观感类型集合的JSplitPane容器外观:Motif,Windows以及Ocean。

Swing_11_9.png

Swing_11_9.png

表11-3显示了JSplitPane可用的UIResource相关的属性集合。对于JSplitPane组件,有25个不同的属性,包括3个分隔符特定的属性。

Swing_table_11_3_1.png

Swing_table_11_3_1.png

Swing_table_11_3_2.png

Swing_table_11_3_2.png

JTabbedPane类

JTabbedPane类表示曾经流行的属性页来支持在一个窗口中多个容器的输入或输出,其中每次只显示一个面板。使用JTabbedPane类似于使用CardLayout管理器,所不同的是添加到修改内建卡片的支持。然而CardLayout是一个LayoutManager,而JTabbedPane是一个完全功能的Container。如果我们不熟悉属性页,标签对话框或是标签面板(所有都是相同的事物的不同名字),图11-10显示了一个JDK 1.2版本所带的原始SwingSet Demo中的标签集合。

Swing_11_10.png

Swing_11_10.png

为了有助于JTabbedPane管理哪一个Component被选中,容器的模型是一个SingleSelectionModel接口的实现,或者更确切的说,是一个DefaultSingleSelectionModel实例。(SingleSelectionModel与DefaultSingleSelectionModel在第6章中进行了描述。)

创建JTabbedPane

JTabbedPane只有三个构造函数:

public JTabbedPane()
JTabbedPane tabbedPane = new JTabbedPane();
public JTabbedPane(int tabPlacement)
JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.RIGHT);
public JTabbedPane(int tabPlacement, int tabLayoutPolicy)
JTabbedPane tabbedPane =
  new JTabbedPane(JTabbedPane.RIGHT, JTabbedPane.SCROLL_TAB_LAYOUT);

可配置的选项是用来修改显示哪一个组件的标签位置与当在一个虚拟行中有多个标签时的标签布局策略。默认情况下,标签位于容器的顶部,并且标签数量超过容器宽度时会进行回环形成多行。然而,我们可以使用JTabbedPane的下列常量之一来显式的指定位置:TOP, BOTTOM, LEFT或RIGHT,或者是使用SCROLL_TAB_LAYOUT或WRAP_TAB_LAYOUT来配置布局策略。图11-11使用其他三个标签位置显示了图11-10的屏幕显示。图11-12显示了带有滚动标签布局的屏幕。

Swing_11_11.png

Swing_11_11.png

Swing_11_12.png

Swing_11_12.png

添加与移除标签

一旦我们创建了基本的JTabbedPane容器,我们需要添加构成JTabbedPane页的面板。我们可以使用两种基本方法来添加面板。

如果我们使用JBuilder或是Eclipse的可视化工具来创建我们的界面,用户界面构建器将会使用我们所熟悉的Container的add()方法来添加Component。所添加的面板使用component.getName()作为默认标题。然而,如果我们手动编程我们不应使用各种add()方法。

添加组件或是面板来创建标签更为合适的方法是使用下面列出的addTab()或是insertTab()方法。insertTab()方法中除了组件与位置索引以外,其他的参数可以为空。(传递null作为Component参数会在运行时抛出NullPointerException。)显示的图标与工具提示设置并没有默认值。

• public void addTab(String title, Component component)
• public void addTab(String title, Icon icon, Component component)
• public void addTab(String title, Icon icon, Component component, String tip)
• public void insertTab(String title, Icon icon, Component component, String tip,
int index)

当使用addTab()时,标签被添加到末尾,也就是对于顶部或是底部标签集合来说是最右边的位置,或是对于在左边或右边放置的标签时位于底部,依据组件的方向,也可以是相反的一边。

在创建面板之后,我们可以通过setXXXAt()方法修改一个特定标签的标题,图标,热键,工具提示或是组件:

• public void setTitleAt(int index, String title)
• public void setIconAt(int index, Icon icon)
• public void setMnemonicAt(int index, int mnemonic)
• public void setDisplayedMnemonicIndexAt(int index, int mnemonicIndex)
• public void setToolTipTextAt(int index, String text)
• public void setComponentAt(int index, Component component)

提示,显示的热键索引指向标题中哪一个字符应高亮。例如,如果我们希望title中第二t高亮显示热键,我们可以使用setMnemonicAt()方法将热键字符设置为KeyEvent.VK_T,并使用setDisplayedMnemonicIndexAt()将热键索引设置为2。

另外,我们可以修改一个特定标签的背景色或前景色,允许或是禁止一个特定的标签,或是使用setXXXAt()方法设置不同的禁止图标:

• public void setBackgroundAt(int index, Color background)
• public void setForegroundAt(int index, Color foreground)
• public void setEnabledAt(int index, boolean enabled)
• public void setDisabledIconAt(int index, Icon disabledIcon)

要移除一个标签,我们可以使用removeTabAt(int index), remove(int index)或是remove(Component component)来移除一个特定的标签。另外,我们可以使用removeAll()移除所有的标签。

JTabbedPane属性

表11-4显示了JTabbedPane的11个属性。因为JTabbedPane的许多setter/getter方法都指定了一个索引参数,事实上他们并不是真正的属性。

Swing_table_11_4.png

Swing_table_11_4.png

我们可以通过selectedComponent或是selectedIndex属性来编程修改显示的标签。

tabRunCount属性表示显示所有的标签所必须的行数(对于顶部或底部标签位置)或是列数(对于左边或是右边位置)。

注意,当要显示容器时修改JTabbedPane的LayoutManager将会抛出异常。换句话说,不要那样做。

监听修改标签选中

如果我们对确定何时选中的标签变化感兴趣,我们需要监听选中模型的变化。这是通过我们将一个ChangeListener关联到JTabbedPane(或是直接关联到SingleSelectionModel)来实现的。注册的ChangeListener报告何时选中模型发生变化,以及选中的面板变化时模型的变化。

显示在列表11-3中的程序演示了监听选中标签的变化并且显示了新选中标签的标题。

package swingstudy.ch11;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.event.KeyEvent;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JTabbedPane;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import swingstudy.ch04.DiamondIcon;

public class TabSample {

    static Color colors[] = {Color.RED, Color.ORANGE, Color.YELLOW, Color.GREEN, Color.BLUE, Color.MAGENTA};
    static void add(JTabbedPane tabbedPane, String label, int mnemonic) {
        int count = tabbedPane.getTabCount();
        JButton button = new JButton(label);
        button.setBackground(colors[count]);
        tabbedPane.addTab(label, new DiamondIcon(colors[count]), button, label);
        tabbedPane.setMnemonicAt(count, mnemonic);
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

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

                JTabbedPane tabbedPane = new JTabbedPane();
                tabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
                String titles[] = {"General", "Security", "Content", "Connection", "Programs", "Advanced"};
                int mnemonics[] = {KeyEvent.VK_G, KeyEvent.VK_S, KeyEvent.VK_C, KeyEvent.VK_0, KeyEvent.VK_P, KeyEvent.VK_A};
                for(int i=0, n=titles.length; i<n; i++) {
                    add(tabbedPane, titles[i], mnemonics[i]);
                }

                ChangeListener changeListener = new ChangeListener() {
                    public void stateChanged(ChangeEvent event) {
                        JTabbedPane sourceTabbedPane = (JTabbedPane)event.getSource();
                        int index = sourceTabbedPane.getSelectedIndex();
                        System.out.println("Tab changed to: "+sourceTabbedPane.getTitleAt(index));
                    }
                };
                tabbedPane.addChangeListener(changeListener);

                frame.add(tabbedPane, BorderLayout.CENTER);
                frame.setSize(400, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

自定义JTabbedPane观感

每一个可安装的Swing观感都提供了不同的JTabbedPane外观以及JTabbedPane组件的默认UIResource值集合。图11-13显示了JTabbedPane容器在预安装的观感类型Motif,Windows以及Ocean下的外观。某些项目是特定观感的:当可用的标签集合对于显示过度时JTabbedPane如何显示,当用户在后一行选择标签时如何响应,如何显示工具提示,以及如何显示滚动标签布局。

Swing_11_13.png

Swing_11_13.png

JTabbedPane可用的UIResource相关的属性集合显示在表11-5中。对于JTabbedPane组件,有34个不同的属性。

Swing_table_11_5_1.png

Swing_table_11_5_1.png

Swing_table_11_5_2.png

Swing_table_11_5_2.png

Swing_table_11_5_3.png

Swing_table_11_5_3.png

JScrollPane类

Swing的JScrollPane容器通过滚动支持(如果需要)来使得当前部分不可见从而为在较小的显示区域内显示大组件提供支持。图11-4显示了一个实现,其中大组件是一个具有ImageIcon的JLabel。

Swing_11_14.png

Swing_11_14.png

可以使用两种方示来标识要滚动的组件。我们不需要将要滚动的组件直接添加到JScrollPane容器中,我们可以将组件添加到已经包含在滚动面板中的另一个组件,JViewport。相对应的,我们可以通过将其传递给构造函数,在构造时标识组件。

Icon icon = new ImageIcon("dog.jpg");
JLabel label = new JLabel(icon);
JScrollPane jScrollPane = new JScrollPane();
jScrollPane.setViewportView(label);
// or
JScrollPane jScrollPane2 = new JScrollPane(label);

一旦我们将组件添加到JScrollPane中,用户可以使用滚动条来查看在JScrollPane的内部区域不可见的大组件部分。

除了为我们提供了设置JScrollPane可滚动组件的方法,显示策略可以决定是否以及何时在JScrollPane周围显示滚动条。Swing的JScrollPane为水平以及垂直滚动条维度了单独的显示策略。

除了使得我们为滚动添加JViewport以及两个JScrollBar组件以外,JScrollPane同时允许我们提供另外两个JViewport对象用于行与列头以及在滚动面板四个角中显示的四个Component对象。这些组件的放置是通过在第10章介绍进行全面描述的ScrollPaneLayout管理器来管理的。JScrollPane实现所用的JScrollBar组件是一个名为JScrollPane.ScrollBar的JScrollBar子类。他们被用来替换通常的JScrollBar,从而在组件实现了Scrollable接口时正确处理JViewport中的滚动组件。

为了帮助我们理解这些组件如何放置在JScrollPane中,图11-15演示了ScrollPaneLayout如何放置各种对象。

Swing_11_15.png

Swing_11_15.png

注意,JScrollPane组件只支持轻量级组件的滚动。我们不应该向容器添加通常的,重量级AWT组件。

创建JScrollPane

JScrollPane有四个构造函数:

public JScrollPane()
JScrollPane scrollPane = new JScrollPane();
public JScrollPane(Component view)
Icon icon = new ImageIcon("largeImage.jpg");
JLabel imageLabel = new JLabel(icon);
JScrollPane scrollPane = new JScrollPane(imageLabel);
public JScrollPane(int verticalScrollBarPolicy, int horizontalScrollBarPolicy)
JScrollPane scrollPane = new
  JScrollPane(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
  JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
public JScrollPane(Component view, int verticalScrollBarPolicy,
  int horizontalScrollBarPolicy)
JScrollPane scrollPane = new JScrollPane(imageLabel,
  JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
  JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);

这些构造函数提供了预安装滚动组件以及配置单独滚动条滚动策略的选项。默认情况下,滚动条只在需要的时候显示。表11-16显示了用来为每一个滚动条显示设置策略的JScrollPane常量。使用其他不正确的设置会导致抛出IllegalArgumentException。

Swing_table_11_6.png

Swing_table_11_6.png

下面的章节将会解释如何在创建JScrollPane之后添加或修改组件。

修改Viewport View

如果我们使用合适的组件创建JScrollPane,我们只需要添加JScrollPane来显示。然而,如果我们并没有在创建时关联组件,或者是希望在稍后进行修改,有两种方法可以为滚动关联一个新的组件。首先,我们可以通过设置viewportView属性直接修改组件:

scrollPane.setViewportView(dogLabel);

修改滚动组件另一种方法就是将JViewport放在JScrollPane的中间,然后修改其view属性:

scrollPane.getViewport().setView(dogLabel);

我们将会在本章稍后的“JViewport类”一节中了解到更多关于JViewport组件的内容。

Scrollable接口

不同于AWT组件,例如List会在一次显示的选项过多时自动提供可滚动区域,Swing组件JList,JTable,JTextComponent,以及JTree并不会自动提供滚动支持。我们必须创建组件,将其添加到JScrollPane,然后将滚动面板添加到屏幕。

JList list = new JList(...);
JScrollPane scrollPane = new JScrollPane(list);
aFrame.add(scrollPane, BorderLayout.CENTER);

将组件添加到JScrollPane起作用的原因在于每一个也许对于屏幕过大的Swing组件(并且需要滚动支持)实现了Scrollable接口。通过实现这个接口,当我们移动与JScrollPane相关联的滚动条时,JScrollPane会查询容器内Scrollable组件的尺寸信息从而基于当前的滚动条位置正确的定位组件。

我们唯一需要担心Scrollable接口的时机就是当我们创建一个需要滚动支持的自定义组件的时候。下面是Scrollable接口的定义。

public interface Scrollable {
  public Dimension getPreferredScrollableViewportSize();
  public boolean getScrollableTracksViewportHeight();
  public boolean getScrollableTracksViewportWidth();
  public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation,
    int direction);
  public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation,
    int direction);
}

如果我们创建一个自定义的Scrollable组件,然后将放在JScrollPane中,当JScrollPane的滚动条或是鼠标滚轮移动时,他就会正确的响应。

JScrollPane属性

表11-7显示了JScrollPane的19个属性。

Swing_table_11_7_1.png

Swing_table_11_7_1.png

Swing_table_11_7_2.png

Swing_table_11_7_2.png

尝试着JScrollPane的布局属性修改为除了ScrollPaneLayout以外的值或是null将会在运行时抛出ClassCastException,因为JScrollPane所用的布局管理器必须为ScrollPaneLayout。

使用ScrollPaneLayout JScrollPane依赖ScrollPaneLayout管理器对容器内的组件进行放置。然而大多数的布局管理器被设置用来布局所有的组件类型,但是ScrollPaneLayout的四个区域只接受特定类型的组件。表11-8显示了可以放置在图11-15中所示区域中显示的组件类型。

Swing_table_11_8.png

Swing_table_11_8.png

注意,区域角有两个常量集合。对于国际化支持,我们可以使用LOWER_LEADING_CORNER, LOWER_TRAILING_CORNER, UPPER_LEADING_CORNER与UPPER_TRAILING_CORNER,这些常量可以为我们处理组件方向。对于由左到右的组件方向,起始是左边,而结束是右边。

正如设计要求,布局管理器描述支持对于可用空间过大的主内容区域(VIEWPORT)所必须的屏幕布局。用于在区域中浏览的滚动条可以被设置在内容区域的右边(VERTICAL_SCROLLABAR)或是下边(HORIZONTAL_SCROLLBAR)。不滚动的固定头可以被放置在内容区域的上部(COLUMN_HEADER)或是其左边(ROW_HEADER)。四个角(*_CORNER)可以配置来显示任意的组件类型,通常是带有图片的标签;然则 ,在其中可以放置任意的组件。

注意,一些开发者会认为ScrollPaneLayout是一个带有自定义约束的GridBagLayout。在通常情况下,大多数开发者并不会在JScrollPane之外使用ScrollPaneLayout。

使用JScrollPane头与角

如图11-15与表11-8所示,在JScrollPane存在多个不同的区域。通常,我们只使用中间的视图,并使用两个滚动条完成相应的任务。另外,当使用JTable组件时,当放置在JScrollPane中时,表会自动将列头放置在列头区域。

我们也可以手动添加或是修改JScrollPane的列头或是行头。尽管我们可以在这里区域完全替换JViewport,但是为此区域中的Component设置columnHeaderView或是rowHeaderView属性更为简单。这一操作可以为我们将组件放置在JViewport中。

要将组件放置在JScrollPane的一个角中,我们需要调用setCorner(String key, Component corner)方法,其中key是JScrollPane中的下列常量之一:LOWER_LEFT_CORNER, LOWER_RIGHT_CORNER, UPPER_LEFT_CORNER,或是UPPER_RIGHT_CORNER。

角区域的使用比较有技巧。只有当两个位于角落右边角的组件是当前显示时,角落组件才会被显示。例如,假如我们要在右下角落放置一个具有公司logo的标签,而两个滚动条的滚动策略只有在必需的时才会显示。在这种情况下,如果一个滚动条不需要,角落中的logo也不会被显示。作为另一个盒子,如果一个JScrollPane具有一个列头显示,但是并没有行头,左上角中的组件也不会被显示。

所以,仅仅因为我们将角落设置为一个组件(例如scrollPane.setCorner(JScrollPane.UPPER_LEFT_CORNER, logLabel)),不要期望组件总是或是自动显示。而且,如图11-16所示,相邻的区域控制角落的尺寸。不要认为角落组件会按需要大小显示。这是因为其最小尺寸,最优尺寸与最大尺寸被完全被忽略了。在图11-16中,用来创建角落组件的实际图片要大于所用的空间。

Swing_11_16.png

Swing_11_16.png

注意,修改JScrollPane的一个角落类似于边界属性,其中属性名是表11-8中所列的角落键值。

重设视图域位置

有时,我们也许会希望将内部视图的内容向JScrollPane的左上角移动。这种变化也许是需要的,因为视图发生了变化,或者是某些事情的发生要求视图域组件返回到JScrollPane的原始位置。移动视图最简单的方法就是JScrollPane的滚动条位置。将每一个滚动条设置为其最小值就有效的将组件视图移动到了组件的左上角区域。列表11-4中所显示的ActionListener可以关联到屏幕中的按钮或是JScrollPane的角落,从而使得JScrollPane的内容返回到原始位置。

package swingstudy.ch11;

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

import javax.swing.JScrollBar;
import javax.swing.JScrollPane;

public class JScrollPaneToTopAction implements ActionListener {

    JScrollPane scrollPane;

    public JScrollPaneToTopAction(JScrollPane scrollPane) {
        if(scrollPane == null) {
            throw new IllegalArgumentException("JScrollPaneToTopAction: null JScrollPane");
        }
        this.scrollPane = scrollPane;
    }
    @Override
    public void actionPerformed(ActionEvent event) {
        // TODO Auto-generated method stub
        JScrollBar verticalScrollBar = scrollPane.getVerticalScrollBar();
        JScrollBar horizontalScrollBar = scrollPane.getHorizontalScrollBar();
        verticalScrollBar.setValue(verticalScrollBar.getMinimum());
        horizontalScrollBar.setValue(horizontalScrollBar.getMinimum());
    }

}

自定义JScrollPane观感

每一个可安装的观感都提供了不同的JScrollPane外观以及默认的组件UIResource值集合。图11-17显示了JScrollPane组件在预安装的观感类型集合下的外观显示。对于JScrollPane,观感类型之间的主要区别与滚动条的外观以及视图周围的边框有关。

表11-9显示了JScrollPane可用的UIResource相关属性集合。对于JScrollPane组件,有十个不同的属性。当滚动条在JScrollPane内可见时,修改与JScrollBar的相关属性会影响其外观。

Swing_11_17_1.png

Swing_11_17_1.png

Swing_11_17_2.png

Swing_11_17_2.png

Swing_table_11_9.png

Swing_table_11_9.png

JViewport类

JViewport很少在JScrollPane之外使用。通常情况下他位于JScrollPane的中间并且使用ViewportLayout管理器来响应在小空间内显示大Component的定位请求。除了位于JScrollPane的中间以外,JViewport也可以用于JScrollPane的行头与列头。

创建JViewport

JViewport只有一个无参数的构造函数:public JViewport()。一旦我们创建了JViewport,我们可以通过setView(Component)向其中添加组件。

JViewport属性

表11-10显示了JViewport的13个属性。将布局管理器设置为ViewportLayout以外的布局管理也可以的,但是并不推荐,因为ViewportLayout布局管理器可以使得JViewport正确工作。

Swing_table_11_10.png

Swing_table_11_10.png

由于滚动的复杂性以及性能原因,JViewport并不支持边框。试着使用setBorder(Border)方法将边框设置为非null会抛出IllegalArgumentException。因为没有边框,所以insets属性的设置总为(0,0,0,0)。我们不能在JViewport周围显示边框,但是我们可以在视图所在的组件周围显示边框。只需要简单的在组件周围放置一个边框,或是将组件放在一个具有边框的JPanel中,然后将其添加到JViewport。如果我们确实在组件周围添加了边框,只有当组件部分可以见时边框才可见。如果我们不希望边框滚动,我们必须将JViewport放在类似JScrollPane这样具有自己边框的组件中。

提示,要设置显示在JScrollPane中的背景色,我们需要设置视图区域的背景色:aScrollPane.getViewport().setBackground(newColor)。

视图的尺寸(viewSize属性)是基于JViewport内组件的尺寸的(view属性)。视图位置(viewPosition属性)是视图矩形区域(viewRect属性)的左上角,其中矩形区域的尺寸是视图区域的扩展尺寸(extentSize属性)。如果感到迷惑,图11-18会有助于我们理解JViewport中的各种属性。

Swing_11_18.png

Swing_11_18.png

scrollMode属性可以设置为表11-11中所列的类常量的一个。在大多数情况下,我们可以使用默认的BLIST_SCROLL_MODE模式。

Swing_table_11_11.png

Swing_table_11_11.png

为了在周围移动视图的可见部分,我们只需要修改viewPosition属性。这会移动viewRect,使得我们可以看到视图的不同部分。为了显示这一行为,列表11-5中的程序将键盘快捷键绑定到了JViewport,从而我们可以使用箭头键来移动视图。(通常情况下,JScrollPane会获得这些键盘动作。)代码的主要部分对于设置相应的输入/动作映射是必须的。以粗体显示的代码是移动视图所必须的。

package swingstudy.ch11;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Point;
import java.awt.event.ActionEvent;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JViewport;
import javax.swing.KeyStroke;

public class MoveViewSample {

    public static final int INCREASE = 0; // direction
    public static final int DECREASE = 1; // direction
    public static final int X_AXIS = 0; // axis
    public static final int Y_AXIS = 1; // axis
    public static final int UNIT = 0;   // type
    public static final int BLOCK = 1;  // type

    static class MoveAction extends AbstractAction {
        JViewport viewport;
        int direction;
        int axis;
        int type;
        public MoveAction(JViewport viewport, int direction, int axis, int type) {
            if(viewport == null) {
                throw new IllegalArgumentException("null viewport not permitted");
            }
            this.viewport = viewport;
            this.direction = direction;
            this.axis = axis;
            this.type = type;
        }

        public void actionPerformed(ActionEvent event) {
            Dimension extentSize = viewport.getExtentSize();
            int horizontalMoveSize = 0;
            int verticalMoveSize = 0;
            if(axis == X_AXIS) {
                if(type == UNIT) {
                    horizontalMoveSize = 1;
                }
                else {
                    // type == BLOCK
                    horizontalMoveSize = extentSize.width;
                }
            }
            else {
                // axis == Y_AXIS
                if(type == UNIT) {
                    verticalMoveSize = 1;
                }
                else {
                    // type = BLOCK
                    verticalMoveSize = extentSize.height;
                }
            }
            if(direction == DECREASE) {
                horizontalMoveSize = -horizontalMoveSize;
                verticalMoveSize = -verticalMoveSize;
            }
            // translate origin by some amount
            Point origin = viewport.getViewPosition();
            origin.x += horizontalMoveSize;
            origin.y += verticalMoveSize;
            // set new viewing origin
            viewport.setViewPosition(origin);
        }
    }

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

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("JViewport Sample");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                Icon icon = new ImageIcon("dog.jpg");
                JLabel dogLabel = new JLabel(icon);
                JViewport viewport =  new JViewport();
                viewport.setView(dogLabel);

                InputMap inputMap = viewport.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
                ActionMap actionMap = viewport.getActionMap();

                // up key moves view up unit
                Action upKeyAction = new MoveAction(viewport, DECREASE, Y_AXIS, UNIT);
                KeyStroke upKey = KeyStroke.getKeyStroke("UP");
                inputMap.put(upKey, "up");
                actionMap.put("up", upKeyAction);

                // down key moves view down unit
                Action downKeyAction = new MoveAction(viewport, INCREASE, Y_AXIS, UNIT);
                KeyStroke downKey = KeyStroke.getKeyStroke("DOWN");
                inputMap.put(downKey, "down");
                actionMap.put("down", downKeyAction);

                // left key moves view left unit
                Action leftKeyAction = new MoveAction(viewport, DECREASE, X_AXIS, UNIT);
                KeyStroke leftKey = KeyStroke.getKeyStroke("LEFT");
                inputMap.put(leftKey, "left");
                actionMap.put("left", leftKeyAction);

                // right key mvoes view right unit
                Action rightKeyAction = new MoveAction(viewport, INCREASE, X_AXIS, UNIT);
                KeyStroke rightKey = KeyStroke.getKeyStroke("RIGHT");
                inputMap.put(rightKey, "right");
                actionMap.put("right", rightKeyAction);

                // pgup key moves view up block
                Action pgUpKeyAction = new MoveAction(viewport, DECREASE, Y_AXIS, BLOCK);
                KeyStroke pgUpKey = KeyStroke.getKeyStroke("PAGE_UP");
                inputMap.put(pgUpKey, "pgUp");
                actionMap.put("pgUp", pgUpKeyAction);

                // pgdn key moves view down block
                Action pgDnKeyAction = new MoveAction(viewport, INCREASE, Y_AXIS, BLOCK);
                KeyStroke pgDnKey = KeyStroke.getKeyStroke("PAGE_DOWN");
                inputMap.put(pgDnKey, "pgDn");
                actionMap.put("pgDn", pgDnKeyAction);

                // shift-pgup key moves view left block
                Action shiftPgUpKeyAction = new MoveAction(viewport, DECREASE, X_AXIS, BLOCK);
                KeyStroke shiftPgUpKey = KeyStroke.getKeyStroke("shift PAGE_UP");
                inputMap.put(shiftPgUpKey, "shiftPgUp");
                actionMap.put("shiftPgUp", shiftPgUpKeyAction);

                // shift-pgdn key moves view right block
                Action shiftPgDnKeyAction = new MoveAction(viewport, INCREASE, X_AXIS, BLOCK);
                KeyStroke shiftPgDnKey = KeyStroke.getKeyStroke("shift PAGE_DOWN");
                inputMap.put(shiftPgDnKey, "shiftPgDn");
                actionMap.put("shiftPgDn", shiftPgDnKeyAction);

                frame.add(viewport, BorderLayout.CENTER);
                frame.setSize(300, 200);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

自定义JViewport观感

每一个可安装的Swing观感通过BasicViewportUI共享相同的JViewport外观,并没有实际外观上的区别。然而,仍然存在一个JViewport的UIResource相关属性集合,如表11-12所示。对于JViewport组件,有四个这样的属性。

Swing_table_11_12.png

Swing_table_11_12.png

小结

在本章中,我们探讨了一些高级的Swing容器。对于Box类,我们可以更容易的使用BoxLayout管理器考虑到组件的最小尺寸,最优尺寸与最大尺寸以最好的可能方式来创建单行或单列的组件。

对于JSplitPane组件,我们可以通过在其所包含的两个组件间添加分隔符来创建一行或一列的组件,并允许用户通过移动分隔符来手动修改组件的尺寸。

JTabbedPane容器每次只显示所包含的组件集合中的一个组件。所显示的组件是通过用户选择标签来选择的,标签中可以包含具有或是不具有热键的标题,图标以及工具提示文本。这就是我们通常在程序中见到的流行的属性页。

JScrollPane与JViewport容器可以使得我们在一小区域内显示一个大组件。JScrollPane添加了滚动条使得终端用户移动可视化部分,而JViewport没有添加这些滚动条。

在第12章中,我们将会再次探讨Swing库中的单个组件,包括JProgressBar,JScrollBar以及共享BoundedRangeModel作为其数据模型的JSlider。

Bounded Range Components

在前面的章节中,我们了解了当在屏幕没有足够的空间显示完整的组件时,JScrollPane如何提供了一个可滚动的区域。Swing同时提供了其他的一些支持某种滚动类型或是边界范围值显示的组件。这些可用的组件有JScrollBar,JSlider,JProgressBar,以及更为有限角度的JTextField。这些组件共享BoundedRangeModel作为他们的数据模型。Swing类所提供的这种数据模型的默认实现是DefaultBoundedRangeModel类。

在本章中,我们将会了解这些Swing组件的类似与不同之处。我们的讨论由共享的数据模型BoundedRangeModel开始。

BoundedRangeModel接口

BondedRangeModel接口是本章中描述的组件所共享的MVC数据模型。这个接口包含了描述范围值minimum, maximum, value, extent所必须的四个交互关联的属性。

minimum与maximum属性定义了模型值的范围。value属性定义了我们所认为的模型的当前设置,而value属性的最大值并不一定是模型的maximum属性值。相反,value属性可以使用的最大设置是小于extent属性的maximum属性。为了有助于我们理解这些属性,图12-1显示了这些设置与JScollBar的关系。extent属性的其他目的依赖于作为模型视图的属性。

Swing_12_1.png

Swing_12_1.png

四个属性的设置必须满足下列关系:

minimum <= value <= value+extent <= maximum

当一个设置发生变化时,也许就会触发其他设置的变化,以满足上面的大小关系。例如,将minimum修改为当前value加上extent与maximum之间的一个设置会使得减少extent并且增加value来保持上面的大小关系。另外,原始属性的变化会导致修改为一个新设置而不是所请求的设置。例如,尝试将value设置为小于minimum或是maximum会将value设置为最接近范围极限的值。

BoundedRangeModel接口的定义如下:

public interface BoundedRangeModel {
  // Properties
  public int  getExtent();
  public void setExtent(int newValue);
  public int  getMaximum();
  public void setMaximum(int newValue);
  public int  getMinimum();
  public void setMinimum(int newValue);
  public int  getValue();
  public void setValue(int newValue);
  public boolean getValueIsAdjusting();
  public void    setValueIsAdjusting(boolean newValue);
  // Listeners
  public void addChangeListener(ChangeListener listener);
  public void removeChangeListener(ChangeListener listener);
  // Other Methods
  public void setRangeProperties(int value, int extent, int minimum,
    int maximum, boolean adjusting);
}

尽管模型可用的不同属性设置是JavaBean属性,当属性设置发生变化时,接口使用Swing的ChangeListener方法而不是java.beans.PropertyChangeListener。

当用户正在执行一系列的模型快速变化时,也许是在屏幕上拖动滑块所造成的,模型的valueIsAdjusing属性就派上用场了。对于某些只对何时设置模型的最终值感兴趣的人,在getValueIsAdjusting()返回false之前监听器会忽略所有的改变。

DefaultBoundedRangeModel类

实际实现BoundedRangeModel接口的Swing类是DefaultBoundedRangeModel。这个类会小心处理为了保证不同属性值的相应顺序所必需的调整。他同时管理ChangeListener列表在模型发生变化时通知监听器。

DefaultBoundedRangeModel有两个构造函数:

public DefaultBoundedRangeModel()
public DefaultBoundedRangeModel(int value, int extent, int minimum, int maximum)

无参数版本会将模型的minimum, value与extent属性设置为0。余下的maximum属性设置为100。

第二个构造函数版本需要四个整形参数,显式设置四个属性。对于这两个构造函数,valuesAdjusting属性的初始值均为false,因为模型值在初始值之外并没有发生变化。

注意,除非我们在多个组件之间共享模型,通常并没有必要创建BoundedRangeMode。如果我们要在多个组件之间共享模型,我们可以创建第一个组件,然后获取其BoundedRangeModel模型进行共享。

类似于通常的管理其监听器列表的所有类,我们可以向DefaultBoundedRangeModel查询赋给他的监听器。这里我们可以使用getListeners(ChangeListener.class)方法获取模型的ChangeListener列表。这会返回一个EventListener对象数组。

JScrollBar类

最简单的边界范围组件是JScrollBar。JScrollBar组件用在我们在第11章所描述的JScrollPane容器中来控制滚动区域。我们也可以将这个组件用在我们自己的容器中,尽管由于JScrollPane的灵活性,通常并不必需这样做。然而关于JScrollBar我们需要记住的一点就是JScrollPane并用于值,而是用于屏幕的滚动区域。对于值,我们要使用在下节将要讨论的JSlider组件。

注意,JScrollPane中的JScrollBar实际是上是JScrollBar的一个特殊子类,他能够正确处理实现了Scrollable接口的可滚动组件。尽管我们可以修改JScrollPane的滚动条,但是通常并不需要这样做,而且所需要工作比我们认为要多得多。

如图12-2所示,水平的JScrollBar由几部分组成。由中间开始向前,我们可以看到滚动条的滑块。滑块的宽度是BoundedRangeModel的extent属性。滚动条的当前值是滑块的左边。滑块的左边与右边是块翻页区域。点击滑块的左边并减少滚动条的值,而点击右边会增加滚动条的值。滑块增加或减少的数量值是滚动条的blockIncrement属性。

Swing_12_2.png

Swing_12_2.png

在滚动条的左边与右边是箭头按钮。当点击左箭头时,滚动条会减少一个单元。滚动条的unitIncrement属性指定了这个单元。通常情况,这个值为1,尽管并不是必须这样。在左箭头的右边是滚动条的最小值与模型。除了用左箭头减少值以外,点击右箭头会使得滚动条增加一个单元。右箭头的左边是滚动条的最大范围。最大值实际上略远于左边,这里略远的距离是由模型的extent属性来指定的。当滑块紧邻右箭头时,这会将滚动条的滚动动条值放在滑块的左边,对于所有其他的位置也是如此。

垂直JScrollBar是由与水平JScrollBar相同的部分组成的,最小值与减少部分位顶部,而且值是由滚动条滑块的上边决定的。最小值与增加部分位于底部。

正如前面所提到的,JScrollBar模型是BoundedRangeModel。用户界面的委托是ScrollBarUI。

现在我们已经了解了JSrollBar的不同部分,现在我们来了解一下如何使用。

创建JScrollBar组件

JScrollBar有三个构造函数:

public JScrollBar()
JScrollBar aJScrollBar = new JScrollBar();
public JScrollBar(int orientation)
// Vertical
JScrollBar aJScrollBar = new JScrollBar(JScrollBar.VERTICAL);
// Horizontal
JScrollBar bJScrollBar = new JScrollBar(JScrollBar.HORIZONTAL);
public JScrollBar(int orientation, int value, int extent, int minimum, int maximum)
// Horizontal, initial value 500, range 0-1000, and extent of 25
JScrollBar aJScrollBar = new JScrollBar(JScrollBar.HORIZONTAL, 500, 25, 0, 1025);

使用无参数的构造函数会使用默认的数据模型创建一个垂直滚动条。模型的初始值为0,最小值为0,最大值为100,而扩展值为10。这个默认模型只提供了0到90的范围。我们可以显示的将方向设置为JScrollBar.HORIZONTAL或是JScrollBar.VERTICAL。如果我们不喜欢其他两个构造函数所提供的初始模型设置,我们需要自己进行显示的设置。如果数据元素没有正确的进行约束,正如前面关于BoundedRangeModel所描述的,则会抛出一个IllegalArgumentException,使得JScrollBar构造中断。

很奇怪没有出现在构造函数列表中的接受BoundedRangeModel参数的构造函数。如果我们有一个模型实例,我们可以在创建了滚动条之后调用setModel(BoundedRangeModel newModel)方法或是在创建构造函数时由模型获取单个属性,如下所示:

JScrollBar aJScrollBar =
  new JScrollBar (JScrollBar.HORIZONTAL, aModel.getValue(), aModel.getExtent(),
    aModel.getMinimum(), aModel.getMaximum())

由J2SE平台的1.3版本开始,滚动条不再参与焦点遍历。

处理滚动事件

一旦我们创建了JScrollBar,如果我们对滚动条的值何时发生变化感兴趣,则我们需要监听这些变化。有两种监听的方法:AWT 1.1事件模型方法以及Swing MVC方法。AWT方法涉及到将AdjustmentListener关联到JScrollBar。MVC方法涉及到将ChangeListener关联到数据模型。每一种方法都可以工作得很好,如果模型通过编程变化或是用户拖动滚动条滑块,两种方法都会得到通知。后一种方法提供了更多的灵活性,因而是一个不错的选择,除非我们是在多个组件之间共享数据模型并且需要知道哪一个组件修改了模型。

使用AdjustmentListsener监听滚动事件

将AdjustmentListener关联到JScrollBar使得我们可以监听用户修改滚动条设置。下面的代码片段,将会用在稍后的列表12-3中,显示了为了监听用户修改JScrollBar的值,我们如何将AdjustmentListsener关联到JScrollBar。

首先,定义简单输出滚动条当前值的AdjustmentListsener:

AdjustmentListener adjustmentListener = new AdjustmentListener() {
  public void adjustmentValueChanged (AdjustmentEvent adjustmentEvent) {
    System.out.println ("Adjusted: " + adjustmentEvent.getValue());
  }
};

在我们创建了监听器之后,我们可以创建组件并且关联监听器:

JScrollBar oneJScrollBar = new JScrollBar (JScrollBar.HORIZONTAL);
oneJScrollBar.addAdjustmentListener(adjustmentListener);

这种监听修改事件的方法可以工作得很完美。然而,我们也许会更喜欢将ChangeListener关联到数据模型。

使用ChangeListener监听滚动事件

将ChangeListener关联到JScrollBar数据模型会在我们的程序设计中提供更多的灵活性。使用AWT AdjustmentListener,只有滚动条的值发生变化时监听器才会得到通知。另一方面,当最小值,最大值,当前值,以及扩展值发生变化时,所关联的ChangeListener会得到通知。另外,由于模型有一个valueIsAdjusting属性,我们可以选择忽略即时变化事件-一些我们可以使用AdjustmentListener,通过Adjustment中相同名的属性处理的事件。

为了进行演示,定义了一个当模型完成调整时输出滚动条当前值的ChangeListener,如列表12-1所示。我们可以通过本章来加强BoundedChangeListener类。

package swingstudy.ch11;

import javax.swing.BoundedRangeModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class BoundedChangeListener implements ChangeListener {

    @Override
    public void stateChanged(ChangeEvent event) {
        // TODO Auto-generated method stub

        Object source = event.getSource();
        if(source instanceof BoundedRangeModel) {
            BoundedRangeModel aModel = (BoundedRangeModel)source;
            if(!aModel.getValueIsAdjusting()) {
                System.out.println("Changed: "+aModel.getValue());
            }
        }
        else {
            System.out.println("Something changed: "+source);
        }
    }

}

一旦我们创建了监听器,我们也可以创建组件并且关联监听器。在这个特定的例子中,我们需要将监听器关联到组件的数据模型,而不是直接关联到组件。

ChangeListener changeListener = new BoundedChangeListener();
JScrollBar anotherJScrollBar = new JScrollBar (JScrollBar.HORIZONTAL);
BoundedRangeModel model = anotherJScrollBar.getModel();
model.addChangeListener(changeListener);

列表12-2显示了测试程序的源码。

package swingstudy.ch11;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;

import javax.swing.BoundedRangeModel;
import javax.swing.JFrame;
import javax.swing.JScrollBar;
import javax.swing.event.ChangeListener;

public class ScrollBarSample {

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

        Runnable runner = new Runnable() {
            public void run() {
                AdjustmentListener adjustmentListener = new AdjustmentListener() {
                    public void adjustmentValueChanged(AdjustmentEvent event) {
                        System.out.println("Adjusted: "+event.getValue());
                    }
                };

                JScrollBar oneJScrollBar = new JScrollBar(JScrollBar.HORIZONTAL);
                oneJScrollBar.addAdjustmentListener(adjustmentListener);

                ChangeListener changeListener = new BoundedChangeListener();
                JScrollBar anotherJScrollBar = new JScrollBar(JScrollBar.HORIZONTAL);
                BoundedRangeModel model = anotherJScrollBar.getModel();
                model.addChangeListener(changeListener);

                JFrame frame = new JFrame("ScrollBars R US");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(oneJScrollBar, BorderLayout.NORTH);
                frame.add(anotherJScrollBar, BorderLayout.SOUTH);
                frame.setSize(300, 200);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

当运行这个程序时,他会显示两个水平滚动条,如图12-3所示。移动滚动条的输出发送到终端容器。

Swing_12_3.png

Swing_12_3.png

JScrollBar属性

在我们创建了JScrollBar之后,修改其底层数据模型就变得必要了。我们可以使用public BoundedRangeModel getModel()方法获得模型,然后直接修改模型。更可能的,我们仅需要调用组件的相应方法:

• setValue(int newValue), setExtent(int newValue), setMinimum(int newValue)
• setMaximum(int newValue)

注意,尽管支持,但是并不推荐在显示组件之后修改JScrollBar方向。这会极大的影响用户的满意度并且使得用户去寻找其他的解决方案。

除了数据模型属性,表12-1显示了JScrollBar的16个属性。

Swing_table_12_1.png

Swing_table_12_1.png

自定义JScrollBar观感

每一个可安装的Swing观感都提供了不同的JScrollBar外观以及默认的UIResource值集合。图12-4显示了预安装的观感类型Motif,Windows,以及Ocean的JScrollBar组件的外观。

Swing_12_4.png

Swing_12_4.png

表12-2显示了JScrollBar的UIResource相关属性集合。有28个不同的属性。

Swing_table_12_2_1.png

Swing_table_12_2_1.png

Swing_table_12_2_2.png

Swing_table_12_2_2.png

Swing_table_12_2_3.png

Swing_table_12_2_3.png

JSlider类

尽管JScrollBar对于屏幕滚动区域十分有用,但是他并不适用于使得用户在一个范围内进行输入。对于这个目的,Swing提供了JSlider组件。除了提供了类似JScrollBar组件所提供的可拖动滑块以外,JSlider同时提供了可视化的标记以及标签来辅助显示当前的设置并且选择新的设置。图12-5显示了几个JSlider组件的示例。

Swing_12_5.png

Swing_12_5.png

JSlider是由几部分组成的。我们所熟悉的BoundedRangeModel存储组件的数据模型,而Dictionary存储用于标记的标签。用户界面委托是SliderUI。

现在我们已经了解了JSlider组件的不同部分,下面我们来探讨如何使用JSlider。

创建JSlider组件

JSlider有六个构造函数:

public JSlider()
JSlider aJSlider = new JSlider();
public JSlider(int orientation)
// Vertical
JSlider aJSlider = new JSlider(JSlider.VERTICAL);
// Horizontal
JSlider bJSlider = new JSlider(JSlider.HORIZONTAL);
public JSlider(int minimum, int maximum)
// Initial value midpoint / 0
JSlider aJSlider = new JSlider(-100, 100);
public JSlider(int minimum, int maximum, int value)
JSlider aJSlider = new JSlider(-100, 100, 0);
public JSlider(int orientation, int minimum, int maximum, int value)
// Vertical, initial value 6, range 1-12 (months of year)
JSlider aJSlider = new JSlider(JSlider.VERTICAL, 6, 1, 12);
public JSlider(BoundedRangeModel model)
// Data model, initial value 3, range 1-31, and extent of 0
// JSlider direction changed to vertical prior to display on screen
DefaultBoundedRangeModel model = new DefaultBoundedRangeModel(3, 0, 1, 31);
JSlider aJSlider = new JSlider(model);
aJSlider.setOrientation(JSlider.VERTICAL);

使用无参数的构造函数会使用默认的数据模型创建一个水平滑动器。模型的初始值为50,最小值为0,最大值为100,而扩展值为0。我们也可以使用JSlider.HORIZONTAL或是JSlider.VERTICAL显示设置方向,使用各种构造函数设置特定的模型属性。另外,我们也可以显式设置组件的数据模型。

如果我们使用一个预配置的BoundedRangeModel,记住当创建模型时要将扩展值设置为0。如果extent属性大于0,value属性的最大设置就会减少相应的值,而且value设置绝不会达到maximum属性设置。

注意,将方向初始化为VERTICAL与HORIZONTAL以外的值会抛出IllegalArgumentException。如果范围与初始值不遵循BoundedRangeModel的规则,则实始化数据模型的所有构造函数会抛出IllegalArgumentException。

处理JSlider事件

我们可以使用ChangeListener监听JSlider的变化。与JScrollBar不同,JSlider并没有AdjustmentListener。可以将前面JScrollBar示例中的BoundedChangeListener添加到JSlider的数据模型,然后当模型发生变化时我们就可以得到通知。

ChangeListener aChangeListener = new BoundedChangeListener();
JSlider aJSlider = new JSlider ();
BoundedRangeModel model = aJSlider.getModel();
model.addChangeListener(changeListener);

除了将ChangeListener关联到模型,我们还可以将ChangeListener直接关联到JSlider本身。这可以使得我们在视图与独立监听变化之间共享数据模型。这需要我们略微修改前面的监听器,因为现在变化的事件源是JSlider,而不是BoundedRangeModel。更新的BoundedChangeListener显示在列表12-3中,可以适用于两种情况下的关联。在下面的列表中变化的部分以粗体标出。

package swingstudy.ch11;

import javax.swing.BoundedRangeModel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class BoundedChangeListener implements ChangeListener {

    @Override
    public void stateChanged(ChangeEvent event) {
        // TODO Auto-generated method stub

        Object source = event.getSource();
        if(source instanceof BoundedRangeModel) {
            BoundedRangeModel aModel = (BoundedRangeModel)source;
            if(!aModel.getValueIsAdjusting()) {
                System.out.println("Changed: "+aModel.getValue());
            }
        }
        else if(source instanceof JSlider) {
            JSlider theJSlider = (JSlider)source;
            if(!theJSlider.getValueIsAdjusting()) {
                System.out.println("Slider changed: "+theJSlider.getValue());
            }
        }
        else {
            System.out.println("Something changed: "+source);
        }
    }

}

与滑动器的关联可以直接进行,而无需通过模型间接进行。

aJSlider.addChangeListener(changeListener);

JSlider属性

在我们创建了JSlider之后,我们也许希望修改其底层数据模型。类似于JScrollBar,我们可以使用public BoundedRangeModel getModel()方法获取模型,然后直接修改模型。我们也可以直接调用组件的方法:

• setValue(int newValue), setExtent(int newValue), setMinimum(int newValue)
• setMaximum(int newValue)

类似于JScrollBar,这些方法只是作为代理,并且将方法调用重定向到对应的模型方法。

表12-3显示了JSlider的19个属性。

Swing_table_12_3_1.png

Swing_table_12_3_1.png

Swing_table_12_3_2.png

Swing_table_12_3_2.png

在JSlider中显示刻度标记

JSlider组件允许我们可以在水平滑动器的下边或是垂直滑动器的右边添加刻度标记。这些标记可以使得用户大致估计滑动器的值与尺度。可以具有主标记与次标记;主标记的绘制要略微长些。可以显示一个或是两个同时显示,也可以都不显示,而这则是默认设置。

注意,由技术上来说,自定义的观感可以将标记放置在任何地方。然而,系统提供的观感类型将标记放置在下边或是右边。

要显示刻度标记,我们需要使用public void setPaintTicks(boolean newValue)方法允许刻度绘制。当使用true设置来调用时,该方法会以允许次标记与主标记的绘制。默认情况下,两种刻度标记类型的刻度空间被设置为0。当有一个设置为0时,则该刻度类型不会显示。因为两个都初始为0,我们必须修改一个刻度空间的值来查看刻度。public void setMajorTickSpacing(int newValue)与public void setMinorTickSpacing(int newValue)方法都支持这种修改。

为了进行演示,图12-6显示了四个滑动器。这有利于主刻度空间是次刻度空间整数倍的情况。另外,刻度空间不应太窄,因而使得刻度看起来像是一个块。

Swing_12_6.png

Swing_12_6.png

图12-6中的示例源码显示在列表12-4中。顶部的滑动器没有刻度。底部的滑动器具有主刻度空间与次刻度空间,次刻度为5个单位,而主刻度为25个单位。左边的滑动器显示了一个糟糕的刻度空间,次刻度为6个单位,而主刻度为25个单位。右边的滑动器次刻度为单个单元,结果导致空间过于紧凑。

package swingstudy.ch11;

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

import javax.swing.JFrame;
import javax.swing.JSlider;

public class TickSliders {

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

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

                // No Ticks
                JSlider jSliderOne = new JSlider();
                // Major tick 25 - Minor 5
                JSlider jSliderTwo =  new JSlider();
                jSliderTwo.setMinorTickSpacing(5);
                jSliderTwo.setMajorTickSpacing(25);
                jSliderTwo.setPaintTicks(true);
                jSliderTwo.setSnapToTicks(true);
                // Major Tick 25 - Minor6
                JSlider jSliderThree = new JSlider(JSlider.VERTICAL);
                jSliderThree.setMinorTickSpacing(6);
                jSliderThree.setMajorTickSpacing(25);
                jSliderThree.setPaintTicks(true);
                // Major Tick 25 - Minor 1
                JSlider jSliderFour = new JSlider(JSlider.VERTICAL);
                jSliderFour.setMinorTickSpacing(1);
                jSliderFour.setMajorTickSpacing(25);
                jSliderFour.setPaintTicks(true);

                frame.add(jSliderOne, BorderLayout.NORTH);
                frame.add(jSliderTwo, BorderLayout.SOUTH);
                frame.add(jSliderThree, BorderLayout.WEST);
                frame.add(jSliderFour, BorderLayout.EAST);
                frame.setSize(300, 200);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

停靠JSlider滑块位置

另一个与刻度标记相关的JSlider属性是通过public void setSnapToTicks(boolean newValue)方法设置的snapToTicks属性。当这个属性为true且显示在刻度标记时,在我们移动了滑块之后,滑块只停留在刻度处。例如,滑动器的滑块范围为0到100,并且每一个刻度为10个单位,如果我们将滑块拖动到33刻度处,滑块就会停靠30刻度处。如果刻度标记没有显示,属性设置没有影响,包括没有刻度标记显示标签时。

标记JSlider位置

如图12-5所示,我们可以使用Component标记JSlider中的位置。当一个位置被标记,组件会相邻显示。标记存储在一个派生自Dictionary类的查询表中,其中键值是Integer位置,而值则是Component。任何的AWT Component都可以是标签;然而,JLabel最合适这一角色。图12-7显示了图12-5中右边滑动器的词典的样子。

Swing_12_7.png

Swing_12_7.png

通常,Dictionary将标签存储在一个Hashtable中。然而,扩展自Dictionary类并且使用Integer键值的类也可以。在我们创建了我们的标签词典以后,我们可以使用public void setLabelTable(Dictionary newValue)方法将词典与滑动器相关联。下面的代码创建与一个与图12-7相关联的标签查询表。

Hashtable<Integer, JLabel> table = new Hashtable<Integer, JLabel>();
table.put (0, new JLabel(new DiamondIcon(Color.RED)));
table.put (10, new JLabel("Ten"));
table.put (25, new JLabel("Twenty-Five"));
table.put (34, new JLabel("Thirty-Four"));
table.put (52, new JLabel("Fifty-Two"));
table.put (70, new JLabel("Seventy"));
table.put (82, new JLabel("Eighty-Two"));
table.put (100, new JLabel(new DiamondIcon(Color.BLACK)));
aJSlider.setLabelTable (table);

注意,请记住在J2SE 5.0中,编译器会自动将一个int参数装箱为一个Integer。

简单的标签表与滑动器相关联并不会显示标签。为了能够绘制标签,我们必须使用参数true来调用public void setPaintLabels(boolean newValue)方法。如果我们没有手动创建标签表,系统会使用反映主标记空间的内部值来为我们创建一个。例如,图12-5中左边的滑动器的范围为0到100,而主标记空间为10。当在这个滑动器上调用stPaintLabels(true)方法,标签创建在0,10,20等处,直到100。次标记空间与标签的自动生成无关。而且为了标签显示标记并不需要绘制;getPaintTicks()方法可以返回false。

标签的自动创建是通过public Hashtable createStandardLabels(int increment)方法实现的,其中increment是主标记空间。我们并不需要直接调用这个方法。如果我们希望并不由最小值创建标签,我们可以调用重载的public Hashtable createStandardLabels(int increment, int start)方法 ,并且将哈希表关联到滑动器。

自定义JSlider观感

每一个可安装的Swing观感都提供了不同的JSlider外观以及默认的UIResource值集合。图12-8显示了预安装的观感类型集合下的JSlider组件的外观。

Swing_12_8.png

Swing_12_8.png

两个观感类型相关的属性是JSlider类定义的一部分。默认情况下,水平滑动器的最小滑块值位于左边;对于垂直滑动器,则是在下边。要修改滑动器的方向,使用参数true调用public void setInverted(boolean newValue)方法。另外,滑块沿着移动的轨道默认显示。我们可以通过public void setPaintTrack(boolean newValue)方法来关闭显示。false值会关闭轨道显示。图12-9显示了JSlider轨道的样子并且标出了常规与相反滑动器的最小值与最大值位置。

Swing_12_9.png

Swing_12_9.png

表12-4显示了JSlider的30个UIResource相关的属性。

Swing_table_12_4_1.png

Swing_table_12_4_1.png

Swing_table_12_4_2.png

Swing_table_12_4_2.png

JSlider资源允许我们自定义通过JSlider或是SliderUI方法不能访问的元素。例如,要自定义我们程序的的JSlider外观,我们也许希望修改可拖动的滑块的图标。通过很少的几行代码,我们可以使用任意的图标作为我们程序中滑动器的滑动图标。

Icon icon = new ImageIcon("logo.jpg");
UIDefaults defaults = UIManager.getDefaults();
defaults.put("Slider.horizontalThumbIcon", icon);

图12-10显示了结果。类似于所有的UIResource属性,这种修改将会影响在设置属性之后所创建的所用的JSlider组件。

Swing_12_10.png

Swing_12_10.png

注意,图标的高度与宽度受限于滑动器的维度。修改icon属性并不会影响滑动器尺寸。

JSlider客户属性

默认情况下,对于Metal观感,当轨道可见时,当滑块在其上移动时轨道并不会变化。而且,我们可以允许将会通知滑块填充滑块所移动过的直到当前的值的轨道部分的客户属性。这个属性名为JSlider.isFilled,而Boolean对象表示当前的设置。默认情况下,这个设置为Boolean.FALSE。图12-11演示了Boolean.TRUE与Boolean.FALSE设置;代码片段如下:

JSlider oneJSlider = new JSlider();
oneJSlider.putClientProperty("JSlider.isFilled", Boolean.TRUE);
JSlider anotherJSlider = new JSlider();
// Set to default setting
anotherJSlider.putClientProperty("JSlider.isFilled", Boolean.FALSE);
Swing_12_11.png

Swing_12_11.png

这个设置只在Metal观感下起作用。Metal观感的Ocean主题会忽略这一设置,总是绘制填充的轨道。要获得这一行为,我们需要将系统属性swign.metalTheme设置为steel,例如java -Dswing.metalTheme=steel ClassName。

JProgressBar类

Swing的JProgressBar不同于其他的BoundedRangeModel组件。其主要目的并不是由用户获取输入,而是展示输出。输出以过程完成百分比的方式进行显示。当百分比增加时,在组件上会显示一个过程栏,直接工作完成并且过程栏被填满。过程栏的运动通常是某些多线程任务的一部分,从而避免影响程序的其他部分。

图12-12显示了一些示例JProgressBar组件。顶部的过程栏使用所有的显示特性。底部的过程栏在组件的周围添加了一个边框,并且显示完成百分比。右边的过程栏移除了边框,而左边的过程栏具有一个固定的字符串表示来替换完成百分比。

Swing_12_12.png

Swing_12_12.png

由面向对象的角度来看,JProgressBar有两个主要部分:我们所熟悉的BoundedRangeModel存储组件的数据模型,而ProgressUI是用户界面委托。

注意,在对话框中显示一个过程栏,使用在第9章所讨论的ProgressMonitor类。

创建JProgressBar组件

JProgressBar有五个不同的构造函数:

public JProgressBar()
JProgressBar aJProgressBar = new JProgressBar();
public JProgressBar(int orientation)
// Vertical
JProgressBar aJProgressBar = new JProgressBar(JProgressBar.VERTICAL);
// Horizontal
JProgressBar bJProgressBar = new JProgressBar(JProgressBar.HORIZONTAL);
public JProgressBar(int minimum, int maximum)
JProgressBar aJProgressBar = new JProgressBar(0, 500);
public JProgressBar(int orientation, int minimum, int maximum)
JProgressBar aJProgressBar = new JProgressBar(JProgressBar.VERTICAL, 0, 1000);
public JProgressBar(BoundedRangeModel model)
// Data model, initial value 0, range 0-250, and extent of 0
DefaultBoundedRangeModel model = new DefaultBoundedRangeModel(0, 0, 0, 250);
JProgressBar aJProgressBar = new JProgressBar(model);

使用无参数的构造函数创建JProgressBar时会使用默认的数据模型创建一个水平的过程栏。模型的初始值为0,最小值为0,而最大值为100,扩展值为0。过程栏有一个扩展,但是不使用,尽管他是数据模型的一部分。

我们可以使用JProgressBar.HORIZONTAL或是JProgressBar.VERTICAL显示设置方向,同时可以使用不同的构造函数设置任意的特定模型属性。另外,我们可以为组件显示设置数据模型。

注意,将方向设置为VERTICAL或HORIZONTAL以外的值会抛出IllegalArgumentException。

由BoundedRangeModel创建JProgressBar有一些笨拙,因为过程栏会忽略一个设置并且初始值被初始化为最小值。假定我们希望JProgressBar向用户所期望的样子启动,我们需要记住当创建模型时要将扩展设置为0,并且将值设置最小值。如果我们增加extent属性,value属性的最大设置就会减少相应的量,从而value设置不会达到maximum属性的设置。

JProgressBar属性

在我们创建了JProgressBar之后,我们需要对其进行修改。表12-5显示了JProgressBar的14个属性。

Swing_table_12_5_1.png

Swing_table_12_5_1.png

Swing_table_12_5_2.png

Swing_table_12_5_2.png

绘制JProgressBar边框

所有的JComponent子类默认都有一个border属性,而JProgressBar有一个特殊的borderPainted属性可以很容易的允许或是禁止边框的绘制。使用参数false调用public void setBorderPainted(boolean newValue)方法可以关闭过程栏边框的绘制。图12-12右侧的过程栏就关闭了其边框。其初始化代码如下:

JProgressBar cJProgressBar = new JProgressBar(JProgressBar.VERTICAL);
cJProgressBar.setBorderPainted(false);

标识JProgressBar JProgressBar支持在组件中间显示文本。这种标签有三种形式:

  • 默认情况下不存在标签。
  • 要显示完成的百分比[100x(value-minimum)/(maximum-minimum)],使用参数true调用public void setStringPainted(boolean newValue)。这会显示0%到100%范围内的值。
  • 要将标签修改为固定的字符串,调用public void setString(String newValue)方法与setStringPainted(true)。在垂直过程栏上,字符串被反转绘制,所以较长的字符串会更适合。

图12-12的左边与底部过程栏分别显示了固定标签与百分比标签。创建这两个过程栏的代码如下:

JProgressBar bJProgressBar = new JProgressBar();
bJProgressBar.setStringPainted(true);
Border border = BorderFactory.createTitledBorder("Reading File");
bJProgressBar.setBorder(border);
JProgressBar dJProgressBar = new JProgressBar(JProgressBar.VERTICAL);
dJProgressBar.setString("Ack");
dJProgressBar.setStringPainted(true);

使用不确定的JProgressBar

某些过程并没有固定的步骤数目,或者是他们具有固定的步骤数目,但是我们并不知道在所有的步骤完成之间的数目。对于这种操作类型,JProgressBar提供了一种不确定模式,在这种模式中依据过程栏的方向,JProgressBar中的过程栏会由一边到另一边不断运行,或是由上到下不断运动。要允许这种模式,只需要使用true值为调用public void setIndeterminate(boolean newValue)方法。图12-13显示了不确定过程栏在不同时刻的样子。滑动块的长度是可用空间的六分之一,并且是不可设置的。

Swing_12_13.png

Swing_12_13.png

沿着JProgressBar步进

JProgressBar的主要用法显示我们在一系列操作中前进的过程。通常情况下,我们将过程栏最小值设置为0,而最大值设置为要执行的步骤数目。由value属性值0开始,当我们执行每一个步骤时向增加值向最大值靠近。所有这些操作意味着多线程,事实上,这是绝对必需的。另外,当更新过程栏的值时,我们需要记住只能在事件分发线程中更新(借助于EventQueue.invokeAndWait()方法)。

过程栏在一个范围内前进的过程如下:

1、初始化。这是使用所需要的方向与范围创建JProgressBar的基本过程。另外,在这个过程中执行边框与标签操作。

JProgressBar aJProgressBar = new JProgressBar(0, 50);
aJProgressBar.setStringPainted(true);

2、启动线程执行所需要的步骤。也许是作为在屏幕上执行动作的结果,我们需要启动线程来完成过程栏的工作。我们需要启动一个新线程,从而用户界面可以保持响应。

Thread stepper = new BarThread (aJProgressBar);
stepper.start();

3、执行步骤。忽略更新过程栏,而是编写相应的代码来执行每一个步骤。

static class BarThread extends Thread {
  private static int DELAY = 500;
  JProgressBar progressBar;
  public BarThread (JProgressBar bar) {
    progressBar = bar;
  }
  public void run() {
    int minimum = progressBar.getMinimum();
    int maximum = progressBar.getMaximum();
    for (int i=minimum; i<maximum; i++) {
      try {
        // Our job for each step is to just sleep
        Thread.sleep(DELAY);
      }  catch (InterruptedException ignoredException) {
      }  catch (InvocationTargetException ignoredException) {
        // The EventQueue.invokeAndWait() call
        // we'll add will throw this
      }
    }
  }
}

4、对于每一个步骤,使得线程在事件线程内更新过程栏。在for循环之外只创建一次Runnable类。没有必要为每一个步骤创建一个。

Runnable runner = new Runnable() {
  public void run() {
    int value = progressBar.getValue();
    progressBar.setValue(value+1);
  }
};

在循环之内,通知runner更新过程栏。这个更新必须使用特殊的EventQueue方法invokeLater()或是invokeAndWait()在事件线程内完成,因为我们正在更新JProgressBar的属性。

EventQueue.invokeAndWait (runner);

完整的运行示例显示在列表12-5中。

package swingstudy.ch12;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.InvocationTargetException;

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

public class ProgressBarStep {

    static class BarThread extends Thread {
        private static int DELAY = 500;
        JProgressBar progressBar;

        public BarThread(JProgressBar bar) {
            progressBar = bar;
        }

        public void run() {
            int minimum = progressBar.getMinimum();
            int maximum = progressBar.getMaximum();
            Runnable runner = new Runnable() {
                public void run() {
                    int value = progressBar.getValue();
                    progressBar.setValue(value+1);
                }
            };
            for(int i=minimum; i<maximum; i++) {
                try {
                    EventQueue.invokeAndWait(runner);
                    // Our job for each step is to just sleep
                    Thread.sleep(DELAY);
                }
                catch(InterruptedException ignoredException) {

                }
                catch(InvocationTargetException ignoredException){

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

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Stepping Progress");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                final JProgressBar aJProgressBar = new JProgressBar(0,50);
                aJProgressBar.setStringPainted(true);

                final JButton aJButton = new JButton("Start");

                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        aJButton.setEnabled(false);
                        Thread stepper = new BarThread(aJProgressBar);
                        stepper.start();
                    }
                };

                aJButton.addActionListener(actionListener);
                frame.add(aJProgressBar, BorderLayout.NORTH);
                frame.add(aJButton, BorderLayout.SOUTH);
                frame.setSize(300, 200);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}
Swing_12_14.png

Swing_12_14.png

通过简单的将列表12-5中的sleep动作修改为所需要的操作,这个示例提供了一个合适的重用框架。

注意,要使得过程栏填充相反的方向,使得值由最大值开始并且在每一个步骤减小。也许我们并不希望显示完成的百分比字符串,因为他将由100%开始减小到0%。

处理JProgressBar事件

由技术上来说,JProgressBar类通过ChangeListener支持数据模型变化的通知。另外,我们可以将ChangeListener关联到其数据模型。因为过程栏更多的意味着提供可视化的输出而不是获得输入,我们通常并会对其使用ChangeListener。然而,有时这却是适用的。要重用本章前面列表12-3中的BoundedRangeChangeListener,对其进行修改(列表12-6中以粗体显示),因为这些变化事件的源是JProgressBar。

package swingstudy.ch12;

import javax.swing.BoundedRangeModel;
import javax.swing.JProgressBar;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class BoundedChangeListener implements ChangeListener {

    @Override
    public void stateChanged(ChangeEvent event) {
        // TODO Auto-generated method stub

        Object source = event.getSource();
        if(source instanceof BoundedRangeModel) {
            BoundedRangeModel aModel = (BoundedRangeModel)source;
            if(!aModel.getValueIsAdjusting()) {
                System.out.println("Changed: "+aModel.getValue());
            }
        }
        else if(source instanceof JSlider) {
            JSlider theJSlider = (JSlider)source;
            if(!theJSlider.getValueIsAdjusting()) {
                System.out.println("Slider changed: "+theJSlider.getValue());
            }
        }
        else if(source instanceof JProgressBar) {
            JProgressBar theJProgressBar = (JProgressBar)source;
            System.out.println("ProgressBar changed: "+theJProgressBar.getValue());
        }
        else {
            System.out.println("Something changed: "+source);
        }
    }

}

自定义JProgressBar观感

每一个可安装的Swing观感都提供了不同的JProgressBar外观以及默认的UIResource值集合。图12-5显示了JProgressBar组件在预安装的观感类型集合下的外观。

表12-6显示了JProgressBar可用的UIResource相关属性的集合。他具有15个不同的属性。

Swing_table_12_6.png

Swing_table_12_6.png

Swing_table_12_6_1.png

Swing_table_12_6_1.png

Swing_12_15.png

Swing_12_15.png

JTextField类与BoundedRangeModel接口

JTextField组件并不是一个技术上的bounded-range组件,但是,他却使用BoundedRangeModel。当组件内容的宽度超出其可见的水平空间时,内建在JTextField内部的是一个可滚动的区域。BoundedRangeModel控制这个滚动区域。我们将会在第15章更详细的探讨JTextField组件。在这里我们可以了解JSrollBar如何跟踪JTextFiled的滚动区域。图12-16显示了一个实际的例子,而列表12-7显示了源码。

Swing_12_16.png

Swing_12_16.png

package swingstudy.ch12;

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

import javax.swing.BoundedRangeModel;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JTextField;

public class TextSlider extends JPanel {

    private JTextField textField;
    private JScrollBar scrollBar;

    public TextSlider() {
        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
        textField =  new JTextField();
        scrollBar = new JScrollBar(JScrollBar.HORIZONTAL);
        BoundedRangeModel brm = textField.getHorizontalVisibility();
        scrollBar.setModel(brm);
        add(textField);
        add(scrollBar);
    }

    public JTextField getTextField() {
        return textField;
    }

    public String getText() {
        return textField.getText();
    }

    public void addActionListener(ActionListener l) {
        textField.addActionListener(l);
    }

    public void removeActionListener(ActionListener l) {
        textField.removeActionListener(l);
    }

    public JScrollBar getScrollBar() {
        return scrollBar;
    }

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

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Text Slider");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                final TextSlider ts = new TextSlider();
                ts.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        System.out.println("Text: "+ts.getText());
                    }
                });
                frame.add(ts, BorderLayout.NORTH);
                frame.setSize(300, 100);
                frame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

通常情况下,JTextField并没有相关联的滚动条。事实上,大多数的观感类型并不提供。然而,如果这个组件是我们希望进行交互的组件,我们可以在我们的程序中进行重用。大量的访问方法可以重用得很简单,而且我们可以避免直接访问内部成员的需要。

小结

在本章中,我们了解了如何使用Swing的JScrollBar,JSlider与JProgressBar组件。我们了解了每一个组件如何使用BoundedRangeModel接口来控制操作这些组件所必需的内部数据,以及DefaultBoundedRangeModel类如何为这个数据模型提供了一个默认实现。

现在我们知道了如何使用各种具有边界范围的组件,我们可以继续进入第13章,在那里我们将会了解提供数据选择的控件:JList与JComboBox。

List Model Controls

12章探讨了支持滚动与输入或是显示某些边界范围值的边界范围组件。在本章中,我们将会探讨表示选项列表的两个选择控件:JList与JComboBox。这两个组件之间的主要区别在于JList组件支持多项选择,而JComboBox不支持。同时,JComboBox允许用户提供不在可用选项中的选项。

ListModel接口

图13-1显示了在本章中我们将要探讨的两个控件。

Swing_13_1.png

Swing_13_1.png

这两个组件之间所共享的数据模型是ListModel,从而形成了ListMode接口。AbstractListModel类通过支持ListDataListener对象的管理与通知提供了实现基础。

对于JList组件,数据模型的实现是DefaultListModel类。这个类添加了一个实际的数据仓库,其遵循Vector API,可以用于在JList组件内显示的不同的元素。

对于JComboBox组件,一个名为ComboBoxModel的ListModel接口扩展提供了在模型内选择项目的概念。DefaultComboBoxModel类通过另一个接口,MutableComboBoxModel实现了ComboBoxModel接口,MutableComboBoxModel为模型中元素的添加与移除提供了支持方法。

注意,BasicDirectoryModel类是另一个ListModel实现。这个实现为第9章所描述的文件选择器组件JFileChooser所用。

实际上ListModel接口非常简单。他提供了ListDataListener管理,并且访问模型特定元素的尺寸。

public interface ListModel {
  // Properties
  public int getSize();
  // Listeners
  public void addListDataListener(ListDataListener l);
  public void removeListDataListener(ListDataListener l);
  // Other methods
  public Object getElementAt(int index);
}

AbstractListModel类

AbstractListModel类提供了ListModel接口的部分实现。我们只需要提供数据结构与数据。这个类为ListDataListener的列表管理提供对象并且当数据变化时为这些监听器的通知提供框架。我们可以使用public ListDataListener[] getListDataListener()方法获取监听器列表。当我们修改数据模型时,我们必须调用AbstractListModel的相应方法来通知监听在ListDataListener对象:

  • protected void fireIntervalAdded(Object source, int index0, int index1):在向列表添加一个连续的范围值之后调用。
  • protected void fireIntervalRemoved(Object source, int index0, int index1):在由列表移除一个连续的范围值之后调用。
  • protected void fireContentsChanged(Object source, int index0, int index1):如果修改的范围对于插入,移除或是两者,不是连续的时调用。

如果我们的数据在一个已存在数据结构中,我们需要将其转换为Swing组件可以理解的格式或是我们自己实现ListModel接口。正如我们将要看到,数据或是Vector是直接为JList与JComboBox所支持的。我们可以将我们的数据结构包装进AbstractListModel。例如,如果我们的初始数据结构是集合框架中的ArrayList,我们可以使用下面的代码转换为一个ListModel:

final List arrayList = ...;
ListModel model = new AbstractListModel() {
  public int getSize() {
    return arrayList.size();
  }
  public Object getElementAt(int index) {
    return arrayList.get(index);
  }
}

另一个选择就是将List传递给Vector构造函数,然后将Vector传递给JList构造函数。事实上,我们已经完成了相同的事件。

DefaultListModel类

DefaultListModel类为我们提供了一个数据结构用来以Vector的形式存储内部数据。我们只需要添加数据,因为这个类为我们管理ListDataListener。

首先,我们使用无参数的构造函数创建数据结构:DefaultListModel model = new DefaultListModel()。然后我们进行冰封装填。如表13-1所示,DefaultListModel类只有两个属性。

Swing_table_13_1.png

Swing_table_13_1.png

DefaultListModel类通过一系列的公开方法提供了所有的操作方法。要添加元素,可以使用下面的方法:

public void add(int index, Object element)
public void addElement(Object element)
public void insertElementAt(Object element, int index)

DefaultListModel的addElement()方法将元素添加到数据模型的尾部。要修改元素,使用下面的方法:

public Object set(int index, Object element)
public void setElementAt(Object element, int index)

要移除元素,可以使用下面的方法:

public void clear()
public Object remove(int index)
public void removeAllElements()
public boolean removeElement(Object element)
public void removeElementAt(int index)
public void removeRange(int fromIndex, int toIndex)

removeElement()方法返回一个状态:如果他找到对象并且移除则返回true,否则返回false。

当我们并没有将数据存储在已存在数据结构中时,DefalutListModel类十分有用。例如,数据库查询的结果会作为JDBC ResultSet返回。如果我们希望使用这些结果作为显示在JList中的内容的基础,我们必须将其存储在某些地方。这就可以存储在DefaultListModel中,如下面的代码所示:

ResultSet results = aJDBCStatement.executeQuery(
   "SELECT columnName FROM tableName");
DefaultListModel model = new DefaultListModel();
while (results.next()) {
  model.addElement(result.getString(1));
}

使用ListDataListener监听ListModel事件

如果我们对确定列表模型的内容何时发生变化感兴趣,我们可以向模型注册一个ListDataListsener。接口的三个单独方法可以告诉我们内容何时被添加,被移除或是被修改。修改数据模型意味着由数据模型的一个或多个区域添加或移除内容或者是没有添加或是移除元素修改已存在的内容。接口定义如下:

public interface ListDataListener extends EventListener {
  public void contentsChanged(ListDataEvent e);
  public void intervalAdded(ListDataEvent e);
  public void intervalRemoved(ListDataEvent e);
}

基于列表修改事件的通知,我们可以传递一个ListDataEvent实现,其包含三修改必,如表13-2所示。

Swing_table_13_2.png

Swing_table_13_2.png

索引并不是顺序所必须的,也不是修改区域的边界。在列表模型内容修改的例子中,并不是区域中的所有内容都会被修改。内容实际变化的区域是通过索引指定的边界区域。type属性的设置是表13-3中的三个常量之一,这直接映射到所调用的接口方法。

Swing_table_13_3.png

Swing_table_13_3.png

如果当DefaultListModel类的操作方法被调用时,ListDataListener对象被关联到数据模型,每一个监听器都会得到数据模型变化的通知。为了演示ListDataListesner的使用以及数据模型的动态更新,列表13-1中的ModifyModelSample程序使用的DefaultListModel类修改方法,以事件的形式发送输出并列出JTextArea的内容。

package swingstudy.ch13;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.PrintWriter;
import java.io.StringWriter;

import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;

public class ModifyModelSample {

    static String labels[] = {"Chardonnay", "Sauvignon", "Riesling", "Cabernet",
        "Zinfandel", "Merlot", "Pinot Noir", "Sauvignon Blanc", "Syrah", "Gewurztraminer"
    };

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

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

                // Fill model
                final DefaultListModel model = new DefaultListModel();
                for(int i=0; i<labels.length; i++) {
                    model.addElement(labels[i]);
                }

                JList jList = new JList(model);
                JScrollPane scrollPane1 = new JScrollPane(jList);
                frame.add(scrollPane1, BorderLayout.WEST);

                final JTextArea textArea = new JTextArea();
                textArea.setEditable(false);
                JScrollPane scrollPane2 = new JScrollPane(textArea);
                frame.add(scrollPane2, BorderLayout.CENTER);

                ListDataListener listDataListener = new ListDataListener() {
                    public void contentsChanged(ListDataEvent event) {
                        appendEvent(event);
                    }
                    public void intervalAdded(ListDataEvent event) {
                        appendEvent(event);
                    }
                    public void intervalRemoved(ListDataEvent event) {
                        appendEvent(event);
                    }
                    private void appendEvent(ListDataEvent event) {
                        StringWriter sw = new StringWriter();
                        PrintWriter pw = new PrintWriter(sw);
                        switch(event.getType()) {
                        case ListDataEvent.CONTENTS_CHANGED:
                            pw.print("Type: contents Changed");
                            break;
                        case ListDataEvent.INTERVAL_ADDED:
                            pw.print("Type: Interval Added");
                            break;
                        case ListDataEvent.INTERVAL_REMOVED:
                            pw.print("Type: Interval Removed");
                            break;
                        }
                        pw.print(", Index0: "+event.getIndex0());
                        pw.print(", Index1 "+event.getIndex1());
                        DefaultListModel theModel = (DefaultListModel)event.getSource();
                        pw.println(theModel);
                        textArea.append(sw.toString());
                    }
                };

                model.addListDataListener(listDataListener);

                // Set up buttons
                JPanel jp = new JPanel(new GridLayout(2,1));
                JPanel jp1 = new JPanel(new FlowLayout(FlowLayout.CENTER, 1,1));
                JPanel jp2 = new JPanel(new FlowLayout(FlowLayout.CENTER, 1,1));
                jp.add(jp1);
                jp.add(jp2);
                JButton jb = new JButton("add F");
                jp1.add(jb);
                jb.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        model.add(0, "First");
                    }
                });
                jb = new JButton("addElement L");
                jp1.add(jb);
                jb.addActionListener(new ActionListener(){
                    public void actionPerformed(ActionEvent event) {
                        model.addElement("Last");
                    }
                });
                jb = new JButton("insertElementAt M");
                jp1.add(jb);
                jb.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        int size = model.getSize();
                        model.insertElementAt("Middle", size/2);
                    }
                });
                jb = new JButton("set F");
                jp1.add(jb);
                jb.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        int size = model.getSize();
                        if (size != 0) {
                            model.set(0, "New First");
                        }
                    }
                });
                jb = new JButton("setElementAt L");
                jp1.add(jb);
                jb.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        int size = model.getSize();
                        if(size!=0)
                            model.setElementAt("New Last", size-1);
                    }
                });
                jb = new JButton("load 10");
                jp1.add(jb);
                jb.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        for(int i=0, n=labels.length; i<n; i++) {
                            model.addElement(labels[i]);
                        }
                    }
                });
                jb =  new JButton("clear");
                jp2.add(jb);
                jb.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        model.clear();
                    }
                });
                jb = new JButton("remove F");
                jp2.add(jb);
                jb.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        int size = model.getSize();
                        if(size !=0)
                            model.remove(0);
                    }
                });
                jb = new JButton("removeAllElements");
                jp2.add(jb);
                jb.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        model.removeAllElements();
                    }
                });
                jb = new JButton("removeElement 'Last'");
                jp2.add(jb);
                jb.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        model.removeElement("Last");
                    }
                });
                jb = new JButton("removeElementAt M");
                jp2.add(jb);
                jb.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        int size = model.getSize();
                        if(size != 0)
                            model.removeElementAt(size/2);
                    }
                });
                jb = new JButton("removeRange FM");
                jp2.add(jb);
                jb.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        int size = model.getSize();
                        if(size !=0)
                            model.removeRange(0, size/2);
                    }
                });
                frame.add(jp, BorderLayout.SOUTH);
                frame.setSize(640, 300);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

图13-2显示了程序的运行结果。

Swing_13_2.png

Swing_13_2.png

DefaultListModel类的获取方法会随着他们的功能而变化。这个类具有基本的访问方法public Object get(int index), public Object getElementAt(int index)以及public Object elementAt(int index),这三个方法都可以完成相同的事情。DefaultListModel类同时也具有更为特殊的方法。例如,为了使用所有的元素,我们可以使用public Enumeration elements()方法获取Enumeration的实例。

或者是如果我们希望以数组的方式操作所有的元素,可以使用public Object[] toArray()或是public void copyInto(Object anArray[])。我们也可以使用方法来检测模型中是否存在某一个元素,public boolean contains(Object element), public int indexOf(Object element), public int indexOf(Object element, int index), public int lastIndexOf(Object element)以及public int lastIndexOf(Object element, int index)。

提示,一旦我们完成了向数据模型添加元素,使用public void trimToSize()方法修整其长度是个好主意。这会移除数据结构内部所分配的额外空间。另外,如果我们知道数据尺寸,我们可以调用public void ensureCapacity(int minCapacity)来预分配空间。这两种方法都可以用于DefaultListModel。

ComboBoxModel接口

ComboBoxModel接口扩展了ListModel接口。扩展的主要原因是因为实现ComboBoxModel接口的类需要通过selectedItem属性来管理被选中的项目,如下面的接口定义所示:

public interface ComboBoxModel extends ListModel {
  // Properties
  public Object getSelectedItem();
  public void setSelectedItem(Object anItem);
}

MutableComboBoxModel接口

除了ComboBoxModel接口以外,另一个数据模型接口MutableComboBoxModel扩展了ComboBoxModel从而构成了可以修改数据模型的方法。

public interface MutableComboBoxModel extends ComboBoxModel {
  // Other methods
  public void addElement(Object obj);
  public void insertElementAt(Object obj, int index);
  public void removeElement(Object obj);
  public void removeElementAt(int index);
}

JComboBox组件默认使用这个接口的实现。

DefaultComboBoxModel类

DefaultComboBoxModel类扩展了AbstractListModel类来为JComboBox提供相应的方法。由于这种扩展,他继承了ListDataListener列表的管理。

类似于DefaultListModel,DefaultComboBoxModel为我们添加了收集显示在组件中的元素所必需的数据结构。同时,由于模型是可修改的,实现MutableComboBoxModel会使得当模型中的数据元素发生变化时,数据模型调用AbstractListModel的各种fileXXX()方法。

注意,如果我们由一个数组创建了DefaultComboBoxModel,数组的元素会被拷贝到一个内部数据结构中。如果我们使用Vector,他们不会被拷贝;相反,在内部会使用实际的Vector。

要使用数据模型,我们必须首先使用下面的构造函数来创建模型:

public DefaultComboBoxModel()
DefaultComboBoxModel model = new DefaultComboBoxModel();
public DefaultComboBoxModel(Object listData[])
String labels[] = { "Chardonnay", "Sauvignon", "Riesling", "Cabernet", "Zinfandel",
  "Merlot", "Pinot Noir", "Sauvignon Blanc", "Syrah", "Gewürztraminer"};
DefaultComboBoxModel model = new DefaultComboBoxModel(labels);
public DefaultComboBoxModel(Vector listData)
Vector vector = aBufferedImage.getSources();
DefaultComboBoxModel model = new DefaultComboBoxModel(vector);

然后,我们操作模型。DefaultComboBoxModel类引入了两个新属性,如表13-4所示。

Swing_table_13_4.png

Swing_table_13_4.png

DefaultComboBoxModel的数据模型修改方法不同于DefaultListModel的模型修改方法。他们来自于MutableComboBoxModel接口:

public void addElement(Object element)
public void insertElementAt(Object element, int index)
public boolean removeElement(Object element)
public void removeElementAt(int index)

由于DefaultComboBoxModel的灵活性(以及功能性),通常并不需要创建我们自己的ComboBoxModel实现。只需要创建一个DefaultComboBoxModel实例,然后简单的使用相应的数据源对其进行装配。

注意,我们也许希望提供我们自己模型的一个例子就是当我们需要支持模型多个项目中相同项目的表示。对于DefaultComboBoxModel,如果我们在其equals()方法返回true的列表中有两个项目,模型不会正确的工作。

如果我们确实希望定义我们自己的模型实现,也许是因为在我们已经有数据存储在我们的数据结构中,最好的方法就是继承AbstractListModel并且实现ComboBoxModel或是MutableComboBoxModel接口方法。当继承AbstractListModel时,我们只需要提供数据结构以及对其的访问接口。因为数据模型的“选中项目”部分是在基本的数据结构之外进行管理的,我们也需要提供的一个位置进行存储。列表13-2中的程序源码演示了使用ArrayList作为数据结构的实现。程序包含main()方法来演示JComboBox中模型的使用。

package swingstudy.ch13;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.util.ArrayList;
import java.util.Collection;

import javax.swing.AbstractListModel;
import javax.swing.ComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JFrame;

public class ArrayListComboBoxModel extends AbstractListModel implements ComboBoxModel{

    private Object selectedItem;
    private ArrayList anArrayList;

    public ArrayListComboBoxModel(ArrayList arrayList) {
        anArrayList = arrayList;
    }

    public Object getSelectedItem() {
        return selectedItem;
    }

    public void setSelectedItem(Object newValue) {
        selectedItem = newValue;
    }

    public int getSize() {
        return anArrayList.size();
    }

    public Object getElementAt(int i) {
        return anArrayList.get(i);
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

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

                Collection<Object> col = System.getProperties().values();
                ArrayList<Object> arrayList = new ArrayList<Object>(col);
                ArrayListComboBoxModel model = new ArrayListComboBoxModel(arrayList);

                JComboBox comboBox = new JComboBox(model);

                frame.add(comboBox, BorderLayout.NORTH);
                frame.setSize(300, 225);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

图13-3实际显示了使用当前的系统属性作为数据模型元素的数据源中模型。

Swing_13_3.png

Swing_13_3.png

JList类

JList组件是用于由一个选项集合中选择一个或多个项目的基本Swing组件。我们向用户展示选项列表,依据于组件的选择模式,用户可以选择一个或多个。

三个关键元素及其实现定义了JList结构:

  • 用于存储JList数据的数据模型,由ListModel接口定义。
  • 用于绘制JList的单元渲染器,由ListCellRenderer接口描述。
  • 用于选择JList元素的选择模式,由ListSelectionModel接口描述。

创建JList组件

JList组件有四个构造函数,可以允许我们基于我们的初始数据结构创建JList实例:

public JList()
JList jlist = new JList();
public JList(Object listData[])
String labels[] = { "Chardonnay", "Sauvignon", "Riesling", "Cabernet", "Zinfandel",
  "Merlot", "Pinot Noir", "Sauvignon Blanc", "Syrah", "Gewürztraminer"};
JList jlist = new JList(labels);
public JList(Vector listData)
Vector vector = aBufferedImage.getSources();
JList jlist = new JList(vector);
public JList(ListModel model)
ResultSet results = aJDBCStatement.executeQuery("SELECT colName FROM tableName");
DefaultListModel model = new DefaultListModel();
while (result.next())
  model.addElement(result.getString(1));
JList jlist = new JList(model);

如果我们使用无参数的构造函数,我们可以稍后填充数据。然而,如果我们使用数组或是Vector构造函数,如果不修改整个模式,那么我们就不能修改内容。

注意,如果我们希望显示一些内容而不是每一个数组元素的toString()结果,可以查看本章稍后的“渲染JList元素”来了解如何实现。

JList属性

在创建了JList组件之后,我们可以修改其每一个属性。表13-5显示了JList的32个属性。

Swing_table_13_5_1.png

Swing_table_13_5_1.png

Swing_table_13_5_2.png

Swing_table_13_5_2.png

Swing_table_13_5_3.png

Swing_table_13_5_3.png

JList属性中的多个都与选择过程有关。例如,anchorSelectionIndex, leadSelectionIndex, maxSelectionIndex, minSelectionIndex, selectedIndex与selectedIndices处理被选中行的索引,而selectedValue与selectedValues与被选中元素的内容有关。anchorSelectionIndex是ListDataEvent最近的index0,而leadSelectionIndex则是最近的index1。

要控制所显示的可视行的数目,设置JList的visibleRowCount属性。这个属性的默认设置为8。

滚动JList组件

当我们使用JList组件时,如果我们希望允许用户在所有的选项中进行选择,我们必须将组件放置在一个JScrollPane中。如果我们没有将其放置在一个JScrollPane中,而默认显示的行数小于数据模型的尺寸,或者是没有足够的空间来显示行,则其他的选项不会显示。当放置在JScrollPane中时,JList提供了一个垂直滚动条来在所有的选项中移动。

如果我们没有将JList放置在JScrollPane中,并且选项的数目超出了可用空间时,只有上部的选项组可见,如图13-4所示。

Swing_13_4.png

Swing_13_4.png

提示,当我们看到了一个类实现了Scrollable接口,我们就应该想起在将其添加到程序之前需要将其放在JScrollPane中。

JScrollPane依赖于preferredScrollableViewportSize属性设置所提供的维度来确定面板内容的最优尺寸。当JList的数据模型为空时,则会使用每个可见行16像素高256像素宽的默认尺寸。否则宽度通过遍历所有的行来查找最宽的行来确定,而高度是通过第一行的高度来确定。

为了快速确定JScrollPane视图区域的尺寸,我们可以通过设置prototypeCellValue属性来定义一个原型行。我们必须保证原型toString()的值足够宽与足够高从而适应JList中的所用内容。然后JScrollPane基于原型视图区域的尺寸,从而JList就没有必要来询问每一行的尺寸;相反,只需要询问原型的尺寸即可。

我们也可以通过为fixedCellHeight与fixedCellWidth属性指定尺寸来改善性能。设置这些属性是避免JList询问每一行渲染尺寸的另一种方法。设置两个属性是使得JList确定在视图区域中尺寸的最快的方法。当然,这也是最不灵活的方法,因为他保证当内容发生变化时JList的选项不会变宽(或变短)。然而,如果我们在数据模型中有大量的条目,这些灵活性的缺失对于改善性能是值得的。图13-5帮助我们理解JList的一些尺寸功能。

Swing_13_5.png

Swing_13_5.png

用来生成图13-5的源码显示在列表13-3中。图中间的列表包含超出1000个固定尺寸的行。顶部的列表显示了我们可以通过setVisibleRowCount()方法设置可视行的数目。然而,因为列表并没有位于JScrollPane中,行数目限制的请求会被忽略。

package swingstudy.ch13;

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

import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JScrollPane;

public class SizingSamples {

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

        Runnable runner = new Runnable() {
            public void run() {
                String labels[] = { "Chardonnay", "Sauvignon", "Riesling", "Cabernet",
                "Zinfandel", "Merlot", "Pinot Noir", "Sauvignon Blanc", "Syrah", "Gewurztraminer"};

                JFrame frame = new JFrame("Sizing Samples");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                JList jlist1 = new JList(labels);
                jlist1.setVisibleRowCount(4);
                DefaultListModel model = new DefaultListModel();
                for(int i=0; i<100; i++) {
                    for(int j=0; j<10; j++) {
                        model.addElement(labels[j]);
                    }
                }
                JScrollPane scrollPane1 = new JScrollPane(jlist1);
                frame.add(scrollPane1, BorderLayout.NORTH);

                JList jlist2 = new JList(model);
                jlist2.setVisibleRowCount(4);
                jlist2.setFixedCellHeight(12);
                jlist2.setFixedCellWidth(200);
                JScrollPane scrollPane2 = new JScrollPane(jlist2);
                frame.add(scrollPane2, BorderLayout.CENTER);

                JList jlist3 = new JList(labels);
                jlist3.setVisibleRowCount(4);
                frame.add(jlist3, BorderLayout.SOUTH);

                frame.setSize(300, 350);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

除了将JList放在JScrollPane中以外,我们可以确定哪些选项是可见或是请求特定的元素可见。firstVisibleIndex与lastVisibleIndex属性使得我们可以确定在JScrollPane中哪些选项是当前可见的。如果没有选项可见,两个方法都会返回-1;这通常在数据模型为空的情况下发生。要请求一个特定的元素可见,使用public void ensureIndexIsVisible(int index)方法。例如,要编程将列表移动到顶部可以使用下面的代码:

jlist.ensureIndexIsVisible(0);

渲染JList元素

JList中的每一个元素被称之为单元。每一个JList都有一个已安装的单元渲染器,当列表需要绘制时渲染器绘制每一个单元。默认的渲染器,DefaultListCellRenderer是JLable的一个子类,这就意味着我们可以使用文本或是图标作为单元的图形显示。这可以满足大多数用户的需要,但是有时单元的外观需要进行某些定制。耏 一,每一个JList至多只有一个安装的渲染器,自定义需要我们替换已安装的渲染器。

ListCellRender接口与DefaultListCellRenderer类

JList有一个已安装的渲染器。实现了ListCellRender接口的类提供了这个渲染器。

public interface ListCellRenderer {
  public Component getListCellRendererComponent(JList list, Object value,
    int index, boolean isSelected, boolean cellHasFocus);
}

当需要绘制单元时,接口的核心方法被调用。返回的渲染器为列表中的单元提供特定的渲染。Jlist使用渲染来绘制元素,然后获取下一个渲染器。

一个到JList的引用会被提供给getListCellRendererComponent()方法,从而渲染器可以共享显示特性。选中的value包含列表数据模型在位置index上的对象。索引由数据模型的开始处由0开始。最后两个参数允许我们基于单元的状态自定义单元的外观,也就是他是否被选中或是具有输入焦点。

列表13-4显示了一个演示这种技术的渲染器。这个渲染器的核心不同之处在于具有输入焦点的单元有一个带有标题的边框。在渲染器被创建之后,我们通过设置JList的cellRenderer属性来安装。

提示,由于性能的原因,最好不要在getListCellRendererComponent()方法创建实际的渲染器。可以派生Component并返回this或是创建一个类变量来存储Component的实例,然后进行自定义并返回。

package swingstudy.ch13;

import java.awt.Component;

import javax.swing.DefaultListCellRenderer;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.ListCellRenderer;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;

public class FocusedTitleListCellRenderer implements ListCellRenderer {

    protected static Border noFocusBorder = new EmptyBorder(15, 1, 1, 1);
    protected static TitledBorder focusBorder = new TitledBorder(LineBorder.createGrayLineBorder(), "Focused");
    protected DefaultListCellRenderer defaultRenderer = new DefaultListCellRenderer();

    public String getTitle() {
        return focusBorder.getTitle();
    }

    public void setTitle(String newValue) {
        focusBorder.setTitle(newValue);
    }
    @Override
    public Component getListCellRendererComponent(JList list, Object value,
            int index, boolean isSelected, boolean cellHasFocus) {
        // TODO Auto-generated method stub
        JLabel renderer = (JLabel)defaultRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
        renderer.setBorder(cellHasFocus ? focusBorder : noFocusBorder);
        return renderer;
    }

}

注意,当创建我们自己的渲染器时一个觉错误就是忘记使得渲染器组件非透明。这会使得渲染器的背景颜色被忽略,并且列表容器的背景外漏。使用DefaultListCellRenderer类,渲染器组件已经是不透明的了。

列表13-5显示了一个使用这个新渲染器的示例程序。他并没有做任何特殊的事情,而只是安装了刚才创建的自定义单元渲染器。

package swingstudy.ch13;

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

import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;

public class CustomBorderSample {

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

        Runnable runner = new Runnable() {
            public void run() {
                String labels[] = {"Chardonnay", "Sauvignon", "Riesling", "Cabernet",
                        "Zinfandel", "Merlot", "Pinot Noir", "Sauvignon Blanc", "Syrah",
                        "Gewurztraminer"
                };
                JFrame frame = new JFrame("Custom Border");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                JList jlist =  new JList(labels);
                ListCellRenderer renderer = new FocusedTitleListCellRenderer();
                jlist.setCellRenderer(renderer);
                JScrollPane sp = new JScrollPane(jlist);
                frame.add(sp, BorderLayout.CENTER);
                frame.setSize(300, 200);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

图13-6显示了示例程序的输出。

Swing_13_6.png

Swing_13_6.png

创建复杂的ListCellRenderer

有时,当数据模型由每个元素中的多个复杂数据组成,不能由文本字符串表示的内容,自定义的单元渲染器(类似于图13-6所示)是必需的。例如,列表13-6显示了一个示例的源码,其中每一个数据模型的元素由字体,前景色,图标与文本字符串组成。保证渲染器中这些元素的正确使用简单的涉及到在配置渲染器组件方面的更多工作。在这个特定的例子中,数据存储在数据模型中数组的每个元素之中。我们可以简单的定义一个新类或是使用散列表。

package swingstudy.ch13;

import java.awt.Color;
import java.awt.Component;
import java.awt.Font;

import javax.swing.DefaultListCellRenderer;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.ListCellRenderer;

public class ComplexCellRenderer implements ListCellRenderer {

    protected DefaultListCellRenderer defaultRenderer = new DefaultListCellRenderer();

    @Override
    public Component getListCellRendererComponent(JList list, Object value,
            int index, boolean isSelected, boolean cellHasFocus) {
        // TODO Auto-generated method stub
        Font theFont = null;
        Color theForeground = null;
        Icon theIcon = null;
        String theText = null;

        JLabel renderer = (JLabel)defaultRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);

        if(value instanceof Object[]) {
            Object values[] = (Object[])value;
            theFont = (Font)values[0];
            theForeground = (Color)values[1];
            theIcon = (Icon)values[2];
            theText = (String)values[3];
        }
        else {
            theFont = list.getFont();
            theForeground = list.getForeground();
            theText = "";
        }
        if(!isSelected) {
            renderer.setForeground(theForeground);
        }
        if(theIcon != null) {
            renderer.setIcon(theIcon);
        }
        renderer.setText(theText);
        renderer.setFont(theFont);
        return renderer;
    }

}

渲染器很少自定义DefaultListCellRenderer返回的渲染器组件。自定义基于作为数组传递给getListCellRendererComponent()方法的value参数的数据模型值。列表13-7显示了测试类。这个演示程序重用了第4章所创建的DiamondIcon。大部分代码用于数据模型的初始化。

package swingstudy.ch13;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;

import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;

import swingstudy.ch04.DiamondIcon;

public class ComplexRenderingSample {

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

        Runnable runner = new Runnable() {
            public void run() {
                Object elements[][] = {
                        {new Font("Helvetica", Font.PLAIN, 20), Color.RED, new DiamondIcon(Color.BLUE), "Help"},
                        {new Font("TimesRoman", Font.BOLD, 14), Color.BLUE, new DiamondIcon(Color.GREEN), "Me"},
                        {new Font("Courier", Font.ITALIC, 18), Color.GREEN, new DiamondIcon(Color.BLACK), "I'm"},
                        {new Font("Helvetica", Font.BOLD|Font.ITALIC, 12), Color.GRAY, new DiamondIcon(Color.MAGENTA), "Trapped"},
                        {new Font("TimesRoman", Font.PLAIN, 32), Color.PINK, new DiamondIcon(Color.YELLOW), "Inside"},
                        {new Font("Courier", Font.BOLD, 16), Color.YELLOW, new DiamondIcon(Color.RED), "This"},
                        {new Font("Helvetica", Font.ITALIC, 8), Color.DARK_GRAY, new DiamondIcon(Color.PINK), "Computer"}
                };
                JFrame frame = new JFrame("Complex Renderer");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                JList jlist = new JList(elements);
                ListCellRenderer renderer = new ComplexCellRenderer();
                jlist.setCellRenderer(renderer);
                JScrollPane scrollPane = new JScrollPane(jlist);
                frame.add(scrollPane, BorderLayout.CENTER);
                frame.setSize(300, 200);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

程序的输出结果如图13-7所示。

Swing_13_7.png

Swing_13_7.png

提示,当我们创建自己的渲染组件时,我们将会发现最好由默认的列表单元渲染器开始。这可以使得我们专注于我们感兴趣的特定细节。否则,我们就需要考虑所有事情,例如默认选择的前景与背景颜色,以及我们是否记得使得组件非透明。当然,如果我们希望亲自配置所有事情,自由去做就是了。

选择JList元素

默认情况下,所有的JList组件处于多项选择模式。这意味着我们可以选择组件中的多个元素。我们如何选择多个元素依赖于我们正在使用的用户界面。例如,对于Ocean观感界面,Ctrl-select可以作为选择切换,而Shift-select可以作为一种范围选择的方法。

ListSelectionModel接口与DefaultListSelectionModel类

ListSelectionModel接口的实现控制JList组件的选择机制。在这里显示的接口定义定义了用于不同选择模式的常量并且描述了如何管理ListSelectionListener对象的列表。他同时提供了一种方法来描述多个内部选择。

public interface ListSelectionModel {
  // Constants
  public final static int MULTIPLE_INTERVAL_SELECTION;
  public final static int SINGLE_INTERVAL_SELECTION;
  public final static int SINGLE_SELECTION;
  // Properties
  public int getAnchorSelectionIndex();
  public void setAnchorSelectionIndex(int index);
  public int getLeadSelectionIndex();
  public void setLeadSelectionIndex(int index);
  public int getMaxSelectionIndex();
  public int getMinSelectionIndex();
  public boolean isSelectionEmpty();
  public int getSelectionMode();
  public void setSelectionMode(int selectionMode);
  public boolean getValueIsAdjusting();
  public void setValueIsAdjusting(boolean valueIsAdjusting);
  // Listeners
  public void addListSelectionListener(ListSelectionListener x);
  public void removeListSelectionListener(ListSelectionListener x);
  // Other methods
  public void addSelectionInterval(int index0, int index1);
  public void clearSelection();
  public void insertIndexInterval(int index, int length, boolean before);
  public boolean isSelectedIndex(int index);
  public void removeIndexInterval(int index0, int index1);
  public void removeSelectionInterval(int index0, int index1);
  public void setSelectionInterval(int index0, int index1);
}

其中有三个不同的选择模式。表13-6中包含了每个模式的名字及其描述。

Swing_table_13_6.png

Swing_table_13_6.png

图13-8显示了每个选择模式的结果。

Swing_13_8.png

Swing_13_8.png

要修改JList的选择模式,将selectionModel属性设置为表13-6中的一个ListSelectionModel常量。例如,下面的代码可以将一个列表的修改为单选模式:

JList list = new JList(...);
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

DefaultListSelectionModel类是ListSelectionModel接口的默认实现。我们可以尝试表13-7中所示的九个属性来了解当前的选中范围。

Swing_table_13_7.png

Swing_table_13_7.png

当selectionEmpty属性为false时选择模式可以显示我们当前在多项选择模式中使用的是哪一种。如果是使用public boolean isSelectedIndex(int index)方法选中的,则只需要简单的查询最小与最大选中索引中的每一个索引。因为多项选择模式支持不连续的区域,这是确定哪一个被选中的唯一方法。然而,JList的selectedIndeices属性提供了这种信息,而不需要我们手动检测。

使用ListSelectionListener监听JList事件

如果我们希望了解何时JList的元素被选中,我们需要向JList或是ListSelectionModel关联一个ListSelectionListener。Jlist的addListListSelectionListener()与removeListSelectionListener()方法只会委托给底层的ListSelectionModel。当被选中的元素集合发生变化时,所关联的监听器对象会得到通知。接口定义如下:

public interface ListSelectionListener extends EventListener {
  public void valueChanged(ListSelectionEvent e);
}

监听器所接收的ListSelectionEvent实例描述了这个选中事件的所影响的元素的范围以及选中是否仍在变化,如表13-8所示。当用户仍在修改被选中的元素,通过valueIsAdjusting设置为true,我们也许会希望延迟执行耗费资源的操作,例如绘制一个高分辨率的图形显示。

Swing_table_13_8.png

Swing_table_13_8.png

为了演示JList的选中,列表13-8中所示的程序向窗口添加了一个JTextArea来显示选中监听器的输出。监听器输出当前被选中的项的位置与值。

package swingstudy.ch13;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.io.PrintWriter;
import java.io.StringWriter;

import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

public class SelectingJListSample {

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

        Runnable runner = new Runnable() {
            public void run() {
                String labels[] = {"Chardonnay", "Sauvignon", "Riesling", "Cabernet",
                        "Zinfandel", "Merlot", "Pinot Noir", "Sauvignon Blanc", "Syrah", "Gewurztraminer"
                };

                JFrame frame = new JFrame("Selecting Jlist");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                JList jlist = new JList(labels);
                JScrollPane scrollPane1 = new JScrollPane(jlist);
                frame.add(scrollPane1, BorderLayout.WEST);

                final JTextArea textArea =  new JTextArea();
                textArea.setEditable(false);
                JScrollPane scrollPane2 = new JScrollPane(textArea);
                frame.add(scrollPane2, BorderLayout.CENTER);

                ListSelectionListener listSelectionListener= new ListSelectionListener() {
                    public void valueChanged(ListSelectionEvent event) {
                        StringWriter sw = new StringWriter();
                        PrintWriter pw = new PrintWriter(sw);
                        pw.println("First index: "+event.getFirstIndex());
                        pw.println(", Last idnex: "+event.getLastIndex());
                        boolean adjust = event.getValueIsAdjusting();
                        pw.println(", Adjusting? "+adjust);
                        if(!adjust) {
                            JList list = (JList)event.getSource();
                            int selections[] = list.getSelectedIndices();
                            Object selectionValues[] = list.getSelectedValues();
                            for(int i=0, n=selections.length; i<n; i++) {
                                if(i==0) {
                                    pw.println(" Selections: ");
                                }
                                pw.print(selections[i]+"/"+selectionValues[i]+" ");
                            }
                            pw.println();
                        }
                        textArea.append(sw.toString());
                    }
                };
                jlist.addListSelectionListener(listSelectionListener);

                frame.setSize(350, 200);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

注意,如果我们知道JList处理单项选择模式中,我们可以使用selectedIndex或是selectedValue属性来获取当前被选中的项目。

图13-9显示了程序运行的结果。

Swing_13_9.png

Swing_13_9.png

列表13-8中的示例在没有快速更新时只输出当前选中的项(当isAdjusting报告false)时。否则,程序仅报告选中范围变化的起始与结束,以及调整状态。程序会检测JList的selectedIndices与selectedValues属性来获得选中项的有序列表。slectedIndices与slectedValues数组以相同的方式排序,所以数据模式中的特定元素显示在两个列表中的相同位置。

并没有特殊的选中事件用于处理列表中元素的双击。如果我们对双击感兴趣,那我们就要求助于AWT的MouseEvent/MouseListener对了。将下面的代码添加到列表13-8的程序中会向TextArea添加相应的文本用于响应双击事件。这里的关键方法是JList的public int locationToIndex(Point location),他会将屏幕坐标映射到列表元素。

import java.awt.event.*;
...
    MouseListener mouseListener = new MouseAdapter() {
      public void mouseClicked(MouseEvent mouseEvent) {
        JList theList = (JList)mouseEvent.getSource();
        if (mouseEvent.getClickCount() == 2) {
          int index = theList.locationToIndex(mouseEvent.getPoint());
          if (index >= 0) {
            Object o = theList.getModel().getElementAt(index);
            textArea.append("Double-clicked on: " + o.toString());
            textArea.append(System.getProperty("line.separator"));
          }
        }
      }
    };
    jlist.addMouseListener(mouseListener);

注意,JList类同时提供了一个public Point indexToLocation(int index)方法,这个方法会生成相反的行为。

手动选择JList事件

除了检测用户何时选择了列表中的项以外,我们也可以编程实现了列表项的选中与非选中。如果ListSelectionListener对象被叛逆到JList,当选中的项目集合被编程修改时他们也会得到相应的通知。可以使用下面的方法:

  • 对于单个项目,public void setSelectedValue(Object element, boolean shouldScroll)选中与element匹配的第一项。如果element并不是前一次被选中的,已被选中的所有条目会首先取消选中。
  • 对于一系列的项目,public void setSelectedInterval(int index0, int index1)选择一个包含的范围。
  • 要向已选中的集合添加一系列的选中项目,使用public void addSelectedInterval(int index0, int index1)。
  • 我们可以使用public void clearSelection()方法清除所有被选中的条目。
  • 我们可以使用public void removeSelectedInterval(int index0, int index1)方法清除一个选中条目的范围。

显示多列

通常,当我们使用JList时,我们在单列中的显示其选项。尽管这是通常的使用方法,Swing JList控件为在多列中显示其选项提供了支持。借且于setLayoutOrientation()方法,我们可以设置JList方法来在水平列或是垂直列中的布局其单元。JList.VERTICAL是默认设置,其中所有的选项在一列中的显示。

要水平布局单元,在进入到下一行之前,使用值JList.HORIZONTAL_WRAP。例如,一个具有九个元素的列表可以以下面的方式显示:

Swing_13_list_1.png

Swing_13_list_1.png

要垂直布局单元,在进入下一列之前,使用值JList.VERTICAL_WRAP。例如,一个具有九个元素的列表可以以下面的方式显示:

Swing_13_list_2.png

Swing_13_list_2.png

设置JList的visibleRowCount属性来控制行数。否则,列表的宽度决定HORIZONTAL_WRAP的行数,而列表的高度决定VERTICAL_WRAP的列数。

图13-10显示了一个具有水平换行的JList,其中显示了一个3x3的网格。注意,他仍然支持多项选中模式。

Swing_13_10.png

Swing_13_10.png

自定义JList观感

每一个可安装的Swing观感都提供了不同的JList外观以及用于组件的默认的UIResource值设置集合。图13-11显示了JList在预安装的观感类型集合Motif,Windows以及Ocean下的外观。

Swing_13_11.png

Swing_13_11.png

JList的UIResource相关的属性集合显示在表13-9中。对于JList组件,有17个不同的属性。

Swing_table_13_9_1.png

Swing_table_13_9_1.png

Swing_table_13_9_2.png

Swing_table_13_9_2.png

类似于大多数的UIResource属性,大多数属性的名字都是自解释的。而List.timeFactor在这里需要解释一下。默认情况下,JList具有键盘选中的行为。当我们输入时,JList会查找到目前为止与我们的输入匹配的项。这是借助于public int getNextMatch(String prefix, int startIndex, Position.Bias bias)方法来实现的。“到目前为止”的量是由List.timeFactory设置来控制的。只要两次击键之间有延迟没有超出List.timeFactory指定的毫秒数(默认为1000),所输入的新键就会添加到前一个键序列中。一旦工厂超时,搜索字符串就会被重置。

创建双列表框

这一节所展示的示例创建了一个新的名为DualListBox的Swing组件。双列表框的基本目的就是创建两个选项列表:一个用于选取,而另一个构成结果集。当初始选项列表是可调整的时双列表框十分有用。尝试由一个跨越多个屏幕包含多个选项的JList中进行多项选择是一个麻烦的事情,特别是由于我们没有按下Shift或是Ctrl组合键时而恰好取消了我们已经选中的选项时。使用双列表模式,用户在每一个列表中选择选项并将其移动到第二个列表中。用户可以很容易的在两个列表之间进行滚动而不无需担心偶然取消了某了个选项。图13-12显示使用中的DualListBox的样子。

Swing_13_12.png

Swing_13_12.png

要使用这个自定义组件,通过调用构造函数DualListBox sdual = new DualListBox()来进行创建,然而使用setSourceElements()或addSourceLements()方法使用数据对其进行填充;每个方法都需要一个ListModel或是一个数组参数。add版本会补充已存在的选项,而set版本会首先清除已存在选项。当需要向组件查询用户选中了哪些选项时,我们可以使用destinationIterator()方法向已选中元素的Iterator进行查询。我们也许希望修改的属性如下所示:

  • 源选项的标题(示例中的可用选项)
  • 目标选项的标题(示例中我们的选项)
  • 源或目标列单元渲染器
  • 源或目标可见行数
  • 源或是目标前景色或后景色

下面显示了这个新的DualListBox组件的完整源码。列表13-9包含了每一个类SortedListModel,他提供了一个已排序的ListModel。在其内部,他利用了TreeSet。

package swingstudy.ch13;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.swing.AbstractListModel;

public class SortedListModel extends AbstractListModel {

    SortedSet<Object> model;

    public SortedListModel() {
        model = new TreeSet<Object>();
    }
    @Override
    public Object getElementAt(int index) {
        // TODO Auto-generated method stub
        return model.toArray()[index];
    }

    @Override
    public int getSize() {
        // TODO Auto-generated method stub
        return model.size();
    }

    public void add(Object element) {
        if(model.add(element)) {
            fireContentsChanged(this, 0, getSize());
        }
    }

    public void addAll(Object elements[]) {
        Collection<Object> c = Arrays.asList(elements);
        model.addAll(c);
        fireContentsChanged(this, 0, getSize());
    }

    public void clear() {
        model.clear();
        fireContentsChanged(this, 0, getSize());
    }

    public boolean contains(Object element) {
        return model.contains(element);
    }

    public Object firstElement() {
        return model.first();
    }

    public Iterator iterator() {
        return model.iterator();
    }

    public Object lastElement() {
        return model.last();
    }

    public boolean removeElement(Object element) {
        boolean removed = model.remove(element);
        if(removed) {
            fireContentsChanged(this, 0, getSize());
        }
        return removed;
    }

}

列表13-10显示了DualListBox的源码。其中包含的main()方法演示了该组件。

package swingstudy.ch13;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Iterator;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.ListModel;

public class DualListBox extends JPanel {

    private static final Insets EMPTY_INSETS = new Insets(0, 0, 0, 0);
    private static final String ADD_BUTTON_LABEL = "Add >>";
    private static final String REMOVE_BUTTON_LABEL = "<< Remove";
    private static final String DEFAULT_SOURCE_CHOICE_LABEL = "Advailable Choices";
    private static final String DEFAULT_DEST_CHOICE_LABEL = "Your Choices";
    private JLabel sourceLabel;
    private JList sourceList;
    private SortedListModel sourceListModel;
    private JList destList;
    private SortedListModel destListModel;
    private JLabel destLabel;
    private JButton addButton;
    private JButton removeButton;

    public DualListBox() {
        initScreen();
    }

    public String getSourceChoicesTitle() {
        return sourceLabel.getText();
    }

    public void setSourceChoicesTitle(String newValue) {
        sourceLabel.setText(newValue);
    }

    public String getDestinationChoicesTitle() {
        return destLabel.getText();
    }

    public void setDestinationChoicesTitle(String newValue) {
        destLabel.setText(newValue);
    }

    public void clearSourceListModel() {
        sourceListModel.clear();
    }

    public void clearDestinationListModel() {
        destListModel.clear();
    }

    public void addSourceElements(ListModel newValue) {
        fillListModel(sourceListModel, newValue);
    }

    public void setSourceElements(ListModel newValue) {
        clearSourceListModel();
        addSourceElements(newValue);
    }

    public void addDestinationElements(ListModel newValue) {
        fillListModel(destListModel, newValue);
    }

    private void fillListModel(SortedListModel model, ListModel newValues) {
        int size = newValues.getSize();
        for(int i=0; i<size; i++) {
            model.add(newValues.getElementAt(i));
        }
    }

    public void addSourceElements(Object newValue[]) {
        fillListModel(sourceListModel, newValue);
    }

    public void setSourceElements(Object newValue[]) {
        clearSourceListModel();
        addSourceElements(newValue);
    }

    public void addDestinationElements(Object newValue[]) {
        fillListModel(destListModel, newValue);
    }

    private void fillListModel(SortedListModel model, Object newValues[]) {
        model.addAll(newValues);
    }

    public Iterator sourceIterator() {
        return sourceListModel.iterator();
    }

    public Iterator destinationIterator() {
        return destListModel.iterator();
    }

    public void setSourceCellRender(ListCellRenderer newValue) {
        sourceList.setCellRenderer(newValue);
    }

    public ListCellRenderer getSourceCellRenderer() {
        return sourceList.getCellRenderer();
    }

    public void setDestinationCellRenderer(ListCellRenderer newValue) {
        destList.setCellRenderer(newValue);
    }

    public ListCellRenderer getDestinationCellRenderer() {
        return destList.getCellRenderer();
    }

    public void stVisibleRowCount(int newValue) {
        sourceList.setVisibleRowCount(newValue);
        destList.setVisibleRowCount(newValue);
    }

    public int getVisibleRowCount() {
        return sourceList.getVisibleRowCount();
    }

    public void setSelectionBackground(Color newValue) {
        sourceList.setSelectionBackground(newValue);
        destList.setSelectionBackground(newValue);
    }

    public Color getSelectionBackground() {
        return sourceList.getSelectionBackground();
    }

    public void setSelectionForeground(Color newValue) {
        sourceList.setSelectionForeground(newValue);
        destList.setSelectionForeground(newValue);
    }

    public Color getSelectionForeground() {
        return sourceList.getSelectionForeground();
    }

    private void clearSourceSelected() {
        Object selected[] = sourceList.getSelectedValues();
        for(int i= selected.length-1; i>=0; i--) {
            sourceListModel.removeElement(selected[i]);
        }
        sourceList.getSelectionModel().clearSelection();
    }

    private void clearDestinationSelected() {
        Object selected[] = destList.getSelectedValues();
        for(int i=selected.length-1; i>=0; --i) {
            destListModel.removeElement(selected[i]);
        }
        destList.getSelectionModel().clearSelection();
    }

    private void initScreen() {
        setBorder(BorderFactory.createEtchedBorder());
        setLayout(new GridBagLayout());
        sourceLabel =  new JLabel(DEFAULT_SOURCE_CHOICE_LABEL);
        sourceListModel = new SortedListModel();
        sourceList = new JList(sourceListModel);
        add(sourceLabel, new GridBagConstraints(0,0,1,1,0,0,GridBagConstraints.CENTER,GridBagConstraints.NONE, EMPTY_INSETS, 0, 0));
        add(new JScrollPane(sourceList), new GridBagConstraints(0,1,1,5,.5,1,GridBagConstraints.CENTER,GridBagConstraints.BOTH,EMPTY_INSETS,0,0));

        addButton = new JButton(ADD_BUTTON_LABEL);
        add(addButton, new GridBagConstraints(1,2,1,2,0,.25,GridBagConstraints.CENTER,GridBagConstraints.NONE,EMPTY_INSETS,0,0));
        addButton.addActionListener(new AddListener());
        removeButton = new JButton(REMOVE_BUTTON_LABEL);
        add(removeButton, new GridBagConstraints(1,4,1,2,0,.25,GridBagConstraints.CENTER,GridBagConstraints.NONE, new Insets(0,5,0,5),0,0));
        removeButton.addActionListener(new RemoveListener());

        destLabel = new JLabel(DEFAULT_DEST_CHOICE_LABEL);
        destListModel = new SortedListModel();
        destList = new JList(destListModel);
        add(destLabel, new GridBagConstraints(2,0,2,1,0,0,GridBagConstraints.CENTER,GridBagConstraints.NONE,EMPTY_INSETS,0,0));
        add(new JScrollPane(destList), new GridBagConstraints(2,1,1,5,.5,1.0, GridBagConstraints.CENTER,GridBagConstraints.BOTH, EMPTY_INSETS,0,0));
    }

    private class AddListener implements ActionListener {
        public void actionPerformed(ActionEvent event) {
            Object selected[] = sourceList.getSelectedValues();
            addDestinationElements(selected);
            clearSourceSelected();
        }
    }

    private class RemoveListener implements ActionListener {
        public void actionPerformed(ActionEvent event) {
            Object selected[] = destList.getSelectedValues();
            addSourceElements(selected);
            clearDestinationSelected();
        }
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Dual List Box Tester");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                DualListBox dual = new DualListBox();
                dual.addSourceElements(new String[] {"One", "Two", "Three"});
                dual.addSourceElements(new String[] {"Four", "Five", "Six"});
                dual.addSourceElements(new String[] {"Seven", "Eight", "Nigh"});
                dual.addSourceElements(new String[] {"Ten", "Eleven", "Twele"});
                dual.addSourceElements(new String[] {"Thirteen", "Fourteen", "Fifteen"});
                dual.addSourceElements(new String[] {"Sixteen", "Seventeen", "Eighteen"});
                dual.addSourceElements(new String[] {"Nineteen", "Twenty", "Thirty"});

                frame.add(dual, BorderLayout.CENTER);
                frame.setSize(400, 300);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

向列表项添加元素及工具提示

正如第4章所描述的,所有的Swing组件支持显示工具提示文本。通过调用组件的setToolTipText()方法 ,我们可以在组件上显示任意的文本字符串。在JList组件的情况下,单个的工具提示文本字符串也许并足够。我们也许希望在一个组件中的每一项上显示一个不同的提示。

显示元素级的提示需要一些工作。要在每一项上显示不同的工具提示文本,我们必须创建一个JList的子类。在这个子类中,我们必须手动向组件注册ToolTipManager。这通常是当我们调用setToolTipText()时为我们完成的。但是因为我们不会调用这个方法,我们必须手动通知管理器,如下所示:

ToolTipManager.sharedInstance().registerComponent(this);

在我们通知ToolTipManager之后,管理器会在鼠标滑过组件时通知组件。这允许我们覆盖public String getToolTipText(MouseEvent mouseEvent)方法来为鼠标点下的项提供相应的提示。使用某些Hashtable,HashMap或是Properties列表可以使得我们将鼠标滑过的项映射到相应的工具提示文本。

public String getToolTipText(MouseEvent event) {
  Point p = event.getPoint();
  int location = locationToIndex(p);
  String key = (String)model.getElementAt(location);
  String tip = tipProps.getProperty(key);
  return tip;
}

图13-13显示了PropertiesList示例如何基于鼠标停留的元素演示各种工具提示。示例的完整源码显示在列表13-11中。

Swing_13_13.png

Swing_13_13.png

package swingstudy.ch13;

import java.awt.EventQueue;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.util.Enumeration;
import java.util.Properties;

import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.ToolTipManager;

public class PropertiesList extends JList {

    SortedListModel model;
    Properties tipProps;

    public PropertiesList(Properties props) {
        model = new SortedListModel();
        setModel(model);
        ToolTipManager.sharedInstance().registerComponent(this);

        tipProps = props;
        addProperties(props);
    }

    private void addProperties(Properties props) {
        // Load
        Enumeration names = props.propertyNames();
        while(names.hasMoreElements()) {
            model.add(names.nextElement());
        }
    }

    public String getToolTipText(MouseEvent event) {
        Point p = event.getPoint();
        int location = locationToIndex(p);
        String key = (String)model.getElementAt(location);
        String tip = tipProps.getProperty(key);
        return tip;
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Custom Tip Demo");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                Properties props = System.getProperties();
                PropertiesList list = new PropertiesList(props);
                JScrollPane scrollPane = new JScrollPane(list);
                frame.add(scrollPane);
                frame.setSize(300, 300);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

JComboBox类

Swing组件集合的JComboBox组件是一个多部分组件,允许用户借助于下拉列表由一个预定义的选项集合中进行选择。在其基本配置中,JComboBox类似于JLabel来显示当前的用户选择。嵌入在JLabel中的一个包含在JList控件中选择的弹出菜单。当所需要选项不可用时,JComboBox可以使用JTextField来输入新的选项。当需要时,JList部分会自被嵌入在JScrollPane中;我们并不需要手动创建JList或是将其放在JScrollPane中。另外,用于编辑的文本框默认是禁止的,只允许用户由预定义的选项集合中进行选择。图13-14演示了两个JComboBox组件:一个是不可以编辑,显示其选项列表,而另一个可以编辑而不显示其选项。

四个核心元素定义了JComboBox组件及其实现:

  • 用于存储JComboBox数据的数据模型,通过ListModel接口定义
  • 用于绘制JComboBox元素的渲染器,通过ListCellRenderer接口描述
  • 用于输入选项的编辑器,通过ComboBoxEditor接口定义
  • 用于处理选择JComboBox元素的键盘输入的击键管理器,通过KeySelectionManager接口描述

JComboBox的许多功能与JList组件所共用。这并不是巧合;这两个组件非常相似。下面我们详细了解JComboBox。

创建JComboBox组件

类似于JList组件,JComboBox组件有四个构造函数,允许我们基于初始的数据结构进行创建。与JList组件不同,数组与Vector构造函数所用的默认模型允许添加或是移除数据元素。

public JComboBox()
JComboBox comboBox = new JComboBox();
public JComboBox(Object listData[])
String labels[] = { "Chardonnay", "Sauvignon", "Riesling", "Cabernet", "Zinfandel",
  "Merlot", "Pinot Noir", "Sauvignon Blanc", "Syrah", "Gewürztraminer"};
JComboBox comboBox = new JComboBox(labels);
public JComboBox(Vector listData)
Vector vector = aBufferedImage.getSources();
JComboBox comboBox = new JComboBox(vector);
public JComboBox(ComboBoxModel model)
ResultSet results = aJDBCStatement.executeQuery("SELECT columnName FROM tableName");
DefaultComboBoxModel model = new DefaultComboBoxModel();
while (result.next())
  model.addElement(results.getString(1));
JComboBox comboBox = new JComboBox(model);

JComboBox属性

在我们创建了JComboBox组件之后,我们可以修改其属性。表13-10显示了JComboBox的22个属性。

Swing_table_13_10_1.png

Swing_table_13_10_1.png

Swing_table_13_10_2.png

Swing_table_13_10_2.png

JComboBox的重要属性关注弹出列表的显示。我们可以通过设置maximumRowCount属性来控制弹出列表可见项的最大数目。lightWeightPopupEnabled属性设置有助于确定当显示弹出选项菜单时所用的窗口类型。如果组件完全适应程序的顶级窗口,组件将是轻量级的。如果不适应,则其将是重量级的。如果我们在程序中混合使用AWT与Swing组件,我们可以通过将lightWeightPopupEnabled属性设置为true强制弹出选项菜单为重量级的。这将强制弹出菜单显示在其他组件之上。其他与弹出列表相关的属性是popupVisible属性,这将允许我们编程显示弹出列表。

注意,除了设置popupVisible属性以外,我们可以使用public void hidePopup()与public void showPopup()方法来切换弹出列表的可视状态。

渲染JComboBox元素

JComboBox内的元素的渲染是使用ListCellRenderer来完成的。这是与JList组件所用的相同的渲染器。一旦我们为这两个组件中的一个创建一个渲染器,我们就可以为另一个组件使用相同的渲染器。为了重用本章前面的ComplexCellRenderer,我们可以将下面的代码添加到ComplexRenderingSample示例中使得两个组件共享相同的渲染器。

JComboBox comboBox = new JComboBox(elements);
comboBox.setRenderer(renderer);
frame.add(comboBox, BorderLayout.NORTH);

最终的结果如图13-15所示。

Swing_13_15.png

Swing_13_15.png

并不是所有的渲染器都会在JComboBox与JList组件之间得到所期望的结果。例如,在前面的图13-6中所演示的FocusedTitleListCellRenderer不会在JComboBox中显示的具有输入焦点的标题边框,因为选项绝不会具有输入焦点。另外,不同的组件也许具有不同的颜色(在这种情况下是不同的未选中背景颜色)。也许询问通常情况下组件以哪种颜色进行渲染是必要的,并且进行相应的响应。

选择JComboBox元素

JComboBox组件支持至少三个与选择相关的不同事件。我们可以监听键盘输入来支持借助于JComboBox.KeySelectionManager类的键盘选择。我们也可以使用ActionListener或ItemListener进行监听来确定何时JComboBox的选中项发生变化。

如果我们希望编程选中一个元素,则可以使用public void setSelectedItem(Object element)或是public void setSelectedIndex(int index)。

提示,要编程实现取消JComboBox的当前选项,使用参数-1来调用setSelectedIndex()方法。

使用KeySelectionManager监听键盘事件

JComboBox包含一个非常重要的公开内联接口。KeySelectionManager及其默认实现管理由键盘实现的对JComboBox中的项的选中。默认管理器定位与按下的键相对应的下一个元素。他具有记忆功能,所以如果我们具有相似前缀的多个条目,用户可以连续输入直接足够匹配唯一的元素。如果我们不喜欢这种行为,我们可以将其关掉或是创建一个新的键盘选择管理器。

注意,KeySelectionManager只可以用在不可编辑的组合框中。

如果我们希望关掉键盘选择功能,我们不能简单的将KeySelectionManager属性设置为null。相反,我们必须以相应的方法创建接口的实现。接口的唯一方法为public int selectionForKey(char aKey, ComboBoxModel aModel)。如果按下的键与任何元素都不匹配,这个方法需要返回-1。否则,他应该返回匹配元素的位置。所以,要忽略键盘输入,这个方法应总是返回-1,如下所示:

JComboBox.KeySelectionManager manager =
  new JComboBox.KeySelectionManager() {
    public int selectionForKey(char aKey, ComboBoxModel aModel) {
      return -1;
    }
  };
aJcombo.setKeySelectionManager(manager);

使用ActionListener监听JComboBox事件

监听选中事件的最基本的方法是使用ActionListsener,通常是使用setAction(Action)来设置的。他会通知我们JComboBox中的元素何时被选中。不幸的,这个监听器并不会知道哪一个元素被选中。

注意,通过setAction(Action)设置ActionListsener同时配置工具提示文本以及基于Action的JComboBoxenabled状态。

因为ActionListener不能标识被选中的元素,他必须询问作为事件源的JComboBox。要确定JComboBx中的选中元素,使用getSelectedItem()或是getSelectedIndex()方法。如果返回的索引为-1,那么当前选中的元素并不是模型的一部分。当JComboBox是可编辑的而用户输入了一个并不是原始模型一部分的值时也许就会发生不可能的情况。

注意,文本字符串comboBoxChanged是当JComboBox中的一个元素发生变化时发送给ActionListener的ActionEvent的动作命令。

使用ItemListsener监听JComboBox事件

如果我们使用ItemListener来确定JComboBox中的选中元素何时发生变化,我们也可以了解哪一个元素被取消选中。

为了演示ActionListener与ItemListener,列表13-12中所示的示例程序将两个监听器关联到同一个JComboBox上。ActionListsener输入其动作命令以及当前选中的元素。ItemListener输出受影响的元素及其状态变化,以及当前被选中的元素。

package swingstudy.ch13;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.ItemSelectable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.PrintWriter;
import java.io.StringWriter;

import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

public class SelectingComboSample {

    static private String selectedString(ItemSelectable is) {
        Object selected[] = is.getSelectedObjects();
        return ((selected.length==0)?"null":(String)selected[0]);
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Runnable runner = new Runnable() {
            public void run() {
                String labels[] = {"Chardonnay", "Sauvignon", "Riesling", "Cabernet",
                        "Zinfandel", "Merlot", "Pinot Noir", "Sauvignon Blanc", "Syrah", "Gewurztraminer"
                };
                JFrame frame = new JFrame("Selecting JComboBox");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                JComboBox comboBox = new JComboBox(labels);
                frame.add(comboBox, BorderLayout.SOUTH);

                final JTextArea textArea = new JTextArea();
                textArea.setEditable(false);
                JScrollPane sp = new JScrollPane(textArea);
                frame.add(sp, BorderLayout.CENTER);

                ItemListener itemListener =  new ItemListener() {
                    public void itemStateChanged(ItemEvent event) {
                        StringWriter sw = new StringWriter();
                        PrintWriter pw =  new PrintWriter(sw);
                        int state = event.getStateChange();
                        String stateString = ((state==ItemEvent.SELECTED)?"Selected":"Deselected");
                        pw.print("Item: "+event.getItem());
                        pw.print(", state: "+stateString);
                        ItemSelectable is = event.getItemSelectable();
                        pw.print(", Selected: "+selectedString(is));
                        pw.println();
                        textArea.append(sw.toString());
                    }
                };
                comboBox.addItemListener(itemListener);

                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        StringWriter sw = new StringWriter();
                        PrintWriter pw = new PrintWriter(sw);
                        pw.print("Command: "+event.getActionCommand());
                        ItemSelectable is = (ItemSelectable)event.getSource();
                        pw.print(", Selected: "+selectedString(is));
                        pw.println();
                        textArea.append(sw.toString());
                    }
                };
                comboBox.addActionListener(actionListener);

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

}

图13-16显示了程序运行一段时间后的结果。

Swing_13_16.png

Swing_13_16.png

使用ListDataListener监听JComboBox事件

我们可以ListDataListener关联到JComboBox的数据模型。当模型选中的元素发生变化时这个监听器会得到通知。不幸的是,监听器也会得到其他数据模型变化的通知。换句话说,使用ListDataListener来确定JComboBox的元素何时被选中并不是推荐的方法。

注意,JComboBox中鼠标移动与当标移动事件不会改变被选中的元素;鼠标释放会修改被选中的元素。当选中的鼠标按钮在JComboBox弹出列表的元素上释放时,所注册的监听器会得到通知。

编辑JComboBox元素

我们也许会希望像文本输入框一样使用组合框,其中列出最户最可能的文本输入,但是同时允许输入一些其他的内容。通过允许JComboBox的editable属性,我们就可以获得这一功能。为了演示,图13-17显示了一个可编辑的JComboBox。这个窗口同时包含一个文本框来报告当前选中的元素及其索引。尽管我们手动在JComboBox输入一个选项,getSelectedIndex()将会报告正确的位置。记住,如果我们输入并没有出现的值,getSelectedIndex()会返回-1。

Swing_13_17.png

Swing_13_17.png

图13-17中的程序源码显示在列表13-13中。

package swingstudy.ch13;

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

import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

public class EditComboBox {

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

        Runnable runner = new Runnable() {
            public void run() {
                String labels[] = {"Chardonnay", "Sauvignon", "Riesling", "Cabernet",
                        "Zinfandel", "Merlot", "Pinot Noir", "Sauvignon Blanc", "Syrah", "Gewurztraminer"
                };
                JFrame frame = new JFrame("Editable JComboBox");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                final JComboBox comboBox = new JComboBox(labels);
                comboBox.setMaximumRowCount(5);
                comboBox.setEditable(true);
                frame.add(comboBox, BorderLayout.NORTH);

                final JTextArea textArea = new JTextArea();
                JScrollPane scrollPane = new JScrollPane(textArea);
                frame.add(scrollPane, BorderLayout.CENTER);

                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        textArea.append("Selected: "+comboBox.getSelectedItem());
                        textArea.append(", Position: "+comboBox.getSelectedIndex());
                        textArea.append(System.getProperty("line.seperator"));
                    }
                };
                comboBox.addActionListener(actionListener);

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

}

默认情况下,用于编辑的输入框是JTextField。如果我们的数据模式由文本字符串组成,则默认的JTextField将是一个好的编辑器。然而,一旦我们的模型包含不同的对象类型(例如,颜色),我们需要提供一个不同的编辑器。默认情况下,一旦我们在文本框中输入(为我们的元素编辑toString的结果),对象会被看作一个String。由技术上来说,不同的编辑器并不总是必需的。如果我们能将作为字符串的文本框内容解析为正确的数据类型,那我们就可以这样做。但是,如查我们希望以某种方式限制输入(例如,只允许输入数字)或是提供一个更好的输入机制,我们必须提供我们自己的编辑器。定义为了必需行为的接口名为ComboBoxEditor,而其定义如下所示:

public interface ComboBoxEditor {
  // Properties
  public Component getEditorComponent();
  public Object getItem();
  public void setItem(Object anObject);
  // Listeners
  public void addActionListener(ActionListener l);
  public void removeActionListener(ActionListener l);
  // Other methods
  public void selectAll();
}

注意,默认的编辑器是javax.swing.plaf.basic包中的BasicComboBoxEditor实现。

add/remove监听器方法对于当ComboBoxEditor值发生变化时通知监听器是必需的。我们并没有必要添加一个监听器,而通常我们也并不希望这样做。无论如何,这些方法是接口的一部分,所以如果我们希望提供自己的编辑器我们需要实现这些方法。

getEditorComponent()方法返回编辑器所用的Component对象。我们可以为编辑器使用AWT或是Swing组件(例如,用于颜色选择的JColorChooser)。当编辑器首次显示时selectAll()方法会被调用。他通知编辑器选中其内的所有内容。选中所有内容允许用户仅在默认JTextField情况的当前输入上输入。某些编辑器并不需要使用这种方法。

当我们提供自定义的编辑器时,item属性需要最多的工作。为了显示要编辑的数据,我们需要提供一个方法来将Object子类的特定片段映射到组件。然后我们需要由编辑器获取数据,从而数据可以存储到原始对象的实例中。

为了演示,列表13-14中的源码是用于Color类的ComboBoxEditor。自定义的编辑器是必需的,因为没有自动的方法来解析用于显示Color的默认字符串的编辑结果。这个编辑器使用JColorChooser从而允许用户选择一个新的颜色值。getItem()方法需要只返回当前值,Color。setItem()方法需要转换传递给Color对象的对象;setItem()方法的参数是一个Object。可以使得setItem()方法只接受Color参数。然而,对于这个例子,使用Color.decode()方法解码的字符串也可以被支持。

package swingstudy.ch13;

import java.awt.Color;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.ComboBoxEditor;
import javax.swing.JButton;
import javax.swing.JColorChooser;
import javax.swing.event.EventListenerList;

public class ColorComboBoxEditor implements ComboBoxEditor {

    final protected JButton editor;
    protected EventListenerList listenerList = new EventListenerList();

    public ColorComboBoxEditor(Color initialColor) {
        editor = new JButton("");
        editor.setBackground(initialColor);
        ActionListener actionListener = new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                Color currentBackground = editor.getBackground();
                Color color = JColorChooser.showDialog(editor, "Color Chooser", currentBackground);
                if((color != null) && (currentBackground != color)) {
                    editor.setBackground(color);
                    fireActionEvent(color);
                }
            }
        };
        editor.addActionListener(actionListener);
    }

    @Override
    public void addActionListener(ActionListener l) {
        // TODO Auto-generated method stub
        listenerList.add(ActionListener.class, l);
    }

    @Override
    public Component getEditorComponent() {
        // TODO Auto-generated method stub
        return editor;
    }

    @Override
    public Object getItem() {
        // TODO Auto-generated method stub
        return editor.getBackground();
    }

    @Override
    public void removeActionListener(ActionListener l) {
        // TODO Auto-generated method stub
        listenerList.remove(ActionListener.class, l);
    }

    @Override
    public void selectAll() {
        // TODO Auto-generated method stub

    }

    @Override
    public void setItem(Object newValue) {
        // TODO Auto-generated method stub
        if(newValue instanceof Color) {
            Color color = (Color)newValue;
            editor.setBackground(color);
        }
        else {
            try {
                Color color = Color.decode(newValue.toString());
                editor.setBackground(color);
            }
            catch(NumberFormatException e) {

            }
        }
    }

    protected void fireActionEvent(Color color) {
        Object listeners[] = listenerList.getListenerList();
        for(int i=listeners.length-2; i>=0; i-=2) {
            if(listeners[i] == ActionListener.class) {
                ActionEvent actionEvent = new ActionEvent(editor, ActionEvent.ACTION_PERFORMED, color.toString());
                ((ActionListener)listeners[i+1]).actionPerformed(actionEvent);
            }
        }
    }

}

要使用新编辑器,我们需要将其关联到JComboBox。在我们修改前面所示的EditorComboBox示例使其数据模型由Color对象的数组组成之后,我们可以通过添加下面的代码来安装编辑器:

Color color = (Color)comboBox.getSelectedItem();
ComboBoxEditor editor = new ColorComboBoxEditor(color);
comboBox.setEditor(editor);

列表13-15显示了完整的测试程序。他不同于EditComboBox,因为在JComboBox下面是一个与JComboBox当前选中的颜色同步的JLabel。同时有一个自定义的单元渲染器将背景色设置为单元的值。

package swingstudy.ch13;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.ComboBoxEditor;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.ListCellRenderer;

public class ColorComboBox {

    static class ColorCellRenderer implements ListCellRenderer {
        protected DefaultListCellRenderer defaultRenderer = new DefaultListCellRenderer();
        // width doesn't matter as the combo box will size
        private final static Dimension preferredSize = new Dimension(0, 20);
        public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
            JLabel renderer = (JLabel)defaultRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
            if(value instanceof Color) {
                renderer.setBackground((Color)value);
            }
            renderer.setPreferredSize(preferredSize);
            return renderer;
        }
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Runnable runner = new Runnable() {
            public void run() {
                Color colors[] = {Color.BLACK, Color.BLUE, Color.CYAN, Color.DARK_GRAY, Color.GRAY, Color.green,
                        Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE, Color.PINK, Color.RED, Color.WHITE, Color.YELLOW
                };
                JFrame frame = new JFrame("Color JComboBox");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                final JComboBox comboBox = new JComboBox(colors);
                comboBox.setMaximumRowCount(5);
                comboBox.setEditable(true);
                comboBox.setRenderer(new ColorCellRenderer());
                Color color = (Color)comboBox.getSelectedItem();
                ComboBoxEditor editor = new ColorComboBoxEditor(color);
                comboBox.setEditor(editor);
                frame.add(comboBox, BorderLayout.NORTH);

                final JLabel label = new JLabel();
                label.setOpaque(true);
                label.setBackground((Color)comboBox.getSelectedItem());
                frame.add(label, BorderLayout.CENTER);

                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        Color selectedColor = (Color)comboBox.getSelectedItem();
                        label.setBackground(selectedColor);
                    }
                };
                comboBox.addActionListener(actionListener);

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

}

图13-18显示了程序运行的结果。

Swing_13_18.png

Swing_13_18.png

自定义JComboBox观感

每一个可安装的Swing观感都提供了不同的JComboBox外观以及组件的默认UIResource值设置集合。图13-19显示在了预安装的观感类型集合Motif,Windows以及Ocean下JComboBox组件的外观。

Swing_13_19.png

Swing_13_19.png

表13-11显示了JComboBox可用的UIResource相关属性的集合。JComboBox组件有21个不同的属性。

Swing_table_13_11_1.png

Swing_table_13_11_1.png

Swing_table_13_11_2.png

Swing_table_13_11_2.png

修改弹出图标是自定义观感的一个示例。要实现这一目的,我们需要安装一个新的用户界面。基本上,我们由BasicComboBoxUI或是MetalComboBoxUI用户界面委托继承默认功能,并且只覆盖protected JButton createArrowButton()方法。

图13-20显示了修改JComboBox用户界面的结果。

列表13-16列出了完整的源码。

package swingstudy.ch13;

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

import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.plaf.ComboBoxUI;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.basic.BasicArrowButton;
import javax.swing.plaf.basic.BasicComboBoxUI;

public class PopupComboSample {

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

        Runnable runner = new Runnable() {
            public void run() {
                String labels[] = {"Chardonnay", "Sauvignon", "Riesling", "Cabernet",
                        "Zinfandel", "Merlot", "Pinot Noir", "Sauvignon Blanc", "Syrah", "Gewurztraminer"
                };
                JFrame frame = new JFrame("Popup JComboBox");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                JComboBox comboBox = new JComboBox(labels);
                comboBox.setMaximumRowCount(5);
                comboBox.setUI((ComboBoxUI)MyComboBoxUI.createUI(comboBox));
                frame.add(comboBox, BorderLayout.NORTH);

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

    static class MyComboBoxUI extends BasicComboBoxUI {
        public static ComponentUI createUI(JComponent c) {
            return new MyComboBoxUI();
        }

        protected JButton createArrowButton() {
            JButton button = new BasicArrowButton(BasicArrowButton.EAST);
            return button;
        }
    }
}

为JComboBox与JList共享数据模型

我们也许已经注意到了构成JComboBox与JList的部分之间的一些相似之处。我们可以为两个组件使用相同的数据模型与相同的渲染器。在本章前面的部分中,我们已经了解了如果在两个组件之间共享渲染器。本节中所展示的示例演示了我们如何在多个组件之间共享相同的数据模型。

这个例子有两个可编辑的组合框与一个JList,所有的组件共享相同的数据模型。这个示例同时提供了一个按钮,我们可以点击来向数据模型动态添加内容。因为数据模型将会与多个组件相关联,我们将会注意到每一个都具有额外的选项在选中按钮之后进行选择。图13-21显示了程序在添加了一些元素之后的样子。

Swing_13_21.png

Swing_13_21.png

列表13-17显示了共享数据模型的例子。

package swingstudy.ch13;

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

import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;

public class SharedDataSample {

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

        Runnable runner = new Runnable() {
            public void run() {
                final String labels[] = {"Chardonnay", "Sauvignon", "Riesling", "Cabernet",
                        "Zinfandel", "Merlot", "Pinot Noir", "Sauvignon Blanc", "Syrah", "Gewurztraminer"
                };

                final DefaultComboBoxModel model = new DefaultComboBoxModel(labels);

                JFrame frame =  new JFrame("Shared Data");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                JPanel panel = new JPanel();
                JComboBox comboBox1 = new JComboBox(model);
                comboBox1.setMaximumRowCount(5);
                comboBox1.setEditable(true);

                JComboBox comboBox2 = new JComboBox(model);
                comboBox2.setMaximumRowCount(5);
                comboBox2.setEditable(true);
                panel.add(comboBox1);
                panel.add(comboBox2);
                frame.add(panel, BorderLayout.NORTH);

                JList jlist = new JList(model);
                JScrollPane scrollPane =  new JScrollPane(jlist);
                frame.add(scrollPane, BorderLayout.CENTER);

                JButton button = new JButton("Add");
                frame.add(button, BorderLayout.SOUTH);
                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        int index = (int)(Math.random()*labels.length);
                        model.addElement(labels[index]);
                    }
                };
                button.addActionListener(actionListener);

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

}

小结

本章演示了如何使用Swing的JList与JComboBox组件。我们已经看到了这两个组件如何他们各自的数据模型,渲染器,选择功能,以及JComboBox组件的自定义编辑器。尽管这些功能是可以自定义的,每一个具有默认配置的组件都可以立即使用。

在第14章中,我们将会开始探讨Swing文本组件,包括JTextField与JTextArea。

Spinner Model Controls

在前一章中,我们了解了如何使用基本的列表组件:JList与JComboBox。在本章中,我们将会开始探讨JDK 1.4版本所引入的JSpinner组件。

JSpinner类

JSpinner的作用类似于JList或是JComboBox组件与JFormattedTextField的结合。在JList与JComboBox控件中,用户可以由一个预定义的值集合中选择输入。JSpinner允许这种选择类型。组件的另一半是JFormattedTextField。如何显示或是输入值并不由列表单元渲染器来控制,类似于JList;相反,我们获取JFormattedTextField用于输入并且在旁边有一对箭头用于在文本域可用的不同值之间进行浏览。

图14-1显示了用于不同的输入类型的微调控件的样子。图14-1的顶部是一个以法国星期显示提供给SpinnerListModel的JSpinner。在中部,我们具有一个依赖SpinnerDateModel类的JSpinner。在底部是一个带有SpinnerNumberModel的JSpinner使用示例。正如我们在本章稍后将会了解到的,每一个都以其自己的方式进行工作。

Swing_14_1.png

Swing_14_1.png

当创建并操作JSpinner组件时会涉及到许多类,首先就是JSpinner类本身。所涉及到的两个基本类集合是包含用于控件可选条目集合的SpinnerModel接口,以及用于捕获所有选择的JSpinnner.DefaultEditor实现。幸运的是,所涉及到的其他许多类在幕后工作,所以,例如,一旦我们在SpinnerNumberModel中提供一个数字范围并且与其模型相叛逆,我们的工作实质上就完成了。

创建JSpinner组件

JSpinner类包含两个用于初始化组件的构造函数:

public JSpinner()
JSpinner spinner = new JSpinner();
public JSpinner(SpinnerModel model)
SpinnerModel model = new SpinnerListModel(args);
JSpinner spinner = new JSpinner(model);

我们可以由无数据模型开始,并使用JSpinner的方法在稍后进行关联。或者是我们可以由一个完全的数据模型开始,数据模型是SpinnerModel接口的实现,其中有三个主要类:SpinnerDateModel,SpinnerListModel与SpinnerNumberModel,及其抽象父类AbstractSpinnerModel。如果我们没有指定一个模型,则使用SpinnerNumberModel。尽管组件的渲染器与编辑器是JFormattedTextField,编辑基本是通过一系列的JSpinner内联类来完成的:DateEditor,ListEditor与NumberFormat,而其支持类则位于父类DefaultEditor中。

JSpinner属性

除了创建JSpinner对象以外,我们当然也可以通过表14-1中所列出的属性对其进行重新配置。

Swing_table_14_1.png

Swing_table_14_1.png

value属性使得我们可以修改组件的当前设置。nextValue与previosValue属性使得我们可以在不同方向的model的条目中进行选择,而无需修改程序本身的选择。

使用ChnageListener监听JSpinner事件

JSpinner只直接支持一种事件监听器:ChangeListener。在其他情况下,当为相关组件调用commitEdit()方法时,监听器会得到通知,告诉我们微调器的值发生了变化。为了进行演示,列表14-1向生成图14-1的源程序关联了一个自定义的ChangeListener。

package swingstudy.ch13;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.text.DateFormatSymbols;
import java.util.Locale;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SpinnerDateModel;
import javax.swing.SpinnerListModel;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class SpinnerSample {

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

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

                DateFormatSymbols symbols = new DateFormatSymbols(Locale.FRENCH);
                ChangeListener listener = new ChangeListener() {
                    public void stateChanged(ChangeEvent e) {
                        System.out.println("Source: "+e.getSource());
                    }
                };

                String days[] = symbols.getWeekdays();
                SpinnerModel model1 = new SpinnerListModel(days);
                JSpinner spinner1 = new JSpinner(model1);
                spinner1.addChangeListener(listener);
                JLabel label1 = new JLabel("French Days/List");
                JPanel panel1 = new JPanel(new BorderLayout());
                panel1.add(label1, BorderLayout.WEST);
                panel1.add(spinner1, BorderLayout.CENTER);
                frame.add(panel1, BorderLayout.NORTH);

                SpinnerModel model2 = new SpinnerDateModel();
                JSpinner spinner2 = new JSpinner(model2);
                spinner2.addChangeListener(listener);
                JLabel label2 = new JLabel("Dates/Date");
                JPanel panel2 = new JPanel(new BorderLayout());
                panel2.add(label2, BorderLayout.WEST);
                panel2.add(spinner2, BorderLayout.CENTER);
                frame.add(panel2, BorderLayout.CENTER);

                SpinnerModel model3 = new SpinnerNumberModel();
                JSpinner spinner3 = new JSpinner(model3);
                spinner3.addChangeListener(listener);
                JLabel label3 = new JLabel("Numbers");
                JPanel panel3 = new JPanel(new BorderLayout());
                panel3.add(label3, BorderLayout.WEST);
                panel3.add(spinner3, BorderLayout.CENTER);
                frame.add(panel3, BorderLayout.SOUTH);

                frame.setSize(200, 90);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

运行这个程序演示了监听器的使用。

自定义JSpinner观感

类似于所有的Swing组件,JSpinner控件在每一个系统定义的观感类型下都会有不同的外观,如图14-2所示。组件的基本外观看起来像一个文本域;不同在于箭头的绘制。

Swing_14_2.png

Swing_14_2.png

表14-2中列出了JSpinner的11个UIResource属性集合。这个属性局限于绘制文本域与箭头。

Swing_table_14_2.png

Swing_table_14_2.png

SpinnerModel接口

到目前为止,我们已经了解了如何与主JSpinner类交互。SpinnerModel接口是组件的数据模型。SpinnerModel的定义如下:

public interface SpinnerModel {
  // Properties
  public Object getValue();
  public void setValue(Object);
  public Object getNextValue();
  public Object getPreviousValue();
  // Listeners
  public void addChangeListener(ChangeListener);
  public void removeChangeListener(ChangeListener);
}

SpinnerMOdel的六个方法直接映射到JSpinner的相应方法。JSpinner方法只是将这些方法调用转向模型的方法,尽管在监听器方法的情况下,事件源是我们关联监听器的地方。

AbstractSpinnerModel类

SpinnerModel接口的基本实现是AbstractSpinnerModel类。他提供了监听器列表的管理与通知。子类必须实现其他的四个与值相关的接口方法。SpinnerModel接口的实现有:SpinnerDateModel,SpinnerListModel与SpinnerNumberModel。

SpinnerDateModel类

正如其名字所暗示的,SpinnerModel提供了日期的选择。这个类有两个构造函数:一个在默认情况下选择所有的日期,而另一个允许我们限制范围。

public SpinnerDateModel()
SpinnerModel model = new SpinnerDateModel();
JSpinner spinner = new JSpinner(model);
public SpinnerDateModel(Date value, Comparable start, Comparable end,
  int calendarField)
Calendar cal = Calendar.getInstance();
Date now = cal.getTime();
cal.add(Calendar.YEAR, -50);
Date startDate = cal.getTime();
cal.add(Calendar.YEAR, 100);
Date endDate = cal.getTime();
SpinnerModel model =
  new SpinnerDateModel(now, startDate, endDate, Calendar.YEAR);
JSpinner spinner = new JSpinner(model);

如果我们没有指定任何参数,则没有起始点与结束点。这里所显示的示例使用参数来提供100年的范围。最后一个域应是Calendar类的一个常量:

•Calendar.AM_PM

•Calendar.DAY_OF_MONTH

•Calendar.DAY_OF_WEEK

•Calendar.DAY_OF_WEEK_IN_MONTH

•Calendar.DAY_OF_YEAR

•Calendar.ERA

•Calendar.HOUR

•Calendar.HOUR_OF_DAY

•Calendar.MILLISECOND

•Calendar.MINUTE

•Calendar.MONTH

•Calendar.SECOND

•Calendar.WEEK_OF_MONTH

•Calendar.WEEK_OF_YEAR

  • Calendar.YEAR

注意,SpinnerModel不包含任何与时区相关的Calendar常量。我们不可以通过SpinnerDateModel在JSpinner内进行滚动。

表14-3列出了SpinnerModel接口的三个属性以四个SpinnerDateModel的特定属性。

Swing_table_14_3.png

Swing_table_14_3.png

通常情况下,我们将会使用的唯一新属性是用于获取最终的日期,尽管他所做的是以合适的数据类型包装getValue()的方法的结果。如果我们为构造函数提供了一个日期范围,在当前值为边界条件时,前一个或是后一个值将为null。

SpinnerListModel类

SpinnerListModel提供了由条目列表中,或者是至少是他们的字符串表示中进行选择。这个类有三个构造函数:

public SpinnerListModel()
SpinnerModel model = new SpinnerListModel();
JSpinner spinner = new JSpinner(model);
public SpinnerListModel(List<?> values)
List<String> list = args;
SpinnerModel model = new SpinnerListModel(list);
JSpinner spinner = new JSpinner(model);
public SpinnerListModel(Object[] values)
SpinnerModel model = new SpinnerListModel(args);
JSpinner spinner = new JSpinner(model);

当没有提供参数时,模型只包含一个元素:字符串empty。List版本具有一个到列表的引用。他并没有拷贝这个列表。如果我们修改这个列表,我们就修改了模型中的元素。数组版本创建了一个不可以添加的私有的List实例的内联类。对于List与数组版本,初始时选中的是第一个元素。如果其中一个为空,则会抛出IllegalArgumentException。

如表14-4所示,在接口之外所添加的唯一属性就是获取或是设置列表。

Swing_table_14_4.png

Swing_table_14_4.png

SpinnerNumberModel类

SpinnerNumberModel提供了由一个无限制或是有限制的值范围内进行数字选择。所选择的数字可以是Number的任意子类,包括Integer与Double。这个类具有四个构造函数,而前三个都是最后一个的简化版。

public SpinnerNumberModel()
SpinnerModel model = new SpinnerNumberModel();
JSpinner spinner = new JSpinner(model);
public SpinnerNumberModel(double value, double minimum, double maximum,
  double stepSize)
SpinnerModel model = new SpinnerNumberModel(50, 0, 100, .25);
JSpinner spinner = new JSpinner(model);
public SpinnerNumberModel(int value, int minimum, int maximum, int stepSize)
SpinnerModel model = new SpinnerNumberModel(50, 0, 100, 1);
JSpinner spinner = new JSpinner(model);
public SpinnerNumberModel(Number value, Comparable minimum, Comparable maximum,
  Number stepSize)
Number value = new Integer(50);
Number min = new Integer(0);
Number max = new Integer(100);
Number step = new Integer(1);
SpinnerModel model = new SpinnerNumberModel(value, min, max, step);
JSpinner spinner = new JSpinner(model);

如果最小值或是最大值为null,则这个范围就是无限制的。对于无参数的版本,初始值为0而步进值为1。步进尺寸是字面值,所以如果我们将这个步进值设置为.333,则并不完美。

表14-5显示了SpinnerNumberModel的属性。所添加的属性与构造函数所提供的相同。

Swing_table_14_5.png

Swing_table_14_5.png

自定义模型

通常情况下,JSpinner的可用模型就足够了,所以我们并不需要派生。然而,所提供的模型并不能总是满足我们的需求。例如,我们也许希望使用一个包装了SpinnerListModel的自定义模型,而不希望停在第一个或是最后一个元素上,他向另一个方向环绕。列表14-2显示了一个这样的实现。

package swingstudy.ch13;

import java.util.List;

import javax.swing.SpinnerListModel;

public class RolloverSpinnerListModel extends SpinnerListModel {

    public RolloverSpinnerListModel(List<?> values) {
        super(values);
    }

    public RolloverSpinnerListModel(Object[] values) {
        super(values);
    }

    public Object getNextValue() {
        Object returnValue = super.getNextValue();
        if(returnValue == null) {
            returnValue = getList().get(0);
        }
        return returnValue;
    }

    public Object getPreviousValue() {
        Object returnValue = super.getPreviousValue();
        if(returnValue == null) {
            List list = getList();
            returnValue = list.get(list.size()-1);
        }
        return returnValue;
    }
}

JSpinner编辑器

对于每一个JSpinner可用的模型,都有一个附属支持的JSpinner内联类可用。在其中模型允许我们控制对于组件哪些可以选择,微调编辑器允许我们控制如何显示与编辑每一个可选中的值。

JSpinner.DefaultEditor类

JSpinner的setEditor()方法允许我们使得任意的JComponent作为JSpiner的编辑顺。虽然我们一定可以做到,但是更为通常的情况是,我们将会使用一个JSpinner.DefaultEditor的一个子类。他提供了当我们使用基于JFormattedTextField的简单编辑器时所需要的基本功能。他只有一个构造函数:

public JSpinner.DefaultEditor(JSpinner spinner)
JSpinner spinner = new JSpinner();
JComponent editor = JSpinner.DefaultEditor(spinner);
spinner.setEditor(editor);

如表14-6所示,编辑器有两个属性。

Swing_table_14_6.png

Swing_table_14_6.png

不知道我们正在使用的是哪一种模型类型,我们在这个级别上也许会做的就是修改JFormattedTextField的一些显示特点。然而更通常的情况是,我们将会修改模型编辑器的自定义方面。

JSpinner.DateEditor类

DateEditor允许我们使用java.text包的SimpleDateFormat类的各种方面来自定义日期显示。查看SimpleDateFormat的Javadoc可以了解可用的格式模型的完整列表。如果我们不喜欢默认的显示输出,我们可以通过向第二个构造函数传递一个新的格式来修改。

public JSpinner.DateEditor(JSpinner spinner)
SpinnerModel model = new SpinnerDateModel();
JSpinner spinner = new JSpinner(model);
JComponent editor = JSpinner.DateEditor(spinner);
spinner.setEditor(editor);
public JSpinner.DateEditor(JSpinner spinner, String dateFormatPattern)
SpinnerModel model = new SpinnerDateModel();
JSpinner spinner = new JSpinner(model);
JComponent editor = JSpinner.DateEditor(spinner, "MMMM yyyy");
spinner.setEditor(editor);

默认格式为M/d/yy h:mm a,或者对于2004年的圣诞节的某一时刻为12/25/04 12:34 PM。后一个示例将显示December 2004.

表14-7显示了编辑器的两个属性。

Swing_table_14_7.png

Swing_table_14_7.png

JSpinner.ListEditor类

当使用SpinnerListModel时,ListEditor并没有提供特殊的格式化支持。相反,他提供了类型支持。因为模型的所有条目都已知,编辑器尝试匹配用户已经输入的以这些条目中的一个开始的条目。他只有一个构造函数,但是我们绝不应访问这个函数。

public JSpinner.ListEditor(JSpinner spinner)

如表14-8所示,ListEditor只有一个属性。

Swing_table_14_8.png

Swing_table_14_8.png

JSpinner.NumberEditor类

NumberEditor的工作方式类似于DateEditor,允许我们输入字符串来自定义显示格式。与使用SimpleDateFormat不同,NumberEditor与java.text包中的DecimalFormat类相关联。类似于DateEditor,他有两个构造函数:

public JSpinner.NumberEditor(JSpinner spinner)
SpinnerModel model = new SpinnerNumberModel(50, 0, 100, .25);
JSpinner spinner = new JSpinner(model);
JComponent editor = JSpinner.NumberEditor(spinner);
spinner.setEditor(editor);
public JSpinner.NumberEditor(JSpinner spinner, String decimalFormatPattern)
SpinnerModel model = new SpinnerNumberModel(50, 0, 100, .25);
JSpinner spinner = new JSpinner(model);
JComponent editor = JSpinner.NumberEditor(spinner, "#,##0.###");
spinner.setEditor(editor);

第二个构造函数的使用显示了默认格式化字符串。如果数字足够大,则编辑器会尝试显示逗号,如果值是一个完整的数字,则他不会显示十进制。

如表14-9所示,编辑器有两个属性。

Swing_table_14_9.png

Swing_table_14_9.png

小结

在本章中,我们了解了Swing的JSpinner组件。当我们的选项集合限制为确定的值集合或是值范围,JSpinner允许我们通过在不同的选项之间进行微调来选择值。我们了解了如何提供选项集合:使用SpinnerDateModel与DateEditor选择日期集合,使用SpinnerListModel与ListEditor或是使用SpinnerNumberModel与NumberEditor。

第15章停止探讨由一个值范围内选择并且继承探讨用户在不同的文本组件中输入完整的内容。

基本文本组件

第14章探讨了Swing组件集合的JSpinner所提供的动态输入选择控件。在本章中,我们将会了解Swing基本文本组件的基本功能。更为高级的文本组件将会下一章中进行探讨。

Swing组件集合包含五个文本组件。他们共享一个共同的父类,JTextComponent,其中定义了所有的文本组件的更同行为。

JTextComponent的直接子类是JTextField,JTextArea以及JEditorPane。JTextField用于单行的单属性文本(也就是单一的字体与单一的颜色)。JTextField有一个子类,JPasswordField,用于JTextField用作密码输入的情况。JTextArea用于单一属性文本的多行输入。JEditorPane是可以支持多属性输入编辑的通用编辑器。其子类JTextPane是输入普通文本格式而定制的。在这两个类中,除了文本,输入还可以图片与组件。

Swing文本组件概述

类似于所有其他的Swing组件,文本组件生活在MVC的世界中。显示在图15-1中的组件,类的层次结构图,是各种可用的UI委托。UI委托模型的其余部分是文本视图,他基于View类,我们会在第16章进行进一步的讨论。

注意,所有的JTextComponent子类位于javax.swing包中。除了事件相关的部分,本章中所讨论的支持接口与类都位于javax.swing.text包(或是子包)中。Swing特定的,文本相关的事件部分位于javax.swing.event包中,其余的位于java.awt.event与java.beans中。

每一个组件的模型都是Document接口的实现,他有五个扩展(或是实现)。单属性组件使用PlainDocument类作为其数据模型,而多属性组件使用DefaultStyledDocument作为其模型。所有这些类派生自AbstractDocument类,在其中定义了他们共同的Document接口实现。DefaultStyleDocument类也实现了StyledDocument接口,他是Document用于支持多属性内容的扩展。另一个Document实现,HTMLDocument,用于JEditorPane的内容类型为text/html的情况。为了限制到这些文档的输入,我们可以使用DocumentFilter类。

Swing_15_1.png

Swing_15_1.png

本章中以及第16章中将要讨论的许多其他具有共同的文本组件特性。类似于许多其他的Swing组件,我们可以自定义组件的观感而无需创建新的UI委托。对于文本组件,Highlighter,Caret以及NavigationFilter接口分别描述了文本如何高亮显示,在哪里插入文本以及如何限制鼠标位置,从而使得我们可以自定义文本组件的外观以及输入行为。另外,InputMap/ActionMap类定义了击键与文本动作之间的绑定,从而允许我们非常容易的修改文本组件的行为。

其他的组件模型部分被设计用于事件处理。我们并没有被限制使用KeyListener/KeyEvent或是TextEvent/TextListener绑定来处理输入验证。Swing组件同时使用DocumentEvent/DocumentListener组合(以及第2章所描述的InputVerifier)。这种组合提供了一种更为灵活的输入验证方式,特别是在Swing文本组件的MVC环境中。额外的事件处理是通过在第2章所介绍的AbstractAction功能扩展来实现的。这就是用于将键盘绑定与Action实现相关联的TextAction类,我们将会在第16章中进行详细的讨论。文本框架中的许多部分是通过所谓的EditorKit连接在一起的,我们也会在第16章中进行讨论。

注意,由于Swing文本组件类之间的相互连接,我们将会在本章中与第16章中的大量引用关联。我们可以自由的在两章之间进行跳转并且接下来阅读某一功能的详细讨论。

JTextComponent类

JTextComponent类是用作所有文本视图的组件的父类。他描述了所有文本组件所共享的共同行为。在这些共同行为中包括用于选中支持的Highlight,用于在内容中浏览的Caret,通过action属性(Action实现数组)支持的命令集合,通过KeyMap或是InputMap/ActionMap组合支持的键盘绑定集合,一个Scrollable接口实现,从而每一个特定的文本组件都可以放在JScrollPane中,以及存储在组件中的文本。如果所有的这些听起来需要大量的管理,不要担心。本章将会为我们给出指导。

JTextComponent属性

表15-1显示了JTextComponent的27个属性。这些属性覆盖我们所期望的文本组件功能。

Swing_table_15_1_1.png

Swing_table_15_1_1.png

Swing_table_15_1_2.png

Swing_table_15_1_2.png

Swing_table_15_1_3.png

Swing_table_15_1_3.png

这些属性被分为八个基本类别:

  • 数据模型:document属性用于所有文本组件的数据模型。text属性用于将这个数据模型看作一个String。
  • 颜色:caretColor,disabledTextColor, selectedTextColor与selectionColor属性,以及继承的foreground与background属性指定了渲染光标,禁止文本,所选文本,所选文本的背景,常规文本以及常规文本的背景等颜色。
  • Caret:caret,caretPosition与navigationFilter属性用于在文档中浏览。
  • Highlighter:highlighter,selectionStart与selectionEnd属性负责高亮显示文档中的selectedText部分。
  • Margin:margin属性用于指定文本内容距离文本组件的边界多远显示。
  • 事件:actions与keymap属性描述了文本组件支持哪些功能。对于actions属性的Action[]情况,功能是我们为了事件处理可以关联到组件的一系列ActionListener实现。例如,不必创建一个ActionListener来执行剪切、复制与粘贴操作,我们会发现actions属性中的相应的Action并将其关联到组件。keymap的作用类似,但是他是将Action关联到特定的键。例如,他包含一个按键映射条目用于处理当PageUp键被按下时如何响应。caretListsener属性允许我们发现观察文本组件的CaretListener对象集合。dragEnabled设置描述了组件是否支持在组件中拖放文本。(要了解Swing中的拖放支持信息,可以查看第19章。)
  • 滚动接口:属性preferredScrollableViewportSize, scrollableTracksViewportHeight,与scrollableTracksViewportWidth是相应的Scrollable接口方法的实现。
  • 状态:editable与focusTraversable属性描述了文本组件的各种状态。editable允许我们文本组件设置为只读。对于只读的focusTraversable属性,当他们被使能时文本组件位于信息循环中(也就是我们可以使用Tab键遍历)。focusAccelerator用于相邻的JLabel在其labelFor属性设置文本组件的情况,允许我们使用JLabel的可视化热键将焦点移动到文本组件。componentOrientation设置描述了组件的文本如何绘制。将这一特性于类似Hebrew那样由右到左的语言并不是必须,但却是绘制字符的最好方法。JTextComponent由JComponent继承了opaque属性。当opaque属性被设置为false时,文本组件后面的区域内容会被看穿,如果需要,可以允许我们具有一个图片背景。图15-2显示了这一效果。

列表15-1是用于生成图15-2的源码。如果我们取消setOpaque(false)一行,则背景不会显现。

package swingstudy.ch13;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Image;

import javax.swing.GrayFilter;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;


public class BackgroundSample {

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

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

                final ImageIcon imageIcon = new ImageIcon("draft.gif");
                JTextArea textArea =  new JTextArea() {
                    Image image = imageIcon.getImage();
                    Image grayImage = GrayFilter.createDisabledImage(image);
                    {
                        setOpaque(false);   // instance initializer
                    }
                        public void paint(Graphics g) {
                            g.drawImage(grayImage, 0, 0, this);
                            super.paint(g);
                        }
                };
                JScrollPane scrollPane = new JScrollPane(textArea);
                frame.add(scrollPane, BorderLayout.CENTER);
                frame.setSize(255, 2550);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

JTextComponent操作

JTextComponent为文本控件上所执行的许多操作定义了基本的框架。

  • I/O:public void read(Reader in, Object description)与public void write(Writer out)方法(都抛出IOException)允许我们简单的读取或是写入文本组件内容。
  • 剪切板访问:public void cut(), public void copy()与public void paste()方法提供了到系统剪切板的直接访问。
  • 位置:public void moveCaretPosition(int position)方法允许我们定位caret。位置表示标明由文本组件开始处到caret之前的字符数的一维定位。
  • 选中:public void replaceSelection(String content), public void selectAll()与public void select(int selectionStart, int selectionEnd)方法允许我们组件中的内容部分与替换所选择的内容。
  • Conversion:public Rectange modelToView(int position) throws BadLocationException与public int viewToModel(Point point)方法允许我们(或者更可能的是系统)将JTextComponent内的一个位置映射到特定文本UI委托的内容表示中的映射。

现在我们已经概述了JTextComponent类,现在是了解其不同的子类的时候了。首先是JTextField,他将会被用来演示刚才所列出的操作。

JTextField类

JTextField组件是用于单行输入的文本组件。JTextField的数据模型是Document接口的PlainDocument实现。PlainDocument模型将输入限制为单属性文本,意味着他必须是单一字体与单一颜色。

当在JTextField输入Enter键时,他自动通知所Actionlistener实现。

创建JTextField

JTextField组件有五个构造函数:

public JTextField()
JTextField textField = new JTextField();
public JTextField(String text)
JTextField textField = new JTextField("Initial Text");
public JTextField(int columnWidth)
JTextField textField = new JTextField(14);
public JTextField(String text, int columnWidth)
JTextField textField = new JTextField("Initial Text", 14);
public JTextField(Document model, String text, int columnWidth)
JTextField textField = new JTextField(aModel, null, 14);

默认情况下,我们会获得一个空的文本域,零列宽,带有默认模型的JTextFiled。我们可以指定JTextField的初始文本以及我们希望组件有多宽。宽度被以当前字体适应组件的m字符数。在可以输入的字符数上并没有限制。如果我们在构造函数中指定Document数据模型,也许我们将会希望指定了一个null初始数据参数。否则,当前文档的内容会被文本域的初始文本所替换。

使用JLabel热键

在第4章热键的讨论中,我们了解到各种按钮类可以有一个按钮组件被选中的键盘快捷键。特殊的热键字符通常以下划线来进行可视化标识。如果用户按下热键字符,以平台特定的热键激活键,例如对于Windows与Unix的Alt,按钮就会被激活/选中。我们可以借助JLabel为JTextField以及其他的文本组件提供类似的功能。

我们可以为标签设置热键显示,但是当热键被按下时并没有选中标签,而是会使得相关联的组件获得输入焦点。显示热键是通过public void setDisplayedMnemonic(character)方法来设置的,其中character可是一个int或是char。当修改热键设置时使用KeyEvent常量可以简化初始化操作。

下面的源代码显示了如何连接一个特定的JLabel与JTextField。

JLabel label = new JLabel("Name: ");
label.setDisplayedMnemonic(KeyEvent.VK_N);
JTextField textField = new JTextField();
label.setLabelFor(textField);

除了调用setDisplayedMnemonic()方法以外,我们必须同时调用JLabel的public void setLabelFor(Component component)方法。这会配置当特定的热键值被按下时,JLable会将输入焦点移动文本域。

图15-3显示了示例程序的样子。完整的程序源码显示在列表15-2中。

Swing_15_2.png

Swing_15_2.png

package swingstudy.ch15;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.KeyEvent;

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

public class LabelSample {

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

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame =  new JFrame("Label Focus Example");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                JPanel panel = new JPanel(new BorderLayout());
                JLabel label = new JLabel("Name: ");
                label.setDisplayedMnemonic(KeyEvent.VK_N);
                JTextField textField = new JTextField();
                label.setLabelFor(textField);
                panel.add(label, BorderLayout.WEST);
                panel.add(textField, BorderLayout.CENTER);
                frame.add(panel, BorderLayout.NORTH);
                frame.add(new JButton("Somewhere Else"), BorderLayout.SOUTH);
                frame.setSize(250, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

JTextField属性

表15-2列出了JTextField的14个属性。

Swing_table_15_2.png

Swing_table_15_2.png

在horizontalVisibility与scrollOffset属性之间有一个简单的关联。用于JTextField的horizontalVisibility属性的BoundedRangeModel表示显示文本域内容所需要的宽度。如果没有足够的空间来显示内容,scrollOffset设置反映已经滚动到距离左边文本多远处。当用户在JTextField的文本中浏览时,scrollOffset值会被自动更新。例如,图15-4中的文本包含26个字母以及10个数字: ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890。并不是所有的字符都能适应文本域;所以,字符A到J已经滚动偏离左边。

Swing_15_4.png

Swing_15_4.png

通过修改scrollOffset设置,我们可以控制文本域的哪一个部分是可见的。要保证文本域内容开始处是可见的,将scrollOffset设置为零。要使得内容的结束处是可见的,我们需要向horizontalVisibility属性查询BoundedRangeModel的extent是什么,来确定范围的宽度,然后将scrollOffset设置为extent设置,如下所示:

BoundedRangeModel model = textField.getHorizontalVisibility();
int extent = model.getExtent();
textField.setScrollOffset(extent);

通过修改horizontalAlignment属性设置,我们可以将一个JTextField的内容右对齐,左对齐或是居中对齐。默认情况下,文本对齐是左对齐。public void setHorizontalAlignment(int alignment)方法需要一个参数:JTextField.LEFT,JTextField.CENTER,JTextField.RIGHT,JTextField.LEADING(默认),或是JTextField.TRAILING来指定内容对齐。图15-5显示了对齐设置如何影响内容。

Swing_15_5.png

Swing_15_5.png

注意,我们可以将由JTextComponent继承来的document属性设置为Document接口的任意实现。如果我们为JTextField使用StyledDocument,UI委托就会忽略所有的格式属性。我们会在第16章中讨论StyledDocument接口。

JTextField中的JTextComponent操作

我们是否在寻找一种简单的方法来载入或是保存文本组件中的内容呢?Swing文本组件提供了这种方法。另外,Swing文本组件对访问系统剪切板用于剪切,复制与粘贴操作的内建支持。这些操作对于所有的JTextComponent子类都是可用的。在这里特别为JTextField显示这些操作,因为为了真正的演示他们需要特定的实现。我们可以使用JPasswordField,JTextArea,JEditorPane与JTextPane执行相同的任务。

载入与保存内容

使用JTextComponent的public void read(Reader in, Object description)与public void write(Writer out)方法(两个方法都抛出IOException),我们可以简单的由任意的文本组件载入与保存内容。使用read()方法,description参数被添加为Document数据模型的一个属性。这可以使得我们保存关于数据来自于哪里的信息。下面的示例演示了如何读取文件名的内容并且存放在textComponent中。文件名自动保存为描述。

FileReader reader = null;
try {
  reader = new FileReader(filename);
  textComponent.read(reader, filename);
}  catch (IOException exception) {
  System.err.println("Load oops");
}  finally {
  if (reader != null) {
    try {
      reader.close();
    } catch (IOException exception) {
      System.err.println("Error closing reader");
      exception.printStackTrace();
    }
  }
}

如果我们稍后希望由数据模型获取描述,在这种情况下恰好为文件中,我们只需要简单的查询,如下所示:

Document document = textComponent.getDocument();
String filename = (String)document.getProperty(Document.StreamDescriptionProperty);

Document属性只是简单的另一个键/值查询表。在这个特定的情况下键为类常量Document.StreamDescriptionProperty。如果我们不希望存储描述,我们可以传递null作为read()方法的descritption参数。(Document接口将会在本章稍后进行详细讨论。)

在我们将一个文件读取到文本组件之前,我们需要创建要读取的文件。这可以在Java程序之外完成,或者是我们可以使用JTextComponent的write()方法来创建文件。下面的代码演示了如何使用write()方法来写入内容。为了简单起见,他并没有处理由Document中获取的文件名,因为这可以不初始化设置。

FileWriter writer = null;
try {
  writer = new FileWriter(filename);
  textComponent.write(writer);
}  catch (IOException exception) {
  System.err.println("Save oops");
}  finally {
  if (writer != null) {
    try {
      writer.close();
    } catch (IOException exception) {
      System.err.println("Error closing writer");
      exception.printStackTrace();
    }
  }
}

图15-6为示了使用了载入与保存功能,并通过按钮来实现这些选项(尽管载入与保存选项在File菜单中更常见)的示例程序。Clear按钮清除文本域中的内容。

Swing_15_6.png

Swing_15_6.png

列表15-3中的源码将所有这些代码段组合在一起来演示载入与保存流。

package swingstudy.ch15;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.text.JTextComponent;

public class LoadSave {

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

        Runnable runner = new Runnable() {
            public void run() {
                final String filename = "text.out";
                JFrame frame = new JFrame("Loading/Saving Example");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                final JTextField textField = new JTextField();
                frame.add(textField, BorderLayout.NORTH);

                JPanel panel = new JPanel();

                // Setup actions
                Action loadAction = new AbstractAction() {
                    {
                        putValue(Action.NAME, "Load");
                    }
                    public void actionPerformed(ActionEvent e) {
                        doLoadCommand(textField, filename);
                    }
                };
                JButton loadButton = new JButton(loadAction);
                panel.add(loadButton);

                Action saveAction = new AbstractAction() {
                    {
                        putValue(Action.NAME, "Save");
                    }
                    public void actionPerformed(ActionEvent e) {
                        doSaveCommand(textField, filename);
                    }
                };
                JButton saveButton = new JButton(saveAction);
                panel.add(saveButton);

                Action clearAction = new AbstractAction() {
                    {
                        putValue(Action.NAME, "Clear");
                    }
                    public void actionPerformed(ActionEvent e) {
                        textField.setText("");
                    }
                };
                JButton clearButton = new JButton(clearAction);
                panel.add(clearButton);

                frame.add(panel, BorderLayout.SOUTH);

                frame.setSize(250, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

    public static void doSaveCommand(JTextComponent textComponent, String filename) {
        FileWriter writer = null;
        try {
            writer = new FileWriter(filename);
            textComponent.write(writer);
        }
        catch (IOException exception) {
            System.err.println("Save oops");
            exception.printStackTrace();
        }
        finally {
            if(writer != null) {
                try {
                    writer.close();
                }
                catch(IOException exception) {
                    System.err.println("Error closing writer");
                    exception.printStackTrace();
                }
            }
        }
    }

    public static void doLoadCommand(JTextComponent textComponent, String filename) {
        FileReader reader = null;
        try {
            reader = new FileReader(filename);
            textComponent.read(reader, filename);
        }
        catch(IOException exception) {
            System.err.println("Load oops");
            exception.printStackTrace();
        }
        finally {
            if(reader != null) {
                try {
                    reader.close();
                }
                catch(IOException exception) {
                    System.err.println("Error closing reader");
                    exception.printStackTrace();
                }
            }
        }
    }

}

注意,默认情况下,文件读取与写入只处理普通文本。如果一个文本组件的内容是格式化的,格式化属性并不会被保存。EditorKit类可以自定义这种载入与保存的行为。我们将会在第16章探讨这个类。

访问剪切板

要使用系统剪切板用于剪切、复制与粘贴操作,我们并不需要手动编写一个Transferable剪切板对象。相反,我们只需要调用JTextComponent类的三个方法中的一个:public void cut(), public void copy()或是public void paste()。

我们可以由与按钮或是菜单项相相关联的ActionListener实现中直接调用这些方法,如下所示:

ActionListener cutListener = new ActionListener() {
  public void actionPerformed(ActionEvent actionEvent) {
    aTextComponent.cut();
  }
};

然而有一种不需要我们手动创建ActionListener实现的简单方法。这种方法是通过向文本组件查询已存在的剪切操作。如果我们看一下表15-1中的JTextComponent属性集合,我们就会注意到一个名为actions的属性,他是一个Action对象数组。这个属性包含一个我们可以直接将其作为ActionListener关联到任意按钮或是菜单项的预定义的Action实现集合。一旦我们获取当前文本组件的actions,我们就可以在数组中遍历直到我们到相应的实现。因为动作是被命名的,我们只需要知道名字的文本字符串。DefaultEditorKit类具有大约40个键作为公共常量。下面是获取剪切动作的示例:

Action actions[] = textField.getActions();
Action cutAction = TextUtilities.findAction(actions, DefaultEditorKit.cutAction);

文本组件集合中的所有动作都是TextAction类型的,他是AbstractAction类的一个扩展。关于TextAction我们需要了解的一件事就是他作用在最后一个获得焦点的文本组件上。(TextAction类以及DefaultEditorKit类将会在第16章中进行详细的讨论。)所以,尽管前面的代码片段获取了一个文本域中的剪切操作,相同的剪切动作也同样适用于同一屏幕上的其他文本组件。当特定的cutAction被激活时,最的一个获得输入焦点的文本组件的内容将会被剪切。

为了有助于我们理解这一行为,图15-7显示了一个屏幕,其中在顶部是一个JTextField,中间是一个JTextArea,而底部是用于剪切,复制与粘贴操作的按钮(尽管这些操作通常是通过编辑菜单获得的)。如果我们运行这个程序,我们就会注意到剪切,复制与粘贴操作作用在最后一个获得输入焦点的文本组件上。

列表15-4是用于查找actions属性数组中的Action并且使用剪切,复制与粘贴操作完整示例的源码。

package swingstudy.ch15;

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

import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.text.DefaultEditorKit;

public class CutPasteSample {

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

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Cupt/Paste Example");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                JTextField textField = new JTextField();
                JTextArea textArea = new JTextArea();
                JScrollPane scrollPane = new JScrollPane(textArea);

                frame.add(textField, BorderLayout.NORTH);
                frame.add(scrollPane, BorderLayout.CENTER);

                Action actions[] = textField.getActions();

                Action cutAction = TextUtilities.findAction(actions, DefaultEditorKit.cutAction);
                Action copyAction = TextUtilities.findAction(actions, DefaultEditorKit.copyAction);
                Action pasteAction = TextUtilities.findAction(actions, DefaultEditorKit.pasteAction);

                JPanel panel = new JPanel();
                frame.add(panel, BorderLayout.SOUTH);

                JButton cutButton = new JButton(cutAction);
                cutButton.setText("Cut");
                panel.add(cutButton);

                JButton copyButton = new JButton(copyAction);
                copyButton.setText("Copy");
                panel.add(copyButton);

                JButton pasteButton = new JButton(pasteAction);
                pasteButton.setText("Paste");
                panel.add(pasteButton);

                frame.setSize(250, 250);
                frame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

列表15-4中的示例使用列表15-5中所示的TextUtilities支持类。并没有直接的方法来确定一个特定按键的一个特定动作是否存在于acions属性数组中。相反,我们必须手动进行查找。public static Action findAction(Action actions[], String key)方法为我们进行相应的查找。

package swingstudy.ch15;

import java.util.Hashtable;

import javax.swing.Action;

public final class TextUtilities {

    private TextUtilities() {

    }

    public static Action findAction(Action actions[], String key) {
        Hashtable<Object, Action> commands = new Hashtable<Object, Action>();
        for(int i=0; i<actions.length; i++) {
            Action action = actions[i];
            commands.put(action.getValue(Action.NAME), action);
        }
        return commands.get(key);
    }
}

注意,出于安全的原因,JPasswordField类的cut()与copy()方法并没有将当前的内容放在系统剪切板中。然而我们仍然可以使用paste()方法将剪切板中的内容粘贴到JPasswordField中。

Document接口

Document接口定义了不同的文本组件的数据模型。这个接口的实现用来存储实际的内容以及标记内容的信息(粗体,斜体或是颜色)。尽管所有的内容都将是文本,然而文本组件显示内容的方式会导致非文本的输出,例如HTML渲染器。

数据模型是与文本组件分开存储的。所以,如果我们对监视文本组件的内容感兴趣,我们必须监视Document本身,而不是文本组件。如果修改到达文本组件,这就太迟了,模型已经发生了改变。要监听变化,向模型关联一个DocumentListener。然而,限制输入更可能的方式是提供一个自定义的模型或是向AbstractDocument关联一个DocumentFilter。我们也可以向文本组件关联一个InputVerifier。然而,直到输入焦点将离开组件时他才会起作用。

注意,除了通过Document接口访问文本内容以外,还定义了一个框架用于支持undo/redo功能。我们将会在第21章中探讨这一框架。

现在我们来了解一下标记Document的片段。首先我们来看一下基本的接口定义:

public interface Document {
  // Constants
  public final static String StreamDescriptionProperty;
  public final static String TitleProperty;
  // Listeners
  public void addDocumentListener(DocumentListener listener);
  public void removeDocumentListener(DocumentListener listener);
  public void addUndoableEditListener(UndoableEditListener listener);
  public void removeUndoableEditListener(UndoableEditListener listener);
  // Properties
  public Element getDefaultRootElement();
  public Position getEndPosition();
  public int getLength();
  public Element[ ] getRootElements();
  public Position getStartPosition();
  // Other methods
  public Position createPosition(int offset) throws BadLocationException;
  public Object getProperty(Object key);
  public String getText(int offset, int length) throws BadLocationException;
  public void getText(int offset, int length, Segment txt)
    throws BadLocationException;
  public void insertString(int offset, String str, AttributeSet a)
    throws BadLocationException;
  public void putProperty(Object key, Object value);
  public void remove(int offset, int len) throws BadLocationException;
  public void render(Runnable r);
}

Document中的内容是通过一系列的元素来描述的,其中每一个元素实现了Element接口。在每一个元素中,我们可以存储属性,从而可以将选中的内容修改为粗体,斜体或是颜色化。元素不存储内容;他们仅存储属性。所以,一个Document可以不同的Element集合进行不同的渲染。

下面的代码是一个带有标题与内容列表的基本的HTML文档。

仔细查看一下这个HTML文档中的元素结构,我们可以得到图15-8所示的层次结构。

Swing_15_8.png

Swing_15_8.png

尽管这个特定的文档并不真实,但是多个元素的层次结构是可能的。每一个存储不同的属性,因为一个特定的文本组件会具有另一个内容渲染。相对应的,不同的格式化页表可以用来渲染相同的HTML标记。

AbstractDocument类

AbstractDocument类提供了Document接口的基本实现。他定义了监听器列表的管理,提供了一个读写锁机制来保证内容不会被破坏,并且提供了一个Dictionary用于存储文档属性。

表15-3列出了AbstractDocument类的11个属性,其中5个是由Document接口本身定义的。

Swing_table_15_3.png

Swing_table_15_3.png

对于属性中的大部分,我们不会直接访问这些属性,也许除了documentFilter。在documentProperties属性的情况下,我们通过public Object getProperty(Object key)与public void putProperty(Object key, Object value)方法获取与设置单个属性。对于length属性,在大多数情况下,我们可以简单的查询文本组件中的文本,然后使用textComponent.getText().length()方法获取其长度。

bidiRootElement属性用于双向根元素,他也许位于一定的Unicode字符集中。我们通常只需要使用defaultRootElement。然而,两者都很少被访问。

PlainDocument类

PlainDocument类是AbstractDocument类的一个特定实现。他并不为内容存储任何字符级的属性。相反,元素描述内容在哪里以及内容开始处的每一行。

列表15-6中的程序在一个PlainDocument中遍历Element树。

package swingstudy.ch15;

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

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.ElementIterator;

public class ElementSample {

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

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

                final JTextArea textArea = new JTextArea();
                JScrollPane scrollPane = new JScrollPane(textArea);

                JButton button =  new JButton("Show Elements");
                ActionListener actionListener =  new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        Document document = textArea.getDocument();
                        ElementIterator iterator = new ElementIterator(document);
                        Element element = iterator.first();
                        while(element != null) {
                            System.out.println(element.getStartOffset());
                            element = iterator.next();
                        }
                    }
                };
                button.addActionListener(actionListener);

                frame.add(scrollPane, BorderLayout.CENTER);
                frame.add(button, BorderLayout.SOUTH);

                frame.setSize(250, 250);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

假定JTextArea的内容如下:

Hello, World

Welcome Home

Adios

程序将会报告Element对象开始于0,0,13与26。第一个0表示内容的开始处;第二个表示第一行的开始处。

我们将会在第16章中了解到关于Element的更多内容。

过滤文档模型

在AWT世界中,如果我们要限制文本域的输入-例如限制为字母数字字符或是某一个范围的值-我们关联KeyListener并处理我们不希望出现在组件中的按键。使用Swing文本组件,我们可以创建一个新的Document实现并且自定义在Document中接受些什么,或者是关联一个DocumentFilter并由他来过滤输入。

虽然我们可以创建一个Document的自定义子类,但是更为面向对象的方法是创建一个过滤器,因为我们不希望修改Document;我们只是希望限制模型的输入。然后我们可以通过调用AbstractDocument的setDocumentFilter()方法来将新创建的过滤器关联到文档。过滤器适用于PlainDocument与StyledDocument子类。

DocumentFilter是一个类,而不是接口,所以我们必须创建一个该类的子类来过滤文本组件的文档的输入。如果我们创建一个DocumentFilter的子类,重写下面三个方法可以使得我们自定义输入:

  • public void insertString(DocumentFilter.FilterBypass fb, int offset,

String string, AttributeSet attributes): 当一个文本字符串被插入到文档中时调用。 • public void remove(DocumentFilter.FilterBypass fb, int offset, int length): 当某些内容被选中时调用。 • public void replace(DocumentFilter.FilterBypass fb, int offset, int length, String text, AttributeSet attrs): 当某些内容被插入到当前被选中的文本中时调用。

要限制输入,只需要覆盖每一个方法并且检测新内容是否合法。如果内容不合法,则拒绝。

例如,创建一个DocumentFilter子类来限制数字范围,我们需要覆盖insertString(),remove()与replace()方法。因为我们要保证输入是数字并且位于是一个合法的范围中,我们需要验证输入并且确定他是否可以接受。如果可以接受,那么我们可以通过调用DocumentFilter的insertString(),remove()或是replace()方法来修改文档模型。当输入不可接受时,我们抛出一个BadLocationException。抛出这个异常保证输入方法模型理解用户的输入不合法。这通常会触发系统发出声响。列表15-7显示了一个限制整数范围的文档过滤器。

package swingstudy.ch15;

import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.DocumentFilter;

public class IntegerRangeDocumentFilter extends DocumentFilter {

    int minimum, maximum;
    int currentValue = 0;

    public IntegerRangeDocumentFilter(int minimum, int maximum) {
        this.minimum = minimum;
        this.maximum = maximum;
    }

    public void insertString(DocumentFilter.FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException {
        if(string == null) {
            return;
        }
        else {
            String newValue;
            Document doc = fb.getDocument();
            int length = doc.getLength();
            if(length == 0) {
                newValue = string;
            }
            else {
                String currentContent = doc.getText(0, length);
                StringBuffer currentBuffer = new StringBuffer(currentContent);
                currentBuffer.insert(offset, string);
                newValue = currentBuffer.toString();
            }
            currentValue = checkInput(newValue, offset);
            fb.insertString(offset, string, attr);
        }
    }

    public void remove(DocumentFilter.FilterBypass fb, int offset, int length)
        throws BadLocationException {
        Document doc = fb.getDocument();
        int currentLength = doc.getLength();
        String currentContent = doc.getText(0, currentLength);
        String before = currentContent.substring(0, offset);
        String after = currentContent.substring(length+offset, currentLength);
        String newValue = before+after;
        currentValue = checkInput(newValue, offset);
        fb.remove(offset, length);
    }

    public void replace(DocumentFilter.FilterBypass fb, int offset, int length, String text, AttributeSet attrs)
        throws BadLocationException {
        Document doc = fb.getDocument();
        int currentLength = doc.getLength();
        String currentContent = doc.getText(0, currentLength);
        String before = currentContent.substring(0, offset);
        String after = currentContent.substring(length+offset, currentLength);
        String newValue = before+(text==null?"":text)+after;
        currentValue = checkInput(newValue, offset);
        fb.replace(offset, length, text, attrs);
    }

    public int checkInput(String proposedValue, int offset)
        throws BadLocationException {
        int newValue = 0;
        if(proposedValue.length()>0) {
            try {
                newValue = Integer.parseInt(proposedValue);
            }
            catch(NumberFormatException e) {
                throw new BadLocationException(proposedValue, offset);
            }
        }
        if((minimum<=newValue) && (newValue<=maximum)) {
            return newValue;
        }
        else {
            throw new BadLocationException(proposedValue, offset);
        }
    }
}

图15-9显示了使用中的数字范围过滤器。

Swing_15_9.png

Swing_15_9.png

列表15-8显示了使用新的IntegerRangeDocumentFilter的示例程序。

package swingstudy.ch15;

import java.awt.EventQueue;
import java.awt.GridLayout;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
import javax.swing.text.AbstractDocument;
import javax.swing.text.Document;
import javax.swing.text.DocumentFilter;

public class RangeSample {

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

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame =  new JFrame("Range Example");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new GridLayout(3,2));

                frame.add(new JLabel("Range: 0-255"));
                JTextField textFieldOne = new JTextField();
                Document textDocOne = textFieldOne.getDocument();
                DocumentFilter filterOne = new IntegerRangeDocumentFilter(0, 255);
                ((AbstractDocument)textDocOne).setDocumentFilter(filterOne);
                frame.add(textFieldOne);

                frame.add(new JLabel("Range: -100-100"));
                JTextField textFieldTwo = new JTextField();
                Document textDocTwo = textFieldTwo.getDocument();
                DocumentFilter filterTwo = new IntegerRangeDocumentFilter(-100,100);
                ((AbstractDocument)textDocTwo).setDocumentFilter(filterTwo);
                frame.add(textFieldTwo);

                frame.add(new JLabel("Range: 1000-2000"));
                JTextField textFieldThree = new JTextField();
                Document textDocThree = textFieldThree.getDocument();
                DocumentFilter filterThree = new IntegerRangeDocumentFilter(1000, 2000);
                ((AbstractDocument)textDocThree).setDocumentFilter(filterThree);
                frame.add(textFieldThree);

                frame.setSize(250, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

如果我们尝试这个程序,我们就会注意到一些有趣的问题。第一个文本域,范围为0到255,可以正常工作。只要内容是在这个范围之内,我们就可以随时输入或是删除字符。

在第二个文本域中,合法的范围为-100到+100。尽管我们可以在这个文本域中输入任意的201个数字,但是如果我们希望一个负数,我们需要输入一个数字,例如3,左箭头,再输入负号。因为文本域会使用每一个键验证输入,负号本身是不合法的。我们需要在自定义的DocumentFilter的checkInput()方法将负号作为合法的输入接受,或者是强制用户以一种后退的方式输入负数。

第三个文本域展示了一种更为麻烦的情况。输入的合法范围为1000-2000。当我们按下每一个键来输入数字时,例如1500,他会被拒绝。我们不能构建1500的输入,因为1,5与0是非法输入。相反,要在这个文本域中输入数字,我们必须在其他位置输入这个数字,将其放入系统剪切板中,然后使用Ctrl-V将其粘贴到文本域中作为文本域的最终值。我们不能使用Backspace来修正错误,因为三位数是非法的。

虽然列表15-7中的IntegerRangeDocumentFilter类表示了一个对于任意的整数范围可用的DocumentFilter,他适用于由零开始的整数范围。如果我们并不介意看到文本域中的临时非法输入,也许更好的方法是仅关联一个InputVerifier在离开文本域的时候处理验证。

DocumentListener与DocumentEvent接口

如果我们对文本组件的内容何时发生变化感兴趣,我们可以向组件的Document模型关联一个DocumentListener接口的实现。

public interface DocumentListener implements EventListener {
  public void changedUpdate(DocumentEvent documentEvent);
  public void insertUpdate(DocumentEvent documentEvent);
  public void removeUpdate(DocumentEvent documentEvent);
}

通过上面的三个接口方法,我们可以确定内容是否被添加(insertUpdate()),移除(removeUpdate())或是格式化修改(changedUpdate())。注意,后者是属性变化而不是内容变化。

接口方法将会接收一个DocumentEvent的实例,从中我们可以确定在哪里发生了变化以及变化的类型,如下所示:

public interface DocumentEvent {
  public Document getDocument();
  public int getLength();
  public int getOffset();
  public DocumentEvent.EventType getType();
  public DocumentEvent.ElementChange getChange(Element element);
}

事件的offset属性是变化的起始处。事件的length属性报告发生变化的长度。事件的类型可以由被调用的三个DocumentListener方法中的一个导出。另外,DocumentEvent.EventType类有三个常量-CHANGE,INSERT与REMOVE-所以我们可以直接由type属性直接确定所发生的事件类型。

DocumentEvent的getChange()方法需要一个Element来返回DocumentEvent.ElementChange。我们通常使用Document的默认根元素,如下面的示例所示。

Document documentSource = documentEvent.getDocument();
Element rootElement = documentSource.getDefaultRootElement();
DocumentEvent.ElementChange change = documentEvent.getChange(rootElement);

一旦我们具有DocumentEvent.ElementChange实例,如果我们需要该级别的信息,我们可以确定添加与移除的元素。

public interface DocumentEvent.ElementChange {
  public Element[ ] getChildrenAdded();
  public Element[ ] getChildrenRemoved();
  public Element getElement();
  public int getIndex();
}

Caret与Highlighter接口

现在我们已经理解了文本组件的数据模型方面,我们可以了解通过Caret与Highlighter接口进行选中渲染的相关知识了。记住这些是文本组件的属性,而不是数据模型的属性。

Caret接口描述通常被作为光标引用的内容:在文档中我们可以插入文本的位置。Highlighter接口提供了如何绘制选中文本的基础。这两个接口,他们相关的接口以及他们的实现都很少被修改。文本组件简单的通过DefaultCaret与DefaultHighlighter类使用他们的默认实现。

尽管我们并不会修改一个文本组件的caret与highlighter的行为,但是我们应该了解有许多内部相关的类协同工作。对于Highlighter接口,预定义的实现被称之为DefaultHighlighter,他扩展了另一个名为LayeredHighlighter的实现。Highlighter同时管理一个Highlighter.Highlight对象的集合来指定被高亮的部分。

DefaultHighlighter创建一个DefaultHighlighter.HighlightPainter来绘制文本的高亮部分。HighlightPainter是Highlighter.HighlightPainter接口的实现,并且扩展了LayeredHighlighter.LayerPainter类。要绘制的每一个部分通过Highlighter.Highlight进行描述,其中Highlighter管理集合。实际的HighlightPainter是通过DefaultCaret实现来创建的。

Highlighter接口描述如何绘制文本组件中被选中的文本。如果我们不喜欢颜色,我们可以简单的将TextField.selectionBackground UI属性设置修改为另一个不同的颜色。

public interface Highlighter {
  // Properties
  public Highlighter.Highlight[ ] getHighlights();
  // Other methods
  public Object addHighlight(int p0, int p1, Highlighter.HighlightPainter p)
    throws BadLocationException;
  public void changeHighlight(Object tag, int p0, int p1)
    throws BadLocationException;
  public void deinstall(JTextComponent component);
  public void install(JTextComponent component)
  public void paint(Graphics g);
  public void removeAllHighlights();
  public void removeHighlight(Object tag);
}

Caret接口描述当前的光标以及一些选中的属性。在Highlighter与Caret接口之间,后者是我们实际使用的,尽管并没有必要对其进行派生。

public interface Caret {
  // Properties
  public int getBlinkRate();
  public void setBlinkRate(int newValue);
  public int getDot();
  public void setDot(int newValue);
  public Point getMagicCaretPosition();
  public void setMagicCaretPosition(Point newValue);
  public int getMark();
  public boolean isSelectionVisible();
  public void setSelectionVisible(boolean newValue);
  public boolean isVisible();
  public void setVisible(boolean newValue);
  // Listeners
  public void addChangeListener(ChangeListener l);
  public void removeChangeListener(ChangeListener l);
  // Other methods
  public void deinstall(JTextComponent c);
  public void install(JTextComponent c);
  public void moveDot(int dot);
  public void paint(Graphics g);
}

表15-4列出了Caret的六个属性。

Swing_table_15_4.png

Swing_table_15_4.png

blinkRate是caret闪烁之间的毫秒延迟。dot属性是文本组件中当前光标的当前位置。要将光标移动另一个位置从而某些文本可以被高亮显示,添加moveDot(int newPosition)方法调用。这会将mark属性设置旧的dot位置并且设置新的dot设置为新位置。

magicCaretPosition属性处理不同长度行的向上移动与向下移动。例如,假定在我们的屏幕上有下面三个文本行:

Friz Freleng

Mel Blanc

What’s up Doc?

现在假定光标位于第一行的n与g之间。如果我们按下向下键两次,我们希望光标位于相同的水平位置,而不是较短的第二处的结束处。保存这个信息的就是magicCursorPosition属性,从而光标停在第三行的D与o之间。如果没有保存位置信息,光标将会停留在最后一行的p与空格之间。

使用caret的十分有用的实例就是响应按键来确定当前的屏幕位置。这样,我们就可以在当前的光标位置弹出一个菜单。这就是类似于JBuilder中的Code Insights或是Visual Studio中的IntelliSense,在其中通过弹出一个方法菜单来帮助我们完成方法调用。指定模型中的当前光标位置,使用JTextComponent(可以抛出BadLocationException)的public Rectangle modelToView(int position)方法将其映射到视图中的位置。然后使用作为位置返回的Rectangle弹出菜单,如图15-10所示。

Swing_15_10.png

Swing_15_10.png

列表15-9中的程序会在文本域中句点被按下的位置显示一个JPopupMenu。

package swingstudy.ch15;

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

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.text.BadLocationException;

public class PopupSample {

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

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

                final JPopupMenu popup = new JPopupMenu();
                JMenuItem menuItem1 = new JMenuItem("Option 1");
                popup.add(menuItem1);

                JMenuItem menuItem2 = new JMenuItem("Option 2");
                popup.add(menuItem2);

                final JTextField textField = new JTextField();
                frame.add(textField, BorderLayout.NORTH);

                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        try {
                            int dotPosition = textField.getCaretPosition();
                            Rectangle popupLocation = textField.modelToView(dotPosition);
                            popup.show(textField, popupLocation.x, popupLocation.y);
                        }
                        catch(BadLocationException badLocationException) {
                            System.err.println("Oops");
                        }
                    }
                };
                KeyStroke keystroke = KeyStroke.getKeyStroke(KeyEvent.VK_PERIOD, 0, false);
                textField.registerKeyboardAction(actionListener, keystroke, JComponent.WHEN_FOCUSED);

                frame.setSize(250, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

CaretListener接口与CareEvent类

我们可以使用两种方法监听光标的移动:将ChangeListener关联到Caret或是将CaretListener关联到JTextComponent。尽管两种方法作用相同,直接JTextComponent则是更为简单的方法。

在CaretListener的情况下,接口只定义了一个方法:

public interface CaretListener implements EventListener {
  public void caretUpdate (CaretEvent caretEvent);
}

当监听器得到通知时,CaretEvent被发送,他会报告新位置并标记位置。

public abstract class CaretEvent extends EventObject {
  public CaretEvent(Object source);
  public abstract int getDot();
  public abstract int getMark();
}

为了进行演示,图15-11显示了一个将CaretListener关联到内联JTextArea的程序。当CaretEvent发生时,当前光标值会发送到顶部文本域,而当前标记设置会被发送到按钮。在这个示例中,光标位置在第二行的起始处,而标记则是在结束处。

Swing_15_11.png

Swing_15_11.png

列表15-10显示了与图15-11中的示例相关联的源码。

package swingstudy.ch15;

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

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 javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;

public class CaretSample {

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

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

                JTextArea textArea = new JTextArea();
                JScrollPane scrollPane = new JScrollPane(textArea);
                frame.add(scrollPane, BorderLayout.CENTER);

                final JTextField dot = new JTextField();
                dot.setEditable(false);
                JPanel dotPanel = new JPanel(new BorderLayout());
                dotPanel.add(new JLabel("Dot: "), BorderLayout.WEST);
                dotPanel.add(dot, BorderLayout.CENTER);
                frame.add(dotPanel, BorderLayout.NORTH);

                final JTextField mark = new JTextField();
                mark.setEditable(false);
                JPanel markPanel = new JPanel(new BorderLayout());
                markPanel.add(new JLabel("Mark: "), BorderLayout.WEST);
                markPanel.add(mark, BorderLayout.CENTER);
                frame.add(markPanel, BorderLayout.SOUTH);

                CaretListener listener = new CaretListener() {
                    public void caretUpdate(CaretEvent event) {
                        dot.setText(Integer.toString(event.getDot()));
                        mark.setText(Integer.toString(event.getMark()));
                    }
                };

                textArea.addCaretListener(listener);

                frame.setSize(250, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

Keymap接口

以MVC的角度来看,文本组件的keymap属性是控制器部分。他通过Keymap接口将KeyStroke对象映射到单个的动作。(KeyStroke类在第2章中进行了讨论。)当我们使用registerKeyboardAction()方法向JTextComponent注册KeyStroke时,如本章前面的列表15-9中的PopupSample程序所示,文本组件在Keymap中存储由KeyStroke到Action的映射。例如,回退键被映射到删除前一个字符。如果我们要添加另一个绑定,我们只需要注册另一个按键。

注意,事实上,Keymap只是ActionMap/InputMap对的前端。JTextComponent依据某些内部作用间接使用ActionMap/InputMap类。

我们也可以直接向Keymap中添加按键动作。这可以使得我们在多个文本组件之间共享同一个按键映射,只要他们共享相同的扩展行为。

public interface Keymap {
  // Properties
  public Action[ ] getBoundActions();
  public KeyStroke[ ] getBoundKeyStrokes();
  public Action getDefaultAction();
  public void setDefaultAction(Action action);
  public String getName();
  public Keymap getResolveParent();
  public void setResolveParent(Keymap parent);
  // Other methods
  public void addActionForKeyStroke(KeyStroke keystroke, Action action);
  public Action getAction(KeyStroke keystroke);
  public KeyStroke[ ] getKeyStrokesForAction(Action action);
  public boolean isLocallyDefined(KeyStroke keystroke);
  public void removeBindings();
  public void removeKeyStrokeBinding(KeyStroke keystroke);
}

对于某些程序,我们也许希望由按键映射中移动按键。例如,JTextField在键盘映射中有一个用于Enter键的实体,从而所注册的ActionListener对象都会得到通知。如果JTextField位于设计有默认按钮的屏幕上,按下Enter并不会选中预期的默认按钮。去掉这种默认行为仅是简单的请求由Keymap中移除KeyStroke,如下所示:

Keymap keymap = textField.getKeymap();
KeyStroke keystroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false);
keymap.removeKeyStrokeBinding(keystroke);

然后,当我们在文本域中按下Enter时,默认按钮就会被激活,如图15-13所示。

Swing_15_13.png

Swing_15_13.png

图15-13的示例程序源码显示在列表15-12中。

package swingstudy.ch15;

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

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.text.Keymap;

public class DefaultSample {

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

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

                JTextField textField = new JTextField();
                frame.add(textField, BorderLayout.NORTH);

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

                JPanel panel = new JPanel();
                JButton defaultButton = new JButton("Default Button");
                defaultButton.addActionListener(actionListener);
                panel.add(defaultButton);

                JButton otherButton =  new JButton("Other Button");
                otherButton.addActionListener(actionListener);
                panel.add(otherButton);

                frame.add(panel, BorderLayout.SOUTH);

                Keymap keymap = textField.getKeymap();
                KeyStroke keystroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false);
                keymap.removeKeyStrokeBinding(keystroke);

                frame.getRootPane().setDefaultButton(defaultButton);

                frame.setSize(250, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

JTextComponent.KeyBinding类

JTextComponent类借且于JTextComponent.KeyBinding类来存储特定的按键绑定。当前的观感定义了文本组件按键绑定的默认集合,例如我们所熟悉的在Microsoft Windows平台上Ctrl-X用于剪切,Ctrl-C用于复制以及Ctrl-V用于粘贴。

处理JTextField事件

处理Swing文本组件中的事件完全不同于处理AWT文本组件中的事件。尽管我们仍然可以关联一个ActionListener来监听用户在文本域中输入Enter键的情况,关联KeyListener或是TextListsener不再有用。

要验证输入,关联InputVerifier要好于关联FocusListener。然而,输入验证最好是留给Document来实现或是当用户提交表单时实现。

使用ActionListener来监听JTextField事件

当用户在文本域中按下Enter后,JTextField会通知所注册的ActionListener对象。组件会向ActionListener对象发送一个ActionEvent。ActionEvent的部分是一个动作命令。默认情况下,事件的动作命令是组件的当前内容。对于Swing的JTextField,我们也可以将动作命令设置为不同于内容的某些东西。JTextField有一个actionCommand属性。当这个属性被设置为null时(默认设置),ActionEvent的动作命令会使用组件的内容。然而,如果我们为JTextField设置actionCommand属性,那么actionCommand就会成为ActionEvent的组成部分。

下面的代码显示了这种区域。有两个文本域。当在第一个文本域中按下Enter时,会使得所注册的ActionListener得到通知,并输出“Yo”。当在第二个文本域中按下Enter时,则内容会被输出。

JTextField nameTextField = new JTextField();
JTextField cityTextField = new JTextField();
ActionListener actionListener = new ActionListener() {
  public void actionPerformed(ActionEvent actionEvent) {
    System.out.println("Command: " + actionEvent.getActionCommand());
  }
};
nameTextField.setActionCommand("Yo");
nameTextField.addActionListener(actionListener);
cityTextField.addActionListener(actionListener);

使用KeyListener监听JTextField事件

对于Swing文本组件,我们通常并不使用KeyListener来监听键盘事件-至少不用来验证输入。运行下面的程序可以演示我们仍然可以确定一个按键何时被按下或释放,而不仅是何时输入。

KeyListener keyListener = new KeyListener() {
  public void keyPressed(KeyEvent keyEvent) {
    printIt("Pressed", keyEvent);
  }
  public void keyReleased(KeyEvent keyEvent) {
    printIt("Released", keyEvent);
  }
  public void keyTyped(KeyEvent keyEvent) {
    printIt("Typed", keyEvent);
  }
  private void printIt(String title, KeyEvent keyEvent) {
    int keyCode = keyEvent.getKeyCode();
    String keyText = KeyEvent.getKeyText(keyCode);
    System.out.println(title + " : " + keyText);
  }
};
nameTextField.addKeyListener(keyListener);
cityTextField.addKeyListener(keyListener);

使用InputVerifer监听JTextField事件

实现InputVerifier接口可以使得我们进行JTextField的域级别验证。在焦点移除一个文本组件之前,验证会运行。如果输入不合法,验证器就会拒绝修改并将焦点保持在指定的组件中。

在下面的示例中,如果我们尝试将输入焦点移除文本域之外,我们就会发现我们并不能办到,除非文本域的内容是空的,或者内容由字符串“Exit”组成。

InputVerifier verifier = new InputVerifier() {
  public boolean verify(JComponent input) {
    final JTextComponent source = (JTextComponent)input;
    String text = source.getText();
    if ((text.length() != 0) && !(text.equals("Exit"))) {
      Runnable runnable = new Runnable() {
        public void run() {
          JOptionPane.showMessageDialog (source, "Can't leave.",
            "Error Dialog", JOptionPane.ERROR_MESSAGE);
        }
      };
      EventQueue.invokeLater(runnable);
      return false;
    }  else {
      return true;
    }
  }
};
nameTextField.setInputVerifier(verifier);
cityTextField.setInputVerifier(verifier);

使用DocumentListener监听JTextField事件

要确定文本组件的内容何时发生变化,我们需要向数据模型关联一个监听器。在这种情况下,数据模型为Document,而监听器为DocumentListener。下面的示例仅是告诉我们模型何时以及如何发生变化。记住changedUpdate()用于属性变化。不要使用DocumentListener进行输入验证。

DocumentListener documentListener = new DocumentListener() {
  public void changedUpdate(DocumentEvent documentEvent) {
    printIt(documentEvent);
  }
  public void insertUpdate(DocumentEvent documentEvent) {
    printIt(documentEvent);
  }
  public void removeUpdate(DocumentEvent documentEvent) {
    printIt(documentEvent);
  }
  private void printIt(DocumentEvent documentEvent) {
    DocumentEvent.EventType type = documentEvent.getType();
    String typeString = null;
    if (type.equals(DocumentEvent.EventType.CHANGE)) {
      typeString = "Change";
    }  else if (type.equals(DocumentEvent.EventType.INSERT)) {
      typeString = "Insert";
    }  else if (type.equals(DocumentEvent.EventType.REMOVE)) {
      typeString = "Remove";
    }
    System.out.print("Type  :   " + typeString + " / ");
    Document source = documentEvent.getDocument();
    int length = source.getLength();
    try {
      System.out.println("Contents: " + source.getText(0, length));
    }  catch (BadLocationException badLocationException) {
      System.out.println("Contents: Unknown");
    }
  }
};
nameTextField.getDocument().addDocumentListener(documentListener);
cityTextField.getDocument().addDocumentListener(documentListener);

将所有内容组合在一起

现在我们已经分别了解了监听器的使用,让我们将这些内容组合在一个示例中。图15-14显示了最终的结果。记住要离开组件的魔法单词是“Exit”。

Swing_15_14.png

Swing_15_14.png

图15-14后面程序的源码显示在列表15-13中。

package swingstudy.ch15;

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

import javax.swing.InputVerifier;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;

public class JTextFieldSample {

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

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

                JPanel namePanel = new JPanel(new BorderLayout());
                JLabel nameLabel = new JLabel("Name: ");
                nameLabel.setDisplayedMnemonic(KeyEvent.VK_N);
                JTextField nameTextField = new JTextField();
                nameLabel.setLabelFor(nameTextField);
                namePanel.add(nameLabel, BorderLayout.WEST);
                namePanel.add(nameTextField, BorderLayout.CENTER);
                frame.add(namePanel, BorderLayout.NORTH);

                JPanel cityPanel = new JPanel(new BorderLayout());
                JLabel cityLabel = new JLabel("City: ");
                cityLabel.setDisplayedMnemonic(KeyEvent.VK_C);
                JTextField cityTextField = new JTextField();
                cityLabel.setLabelFor(cityTextField);
                cityPanel.add(cityLabel, BorderLayout.WEST);
                cityPanel.add(cityTextField, BorderLayout.CENTER);
                frame.add(cityPanel, BorderLayout.SOUTH);

                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        System.out.println("Command: "+event.getActionCommand());
                    }
                };
                nameTextField.setActionCommand("Yo");
                nameTextField.addActionListener(actionListener);
                cityTextField.addActionListener(actionListener);

                KeyListener keyListener = new KeyListener() {
                    public void keyPressed(KeyEvent event) {
                        printIt("Pressed", event);
                    }
                    public void keyReleased(KeyEvent event) {
                        printIt("Released", event);
                    }
                    public void keyTyped(KeyEvent event) {
                        printIt("Typed", event);
                    }
                    private void printIt(String title, KeyEvent event) {
                        int keyCode = event.getKeyCode();
                        String keyText = event.getKeyText(keyCode);
                        System.out.println(title+" : "+keyText+" / "+event.getKeyChar());
                    }
                };

                nameTextField.addKeyListener(keyListener);
                cityTextField.addKeyListener(keyListener);

                InputVerifier verifier = new InputVerifier() {
                    public boolean verify(JComponent input) {
                        final JTextComponent source = (JTextComponent)input;
                        String text = source.getText();
                        if((text.length()!=0) && !(text.equals("Exit"))) {
                            JOptionPane.showMessageDialog(source, "Can't leave.", "Error Dialog", JOptionPane.ERROR_MESSAGE);
                            return false;
                        }
                        else {
                            return true;
                        }
                    }
                };
                nameTextField.setInputVerifier(verifier);
                cityTextField.setInputVerifier(verifier);

                DocumentListener documentListener = new DocumentListener() {
                    public void changedUpdate(DocumentEvent event) {
                        printIt(event);
                    }
                    public void insertUpdate(DocumentEvent event) {
                        printIt(event);
                    }
                    public void removeUpdate(DocumentEvent event) {
                        printIt(event);
                    }
                    private void printIt(DocumentEvent event) {
                        DocumentEvent.EventType type = event.getType();
                        String typeString = null;
                        if(type.equals(DocumentEvent.EventType.CHANGE)) {
                            typeString = "Change";
                        }
                        else if(type.equals(DocumentEvent.EventType.INSERT)) {
                            typeString = "Insert";
                        }
                        else if(type.equals(DocumentEvent.EventType.REMOVE)) {
                            typeString = "Remove";
                        }
                        System.out.println("Type : "+typeString+" / ");
                        Document source = event.getDocument();
                        int length = source.getLength();

                        try {
                            System.out.println("Contents: "+source.getText(0, length));
                        }
                        catch(BadLocationException badLocationException) {
                            System.out.println("Contents: Unknown");
                        }
                    }
                };
                nameTextField.getDocument().addDocumentListener(documentListener);
                cityTextField.getDocument().addDocumentListener(documentListener);

                frame.setSize(250, 100);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

自定义JTextField观感

每一个可安装的Swing观感都提供了一个不同的JTextField外观以及默认的UIResource值集合。表15-5显示了JTextField的25 UIResource相关的属性。

Swing_table_15_5_1.png

Swing_table_15_5_1.png

Swing_table_15_5_2.png

Swing_table_15_5_2.png

图15-15显示了JTextField在预安装的观感类型集合Motif,Windows与Ocean下的外观。

Swing_15_15.png

Swing_15_15.png

JPasswordField类

JPasswordField组件被设计用来处理密码输入。密码文本域会显示一个特殊的输入掩码,而不会回显用户的输入。其作用类似于具有*输入掩码的JTextField。我们不能取消掩码设置,也不能剪切或是复制密码组件的内容。其目的就是为了强强安全性。

创建JPasswordField

与JTextField相同,JPasswordField类有五个构造函数:

public JPasswordField()
JPasswordField passwordField = new JPasswordField();
public JPasswordField(String text)
JPasswordField passwordField = new JPasswordField("Initial Password");
public JPasswordField(int columnWidth)
JPasswordField passwordField = new JPasswordField(14);
public JPasswordField(String text, int columnWidth)
JPasswordField passwordField = new JPasswordField("Initial Password", 14);
public JPasswordField(Document model, String text, int columnWidth)
JPasswordField passwordField = new JPasswordField(aModel, "Initial Password", 14);

使用无参数的构造函数,我们可以获得一个空的,零列宽的输入域,默认的初始化Document模型,以及*回显字符。尽管我们可以在构造函数中指定初始化文本,但是通常我们所要做的是提示用户输入密码来验证用户的标识,而不是确定用户是否可以提交一个表单。所以,JPasswordField的本意是在启动时是空的。类似于JTextField,我们也可以指定初始宽度,假定JPasswordField所在的窗口的布局管理器将处理这种请求。

我们也可以在构造函数中指定密码域的Document数据模型。当指定Document数据模型时,我们应该指定一个null初始化文本参数;否则,文档的当前内容就会被密码域的初始文本所替换。另外,我们不应尝试在JPasswordField中使用自定义的Document。因为组件在已输入多少字符之外并不会显示任何可视化的回馈,如果我们尝试将输入限制为数字数据,这会使用户感到迷惑。

JPasswordField属性

表15-6显示了JPasswordField的四个属性。

Swing_table_15_6.png

Swing_table_15_6.png

设置echoChar属性可以使得我们使用默认星号字符以外的掩码字符。如查echoChar属性被设置为字符\u0000(0),public boolean echoCharIsSet()方法会返回false。在其他的情况下,方法会返回true。

注意,JPasswordField有一个受保护的只读的text属性,我们应避免使用这个属性。相反,我们应使用password属性,因为他会返回一个char[]并在使用之后立即清除。一个String必须等待垃圾回收器来翻译。

自定义JPasswordField观感

JPasswordField是JTextField的一个子类。在所有预定义的观感类型下,他与JTextField具有相同的外观(如图15-15所示)。一个不同就是当前的echoChar属性设置隐藏内容。如图15-16所示。顶部的文本组件是一个JTextField;而底部则是一个JPasswordField。

Swing_15_16.png

Swing_15_16.png

表15-7显示了JPasswordField的17个UIResource相关的属性。

Swing_table_15_7_1.png

Swing_table_15_7_1.png

Swing_table_15_7_2.png

Swing_table_15_7_2.png

JFormattedTextField类

JFormattedTextField提供了格式化文本输入的支持。当这个组件创建时,我们为输入定义了一个掩码。这个掩码可以是以下四种格式之一:一个java.text.Format对象,一个AbstractFormatter,一个AbstractFormatterFactory或是一个不同类型的实际值(例如3.141592)。

依据我们希望用户输入的数据类型,系统为我们的使用提供了一些抽象格式器。例如,NumberFormatter可以用来输入数字,而DateFormatter可以用来输入整个日期。同时还有一个MaskFormatter用于描述具有编辑字行串的输入,例如用于美国社会保险号码的“XXX-XX-XXX”。如果我们希望不同的显示与编辑格式,我们可以使用AbstractFormatterFactory。我们将会在第16章中了解到关于格式器与格式器工厂的更多内容。

创建JFormattedTextField

JFormattedTextField类有六个构造函数:

public JFormattedTextField()
JFormattedTextField formattedField = new JFormattedTextField();
public JFormattedTextField(Format format)
DateFormat format = new SimpleDateFormat("yyyy--MMMM--dd");
JFormattedTextField formattedField = new JFormattedTextField(format);
public JFormattedTextField(JFormattedTextField.AbstractFormatter formatter)
DateFormat displayFormat = new SimpleDateFormat("yyyy--MMMM--dd");
DateFormatter displayFormatter = new DateFormatter(displayFormat);
JFormattedTextField formattedField = new JFormattedTextField(displayFormatter);
public JFormattedTextField(JFormattedTextField.AbstractFormatterFactory factory)
DateFormat displayFormat = new SimpleDateFormat("yyyy--MMMM--dd");
DateFormatter displayFormatter = new DateFormatter(displayFormat);
DateFormat editFormat = new SimpleDateFormat("MM/dd/yy");
DateFormatter editFormatter = new DateFormatter(editFormat);
DefaultFormatterFactory factory = new DefaultFormatterFactory(
  displayFormatter, displayFormatter, editFormatter);
JFormattedTextField formattedField = new JFormattedTextField(factory);
public JFormattedTextField(JFormattedTextField.AbstractFormatterFactory factory,
  Object currentValue)
DateFormat displayFormat = new SimpleDateFormat("yyyy--MMMM--dd");
DateFormatter displayFormatter = new DateFormatter(displayFormat);
DateFormat editFormat = new SimpleDateFormat("MM/dd/yy");
DateFormatter editFormatter = new DateFormatter(editFormat);
DefaultFormatterFactory factory = new DefaultFormatterFactory(
  displayFormatter, displayFormatter, editFormatter);
JFormattedTextField formattedField = new JFormattedTextField(factory, new Date());
public JFormattedTextField(Object value)
JFormattedTextField formattedField = new JFormattedTextField(new Date());

无参数的构造函数需要我们在稍后进行配置。其他的构造函数允许我们配置内容将会接受什么以及如何接受。

JFormattedTextField属性

表15-8显示了JFormattedTextField的八个属性。我们不必像使用JTextField时一样,通过text属性将JFormattedTextField的内容获取为一个String,而是可以通过value属性将其获取为一个Object。所以,如果我们的格式器用于一个Date对象,我们所获得的值可以转换为java.util.Date类型。

Swing_table_15_8.png

Swing_table_15_8.png

列表15-14演示了具有自定义格式器与工厂的JFormattedTextField的用户。注意,当我们编辑底部的文本域时,显示格式与编辑格式是不同的。

/**
 *
 */
package swingstudy.ch15;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.text.DateFormatter;
import javax.swing.text.DefaultFormatterFactory;

/**
 * @author mylxiaoyi
 *
 */
public class FormattedSample {

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

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

                JPanel datePanel = new JPanel(new BorderLayout());
                JLabel dateLabel =  new JLabel("Date: ");
                dateLabel.setDisplayedMnemonic(KeyEvent.VK_D);
                DateFormat format = new SimpleDateFormat("yyyy--MMMM--dd");
                JFormattedTextField dateTextField = new JFormattedTextField(format);
                dateLabel.setLabelFor(dateTextField);
                datePanel.add(dateLabel, BorderLayout.WEST);
                datePanel.add(dateTextField, BorderLayout.CENTER);
                frame.add(datePanel, BorderLayout.NORTH);

                JPanel date2Panel = new JPanel(new BorderLayout());
                JLabel date2Label = new JLabel("Date 2: ");
                date2Label.setDisplayedMnemonic(KeyEvent.VK_A);
                DateFormat displayFormat = new SimpleDateFormat("yyyy--MMMM--dd");
                DateFormatter displayFormatter = new DateFormatter(displayFormat);
                DateFormat editFormat = new SimpleDateFormat("MM/dd/yy");
                DateFormatter editFormatter = new DateFormatter(editFormat);
                DefaultFormatterFactory factory = new DefaultFormatterFactory(displayFormatter, displayFormatter, editFormatter);
                JFormattedTextField date2TextField = new JFormattedTextField(factory, new Date());
                date2Label.setLabelFor(date2TextField);
                date2Panel.add(date2Label, BorderLayout.WEST);
                date2Panel.add(date2TextField, BorderLayout.CENTER);
                frame.add(date2Panel, BorderLayout.SOUTH);

                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        JFormattedTextField source = (JFormattedTextField)event.getSource();
                        Object value = source.getValue();
                        System.out.println("Class: "+value.getClass());
                        System.out.println("Value: "+value);
                    }
                };
                dateTextField.addActionListener(actionListener);
                date2TextField.addActionListener(actionListener);

                frame.setSize(250, 100);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

自定义JFormattedTextField观感

类似于JPasswordField,JFormattedTextField也是JTextField的一个子类。在所有的预定义的观感类型下,同样与JTextField具有相同的外观(如图15-15)。要自定义其显示,我们可以修改JFormattedTextField的16个UIResource相关的属性集合中的任何一个,如表15-9所示。

Swing_table_15_9.png

Swing_table_15_9.png

JTextArea类

JTextArea是用于多行输入的文本组件。类似于JTextField,JTextArea的数据模型是Document接口的PlainDocument实现。所以,JTextArea被限制为单属性文本。类似于其他的需要滚动的Swing组件,JTextArea本身不支持滚动。我们需要将JTextArea放在JScrollPane中来允许在JTextArea中进行滚动。

创建JTextArea

JTextArea有六个构造函数:

public JTextArea()
JTextArea textArea = new JTextArea();
public JTextArea(Document document)
Document document = new PlainDocument();
JTextArea textArea = new JTextArea(document);
public JTextArea(String text)
JTextArea textArea = new JTextArea("...");
public JTextArea(int rows, int columns)
JTextArea textArea = new JTextArea(10, 40);
public JTextArea(String text, int rows, int columns)
JTextArea textArea = new JTextArea("...", 10, 40);
public JTextArea(Document document, String text, int rows, int columns)
JTextArea textArea = new JTextArea(document, null, 10, 40);

除非特别指定,文本区域也可以存储零行与零列的内容。尽管这听起来像是一个严重的限制,我们只需要告诉文本区域来使得当前的LayoutManager处理我们的文本区域的尺寸。JTextArea的内容初始时是空的,除非使用起始文本字符串或是Document模型指定。

注意,其他的JTextArea初始设置包括一个Tab为八个位置以及关闭文字换行。要了解关于Tab的更多内容,可以查看第16章中的TabStop与TabSet类。

在创建了JTextArea之后,记得将JTextArea放在JScrollPane中。然后在屏幕上如果没有足够的空间,JScrollPane就会为我们管理滚动。

JTextArea textArea = new JTextArea();
JScrollPane scrollPane = new JScrollPane(textArea);
content.add(scrollPane);

图15-17显示了在JScrollPane之内以及在JScrollPane之外的JTextArea的样子。不在JScrollPanel中的JTextArea,我们不能看到超出屏幕边界的文本。依照设计,将光标移动进区域并不会使得顶部的内容向上移动。

Swing_15_17.png

Swing_15_17.png

JTextArea属性

表15-10显示了JTextArea的12个属性。

Swing_table_15_10.png

Swing_table_15_10.png

rows与columns属性直接来自于构造函数的参数。preferredScrollableViewportSize与scrollableTracksViewportWidth属性来自于用于滚动支持的Scrollable接口实现。font与preferredSize属性仅是自定义由JTextComponent继承的行为。

更为有趣的属性是lineCount,tabSize以及lineWrap与wrapStyledWorld。lineCount属性可以使得我们确定文本域中有多少行。这对于调整尺寸十分有用。tabSize属性可以使得我们控制文本区域中tab位置。默认情况下,这个值为8.

lineWrap与wrapStyleWord属性配合使用。默认情况下,较长行的换行是禁止的。如果我们允许换行(通过将lineWrap属性设置为true),较长行换行的时机依赖于wrapStyleWord属性设置。初始时,这个属性为false,意味着如果lineWrap属性为true,将会在字符边界处换行。如果lineWrap与wrapStyleWord都为true,那么一行中不会适的单词将会被换到下一行,类似于字处理器中的样子。所以,要获得大多数人所希望的单词换行的功能,我们应将JTextArea的两个属性都设置为true:

JTextArea textArea = new JTextArea("...");
textArea.setLineWrap(true);
textArea.setWrapStyleWord(true);
JScrollPane scrollPane = new JScrollPane(textArea);

注意,Ctrl-Tab与Shift-Ctrl-Tab键的组合可以使得用户在JTextArea组件中变换焦点,而无需继承组件生成新类。

处理JTextArea事件

JTextArea并没有特定的事件。我们可以使用由JTextComponent继承的监听器中的一个或是关联一个InputVerifier。

有时,我们会在屏幕上放置一个JTextArea并在用户按下按钮之后获取其内容。而在另外一些时间,会涉及到更多的规划,此时我们希望在输入时监视输入,并且也许会进行相应的转换,例如:-)或是简单的笑脸。

自定义JTextArea观感

每一个可安装的Swing观感都提供了不同的JTextArea外观以及默认的UIResource值集合。图15-18显示在JTextArea组件在预安装的观感类型下的外观。注意,每一个外观上的基本区别在于JScrollPane的滚动条,他并不是JTextArea的实际部分。

Swing_15_18.png

Swing_15_18.png

表15-11显示了JTextArea的15个UIResource相关的属性集合。

Swing_table_15_11.png

Swing_table_15_11.png

JEditorPane类

JEditorPane类提供了显示与编辑多属性文本的功能。虽然JTextField与JTextArea只支持单颜色,单字体内容,JEditorPane允许我们使用各种风格(例如粗体,14点Helvetica,段落右对齐)或是HTML查看器的外观来标记我们的内容,如图15-19所示。

Swing_15_19.png

Swing_15_19.png

注意,JEditorPane的HTML支持只在具有某些扩展的HTML 3.2级别上可用,而编写本书时HTML 4.0x是当前的版本。级联样式表(CSS)被部分支持。

JEditorPane借助于一个特定的文本标记机制的EditorKit来支持多属性文本的显示与编辑。存在预定义的工具集来支持原始文本,HTML文档以及RTF文档。因为内容是多属性的,PlainDocument模型不再够用。相反,Swing以DefaultStyledDocument类的形式提供了一个StyledDocument来维护文档模型。其余的部分是一个新的HyperlinkListsener/HyperlinkEvent事件处理对用来监视文档中的超连接操作。

创建JEditorPane

JEditorPane有四个构造函数:

public JEditorPane()
JEditorPane editorPane = new JEditorPane();
public JEditorPane(String type, String text)
String content = "<H1>Got Java?</H1>";
String type = "text/html";
JEditorPane editorPane = new JEditorPane(type, content);
public JEditorPane(String urlString) throws IOException
JEditorPane editorPane = new JEditorPane("http://www.apress.com");
public JEditorPane(URL url) throws IOException
URL url = new URL("http://www.apress.com");
JEditorPane editorPane = new JEditorPane(url);

无参数的构造函数创建了一个空的JEditorPane。如果我们要初始化内容,我们可以直接指定文本或是其MIME类型。或者是我们可以指定获取内容的URL。URL可以作为一个String或是一个URL对象来指定。当我们将内容指定为一个URL,JEditorPane会由响应来确定MIME类型。

JEditorPane属性

表15-12显示了JEditorPane的11个属性。这些属性中的大部分仅是自定义父类的行为。

Swing_table_15_12.png

Swing_table_15_12.png

注意,page属性是非标准的,因为他有两个setter方法,但是只有一个getter方法。

JEditorPane的四个有趣属性是editorKit,contentType,page与text。editorKit属性是依据编辑器面板中的内容类型来配置的。我们将会在第16章中进行详细探讨其DefaultEditorKit,StyledEditorKit与HTMLEditorKit实现。contentType属性表示文档中内容类型的MIME类型。当我们在构造函数中(或是其他位置)设置内容时,这个属性会被自动设置。如果编辑器工具集不能确定MIME类型,我们可以进行手动设置。三个内建支持的数据类型是text/html,text/plain与text/rtf,通过预定义编辑器工具集的getContentType()方示可以获取这些类型。

page属性可以使得我们修改所显示的内容来反映一个特定URL的内容,从而我们可以以某种方式使用这些内容。text属性使得我们确定哪些文本内容基于当前的Document模型。

处理JEditorPane事件

因为JEditorPane仅是一个具有一些特殊显示特性的另一个文本区域组件,他支持与JTextArea组件相同的用于事件处理的监听器。另外,JEditorPane提供了一个特殊的监听器事件组合来处理文档中的超链接。

HyperlinkListener接口定义了一个方法,public void hyperlinkUpdate(HyperlinkEvent hyperlinkEvent),他使用一个HyperlinkEvent来响应-不要惊奇-超链接事件。事件包含一个报告事件类型的HyperlinkEvent.EventType并且使得我们进行不同的响应,或者是当选中时跟随链接或者是当在超链接上移动鼠标时改变光标(尽管这是默认发生的)。

下面是HyperlinkListener定义:

public interface HyperlinkListener implements EventListener {
  public void hyperlinkUpdate(HyperlinkEvent hyperlinkEvent);
}
And, here is the HyperlinkEvent definition:
public class HyperlinkEvent extends EventObject {
  // Constructors
  public HyperlinkEvent(Object source, HyperlinkEvent.EventType type, URL url);
  public HyperlinkEvent(Object source, HyperlinkEvent.EventType type, URL url,
    String description);
  public HyperlinkEvent(Object source, HyperlinkEvent.EventType type, URL url,
    String description, Element sourceElement)
  // Properties
  public String getDescription();
  public HyperlinkEvent.EventType getEventType();
  public Element getSourceElement();
  public URL getURL();
}

超链接类型将会是HyperlinkEvent.EventType类的三个常量之一:

  • ACTIVATED:通常涉及到在合适的内容上进行鼠标点击
  • ENTERED:在超链接内容上移动鼠标
  • EXITED:将鼠标移出超链接内容

所以,如果我们希望在工具栏上创建一个显示URL的HyperlinkListener,当在超链接之前并在激活时跟随超链接,我们可以创建我们自己的最简单的HTML帮助查看器。列表15-15中的HyperlinkListener实现将会为我们实现这一技巧。在监听器中提供了一些println语句,当鼠标位于URL之上并且URL被激活时显示URL。

/**
 *
 */
package swingstudy.ch15;

import java.awt.EventQueue;
import java.awt.Frame;
import java.io.IOException;
import java.net.URL;

import javax.swing.JEditorPane;
import javax.swing.JOptionPane;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.text.Document;

/**
 * @author mylxiaoyi
 *
 */
public class ActivatedHyperlinkListener implements HyperlinkListener {

    Frame frame;
    JEditorPane editorPane;

    public ActivatedHyperlinkListener(Frame frame, JEditorPane editorPane) {
        this.frame = frame;
        this.editorPane = editorPane;
    }
    /* (non-Javadoc)
     * @see javax.swing.event.HyperlinkListener#hyperlinkUpdate(javax.swing.event.HyperlinkEvent)
     */
    @Override
    public void hyperlinkUpdate(HyperlinkEvent event) {
        // TODO Auto-generated method stub

        HyperlinkEvent.EventType type= event.getEventType();
        final URL url = event.getURL();
        if(type==HyperlinkEvent.EventType.ENTERED) {
            System.out.println("URL: "+url);
        }
        else if(type==HyperlinkEvent.EventType.ACTIVATED) {
            System.out.println("Activated");
            Runnable runner = new Runnable() {
                public void run() {
                    Document doc = editorPane.getDocument();
                    try {
                        editorPane.setPage(url);
                    }
                    catch(IOException ioException) {
                        JOptionPane.showMessageDialog(frame, "Error following link", "Invalid link", JOptionPane.ERROR_MESSAGE);
                        editorPane.setDocument(doc);
                    }
                }
            };
            EventQueue.invokeLater(runner);
        }
    }
}

提示,不要忘记调用setEditable(false)方法将JEditorPane设置为只读。否则,查看器就成为了编辑器。

列表15-16是使用我们新创建的ActivatedHyperlinkListener类的完整示例。他所创建的窗体类似于前面图15-19中所示的页面,尽管是在图片中,About链接已经被跟随。

/**
 *
 */
package swingstudy.ch15;

import java.awt.EventQueue;
import java.io.IOException;

import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.event.HyperlinkListener;



/**
 * @author mylxiaoyi
 *
 */
public class EditorSample {

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

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

                try {
                    JEditorPane editorPane = new JEditorPane("http://www.google.com");
                    editorPane.setEditable(false);

                    HyperlinkListener hyperlinkListener = new ActivatedHyperlinkListener(frame, editorPane);
                    editorPane.addHyperlinkListener(hyperlinkListener);
                    JScrollPane scrollPane = new JScrollPane(editorPane);
                    frame.add(scrollPane);
                }
                catch(IOException e) {
                    System.err.println("Unable to load: "+e);
                }

                frame.setSize(640, 480);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

自定义JEditorPane观感

JEditorPane的外观类似于JTextArea。尽管所支持的内容不同,观感相关的属性通常是不同的。

表15-13显示了JEditorPane的15个UIResource相关的属性集合。属性的名字于JTextArea设置中的属性名字。

Swing_table_15_13.png

Swing_table_15_13.png

JTextPane类

JTextPane是JEditorPane的一种特殊形式,特别设计用来编辑(与显示)格式化文本。他与JEditorPane的唯一不同在于提供显示内容的方式,因为文本并不是像在HTML或是RTF文档中一样使用格式标记的。

JTextPane依赖设置文本属性的三个接口:AttributeSet用于基本的属性集合,MutableAttributeSet用于可修改的属性集合,Style用作与StyledDocument的部分相关联的属性集合。

本节将会介绍JTextPane。要了解关于在JTextPane中配置格式化内容不同部分的格式的信息可以查看第16章。

创建JTextPane

JTextPane只有两个构造函数:

public JTextPane()
JTextPane textPane = new JTextPane();
JScrollPane scrollPane = new JScrollPane(textPane);
public JTextPane(StyledDocument document)
StyledDocument document = new DefaultStyledDocument();
JTextPane textPane = new JTextPane(document);
JScrollPane scrollPane = new JScrollPane(textPane);

无参数的构造函数初始时没有内容。第二个构造函数使得我们先创建Document,然后在JTextPane中使用。

提示,如果内容大于可用的屏幕空间,记得将我们的JTextPane放在JScrollPane中。

JTextPane属性

表15-14显示了JTextPane的八个属性。我们将会在第16章中详细探讨这些属性。

Swing_table_15_14_1.png

Swing_table_15_14_1.png

Swing_table_15_14_2.png

Swing_table_15_14_2.png

自定义JTextPane观感

JTextPane是JEditorPane的一个子类。他在所有预定义的观感类型下与JTextArea具有相同的外观(如图15-18所示)。尽管内容也许不同,但是观感是相同的。

表15-15中显示了JTextPane UIResource相关属性的可用集合。对于JTextPane组件,有15个不同的属性。其属性名字类似于JTextArea设置中的属性名字。

Swing_table_15_15.png

Swing_table_15_15.png

载入具有内容的JTextPane

列表15-17提供了一个向JTextPane载入StyledDocument内容的示例。这仅是向我们展示功能。Style,SimpleAttributeSet与StyledConstants的详细使用将会在第16章中进行探讨。

/**
 *
 */
package swingstudy.ch15;

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

import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;
import javax.swing.text.StyledDocument;

/**
 * @author mylxiaoyi
 *
 */
public class TextPaneSample {
    private static String message = "In the beginning, there was COBOL, then there was FORTRAN, "+
    "then there was BASIC, ... and now there is Java.\n";

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

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

                StyleContext context = new StyleContext();
                StyledDocument document = new DefaultStyledDocument(context);

                Style style = context.getStyle(StyleContext.DEFAULT_STYLE);
                StyleConstants.setAlignment(style, StyleConstants.ALIGN_RIGHT);
                StyleConstants.setFontSize(style, 14);
                StyleConstants.setSpaceAbove(style, 4);
                StyleConstants.setSpaceBelow(style, 4);

                // Inset content
                try {
                    document.insertString(document.getLength(), message, style);
                }
                catch(BadLocationException badLocationException) {
                    System.err.println("Oops");
                }

                SimpleAttributeSet attributes = new SimpleAttributeSet();
                StyleConstants.setBold(attributes, true);
                StyleConstants.setItalic(attributes, true);

                // Insert content
                try {
                    document.insertString(document.getLength(), "Hello Java", attributes);
                }
                catch(BadLocationException badLocationException) {
                    System.err.println("Oops");
                }

                // Third style for icon/component
                Style labelStyle = context.getStyle(StyleContext.DEFAULT_STYLE);

                Icon icon = new ImageIcon("Computer.gif");
                JLabel label = new JLabel(icon);
                StyleConstants.setComponent(labelStyle, label);

                // Insert content
                try {
                    document.insertString(document.getLength(), "Ignored", labelStyle);
                }
                catch(BadLocationException badLocationException) {
                    System.err.println("Oops");
                }

                JTextPane textPane = new JTextPane(document);
                textPane.setEditable(false);
                JScrollPane scrollPane = new JScrollPane(textPane);
                frame.add(scrollPane, BorderLayout.CENTER);

                frame.setSize(300, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

上述源码的关键行是调用insertString()与其style参数:

document.insertString(document.getLength(), message, style);

图15-20显示了具有一些段落内容的JTextPane的样子。注意,内容并没有限制为仅是广西;他也可以具有图片。

Swing_15_20.png

Swing_15_20.png

小结

在本章中,我们开始对Swing文本组件的细节进行探讨。我们首先了解了根文本组件,JTextComponent,以及为其他的文本组件定义了许多操作。然后我们探讨特定的文本组件,JTextField,JPasswordField,JFormattedTextField,JTextArea,JEditorPane与JTextPane。

我们同时探讨了构成不同的组件的各种片段。我们深入了基于Document接口,用于AbstractDocument与PlaintDocument类的文本组件模型。我们同时了解了使用DocumentFilter来创建自定义的组件限制文本组件的输入。噣上,我们探讨了用于显示光标与高亮文本的Caret与Highlighter接口,用于限制文本组件中移动的NavigationFilter,以及使得文本组件作为控制器的Keymap。类似于控制器,Keymap将用户的按钮转换为影响文本组件模型的特定动作。

我们同时了解了Swing文本组件中是如何处理事件的。除了基本的AWT事件处理类,Swing添加了一些特别设计的新类,使用CaretListener来监听光标移动,使用DocumentListener监听文档内容变化。而且通过InputVerifer还有一个通用的Swing输入验证支持。

在第16章中,我们将会进一步探讨Swing文本组件。本章仅是所有组件的基本特性,而下一章将会探讨使用TextAction,JFormattedField的格式化输入以及使用StyledDocument的配置Style对象的细节。同时我们还会在HTMLDocument标记中进行探讨。

高级文本功能

在第15章中,我们介绍了Swing文本组件的各种功能。在本章中,我们将会通过在了解在特殊情况下证明有用的高级功能来继续我们的探讨。

Swing文本组件带有许多预定义的功能。例如,正如我们在第15章中看到的,尽管文本组件具有如cut(),copy()与paste()方法来使用系统剪切板,但是事实上我们并不使用这些方法。这是因为Swing文本组件带有他们自己预定义的Action对象集合,我们将会在本章中探讨这一集合。要使用Action对象,只需要将其关联到组件,例如一个按钮或是菜单项,然后简单的选中激发Action的组件。对于文本组件,Action对象是TextAction的一个实例,他具有知道哪一个组件最后具有输入焦点的额外特性。

在本章中,我们同时还会了解如何创建在JTextPane中显示的格式化文本。如果我们希望显示多种颜色的文本文档或是不同字体风格,JTextPane组件提供了一系列的接口与类来描述与文档相关联的属性。AttributeSet接口在只读基础上为我们提供这些功能,而MutableAttributeSet接口为了设置属性扩展了AttributeSet。我们将会看到SimpleAttributeSet类如何通过提供Hashtable存储文本属性来实现这些接口,以及StyleConstants类如何有助于配置我们可以应用的多种文本属性。而且,我们将会了解如何在我们的文档中使用Tab,包括如何定义起始字符以及文本如何对齐。

接下来,我们将会概略了解Swing所提供的不同的编辑器工具集,我们将会关注HTMLDocument的内部工作。当JEditorPane显示HTML时,HTMLEditorKit控制如何在HTMLDocument中载入与显示HTML内容。我们将会了解分析器如何载入内容以及如何在文档的不同标记间进行遍历。

最后,我们将会了解如何利用JFormattedTextField组件的格式化输入选项以及验证合法性。我们将会了解如何提供格式化日期与数字,以及隐藏类似于电话与社会安全号码的输入。

配合文本组件使用Action

TextAction类是Action接口的一个特殊类,在第2章定义了Action接口以及其他的Swing事件处理功能并且在第15章进行了概述。TextAction类的目的就是提供可以用于文本组件的Action实现。这些实现是如此精巧,可以知道哪一个组件是最近具有输入焦点的,从而应是动作的目标。

对于所有的文本组件,我们需要一种方法将按键与特定的动作相关联。这是通过Keymap接口来实现的,他将KeyStroke映射到TextAction,从而为了监听组件,单独的KeyListener对象不需要与文本组件相关联。键盘映射可以在多个组件之间共享并且/或者为特定的观感进行定制。JTextComponent还具有允许我们读取或是自定义按键映射的getKeymap()与setKeymap()方法。

注意,尽管Swing文本组件使用TextAction,KeyStroke与Keymap,他们仍然支持关联KeyListener的功能。然而使用KeyListener通常并不合适,特别是我们希望限制输入来匹配特定的情况时更是如此。限制输入的更好的方法就是构建自定义的DocumentFilter,如第15章中演示所示,或是使用InputVerifier。另外,实际的Keymap实现仅是对在非文本Swing组件中按键动作映射所用的InputMap/ActionMap组合的包装。

文本组件带有许多预定义的TextAction实现。通过默认的按键映射,文本组件知道这些预定义的动作,从而他们知道如何插入或是移除内容,以及如何跟踪光标与Caret的位置。如果文本组件支持格式化内容,如JTextPane所做的那样,还有额外的默认动作来支持这些内容。所有这些实现派生于JFC/Swing技术编辑器工具集。正如本章稍后在“编辑器工具集”中所讨论的,编辑器工具集提供了编辑特定文本组件类型的各种方法的逻辑组合。

列出Action

要确定JTextComponent支持哪些动作,我们仅需要通过public Action[] getActions()方法进行查询。这会返回一个Action对象的数组,通常是TextAction,他可以像其他的Action一样使用,例如在JToolBar上创建按钮。

图16-1显示了将会列出不同的预定义组件的动作的程序。由JRadioButton组合中选择一个组件,而其文本动作列表将会显示在文本区域中。对于每一个动作,程序会显示出动作名与类名。

Swing_16_1.png

Swing_16_1.png

相同的53个动作集合可以适用于所有的文本组件。JTextField,JFormattedTextField与JPasswordField还有一个额外的动作,被称为notify-field-accept,用于当在文本组件中按下Enter键时进行检测。JFormattedTextField具有第二个额外动作,reset-field-edit,用于内容不符合所提供的格式掩码的情况。JTextPane添加了他独有的20个动作集合用于处理多属性文本。

列表16-1显示了用于生成图16-1的源友。RadioButtonUtils类在第5章中创建。

/**
 *
 */
package swingstudy.ch16;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Comparator;

import javax.swing.Action;
import javax.swing.JEditorPane;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JPasswordField;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.text.JTextComponent;

/**
 * @author mylxiaoyi
 *
 */
public class ListActions {

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

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

                String components[] = {
                        "JTextField", "JFormattedTextField", "JPasswordField",
                        "JTextArea", "JTextPane", "JEditorPane"
                };

                final JTextArea textArea = new JTextArea();
                textArea.setEditable(false);
                JScrollPane scrollPane =  new JScrollPane(textArea);
                frame.add(scrollPane, BorderLayout.CENTER);

                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        // Determine which component selected
                        String command = event.getActionCommand();
                        JTextComponent component = null;
                        if(command.equals("JTextField")) {
                            component = new JTextField();
                        }
                        else if(command.equals("JFormattedTextField")){
                            component = new JFormattedTextField();
                        }
                        else if(command.equals("JPasswordField")) {
                            component = new JPasswordField();
                        }
                        else if(command.equals("JTextArea")) {
                            component = new JTextArea();
                        }
                        else if(command.equals("JTextPane")) {
                            component = new JTextPane();
                        }
                        else {
                            component = new JEditorPane();
                        }

                        // Process action list
                        Action actions[] = component.getActions();
                        // Define comparator to sort actions
                        Comparator<Action> comparator = new Comparator<Action>() {
                            public int compare(Action a1, Action a2) {
                                String firstName = (String)a1.getValue(Action.NAME);
                                String secondName = (String)a2.getValue(Action.NAME);
                                return firstName.compareTo(secondName);
                            }
                        };
                        Arrays.sort(actions, comparator);
                        StringWriter sw = new StringWriter();
                        PrintWriter pw = new PrintWriter(sw, true);
                        int count = actions.length;
                        pw.println("Count: "+count);
                        for(int i=0; i<count; i++) {
                            pw.print(actions[i].getValue(Action.NAME));
                            pw.print(" : ");
                            pw.println(actions[i].getClass().getName());
                        }
                        pw.close();
                        textArea.setText(sw.toString());
                        textArea.setCaretPosition(0);
                    }
                };

                final Container componentsContainer = RadioButtonUtils.createRadioButtonGrouping(components, "Pick to List Actions", actionListener);

                frame.add(componentsContainer, BorderLayout.WEST);
                frame.setSize(400, 250);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

Using Actions

到目前为止,我们已经了解对于各种文本组件有许多预定义的TextAction实现可用,但是我们还没有使用其中的任何一个。通过对列表16-1做一些小的修改,我们就可以对程序进行加强。修改后的程序显示在列表16-2中。在这个版本中,当一个单选按钮被选中,文本组件的类型就会显示在Action对象的文本列表显示在图16-1中。另外,不同的Action对象被添加到位于显示窗口顶部的新JMenuBar中。

注意,在列表16-2所显示的程序中,在所有的菜单按钮被激活之后,我们也许会停留在一个也许我们并不希望的文本标签上。然而,我们可以很容易的通过JMenuItem的public void setText(String label)方法来修改。如果我们这样做,记住我们需要知道哪些位于菜单项中从而将会标签修改为某些有意义的说明。

package swingstudy.ch16;

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

import javax.swing.Action;
import javax.swing.JEditorPane;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JPasswordField;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.text.JTextComponent;

public class ActionsMenuBar {

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

        Runnable runner = new Runnable() {
            public void run() {
                final JFrame frame = new JFrame("TextAction Ussage");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                final JScrollPane scrollPane = new JScrollPane();
                frame.add(scrollPane, BorderLayout.CENTER);

                final JMenuBar menuBar = new JMenuBar();
                frame.setJMenuBar(menuBar);

                ActionListener actionListener = new ActionListener() {
                    JTextComponent component;
                    public void actionPerformed(ActionEvent event) {
                        // Determine which component selected
                        String command = event.getActionCommand();
                        if(command.equals("JTextField")) {
                            component = new JTextField();
                        }
                        else if(command.equals("JFormattedTextField")) {
                            component = new JFormattedTextField();
                        }
                        else if(command.equals("JPasswordField")) {
                            component = new JPasswordField();
                        }
                        else if(command.equals("JTextArea")) {
                            component = new JTextArea();
                        }
                        else if(command.equals("JTextPane")) {
                            component =  new JTextPane();
                        }
                        else {
                            component = new JEditorPane();
                        }
                        scrollPane.setViewportView(component);
                        // Process action list
                        Action actions[] = component.getActions();
                        menuBar.removeAll();
                        menuBar.revalidate();
                        JMenu menu = null;
                        for(int i=0, n=actions.length; i<n; i++) {
                            if((i%10)==0) {
                                menu = new JMenu("From "+i);
                                menuBar.add(menu);
                            }
                            menu.add(actions[i]);
                        }
                        menuBar.revalidate();
                    }
                };

                String components[] = {
                        "JTextField", "JFormattedTextField", "JPasswordField",
                        "JTextArea", "JTextPane", "JEditorPane"
                };
                final Container componentContainer = RadioButtonUtils.createRadioButtonGrouping(components, "Pick to List Actions", actionListener);
                frame.add(componentContainer, BorderLayout.WEST);

                frame.setSize(400, 300);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

图16-2显示了JTextArea的一些可用操作。当我们选择不同的菜单选项时,JTextComponent就会受到相应的影响。

Swing_16_2.png

Swing_16_2.png

这一技术十分有用,因为他显示了我们可以发现一个文本组件所支持的操作,并且可以在没有确切知道实际行为是什么的情况下提供到这种行为的访问。这仅是我们可以使用TextAction对象的许多方法中的一种演示。

Finding Actions

尽管列出与使用与一个文本组件相关的Action对象是一个相当具有扩展性的过程,除非我们知道我们正在查找什么,否则这种技术并不是十分有用。幸运的是,DefaultEditorKit具有46个与所有的文本组件所共享的46个Action对象相匹配的类常量。这些类常量的名字或多或少的反映了他们的功能。JTextField添加了一个与JFormattedTextField和JPasswordField共享的Action的额外常量。不幸的是,与JTextPane可用的额外动作相关联的名字并不是任何文本组件的类常量,而仅是在StyledEditorKit内部使用,在那里我们可以看到定义的额外的Action实现。

注意,存在一个的Action仅是出于高度的目的。其Action名字为dump-model,并没有与其相关的类常量。当初始化时,方法会在内部导出文本组件的Document模型Element结构。

表16-1列出了可以帮助我们定位我们正在查找的预定义Action的47个常量。

Swing_table_16_1_1.png

Swing_table_16_1_1.png

Swing_table_16_1_2.png

Swing_table_16_1_2.png

有了这些常量列表,实际上我们要如何使用他们呢?首先我们要查找我们希望使用的预定义的TextAction的常量(如果没有常量则要了解必须的文本字符串)。这相对来说较为简单因为名字是自解释的。

为了演示,列表16-3包含了一个程序,显示了如何使用这些常量。这个程序有两个文本区域来显示TextAction对象确实知道使用具有输入焦点的文本组件。菜单项的一个集合包含两个用于将文本区域由只读切换到可写的选项。这个动作是通过使用DefaultEditorKit.readOnlyAction与DefaultEditorKit.writableAction名字来实现的。另一个菜单选项集合包含用于剪切,粘贴与复制支持的选项,其相应的常量分别为DefaultEditorKit.cutAction,DefaultEditorKit.copyAction与DefaultEditorKit.pasteAction。因为这些常量是String值,我们需要查找要使用的实际的Action对象。

查找的过程需要使用getActionMap()方法获得组件的ActionMap,然后使用ActionMap的get()方法查找键值,如下面的示例所示:

Action readAction = component.getActionMap().get(DefaultEditorKit.readOnlyAction);
package swingstudy.ch16;

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

import javax.swing.Action;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.text.DefaultEditorKit;

public class UseActions {

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

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Use TextAction");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                Dimension empty = new Dimension(0,0);

                final JTextArea leftArea = new JTextArea();
                JScrollPane leftScrollPane = new JScrollPane(leftArea);
                leftScrollPane.setPreferredSize(empty);

                final JTextArea rightArea = new JTextArea();
                JScrollPane rightScrollPane = new JScrollPane(rightArea);
                rightScrollPane.setPreferredSize(empty);

                JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftScrollPane, rightScrollPane);

                JMenuBar menuBar = new JMenuBar();
                frame.setJMenuBar(menuBar);
                JMenu menu = new JMenu("Options");
                menuBar.add(menu);
                JMenuItem menuItem;

                Action readAction = leftArea.getActionMap().get(DefaultEditorKit.readOnlyAction);
                menuItem = menu.add(readAction);
                menuItem.setText("Make read-only");
                Action writeAction = leftArea.getActionMap().get(DefaultEditorKit.writableAction);
                menuItem = menu.add(writeAction);
                menuItem.setText("Make writable");

                menu.addSeparator();

                Action cutAction = leftArea.getActionMap().get(DefaultEditorKit.cutAction);
                menuItem = menu.add(cutAction);
                menuItem.setText("Cut");
                Action copyAction = leftArea.getActionMap().get(DefaultEditorKit.copyAction);
                menuItem = menu.add(copyAction);
                menuItem.setText("Copy");
                Action pasteAction = leftArea.getActionMap().get(DefaultEditorKit.pasteAction);
                menuItem = menu.add(pasteAction);
                menuItem.setText("Paste");

                frame.add(splitPane, BorderLayout.CENTER);
                frame.setSize(400, 250);
                frame.setVisible(true);
                splitPane.setDividerLocation(.5);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

图16-3显示了程序运行时的样子。注意,每一个JMenuItem被创建后,文本标签被修改为一个更为用户友好的设置。

Swing_16_3.png

Swing_16_3.png

通过查找特定的TextAction实例,我们并不需要记录重复操作。事实上,如果我们发现我们在一个文本组件上一次次重复相同的操作,那么也许我们就需要考虑创建自己的TextAction对象了。

Creating Styled Text

在第15章中,我们已经了解了显示了普通文本与HTML。通过Swing文本组件-或者至少是JTextPane-我们也可以显示格式化文本,其中不同的文本块可以具有多种属性。这些属性也许包含粗体,斜体,不同的字体或是字符级别的颜色,或者是段落级别的对齐,就如现现代的字处理软件一样。

要支持这些功能,Swing提供了许多不同的接口与类,所有这些接口与类以特殊的StyledDocument的Document接口扩展开始。我们在第15章中介绍了Document接口,专注于PlainDocument实现类。StyledDocument接口,或者更确切的说是DefaultStyledDocument实现,管理Document内容的一系列格式与属性集合。

StyleDocument所用的各种格式初始时是由AttributeSet接口来描述的,他是一个只读属性的键/值对集合。一个属性的键也许是“当前的字体”,在这种情况下设置将是所用的字体。要实际修改字体,我们需要求助于MutableAttributeSet接口,他提供了添加也移除属性的功能。例如,如果我们有一个用于“粗体”的AttributeSet,我们可以使用MutableAttributeSet来添加斜体,下划线或是颜色设置。

对于AttributeSet的简单实现,有一个StyleContext.SmallAttributeSet类,他使用一个数组来管理属性集合。对于MutableAttributeSet接口的实现,有一个SimpleAttributeSet类,他使用Hashtable来管理属性。更为复杂的属性设置需要Style接口,他向属性集合添加由MutableAttributeSet定义的名字。实际的Style实现类是StyleContext.NamedStyle类。除了添加一个名字,Style接口添加功能来使得ChangeListener监视属性集合的变化。

为StyledDocument管理Style对象集合的类是StyleContext类。他是AbstractDocument.AttributeContext接口的一个实现,StyleContext使用StyleConstants类为常用的格式定义各种属性。当使用HTML文档时,StyleContext实际上是一个StyleSheet,这他也许会有助于我们理解整个布局。记住在这里所讨论的所有类与接口(除了StyleSheet)仅需要用来为一个特定的JTextPane设置Document数据模型。

StyledDocument接口与DefaultStyledDocument类

StyledDocument接口通过添加了存储文档内容格式的功能来扩展Document接口。这些格式可以描述字符或是段落属性,例如颜色,方向或是字体。

public interface StyledDocument extends Document {
  public Style addStyle(String nm, Style parent);
  public Color getBackground(AttributeSet attribute);
  public Element getCharacterElement(int position);
  public Font getFont(AttributeSet attribute);
  public Color getForeground(AttributeSet attribute);
  public Style getLogicalStyle(int position);
  public Element getParagraphElement(int position);
  public Style getStyle(String name);
  public void removeStyle(String name);
  public void setCharacterAttributes(int offset, int length, AttributeSet s,
    boolean replace);
  public void setLogicalStyle(int position, Style style);
  public void setParagraphAttributes(int offset, int length, AttributeSet s,
    boolean replace);
}

DefaultStyledDocument类是Swing组件所提供的StyledDocument接口的实现。他作为JTextPane组件的数据模型。

创建DefaultStyledDocument

我们可以使用下面所列的三种方法来创建DefaultStyledDocument:

public DefaultStyledDocument()
DefaultStyledDocument document = new DefaultStyledDocument();
public DefaultStyledDocument(StyleContext styles)
StyleContext context = new StyleContext();
DefaultStyledDocument document = new DefaultStyledDocument(context);
public DefaultStyledDocument(AbstractDocument.Content content, StyleContext styles)
AbstractDocument.Content content = new StringContent();
DefaultStyledDocument document = new DefaultStyledDocument(content, context);

我们可以在多个文档之间共享StyleContext或是使用默认的上下文环境。另外,我们可以使用AbstractDocument.Content的一个实现,GapContent或是StringContent预定义内容。存储实际的Document内容是这些Content实现的职责。

DefaultStyledDocument属性

除了具有默认的根元素来描述文档的内容以外,DefaultStyledDocument将可用的格式名字作为Enumeration。这是在DefaultStyledDocument级别所定义的两个属性,如表16-2所示。当然我们也可以获得DefaultStyledDocument的其他属性;然而,他们需要获取时的位置或是AttributeSet。

Swing_table_16_2.png

Swing_table_16_2.png

AttributeSet接口

AttributeSet接口描述了一个只读的键/值属性集合,允许我们访问一系列属性的描述性内容。如果属性集合缺少特定的键定义,AttributeSet通过在一个链中遍历解析属性的父属性定义。这使得AttributeSet定义一个核心属性集合,并且允许开发者(或者可能是用户)仅修改他们所希望修改的属性集合。除非我们希望有人修改默认全局设置,我们不应提供到解析父属性的直接访问。这样,我们就不会丢失任何的原始设置。

public interface AttributeSet {
  // Constants
  public final static Object NameAttribute;
  public final static Object ResolveAttribute;
  // Properties
  public int getAttributeCount();
  public Enumeration getAttributeNames();
  public AttributeSet getResolveParent();
  // Other methods
  public boolean containsAttribute(Object name, Object value);
  public boolean containsAttributes(AttributeSet attributes);
  public AttributeSet copyAttributes();
  public Object getAttribute(Object key);
  public boolean isDefined(Object attrName);
  public boolean isEqual(AttributeSet attr);
}

MutableAttributeSet接口

MutableAttributeSet接口描述了我们如何由属性集合中添加或是移除属性,以及如何设置解析父属性。

public interface MutableAttributeSet extends AttributeSet {
  public void addAttribute(Object name, Object value);
  public void addAttributes(AttributeSet attributes);
  public void removeAttribute(Object name);
  public void removeAttributes(AttributeSet attributes);
  public void removeAttributes(Enumeration names);
  public void setResolveParent(AttributeSet parent);
}

SimpleAttributeSet类

SimpleAttributeSet类是AttributeSet接口的第一个实现。当我们开始使用这个类时,我们最终将会看到如何创建显示在JTextPane中的多属性文本。SimpleAttributeSet类是依赖用于管理键/属性对的标准Hashtable的AttributeSet的一个特定实现。

创建SimpleAttributeSet

SimpleAttributeSet有两个构造函数:

public SimpleAttributeSet()
SimpleAttributeSet attributeSet1 = new SimpleAttributeSet();
public SimpleAttributeSet(AttributeSet source)
SimpleAttributeSet attributeSet2 = new SimpleAttributeSet(attributeSet1);

我们通常会创建一个空的SimpleAttributeSet,然后设置其属性。或者我们可以在构造函数中提供一个初始的设置集合。注意,这并不是解析父属性-他仅是一个初始化的数据结构。

SimpleAttributeSet属性

表16-3显示了SimpleAttributeSet的四个属性。他们提供了到属性集合的访问,使得我们可以了解属性是否存在,并且标记解析父属性。

Swing_table_16_3.png

Swing_table_16_3.png

使用SimpleAttributeSet

要创建SimpleAttributeSet所用的AttributeSet,我们需要查找我们要修改的属性的键。我们会在稍后讨论StyleConstants时了解一些助手方法。所有的键隐藏在StyleConstants的四个公开内联类中:CharacterConstants,ColorConstants,FontConstants与ParagraphConstants,如表16-4所示。

Swing_table_16_4_1.png

Swing_table_16_4_1.png

Swing_table_16_4_2.png

Swing_table_16_4_2.png

Swing_table_16_4_3.png

Swing_table_16_4_3.png

例如,要在创建DefaultStyledDocument之后填充JTextPane的StyledDocument,我们通过调用public void insertString(int offset, String contents, AttributeSet attributes)方法来向其中添加内容,该方法会抛出BadLocationException。然后我们可以修改属性集合并且添加更多的属性。所以,如果我们希望创建同时为粗体与斜体的内容,我们需要向SimpleAttributeSet中添加两个属性并将内容添加到文档中:

SimpleAttributeSet attributes = new SimpleAttributeSet();
attributes.addAttribute(StyleConstants.CharacterConstants.Bold, Boolean.TRUE);
attributes.addAttribute(StyleConstants.CharacterConstants.Italic, Boolean.TRUE);
// Insert content
try {
  document.insertString(document.getLength(), "Hello, Java", attributes);
}  catch (BadLocationException badLocationException) {
  System.err.println("Oops");
}

图16-4显示程序运行的结果。

Swing_16_4.png

Swing_16_4.png

列表16-4显示了完整的示例源码。

package swingstudy.ch16;

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

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;

public class SimpleAttributeSample {

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

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

                StyledDocument document = new DefaultStyledDocument();

                SimpleAttributeSet attributes = new SimpleAttributeSet();
                attributes.addAttribute(StyleConstants.CharacterConstants.Bold, Boolean.TRUE);
                attributes.addAttribute(StyleConstants.CharacterConstants.Italic, Boolean.TRUE);

                // Insert content
                try {
                    document.insertString(document.getLength(), "Hello, Java", attributes);
                }
                catch(BadLocationException badLocationException) {
                    System.err.println("Bad Insert");
                }

                attributes = new SimpleAttributeSet();
                attributes.addAttribute(StyleConstants.CharacterConstants.Bold, Boolean.FALSE);
                attributes.addAttribute(StyleConstants.CharacterConstants.Italic, Boolean.FALSE);
                attributes.addAttribute(StyleConstants.CharacterConstants.Foreground, Color.LIGHT_GRAY);

                // Insert content
                try {
                    document.insertString(document.getLength(), " - Good-bye Visual Basic", attributes);
                }
                catch(BadLocationException badLocationException) {
                    System.err.println("Bad Insert");
                }

                JTextPane textPane = new JTextPane(document);
                textPane.setEditable(false);
                JScrollPane scrollPane = new JScrollPane(textPane);
                frame.add(scrollPane, BorderLayout.CENTER);

                frame.setSize(300, 150);
                frame.setVisible(true);

            }
        };
        EventQueue.invokeLater(runner);
    }

}

概括来说,要指定文档的格式,只需要简单的设置属性集合,插入内容,然后为我们要添加到的每一个内容重复以上步骤。

StyleConstants类

StyleConstants类满是简化设置属性集合的助手方法。而且我们并不需要深入StyleConstants内联类的常量,因为通守StyleConstants级别的类常量就可以进行访问。

public static final Object Alignment;
public static final Object Background;
public static final Object BidiLevel;
public static final Object Bold;
public static final Object ComponentAttribute;
public static final String ComponentElementName;
public static final Object ComposedTextAttribute;
public static final Object Family;
public static final Object FirstLineIndent;
public static final Object FontFamily;
public static final Object FontSize;
public static final Object Foreground;
public static final Object IconAttribute;
public static final String IconElementName;
public static final Object Italic;
public static final Object LeftIndent;
public static final Object LineSpacing;
public static final Object ModelAttribute;
public static final Object NameAttribute;
public static final Object Orientation;
public static final Object ResolveAttribute;
public static final Object RightIndent;
public static final Object Size;
public static final Object SpaceAbove;
public static final Object SpaceBelow;
public static final Object StrikeThrough;
public static final Object Subscript;
public static final Object Superscript;
public static final Object TabSet;
public static final Object Underline;

一些静态方法可以使得我们使用更符合逻辑的方法来修改MutableAttributeSet,而不需要我们了解更为隐蔽的AttributeSet名字。使用StyleConstants变量的ALIGN_CENTER,ALIGN_JSTIFIED,ALIGN_LEFT与ALIGN_RIGH可以作为int参数用于setAlignment()方法。其余的设置都是自解释的。

public static void setAlignment(MutableAttributeSet a, int align);
public static void setBackground(MutableAttributeSet a, Color fg);
public static void setBidiLevel(MutableAttributeSet a, int o);
public static void setBold(MutableAttributeSet a, boolean b);
public static void setComponent(MutableAttributeSet a, Component c);
public static void setFirstLineIndent(MutableAttributeSet a, float i);
public static void setFontFamily(MutableAttributeSet a, String fam);
public static void setFontSize(MutableAttributeSet a, int s);
public static void setForeground(MutableAttributeSet a, Color fg);
public static void setIcon(MutableAttributeSet a, Icon c);
public static void setItalic(MutableAttributeSet a, boolean b);
public static void setLeftIndent(MutableAttributeSet a, float i);
public static void setLineSpacing(MutableAttributeSet a, float i);
public static void setRightIndent(MutableAttributeSet a, float i);
public static void setSpaceAbove(MutableAttributeSet a, float i);
public static void setSpaceBelow(MutableAttributeSet a, float i);
public static void setStrikeThrough(MutableAttributeSet a, boolean b);
public static void setSubscript(MutableAttributeSet a, boolean b);
public static void setSuperscript(MutableAttributeSet a, boolean b);
public static void setTabSet(MutableAttributeSet a, TabSet tabs);
public static void setUnderline(MutableAttributeSet a, boolean b);

例如,我们并不需要使用下面的代码来使得SimpleAttributeSet成为粗体与斜体:

attributes.addAttribute(StyleConstants.CharacterConstants.Bold, Boolean.TRUE)
attributes.addAttribute(StyleConstants.CharacterConstants.Italic, Boolean.TRUE)

相反,我们可以使用下面的方法来替代:

StyleConstants.setBold(attributes, true);
StyleConstants.setItalic(attributes, true);

正如我们所看到的,后一种方法更易于阅读且更易于维护。

提示,除了修改AttributeSet对象的方法,StyleConstants类还提供了许多其他的方法可以使得我们检测AttributeSet的状态来确定某一个当前设置是否被允许或是禁止。

TabStop and TabSet类

用于存储AttributeSet值的一个关键常量是ParagraphConstants.TabSet属性。TabSet类表示一个TabStop对象的集合,其中的每一个定义了tab位置,对象方式以及导引。如果我们希望为JTextPane定义我们自己的tab定位,我们需要创建一个TabStop对象集合,为每一个tab定位创建一个TabSet,然后将TabSet关联到MutableAttributeSet。

创建TabStop

TabStop类并不是通常意义上的JavaBean组件;他并没有一个无参数的构造函数。相反,我们必须以像素指定tab定位的位置。他有两个构造函数:

public TabStop(float position)
TabStop stop = new TabStop(40);
public TabStop(float position, int align, int leader)
TabStop stop = new TabStop(40, TabStop.ALIGN_DECIMAL, TabStop.LEAD_DOTS);

注意,尽管由技术上来说可以指定,但是TabStop构造函数的leader参数当前被预定义的文本组件所忽略。

TabStop属性

表16-5显示了TabStop的三个属性,每一个都可以通过构造函数初始化。

Swing_table_16_5.png

Swing_table_16_5.png

四个对齐设置是通过表16-6中的四个常量来指定的。图16-5显示了不同的设置。

Swing_table_16_6.png

Swing_table_16_6.png

Swing_16_5.png

Swing_16_5.png

注意,尽管ALIGN_BAR与ALIGN_LEFT由技术上来说是不同的常量,但是当前他们的对齐设置会产生相同的结果。他们是为RTF规范所定义的。

使用TabStop对象

一旦我们拥有了一个TabStop对象或是一个TabStop对象集合,我们将对象以TabStop对象数组的形式传递给TabSet构造函数,如下所示:

TabSet tabset = new TabSet(new TabStop[] {tabstop})

作为一个示例,列表16-5显示了产生图16-5的TabStop对象程序的源码。

/**
 *
 */
package swingstudy.ch16;

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

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import javax.swing.text.TabSet;
import javax.swing.text.TabStop;

/**
 * @author mylxiaoyi
 *
 */
public class TabSample {

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

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

                StyledDocument document = new DefaultStyledDocument();

                int positions[] = {TabStop.ALIGN_BAR, TabStop.ALIGN_CENTER, TabStop.ALIGN_DECIMAL,
                        TabStop.ALIGN_LEFT, TabStop.ALIGN_RIGHT };
                String strings[] = {"\tBAR\n", "\tCENTER\n", "\t3.14159265\n",
                        "\tLEFT\n", "\tRIGHT\n" };

                SimpleAttributeSet attributes =  new SimpleAttributeSet();

                for(int i=0, n=positions.length; i<n; i++) {
                    TabStop tabstop = new TabStop(150, positions[i], TabStop.LEAD_DOTS);
                    try {
                        int position = document.getLength();
                        document.insertString(position, strings[i], null);
                        TabSet tabset = new TabSet(new TabStop[] {tabstop});
                        StyleConstants.setTabSet(attributes, tabset);
                        document.setParagraphAttributes(position, 1, attributes, false);
                    }
                    catch(BadLocationException badLocationExeption) {
                        System.err.println("Bad Location");
                    }
                }
                JTextPane textPane = new JTextPane(document);
                textPane.setEditable(false);
                JScrollPane scrollPane = new JScrollPane(textPane);
                frame.add(scrollPane, BorderLayout.CENTER);

                frame.setSize(300, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

除了指定位置与对齐方式,我们还可以指定我们希望使用哪种字符作为导引字符显示在通过tab字符所创建的空白空间中。默认情况下并不存在任何内容;所以是常量LEAD_NONE。其他的TabStop值可以创建句点线,等号,连字符,细线或是下划线:LEAD_DOTS,LEAD_EQUALS,LEAD_HYPHENS,LEAD_THICKLINE或是LEAD_UNDERLINE。不幸的是,这个选项可用但是并不被支持。虽然非标准的Swing组件也许会支持这种功能,但是标准的Swing组件当前并不支持不同的导引字符串。

Style接口

Style接口是一种指定AttributeSet的加强方法。他为MutableAttributeSet添加了一个名字以及为了监视属性设置的变化将ChangeListener关联到Style的能力。例如,我们可以以下面的方式来配置粗体斜体格式:

String BOLD_ITALIC = "BoldItalic";
Style style = (Style)document.getStyle(StyleContext.DEFAULT_STYLE);
StyleConstants.setBold(style, true);
StyleConstants.setItalic(style, true);
document.addStyle(BOLD_ITALIC, null);

稍后,我们可以将这个种格式关联到文本:

style = document.getStyle(BOLD_ITALIC);
document.insertString(document.getLength(), "Hello, Java", style);

StyleContext类

StyleContext类管理格式化文档的格式。借助于StyleContext.NamedStyle类,我们可以使用JTextPane仅做他自己的事情,因为StyleContext知道某些事情何时完成。对于HTMl文档,StyleContext就变为更为特殊的StyleSheet。

The Editor Kits

我们已经概略的看到了本章前面所介绍的TextAction对象的一些默认的EditorKit功能。EditorKit类扮演将文本组件的所有不同方面组合在一起的粘合剂。他创建文档,管理动作,并且创建文档或是视图的可视化表示。另外,EditorKit知道如何读取或是写入流。每一个文档类型需要其自己的EditorKit,所以JFC/Project Swing组件为HTML与RTF文本以及普通文本与格式化文本提供了不同的EditorKit。

Document内容的实际显示是通过EditorKit借助于ViewFactory来实现的。对于Document的每一个Element,ViewFatory确定哪一个View是为这个元素创建并且通过文本组件委托进行渲染。对于不同的元素类型,有不同的View子类。

载入HTML文档

在第15章中,我们看到JTextComponent的read()与write()方法如何允许我们读取或是写入一个文本组件的内容。尽管第15章中的列表15-3中的LoadSave示例显示了JTextField的这一过程,但是正如我们所希望的,他同样适用于所有的文本组件。确保载入与保存为正确的文档类型完成的唯一需求是为文档修改编辑器工具集。

为了演示,在这里我们显示了我们如何将一个HTML文件作为StyledDocument载入到JEditorPane中:

JEditorPane editorPane = new JEditorPane();
editorPane.setEditorKit(new HTMLEditorKit());
reader = new FileReader(filename);
editorPane.read(reader, filename);

这非常简单。组件的内容类型被设置为text/html,并且由filename载入作为HTML内容显示。值得注意的一件事就是载入是异步完成的。

如果我们需要同步载入内容,从而我们可以等待所有的内容载入完成,例如用于分析目的,则这个过程有一些麻烦。我们需要使用HTML分析器(HTMLEditorKit.Parset类位于javax.swing.text.html包中),解析器委托器(ParsetDeleegator位于javax.swing.text.html.parset包中),以及我们可以由HTMLDocument(作为HTMLDocument.HTMLReader)中获取的解析器回调(HTMLEditorKit.ParsetCallback)。听起来要比实际复杂。为了演示,下面的代码异步载入一个文件到JEditorPane中。

reader = new FileReader(filename);
// First create empty empty HTMLDocument to read into
HTMLEditorKit htmlKit = new HTMLEditorKit();
HTMLDocument htmlDoc = (HTMLDocument)htmlKit.createDefaultDocument();
// Next create the parser
HTMLEditorKit.Parser parser = new ParserDelegator();
// Then get HTMLReader (parser callback) from document
HTMLEditorKit.ParserCallback callback = htmlDoc.getReader(0);
// Finally load the reader into it
// The final true argument says to ignore the character set
parser.parse(reader, callback, true);
// Examine contents

在HTML文档中遍历

在我们载入HTML文档之后,除了在JEditorPane中显示内容以外,也许我们会发现我们需要我们自己解析文档内容。HTMLDocument通过HTMLDocument.Iterator与ElementIterator类支持两种遍历方式。

HTMLDocument.Iterator类

要使用HTMLDocument.Iterator类,我们请求HTMLDocument为特定的HTML.Tag提供迭代器。然后,对于文档中标记的每个实例, 我们可以查看标签的属性。

HTML.Tag类包含用于所有标准HTML标签(HTMLEditorKit可以理解的)的76个类常量,例如用于H1标签的HTML.Tag.H1。表16-7列出了这些常量。

Swing_table_16_7_1.png

Swing_table_16_7_1.png

Swing_table_16_7_2.png

Swing_table_16_7_2.png

在我们有了可以使用的特定迭代器之后,我们就可以借助于表16-8中所显示的类属性查看每一个标签实例的特定属性与内容。

Swing_table_16_8.png

Swing_table_16_8.png

迭代过程的另一个方面就是next()方法,这个方法可以使得我们获得文档中的下一个标签实例。使用这个迭代器的基本结构如下:

// Get the iterator
HTMLDocument.Iterator iterator = htmlDoc.getIterator(HTML.Tag.A);
// For each valid one
while (iterator.isValid()) {
// Process element
// Get the next one
 iterator.next();
}

这也可以表示为一个基本的for循环结构:

for (HTMLDocument.Iterator iterator = htmlDoc.getIterator(HTML.Tag.A);
       iterator.isValid();
       iterator.next()) {
  // Process element
}

列表16-6演示了HTMLDocument.Iterator的用法。这个程序会在命令行提示我们输入URL,异步载入文件,查找所有的标签,然后显示所有的以HREF属性列出的锚点。可以将这个程序看作一个简单的的爬虫程序,因而我们可以构建一个文档之间URL的数据库。起始与结束偏移也可以用来获取链接文本。输入我们需要浏览的URL来启动这个程序。

/**
 *
 */
package swingstudy.ch16;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;

import javax.swing.text.AttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.parser.ParserDelegator;

/**
 * @author mylxiaoyi
 *
 */
public class DocumentIteratorExample {

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

        if(args.length != 1) {
            System.err.println("Usage: java DocumentIteratorExample input-URL");
        }

        // Load HTML file synchronously
        URL url = new URL(args[0]);
        URLConnection connection = url.openConnection();
        InputStream is = connection.getInputStream();
        InputStreamReader isr = new InputStreamReader(is);
        BufferedReader br =  new BufferedReader(isr);

        HTMLEditorKit htmlKit = new HTMLEditorKit();
        HTMLDocument htmlDoc = (HTMLDocument)htmlKit.createDefaultDocument();
        HTMLEditorKit.Parser parser = new ParserDelegator();
        HTMLEditorKit.ParserCallback callback = htmlDoc.getReader(0);
        parser.parse(br, callback, true);

        // Parser
        for(HTMLDocument.Iterator iterator = htmlDoc.getIterator(HTML.Tag.A); iterator.isValid(); iterator.next()) {
            AttributeSet attributes = iterator.getAttributes();
            String srcString = (String)attributes.getAttribute(HTML.Attribute.HREF);
            System.out.print(srcString);
            int startOffset = iterator.getStartOffset();
            int endOffset = iterator.getEndOffset();
            int length = endOffset - startOffset;
            String text = htmlDoc.getText(startOffset, endOffset);
            System.out.println(" - "+text);
        }
        System.exit(0);
    }

}

ElementIterator类

检测HTMLDocument内容的另一种方法就是使用ElementIterator(并不是特定于HTML文档)。当使用ElementIteartor时,我们会看到文档的所有的Element对象并且询问每一个是什么。如果对象是我们感兴趣的,我们就可以近距离查看。

要获得文档的迭代器,可以使用如下的代码:

ElementIterator iterator = new ElementIterator(htmlDoc);

ElementIterator并不意味着一个简单的顺序迭代器。他是具有next()与previous()方法的双向迭代器,并且支持使用first()方法回到起始处。尽管next()与previous()方法返回要处理的下一个或前一个元素,我们还可以通过current()方法获取当前位置的元素。下面的代码显示在一个文档中遍历的基本循环方法:

Element element;
ElementIterator iterator = new ElementIterator(htmlDoc);
while ((element = iterator.next()) != null) {
 // Process element
}

我们如何确定我们获得了哪一个元素,并且如果不是我们感兴趣的我们希望忽略?我们需要由其属性集合中获取其名字与类型。

AttributeSet attributes = element.getAttributes();
Object name = attributes.getAttribute(StyleConstants.NameAttribute);
if (name instanceof HTML.Tag) {

现在我们可以查找特定的标签类型,例如HTML.Tag.H1,HTML.Tag.H2等。标签的实际内容将会位于元素的子元素中。为了进行演示,下面的代码显示了如何在文档中查找H1,H2与H3标签,同时显示了与文档相关联的合适标题。

if ((name instanceof HTML.Tag) && ((name == HTML.Tag.H1) ||
    (name == HTML.Tag.H2) || (name == HTML.Tag.H3))) {
  // Build up content text as it may be within multiple elements
  StringBuffer text = new StringBuffer();
  int count = element.getElementCount();
  for (int i=0; i<count; i++) {
    Element child = element.getElement(i);
    AttributeSet childAttributes = child.getAttributes();
    if (childAttributes.getAttribute(StyleConstants.NameAttribute) ==
        HTML.Tag.CONTENT) {
      int startOffset = child.getStartOffset();
      int endOffset = child.getEndOffset();
      int length = endOffset - startOffset;
      text.append(htmlDoc.getText(startOffset, length));
    }
  }
}

要实际尝试,我们需要实际查找一个使用H1,H2或是H3标签的页面。

JFormattedTextField格式

在第15章中,我们简单尝试了JFormattedTextField组件。现在,我们将会探讨该组件的其他方面。JFormattedTextField用来接收用户的格式化输入。这听起来简单,但是实际是这非常重要且复杂。如果没有JFormattedTextField,获得格式化输入就不会像听起来这样简单。如果考虑本地化需求则会使得事情更为有趣。

JFormattedTextField组件不仅支持格式化输入,但是还会允许用户使用键盘来增加或是减少输入值;例如,在日期月份中滚动。

对于JFormattedTextField,验证是通过focusLostBehavior属性来控制的。这可以设置为如下的四个值:

  • COMMIT_OR_REVERT:这是默认值。当组件失去焦点时,组件会自动调用内部的commitEdit()方法。这会分析组件的内容并且在发生错误的情况下抛出ParseException,并将内容恢复为最近的正确值。
  • COMMIT:该设置类似于COMMIT_OR_REVERT,但是他会将不正确的值保留在文本域中,并允许用户进行修正。
  • REVERT:该设置总是会恢复值。
  • PERSIST:该设置实际是并不会做任何事情。当focusLostBehavior属性被设置为PERSIST时,我们应手动调用commitEdit()方法来在使用内容之前检测内容是否合法。

日期与数字

作为开始,我们先来看一下如何使用JFormattedTextField接收应进行本地化的输入。这包括所有的日期,时间与数字格式,基本是由DateFormat或是NumberFormat对象所获得的所有内容。

如果我们为JFormattedTextField构造函数提供了一个Date对象或是Number子类,组件会将输入String传递该对象类型的构造函数进行输入验证。相反,我们应通过向构造函数传递DateFormat或是NumberFormat来使用位于java.swing.text包中的InternationalFormatter类。这允许我们指定长的或是短的日期与时间,以及货币,百分比,数字的小数或是整数格式。

日期与时间格式

为了演示日期与时间格式,列表6-7的示例接收各种日期与时间输入。由上至下,输入分别为默认locale的短日期格式,对于美国英语的完全日期格式,意大利的中等日期格式,法国的星期以及默认locale的短时间格式。

/**
 *
 */
package swingstudy.ch16;

import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.text.DateFormat;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

import javax.swing.BoxLayout;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

/**
 * @author mylxiaoyi
 *
 */
public class DateInputSample {

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

        Runnable runner =  new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Date/Time Input");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                JLabel label;
                JFormattedTextField input;
                JPanel panel;

                BoxLayout layout = new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS);
                frame.setLayout(layout);

                Format shortDate = DateFormat.getDateInstance(DateFormat.SHORT);
                label = new JLabel("Short date:");
                input = new JFormattedTextField(shortDate);
                input.setValue(new Date());
                input.setColumns(20);
                panel =  new JPanel(new FlowLayout(FlowLayout.RIGHT));
                panel.add(label);
                panel.add(input);
                frame.add(panel);

                Format fullUSDate = DateFormat.getDateInstance(DateFormat.FULL, Locale.US);
                label = new JLabel("Full US date:");
                input =  new JFormattedTextField(fullUSDate);
                input.setValue(new Date());
                input.setColumns(20);
                panel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
                panel.add(label);
                panel.add(input);
                frame.add(panel);

                Format mediumItalian = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.ITALIAN);
                label = new JLabel("Medium Italian date:");
                input = new JFormattedTextField(mediumItalian);
                input.setValue(new Date());
                input.setColumns(20);
                panel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
                panel.add(label);
                panel.add(input);
                frame.add(panel);

                Format dayOfWeek = new SimpleDateFormat("E", Locale.FRENCH);
                label = new JLabel("French day of week:");
                input = new JFormattedTextField(dayOfWeek);
                input.setValue(new Date());
                input.setColumns(20);
                panel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
                panel.add(label);
                panel.add(input);
                frame.add(panel);

                Format shortTime = DateFormat.getTimeInstance(DateFormat.SHORT);
                label = new JLabel("Short time:");
                input = new JFormattedTextField(shortTime);
                input.setValue(new Date());
                input.setColumns(20);
                panel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
                panel.add(label);
                panel.add(input);
                frame.add(panel);

                frame.pack();
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

图16-6显示了程序运行结果。要使用不同的locale启动程序,我们可以在命令行使用类似下面的命令来设置user.language与user.country设置。

java -Duser.language=fr -Duser.country=FR DateInputSample

然而,这只会修改没有指定locale集合的输入格式。

Swing_16_6.png

Swing_16_6.png

数字格式

数字的使用类似于日期,所不同的是使用java.text.NumberFormat类,而不是DateFormat类。可以实现的本地化是通过getCurrencyInstance(),getInstance(),IntegerInstance(),getNumberInstance()与getPercentInstance()方法来实现的。

NumberFormat类会处理必要的逗号,句点,百分号等占位符。当输入数字时,并不需要输入额外的字符,例如用于千的逗号。组件会在输入之后在合适的位置进行添加,如图16-7的示例所示。注意,十进制的小数点以及逗号的位置会以及他们的格式会因locale的不同而不同。

Swing_16_7.png

Swing_16_7.png

列表16-8显示了生成图16-7的程序源码。所有的输入域都以值2425.50开始。在整数的情况下,输入值会进行近似处理。当设置JFormattedTextField的内容时,使用setValue()方法,而不是setText()方法。这会保证文本内容进行验证。

/**
 *
 */
package swingstudy.ch16;

import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.Font;
import java.text.Format;
import java.text.NumberFormat;
import java.util.Locale;

import javax.swing.BoxLayout;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

/**
 * @author mylxiaoyi
 *
 */
public class NumberInputSample {

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

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Number Input");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                Font font = new Font("SansSerif", Font.BOLD, 16);

                JLabel label;
                JFormattedTextField input;
                JPanel panel;

                BoxLayout layout = new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS);
                frame.setLayout(layout);

                Format currency = NumberFormat.getCurrencyInstance(Locale.UK);
                label =  new JLabel("UK Currency");
                input = new JFormattedTextField(currency);
                input.setValue(2424.50);
                input.setColumns(20);
                input.setFont(font);
                panel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
                panel.add(label);
                panel.add(input);
                frame.add(panel);

                Format general = NumberFormat.getInstance();
                label =  new JLabel("General/Instance");
                input = new JFormattedTextField(general);
                input.setValue(2424.50);
                input.setColumns(20);
                input.setFont(font);
                panel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
                panel.add(label);
                panel.add(input);
                frame.add(panel);

                Format integer = NumberFormat.getIntegerInstance(Locale.ITALIAN);
                label = new JLabel("Italian integer:");
                input = new JFormattedTextField(integer);
                input.setValue(2424.50);
                input.setColumns(20);
                input.setFont(font);
                panel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
                panel.add(label);
                panel.add(input);
                frame.add(panel);

                Format number = NumberFormat.getNumberInstance(Locale.FRENCH);
                label = new JLabel("French Number:");
                input = new JFormattedTextField(number);
                input.setValue(2424.50);
                input.setColumns(20);
                input.setFont(font);
                panel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
                panel.add(label);
                panel.add(input);
                frame.add(panel);

                label = new JLabel("Raw Number:");
                input = new JFormattedTextField(2424.50);
                input.setColumns(20);
                input.setFont(font);
                panel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
                panel.add(label);
                panel.add(input);
                frame.add(panel);

                frame.pack();
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

图16-7中五个JFormattedTextField示例中的最后一个使用一个double初始化组件。值2424.50会被自动装箱为一个Double对象。向构造函数传递一个对象并没有错。然而,当我们在文本域内输入值的时候我们会发现一些不规则的地方。值似乎总是以一个十进制小数点开头,尽管已经接受更多的输入数字。我们不需要使用一个Format对象进行文本与Object对象的转换,在这里我们使用接收String的Double构造函数。

当我们将java.text.Format对象传递给JFormattedTextField构造函数时,这在内部会映射到DateFormatter或是NumberFormatter对象。这两个对象都是InternationalFormatter类的子类。名为JFormattedTextField.AbstractFormatterFactory的内联类在JFormattedTextField内管理格式化对象的使用。工厂会在用户输入JFormattedTextField的时候install()格式器并且在离开时uninstall()格式器,从而保证格式器每次只在一个文本域内被激活。install()与uninstall()方法是由所有格式器的JFormattedTextField.AbstractFormatter超类继承来的。

输入隐藏

除了数字与日期,JFormattedTextField支持遵循某种模式或是隐藏的用户输入。例如,如果一个输入域是一个美国社会保险号(SSN),则他有一个典型的数字,数字,数字,短划线,数字,数字,短划线,数字,数字,数字,数字的模型。借助于MaskFormatter类,我们可以使用表16-9中所列的字符来指定输入隐藏。

Swing_table_16_9.png

Swing_table_16_9.png

例如,下面的格式器创建了SSN输入掩码:

new MaskFormatter("###'-##'-####")

输入掩码中单引号后的字符会被作为字面量处理,在这种情况是一个短划线。我们可以将这个格式器传递给JFormattedTextField的构造函数或是使用setMask()方法配置文本域。

为了演示,列表16-9包含了两个JFormattedTextField组件:一个来接受SSN而另一个接收美国电话号码。

/**
 *
 */
package swingstudy.ch16;

import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.text.ParseException;

import javax.swing.BoxLayout;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.text.MaskFormatter;

/**
 * @author mylxiaoyi
 *
 */
public class MaskInputSample {

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

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

                JLabel label;
                JFormattedTextField input;
                JPanel panel;
                MaskFormatter formatter;

                BoxLayout layout = new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS);
                frame.setLayout(layout);

                try {
                    label = new JLabel("SSN");
                    formatter = new MaskFormatter("###'-##'-####");
                    input =  new JFormattedTextField(formatter);
                    input.setValue("123-45-6789");
                    input.setColumns(20);
                    panel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
                    panel.add(label);
                    panel.add(input);
                    frame.add(panel);
                }
                catch(ParseException e) {
                    System.err.println("Unable to add SSN");
                }

                try {
                    label = new JLabel("US Phone");
                    formatter = new MaskFormatter("'(###')' ###'-####");
                    input = new JFormattedTextField(formatter);
                    input.setColumns(20);
                    panel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
                    panel.add(label);
                    panel.add(input);
                    frame.add(panel);
                }
                catch(ParseException e) {
                    System.err.println("Unable to add Phone");
                }

                frame.pack();
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

图16-8显示了程序的输出。在这个例子中,SSN文本域以初始值开始,然而电话号码文本域却没有。

MaskFormatter提供了一些自定义选项。默认情况下,格式器处于覆写模式,所以当我们输入时,所输入的数字会替换文本域中的数字与空格。将overwriteModel属性设置为false可以禁止这一行为。通常情况下,这并没有必要,尽管对于输入长的日期会比较有帮助。

如果我们希望使用不同的字符作为占位符,在位置被填充到掩码之前,设置MaskFormatter的placeholderCharacter属性。为了演示,将下面的代码行添加到列表16-9中的电话号码格式器之前:

formatter.setPlaceholder(‘*’);

我们将会看到显示在图16-9中底部的文本域的结果。

Swing_16_9.png

Swing_16_9.png

另一个比较有用的MaskFormatter属性就是validCharacters,用于限制哪一个字母数字字符对于输入域是合法的。

DefaultFormatterFactory类

javax.swing.text包中的DefaultFormatterFactory类提供了一种方法来使得不同的格式器显示值,编辑以及null值的特殊情况。他提供了五个构造函数,由无参数的构造函数开始,然后为每一个构造函数添加了一个额外的AbstractFormatter参数。

public DefaultFormatterFactory()
DefaultFormatterFactory factory = new DefaultFormatterFactory()
public DefaultFormatterFactory(JFormattedTextField.AbstractFormatter defaultFormat)
DateFormat defaultFormat = new SimpleDateFormat("yyyy--MMMM--dd");
DateFormatter defaultFormatter = new DateFormatter(displayFormat);
DefaultFormatterFactory factory = new DefaultFormatterFactory(defaultFormatter);
public DefaultFormatterFactory(JFormattedTextField.AbstractFormatter defaultFormat,
  JFormattedTextField.AbstractFormatter displayFormat)
DateFormat displayFormat = new SimpleDateFormat("yyyy--MMMM--dd");
DateFormatter displayFormatter = new DateFormatter(displayFormat);
DefaultFormatterFactory factory = new DefaultFormatterFactory(displayFormatter,
  displayFormatter);
public DefaultFormatterFactory(JFormattedTextField.AbstractFormatter defaultFormat,
  JFormattedTextField.AbstractFormatter displayFormat,
  JFormattedTextField.AbstractFormatter editFormat)
DateFormat displayFormat = new SimpleDateFormat("yyyy--MMMM--dd");
DateFormatter displayFormatter = new DateFormatter(displayFormat);
DateFormat editFormat = new SimpleDateFormat("MM/dd/yy");
DateFormatter editFormatter = new DateFormatter(editFormat);
DefaultFormatterFactory factory = new DefaultFormatterFactory(
  displayFormatter, displayFormatter, editFormatter);
public DefaultFormatterFactory(JFormattedTextField.AbstractFormatter defaultFormat,
  JFormattedTextField.AbstractFormatter displayFormat,
  JFormattedTextField.AbstractFormatter editFormat,
  JFormattedTextField.AbstractFormatter nullFormat)
DateFormat displayFormat = new SimpleDateFormat("yyyy--MMMM--dd");
DateFormatter displayFormatter = new DateFormatter(displayFormat);
DateFormat editFormat = new SimpleDateFormat("MM/dd/yy");
DateFormatter editFormatter = new DateFormatter(editFormat);
DateFormat nullFormat = new SimpleDateFormat("'null'");
DateFormatter nullFormatter = new DateFormatter(nullFormat);
DefaultFormatterFactory factory = new DefaultFormatterFactory(
  displayFormatter, displayFormatter, editFormatter, nullFormatter);

DefaultFormatterFactory的使用并没有什么神奇之处。只需要创建一个实例然后传递给JFormattedTextField的构造函数。然后文本域的状态将会决定使用哪种格式器来显示当前值。通常情况下,显示格式器会为默认设置进行重复。如果格式器中的任何一个为null或是没有设置,则会使用默认的格式器。

小结

在本章中,我们了解了使用JFC/Project Swing文本组件的一些高级方面。我们了解了如何使用预定义的TextAction对象来创建工作用户界面,而不需要定义我们自己的事件处理功能。另外,我们了JTextPane以及如何通过AttributeSet,MutableAttributeSet,SimpleAttributeSet与StyleConstants在JTextPane内创建多属性文本。我们同时了解了如何在Document内创建tab stop以及Swing的EditorKit实用程序,特别探讨了HTMLEditorKit的细节。最后,我们了解了使用JFormattedTextField接收格式化输入。

在第17章中,我们将会探讨用于显示层次结构数据的Swing组件:JTree。

在第16章中,我们了解了如何使用Swing组件集合中的文本文档功能。在本章中,我们将会了解如何使用Swing树类,JTree组件。

树简介

JTree组件是用于显示层次数据元素的可视化组件,也称之为节点。使用树这个隐喻,可以想像一棵倒长的树。树顶部的节点称之为根。树的根节点的扩展是到其他节点的分支。如果节点没有任何由其展开的分支,这个节点就称之为叶节点。图17-1是一棵简单的树。

Swing_17_1.png

Swing_17_1.png

在JTree的组合中使用了许多相互连接的类。首先,JTree实现在Scrollable接口,从而我们可以将树放在一个JScrollPane中进行滚动管理。树中每个节点的显示是通过TreeCellRenderer接口的实现来控制的;默认情况下,实现为DefaultTreeCellRenderer类。树的节点使用TreeCellEditor的实现进行编辑。有两个编辑器实现:一个使用DefaultTreeCellEditor提供文本域,而另一个使用DefaultCellEditor提供复选框与组合框,后者是对AbstractCellEditor的扩展。如果这些类并没有提供我们所需要的内容,我们可以在EditorContainer中放置一个自定义编辑器。

注意,DefaultCellEditor类也可以用作JTable组件的单元编辑器。我们将会在第18章中讨论JTable组件。

默认情况下,JTree的实际节点是TreeNode接口或是其子接口MutableTreeNode的实现。DefaultMutableTreeNode类就是这样一个并不常用的实现,使用JTree.DynamicUtilTreeNode类的帮助来创建树节点。许多的树节点构成了JTree的TreeModel,默认存储在DefaultTreeModel类的实例中。

树的选择是通过TreeSelectionModel实现,使用默认的DefaultTreeSelectionModel实现来管理的。如果我们不希望树的节点成为可选择的,我们还可以使用JTree.EmptySelectionModel。由树根到所选择节点的路径是在TreePath内维护的,借助于RowMapper实现将行映射到路径。

注意,树相关的类位于javax.swing.tree包中。相关的事件类位于javax.swing.event包中。

JTree类

JTree类构成了显示层次结构数据元素集合的基础。

创建JTree

有七种不同的方法可以创建JTree,使用五种不同的方法来指定节点:

public JTree()
JTree tree = new JTree();
public JTree(Hashtable value)
JTree tree = new JTree(System.getProperties());
public JTree(Object value[])
public static void main (String args[]) {
  JTree tree = new JTree(args);
  ...
}
public JTree(Vector value)
Vector vector = new Vector();
vector.add("One");
vector.add("Two");
JTree tree = new JTree(vector);
public JTree(TreeModel value)
JTree tree = new JTree(aTreeModel);
public JTree(TreeNode value)
JTree tree = new JTree(aTreeNode);
public JTree(TreeNode value, boolean asksAllowsChildren)
JTree tree = new JTree(aTreeNode, true);

第一个构造函数是无参数版本。奇怪的是,他有一个带有一些节点的默认数据模型。通常情况下,我们应该在创建之后使用setModel(TreeModel newModel)来修改数据模型。

接下来的三个构造函数看起来是彼此相关的。通过由键/值对构成的Hashtable进行的JTree创建使用键集合用于节点,则值用于孩子,而通过数组或是Vector创建的树使用元素作为节点。这似乎意味着树只有一层深,但是事实上,如果键或是元素本身位于Hashtable,数组或是Vector中时,树的深度可以是无限的。

其余的三个构造函数使用JTree的自定义数据结构,我们将会在本章稍后进行解释。默认情况下,只有具有孩子的节点者是叶子节点。然而,树可以使用稍后才获得孩子的部分节点进行构建。最后的三个构造函数会使得当我们深度打开一个父节点时引起方法的调用,而不仅仅是父节点查找子节点。

提示,如果Hashtable中键的值是另一个Hashtable,数组或是Vector,我们可以通过使用顶层的Hashtable作为构造函数的参数来创建多级树。

正如我们前面所提到的,使用Hashtable,数组或是Vector作为构造函数中的参数,事实上,可以允许我们创建多层次树。然而这有两个小问题:根节点是不可见的,而且他自动有一个root数据元素。Hashtable,数组,或是Vector类型的其他节点的文本标签是toString()的结果。在这些树的实例中,默认文本并不是必须的。我们可以获得数组的Object类的toString()方法的结果或是包含Hashtable或是Vector中所有元素列表的标签。在Object数组的情况下,输出的结果也许是如[Ljava.lang.Object;@fa8d8993的样子。这并不是我们希望显示给用户的内容。

尽管我们并不能重写toString()方法(因为没有数组类来派生),我们可以派生Hashtable或是Vector来提供一个不同的toString()行为。为这个新类的构造函数提供一个名字可以允许我们提供当Hashtable或是Vector不是根节点时树中所用的文本标签。列表17-1中所示的类为Vector子类定义了这种行为。除了为构造函数提供名字,类同时添加了将Vector初始化为数组内容的构造函数。

package swingstudy.ch17;

import java.util.Vector;

public class NamedVector<E> extends Vector<E> {

    String name;
    NamedVector(String name) {
        this.name = name;
    }
    NamedVector(String name, E elements[]) {
        this.name = name;
        for(int i=0, n=elements.length; i<n; i++) {
            add(elements[i]);
        }
    }
    public String toString() {
        return "["+name+"]";
    }
}

图17-2显示了NamedVector类实战的示例。

Swing_17_2.png

Swing_17_2.png

列表17-2显示了用来生成图17-2中示例的源码。

package swingstudy.ch17;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.util.Vector;

import javax.swing.JFrame;
import javax.swing.JTree;

public class TreeArraySample {

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

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

                Vector<String> oneVector = new NamedVector<String>("One", args);
                Vector<String> twoVector = new NamedVector<String>("Two", new String[] {"Mercury", "Venus", "Mars"});
                Vector<Object> threeVector = new NamedVector<Object>("Three");
                threeVector.add(System.getProperties());
                threeVector.add(twoVector);
                Object rootNodes[] = {oneVector, twoVector, threeVector};
                Vector<Object> rootVector = new NamedVector<Object>("Root", rootNodes);
                JTree tree =  new JTree(rootVector);
                frame.add(tree, BorderLayout.CENTER);
                frame.setSize(300, 300);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

滚动树

如果我们创建并运行列表17-2中的程序,我们就会注意到一个小问题。当所有的父节点被展开时,树对于初始的屏幕尺寸显得过大。不仅是这样,而且我们不能看到位于树底部的节点。要解决这个问题,需要将JTree树的实例放在一个JScrollPane中,从而滚动面板可以管理树的滚动。类似于我们在第15章中所讨论的JTextArea,JTree类实现了JScrollable接口用于滚动支持。

将列表17-2中的示例中的两行粗体代码替换为下面的三行代码可以将树放在一个滚动面板中。这会使得当树对于可用的显示空间过大时树会显示在一个可滚动的区域中。

// Change from
JTree tree = new JTree(rootVector);
frame.add(tree, BorderLayout.CENTER);
// To
JTree tree = new JTree(rootVector);
JScrollPane scrollPane = new JScrollPane(tree);
frame.add(scrollPane, BorderLayout.CENTER);

除了使用JScrollPane用于滚动以外,我们可以在滚动区域中手动滚动可视化内容。使用public void scrollPathToVisible(TreePath path)与public void scrollRowToVisible(int row)方法可以将一个特定的树路径或是行移动到可视区域部分。节点的行标识了在当前节点以上到树的顶部的节点数目。这与树的层次不同,他是一个节点所具有的祖先节点(或父节点)的个数。图17-3也许会有助于我们理解这种区别。在左边的窗口中,soccer节点的位于第2层与第8行。当colors节点被关闭时,如右边的窗口所示,scoccer节点仍然位于第2层,但是却移动到第4行,因为blue,violet,red与yellow行不再可见。

Swing_17_3.png

Swing_17_3.png

JTree属性

表17-1列出了JTree的40个特定属性。当我们了解构成JTree的不同类时我们将会探讨这些属性。

Swing_table_17_1_1.png

Swing_table_17_1_1.png

Swing_table_17_1_2.png

Swing_table_17_1_2.png

Swing_table_17_1_3.png

Swing_table_17_1_3.png

JTree的一些属性是彼此紧密相关的。例如,当rowHeight属性为正数时,他意味着每一行的节点是以固定的高度显示的,而不论树中节点的尺寸是多少。当rowHeight属性为负数时,cellRenderer属性决定rowHeight。所以,rowHeight的值决定了fixedRowHeight属性的设置。将rowHeight的值修改为例如12像素会导致fixedRowHeight属性的设置为true。

largeModel属性设置是TreeUI的一个建议帮助其显示树。初始时,这个设置为false,因为树有许多的数据元素而我们并不希望用户界面组件缓存关于树的过多信息(例如节点渲染器数目)。对于较小的模型,缓存关于树的信息并不会需要较多的内存。

lastSelectedPathComponent属性的当前设置是最后一个被选中的节点的内容。在任何时候,我们都可以询问树哪一个被选中。如果没有任何内容被选中,这个属性的值将会null。因为树支持多项选中,lastSelectedPathComponent的属性并没有必要返回所有被选中的节点。我们也可以使用anchorSelectionPath与leadSelectionPath属性来修改选中路径。

三个选中行属性-leadSelectionRow,minSelectionRow与maxSelectionRow,是比较有趣的,因为他们这些行值会依据其他的父节点是打开还是关闭而变化。我们可以使用selectionRows属性来获取所有选中行索引的数组。然而,并没有办法将一个行号映射到树中的一个节点。相反,使用selectionPaths属性,他提供了一个TreePath元素数组。正如我们将要看到的,每一个TreePath包含被选中的节点以及路径上由根节点到选中节点的所有节点。

有三个树的可视化相关的设置。我们可以通过设置visibleRowCount属性来设置显示树的合适行数。默认情况下,这个设置为20。只有当这个树位于JScrollPane或是其他的一些使用Scrollable接口的组件中时这个设置才可用。第二个可视化相关的属性与根节点是否可见有关。当树是由Hashtable,数组或是Vector构造函数创建时,根是不可见的。否则,根节点在初始时是可见的。修改rootVisible属性可以使得我们修改这一设置。其他的可视化相关的属性设置与根节点旁边的图标有关。默认情况下,在根层次并没有图标来显示树根打开或是关闭的状态。所有的非根节点总是有这个图标类型。要显示在根图标,将showsRootHandles属性设置为true。

还有三个额外的面向选中的属性。toggleClickCount属性可以使得我们控制在一个父节点上多少次点击可以触发选中或是节点展开。默认设置为2。scrollsOnExpand属性会在当节点被展开从而有过多的子节点要显示时使得树滚动。默认情况下,这个设置为true。第三个属性,expandsSelectedPath,默认情况下为true,会使得节点的选中路径在编程选中时展开。然而,如果我们并不希望在编程选中时展开树,我们可以将其设置为false,并且将路径隐藏。

自定义JTree观感

每一个可安装的Swing观感都提供了一个不同的JTree外观以及默认的UIResoure值集合。图17-4显示了预安装的观感类型:Motif,Widnows与Ocean下的JTree容器外观。

Swing_17_4.png

Swing_17_4.png

表17-2显示了JTree的可用的UIResource相关属性的集合。对于JTree组件,有43个不同的属性。

Swing_table_17_2_1.png

Swing_table_17_2_1.png

Swing_table_17_2_2.png

Swing_table_17_2_2.png

Swing_table_17_2_3.png

Swing_table_17_2_3.png

在这些不同的JTree资源中,五个用于JTree中所显示的各种图标。要了解这五个图标是如何放置的,可以参考图17-5。如果我们只是希望修改树的图标(以及可能的颜色),我们所需要做的就是修改图标属性,如下面的代码行所示:

UIManager.put("Tree.openIcon", new DiamondIcon(Color.RED, false));
Swing_17_5.png

Swing_17_5.png

Tree.has颜色属性的目的也许并不明显。这一颜色用于绘制连接节点的线。对于Metal观感以及Ocean主题,默认情况下,使用角度线连接节点。要允许这些线的绘制,我们必须设置JTree.lineStyle客户属性。这个属性并不是一个UIResource属性,而是一个通过JComponent的public final void putClientProperty(Object key, Object value)方法设置的客户属性。JTree.lineStyle属性具有下列的可用设置:

  • None,用于不绘制连接节点的线
  • Angled,Ocean的默认设置,用于以Tree.has颜色绘制连接节点的线
  • Horizontal,用于以Tree.line颜色绘制第一层节点之间的水平线

注意,JTree.lineStyle客户属性只为Metal观感所用。如果当前的观感类型并不是Metal,则该属性设置会被忽略。其他系统提供的观感类并不使用这个设置。

对于客户属性,我们首先必须创建树,然后设置属性。客户属性是特定于树组件的,而且他并不为所有的树进行设置。所以,创建一个没有连接线的树可以使用下面的代码:

JTree tree = new JTree();
tree.putClientProperty("JTree.lineStyle", "None");

图17-6显示了这一结果。

Swing_17_6.png

Swing_17_6.png

下面的代码在第一层节点之间生成水平线:

UIManager.put("Tree.line", Color.GREEN);
JTree tree = new JTree();
tree.putClientProperty("JTree.lineStyle", "Horizontal");

图17-7显示水平线的样子。

Swing_17_7.png

Swing_17_7.png

TreeCellRenderer接口

JTree中的每一个节点都有一个已安装的单元渲染器。渲染器负责绘制节点并且清晰的显示其状态。默认的渲染器是一个基本的JLabel,可以使得我们在一个节点内同时具有文本与图标。然而,任意的组件都可以作为节点渲染器。默认的渲染器显示一个代表节点状态的图标。

注意,树单元渲染器仅是一个渲染器。假定,如果渲染器是一个JButton,他将是不可选择的,但是绘制可以看起来像是一个JButton。

每一个节点渲染器的配置都是由TreeCellRenderer接口来定义的。任何实现了这个接口的类都可以作为我们JTree的渲染器。

public interface TreeCellRenderer {
  public Component getTreeCellRendererComponent(JTree tree, Object value,
    boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus);
}

当需要绘制树节点时,树会询问他所注册的TreeCellRenderer如何显示特定的节点。节点本身被作为value参数传递,从而渲染器可以访问其当前状态来确定如何渲染其状态。要修改已安装的渲染器,使用 public void setCellRenderer(TreeCellRenderer renderer)。

DefaultTreeCellRenderer类

DefaultTreeCellRenderer类作为默认的树单元渲染器。这个类是JLable类的一个子类,所以他支持例如显示工具提示文本或是特定于节点的弹出菜单的功能。他只有一个无参数的构造函数。

当被JTree使用时,DefaultTreeCellRenderer使用各种默认图标(如前面的图17-5所示)来显示节点的当前状态与节点数据的文本表示。文本表示是通过树的每个节点的toString()方法获得的。

DefaultTreeCellRenderer属性

表17-3显示了DefaultTreeCellRenderer添加(或修改)的14个属性。因为默认的渲染器恰好是一个JLabel,我们也可以由其获得许多额外的属性。

Swing_table_17_3.png

Swing_table_17_3.png

如果我们不想使用UIManager或是仅希望修改单个树的图标,字体或是颜色,我们并不需要创建一个自定义的树单元渲染器。相反,我们可以向树请求其渲染器,并且进行自定义来显示我们希望的图标,字体或是颜色。图17-8显示了一个使用修改渲染器的JTree。无需创建新的渲染器,已存在的默认渲染器使用下面的代码进行定制:

JTree tree = new JTree();
DefaultTreeCellRenderer renderer = (DefaultTreeCellRenderer)tree.getCellRenderer();
// Swap background colors
Color backgroundSelection = renderer.getBackgroundSelectionColor();
renderer.setBackgroundSelectionColor(renderer.getBackgroundNonSelectionColor());
renderer.setBackgroundNonSelectionColor(backgroundSelection);
// Swap text colors
Color textSelection = renderer.getTextSelectionColor();
renderer.setTextSelectionColor(renderer.getTextNonSelectionColor());
renderer.setTextNonSelectionColor(textSelection);
Swing_17_8.png

Swing_17_8.png

记住TreeUI缓存渲染器尺寸信息。如果渲染器的修改改变了渲染器的尺寸,缓存不会更新。为了解决这一问题,有必要通知树缓存已经无效。这样的通知就是修改rowHeight属性。只要当前rowHeight属性设置不为负数,TreeUI必须向渲染器查询其高度。所以,将这个值减少1具有使得缓存的渲染器尺寸信息无效的副作用,从而使得树为所有的渲染器使用相应的初始尺寸进行显示。将下面的代码添加到前面的示例中会演示这一效果。

renderer.setFont(new Font("Dialog", Font.BOLD | Font.ITALIC, 32));
int rowHeight = tree.getRowHeight();
if (rowHeight <= 0) {
  tree.setRowHeight(rowHeight - 1);
}

图17-9中左边的窗口显示了图17-8中所产生的额外作用。如查我们没有修改rowHeight属性来使得显示缓存无效,我们就会得到右边窗口所显示的效果。

Swing_17_9.png

Swing_17_9.png

创建自定义的渲染器

如果我们树中的节点由过于复杂的信息构成而不能在一个JLabel中进行文本显示时,我们可以创建我们自己的渲染器。作为一个示例,考虑这样一棵树,这棵树中的节点通过标题,作者,价格来描述一本书,如图17-10所示。在这种情况下,渲染器可以是一个容器,其中使用单独的组件显示每一部分。

Swing_17_10.png

Swing_17_10.png

要描述这个示例中的第一本书,我们需要定义一个存储必须信息的类,如列表17-3所示。

package swingstudy.ch17;

public class Book {

    String title;
    String authors;
    float price;

    public Book(String title, String authors, float price) {
        this.title = title;
        this.authors = authors;
        this.price = price;
    }

    public String getTitle() {
        return title;
    }

    public String getAuthors() {
        return authors;
    }

    public float getPrice() {
        return price;
    }
}

要将一本书渲染为树中的一个节点,我们需要创建一个TreeCellRenderer实现。因为书是叶子节点,自定义的渲染器将会使用DefaultTreeCellRenderer来渲染所有的其他节点。渲染器的核心部分是getTreeCellRendererComponent()。如果这个方法所接收到的节点数据是Book类型,他会在不同的标签中存储相应的信息并且返回一个JPanel作为渲染器,并有相应的标签用于每本书的标题,作者与价格。否则,getTreeCellRendererComponent()方法返回默认渲染器。

列表17-4包含这个自定义渲染器的源码。注意,他使用与树中其他节点相同的选择颜色,从而书节点不会显示在外。

package swingstudy.ch17;

import java.awt.Color;
import java.awt.Component;
import java.awt.GridLayout;

import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeCellRenderer;

public class BookCellRenderer implements TreeCellRenderer {

    JLabel titleLabel;
    JLabel authorsLabel;
    JLabel priceLabel;
    JPanel renderer;

    DefaultTreeCellRenderer defaultRenderer = new DefaultTreeCellRenderer();

    Color backgroundSelectionColor;
    Color backgroundNonSelectionColor;

    public BookCellRenderer() {
        renderer = new JPanel(new GridLayout(0,1));
        titleLabel =  new JLabel(" ");
        titleLabel.setForeground(Color.BLUE);
        renderer.add(titleLabel);
        authorsLabel = new JLabel(" ");
        authorsLabel.setForeground(Color.BLUE);
        renderer.add(authorsLabel);
        priceLabel =  new JLabel(" ");
        priceLabel.setHorizontalAlignment(JLabel.RIGHT);
        priceLabel.setForeground(Color.RED);
        renderer.add(priceLabel);
        renderer.setBorder(BorderFactory.createLineBorder(Color.BLACK));
        backgroundSelectionColor = defaultRenderer.getBackgroundSelectionColor();
        backgroundNonSelectionColor = defaultRenderer.getBackgroundNonSelectionColor();
    }
    @Override
    public Component getTreeCellRendererComponent(JTree tree, Object value,
            boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
        // TODO Auto-generated method stub
        Component returnValue = null;
        if((value != null) && (value instanceof DefaultMutableTreeNode)) {
            Object userObject = ((DefaultMutableTreeNode)value).getUserObject();
            if(userObject instanceof Book) {
                Book book = (Book)userObject;
                titleLabel.setText(book.getTitle());
                authorsLabel.setText(book.getAuthors());
                priceLabel.setText(""+book.getPrice());
                if(selected) {
                    renderer.setBackground(backgroundSelectionColor);
                }
                else {
                    renderer.setBackground(backgroundNonSelectionColor);
                }
                renderer.setEnabled(tree.isEnabled());
                returnValue = renderer;
            }
        }
        if(returnValue == null) {
            returnValue = defaultRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
        }
        return returnValue;
    }

}

提示,JLabel组件是使用由空格构成的初始文本标签创建的。使用非空的标签会为每一个组件指定一些维度。TreeUI缓存节点尺寸来改进性能。为标签指定初始尺寸可以保证缓存被正确初始化。

最后一部分是测试程序,如列表17-5所示。其主体部分只是创建了一个Book对象数组。他重用了列表17-1中的NamedVector类来创建树分枝。用于修改树单元渲染器的代码以粗体显示。运行这个程序演示了自定义渲染器,如前面的图17-10所示。

package swingstudy.ch17;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.util.Vector;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.tree.TreeCellRenderer;

public class BookTree {

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

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

                Book javaBooks[] = {
                        new Book("Core Java 2", "Horstmann/Cornell", 49.99f),
                        new Book("Effective Java", "Bloch", 34.99f),
                        new Book("Java Collections", "Zukowski", 49.95f)
                };

                Book netBooks[] = {
                        new Book("Beginning VB.NET 1.1 Databases", "Maharry", 49.99f),
                        new Book("Beginning VB.NET Databases", "Willis", 39.99f)
                };

                Vector<Book> javaVector = new NamedVector<Book>("Java Books", javaBooks);
                Vector<Book> netVector = new NamedVector<Book>(".NET Books", netBooks);
                Object rootNodes[] = {javaVector, netVector};
                Vector<Object> rootVector = new NamedVector<Object>("Root", rootNodes);
                JTree tree = new JTree(rootVector);
                TreeCellRenderer renderer = new BookCellRenderer();
                tree.setCellRenderer(renderer);
                JScrollPane scrollPane = new JScrollPane(tree);
                frame.add(scrollPane, BorderLayout.CENTER);
                frame.setSize(300, 300);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

注意,不要担心DefaultMutableTreeNode的细节。除非指定,每棵树的所有节点都是DefaultMutableTreeNode。放置在列表17-5中的Vector中的每一个数组元素定义了特定节点的数据。然后这个数据在存储在DefaultMutableTreeNode的userObject属性中。

使用树工具提示

如果我们希望树为节点显示工具提示,我们必须将组件注册到ToolTipManager。如果我们没有注册组件,渲染器就不会获得显示工具提示的机会。渲染器显示提示,而不是树显示提示,所以为树设置工具提示文本会被忽略。下面的代码显示了我们如何向ToolTipManager注册特定的树。

ToolTipManager.sharedInstance().registerComponent(aTree);

一旦我们通知ToolTipManager我们希望树显示工具提示文本,我们必须通知渲染器要显示什么文本。尽管我们可以使用下面的代码直接设置文本,这会导致所有节点的固定设置。

DefaultTreeCellRenderer renderer = (DefaultTreeCellRenderer)aTree.getCellRenderer();
renderer.setToolTipText("Constant Tool Tip Text");

除了提供固定的设置,另一种方法就是为渲染器提供一个工具提示字符串表格,从而渲染器可以在运行时决定要显示了工具提示文本的字符串。列表17-6中的渲染器就是依赖java.util.Dictionary实现(类似Hashtable)来存储节点到工具提示文本映射的示例。如果存在特定节点的提示,渲染器会将提示与其相关联。

package swingstudy.ch17;

import java.awt.Component;
import java.util.Dictionary;

import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeCellRenderer;

public class ToolTipTreeCellRenderer implements TreeCellRenderer {

    DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer();
    Dictionary tipTable;

    public ToolTipTreeCellRenderer(Dictionary tipTable) {
        this.tipTable = tipTable;
    }

    @Override
    public Component getTreeCellRendererComponent(JTree tree, Object value,
            boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
        // TODO Auto-generated method stub
        renderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
        if(value != null) {
            Object tipKey;
            if(value instanceof DefaultMutableTreeNode) {
                tipKey = ((DefaultMutableTreeNode)value).getUserObject();
            }
            else {
                tipKey = tree.convertValueToText(value, selected, expanded, leaf, row, hasFocus);
            }
            renderer.setToolTipText((String)tipTable.get(tipKey));
        }
        return renderer;
    }

}

注意,列表17-6中的示例利用了JTree的public String convertValueToText(Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus)来将树节点的值转换为文本字符串。value参数通常是DefaultMutableTreeNode,我们将在本章稍后描述。当value参数不是DefaultMutableTreeNode时,使用convertValueToText()允许渲染器支持其他的树节点类型。

使用新的ToolTipTreeCellRenderer类简单的创建了Properties列表,使用所必要节点的工具提示进行填充,然后将渲染器关联到树。图17-11显示了运行中的渲染器。

Swing_17_11.png

Swing_17_11.png

用于生成图17-11中的屏幕完整代码显示在下面的列表17-7中。这棵树使用系统属性列表作为树节点。工具提示文本是特定属性的当前设置。当使用ToolTipTreeCellRenderer时,确保要使用ToolTipManager来注册树。

package swingstudy.ch17;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.util.Properties;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.ToolTipManager;
import javax.swing.tree.TreeCellRenderer;

public class TreeTips {

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

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

                Properties props = System.getProperties();
                JTree tree = new JTree(props);
                ToolTipManager.sharedInstance().registerComponent(tree);
                TreeCellRenderer renderer = new ToolTipTreeCellRenderer(props);
                tree.setCellRenderer(renderer);
                JScrollPane scrollPane =  new JScrollPane(tree);
                frame.add(scrollPane, BorderLayout.CENTER);
                frame.setSize(300, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

尽管这个示例创建了一个新的树单元渲染器,其行为仅是自定义DefaultTreeCellRenderer的功能。无需我们亲自配置图标与文本,默认的渲染器可以为我们完成这些工作。然后添加工具提示文本。

编辑树节点

除了支持单个的树单元渲染器,JTree组件还可以是可编辑的,从而以许用户修改树的节点的内容。默认情况下,树是只读的。要使得树成为可编辑的,只需要将editable属性设置修改为true即可:

aTree.setEditable(true);

默认情况下,编辑器是一个文本域。同时对由组合框或是复选框中选择也具有内建支持。如果我们喜欢,我们可以为树创建一个自定义的编辑器,就像我们可以创建一个自定义单元渲染器一样。

注意,不幸的是,内建的复选框编辑器在表格中的表现要优于树中的表现,其中列标签是名字而值是单元。

图17-12显示了一个使用默认编辑器的树。要打开编辑器,选择一个节点,然后双击。如果节点不是叶子节点,选择该节点同时会显示或隐藏其子节点。

Swing_17_12.png

Swing_17_12.png

有一系列的类支持编辑树节点。许多是为JLabel组件所共享的,因为他们都支持可编辑单元。CellEditor接口形成了TreeCellEditor接口的基础。JTree的任何编辑器实现必须实现TreeCellEditor接口。DefaultCellEditor(其扩展了AbstractCellEditor)提供了一个这样的实现,而DefaultTreeCellEditor提供了另一个实现。下面我们详细探讨这些接口与类。

CellEditor接口

CellEditor接口定义了JTree或JTable以及需要编辑器的第三方组件所用的编辑器的基础。除了定义如何管理CellEditorListener对象列表,接口描述了如何确定一个节点或是单元是否是可编辑的及编辑器在修改了其值以后其新值是什么。

public interface CellEditor {
  // Properties
  public Object getCellEditorValue();
  // Listeners
  public void addCellEditorListener(CellEditorListener l);
  public void removeCellEditorListener(CellEditorListener l);
  // Other methods
  public void cancelCellEditing();
  public boolean isCellEditable(EventObject event);
  public boolean shouldSelectCell(EventObject event);
  public boolean stopCellEditing();
}

TreeCellEditor接口

TreeCellEditor接口的作用类似于TreeCellRenderer接口。然而,getXXXComponent()方法并没有分辨编辑器是否具有输入焦点的参数,因为在编辑器的情况下,他必须具有焦点。任何实现了TreeCellEditor接口的类都可以作为我们的JTree所用的编辑器。

public interface TreeCellEditor implements CellEditor {
  public Component getTreeCellEditorComponent(JTree tree, Object value,
    boolean isSelected, boolean expanded, boolean leaf, int row);
}

DefaultCellEditor类

DefaultCellEditor类用作树节点与表格单元的编辑器。这个类可以使得我们很容易提供文本编辑器,组合框编辑器,或是复选框编辑器来修改节点或是单元的内容。

下面将要描述DefaultTreeCellEditor类,使用这个类可以为自定义的文本域提供一个编辑器,维护基于TreeCellRenderer的相应节点类型图标。

创建DefaultCellEditor

当我们创建DefaultCellEditor实例时,我们提供JTextField,JComboBox或是JCheckBox来用作编辑器。

public DefaultCellEditor(JTextField editor)
JTextField textField = new JTextField();
TreeCellEditor editor = new DefaultCellEditor(textField);
public DefaultCellEditor(JComboBox editor)
public static void main (String args[]) {
  JComboBox comboBox = new JComboBox(args);
  TreeCellEditor editor = new DefaultCellEditor(comboBox);
  ...
}
public DefaultCellEditor(JCheckBox editor)
JCheckBox checkBox = new JCheckBox();
TreeCellEditor editor = new DefaultCellEditor(checkBox);

对于JTree,如果我们需要一个JTextField编辑器,我们应该使用DefaultTreeCellEditor。这个文本域将会共享相同的字体并且使用树的相应的编辑器边框。当JCheckBox被用作编辑器时,树的节点应该是一个Boolean值或是一个可以转换为Boolean的String。(如果我们不熟悉String到Boolean的转换,可以参考接收String的Boolean构造函数的Javadoc。)

在创建了编辑器之后,我们使用类似的tree.setCellEditor(editor)来使用这个编辑器。并且不要忘记使用tree.setEditable(true)来使得树可编辑。例如,如果我们希望一个可编辑器的组合框作为我们的编辑器,下面的代码可以实现相应的目的:

JTree tree = new JTree(...);
tree.setEditable(true);
String elements[] = { "Root", "chartreuse", "rugby", "sushi"} ;
JComboBox comboBox = new JComboBox(elements);
comboBox.setEditable(true);
TreeCellEditor editor = new DefaultCellEditor(comboBox);
tree.setCellEditor(editor);

上面的代码将会产生如图17-13所示的编辑basketball节点时的屏幕。注意,树并没有图标来标识正在被编辑的节点的类型。这可以通过DefaultTreeCellEditor类来修正。DefalutCellEditor最初是用于JTable的,而不是用于JTree。

Swing_17_13.png

Swing_17_13.png

注意,当我们使用一个不可编辑的JComboBox作为单元编辑器时,如果选项集合不包括原始的代码设置,一旦节点的值发生变化,他就有可能回到原始的设置。

要了解当使用DefaultCellEditor作为TreeCellEditor时,JCheckBox笨拙的外观,可以参看图17-14。

Swing_17_14.png

Swing_17_14.png

图17-14使用下面的代码:

Object array[] =
  {Boolean.TRUE, Boolean.FALSE, "Hello"}; // Hello will map to false
JTree tree = new JTree(array);
tree.setEditable(true);
tree.setRootVisible(true);
JCheckBox checkBox = new JCheckBox();
TreeCellEditor editor = new DefaultCellEditor(checkBox);
tree.setCellEditor(editor);

JCheckBox编辑器的笨拙以及DefaultTreeCellEditor中自定义的文本域编辑器使得JComboBox成为我们可以由DefaultCellEditor中获得的唯一的TreeCellEditor。然而,也许我们仍然希望将组合框编辑器放在DefaultTreeCellEditor中来显示与图标紧邻的相应的类型图标。

DefaultCellEditor属性

DefaultCellEditor只有三个属性,如表17-4所示。编辑器可以是任意的AWT组件,而不仅仅是轻量级的Swing组件。记住,如果我们确实要选择使用一个重量级组件作为编辑器,我们就要承担混合重量级组件与轻量级组件的风险。如查我们在确定编辑器组件的当前设置是什么,我们可以请求cellEditorValue属性设置。

Swing_table_17_4.png

Swing_table_17_4.png

DefaultTreeCellEditor类

当我们使得树成为可编辑的,但是并没有向树关联编辑器时,JTree自动所用的TreeCellEditor就是DefaultTreeCellEditor。DefaultTreeCellEditor组合了TreeCellRenderer的图标与TreeCellEditor来返回一个组合的编辑器。

编辑器所用的默认组件是JTextField。文本编辑器比较特殊,因为他会尝试将其高度限制为原始的单元渲染器并且使用树的字体,从而不会出现不合适的显示。编辑器使用两个公开的内联类来实现在这一目的:DefaultTreeCellEditor.EidtorContainer与DefaultTreeCellEditor.DefaultTextField。

DefaultTreeCellEditor有两个构造函数。通常我们并不需要调用第一个构造函数,因为他是当确定节点为可编辑器的时由用户界面自动为我们创建的。然而,如果我们希望以某种方式自定义默认编辑器时,第一个构造函数则是必需的。

public DefaultTreeCellEditor(JTree tree, DefaultTreeCellRenderer renderer)
JTree tree = new JTree(...);
DefaultTreeCellRenderer renderer = (DefaultTreeCellRenderer)tree.getCellRenderer();
TreeCellEditor editor = new DefaultTreeCellEditor(tree, renderer);
public DefaultTreeCellEditor(JTree tree, DefaultTreeCellRenderer renderer,
  TreeCellEditor editor)
public static void main (String args[]) {
  JTree tree = new JTree(...);
  DefaultTreeCellRenderer renderer =
    (DefaultTreeCellRenderer)tree.getCellRenderer();
  JComboBox comboBox = new JComboBox(args);
  TreeCellEditor comboEditor = new DefaultCellEditor(comboBox);
  TreeCellEditor editor = new DefaultTreeCellEditor(tree, renderer, comboEditor);
  ...
}

为树创建合适的组合框编辑器

如图17-13所示,通过DefaultCellEditor使用JComboBox作为TreeCellEditor并不会在编辑器旁边放置合适的代码类型图标。如果我们希望显示图标,我们需要组合DefaultCellEditor与DefaultTreeCellEditor来获得一个同时具有图标与编辑器的编辑器。事实上他并没有听起来这样困难。他只涉及到两个额外的步骤:获得树的渲染器(由其获得图标),然后组合图标与编辑器从而获得一个新的编辑器。下面的代码演示了这一操作:

JTree tree = new JTree();
tree.setEditable(true);
DefaultTreeCellRenderer renderer = (DefaultTreeCellRenderer)tree.getCellRenderer();
String elements[] = { "Root", "chartreuse", "rugby", "sushi"} ;
JComboBox comboBox = new JComboBox(elements);
comboBox.setEditable(true);
TreeCellEditor comboEditor = new DefaultCellEditor(comboBox);
TreeCellEditor editor = new DefaultTreeCellEditor(tree, renderer, comboEditor);
tree.setCellEditor(editor);

改进的输入如图17-15所示。

Swing_17_15.png

Swing_17_15.png

只为叶节点创建编辑器

在某些情况下,我们希望只有叶子节点是可以编辑的。由getTreeCellEditorComponent()请求返回null等效于使得一个节点不可以编辑。不幸的是,这会使得用户界面类抛出NullPointerException。

不能返回null,我们可以覆盖public boolean isCellEditable(EventObject object)方法的默认行为,他是CellEditor接口的一部分。如果原始的返回值为true,我们可以进行额外的检测以确定所选中的树的节点是否是叶子节点。树的节点实现了TreeNode接口(在本章稍后进行描述)。这个接口恰好具有方法public boolean isLeaf(),该方法可以为我们提供所寻求的答案。叶子节点的单元编辑器的类定义显示在列表17-8中。

package swingstudy.ch17;

import java.util.EventObject;

import javax.swing.JTree;
import javax.swing.tree.DefaultTreeCellEditor;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreeNode;

public class LeafCellEditor extends DefaultTreeCellEditor {

    public LeafCellEditor(JTree tree, DefaultTreeCellRenderer renderer) {
        super(tree, renderer);
    }

    public LeafCellEditor(JTree tree, DefaultTreeCellRenderer renderer, TreeCellEditor editor) {
        super(tree, renderer, editor);
    }

    public boolean isCellEditable(EventObject event) {
        // Get initial setting
        boolean returnValue = super.isCellEditable(event);
        // If still possible, check if current tree nod is a leaf
        if(returnValue) {
            Object node = tree.getLastSelectedPathComponent();
            if((node != null) && (node instanceof TreeNode)) {
                TreeNode treeNode = (TreeNode)node;
                returnValue = treeNode.isLeaf();
            }
        }
        return returnValue;
    }
}

我们使用LeafCellEditor的方式类似于DefaultTreeCellRenderer。其构造函数要求JTree与DefaultTreeCellRenderer。另外,他支持一个额外的TreeCellEditor。如果没有提供,则JTextField会被用作编辑器。

JTree tree = new JTree();
tree.setEditable(true);
DefaultTreeCellRenderer renderer = (DefaultTreeCellRenderer)tree.getCellRenderer();
TreeCellEditor editor = new LeafCellEditor(tree, renderer);
tree.setCellEditor(editor);

CellEditorListener接口与ChangeEvent类

在探讨完整的TreeCellEditor创建之前,我们先来了解一下CellEditorListener接口定义。这个接口包含CellEditor所用的两个方法。

public interface CellEditorListener implements EventListener {
  public void editingCanceled(ChangeEvent changeEvent);
  public void editingStopped(ChangeEvent changeEvent);
}

编辑器调用所注册的监听器的editingCanceled()方法来通知节点值的编辑器已经退出。editingStopped()方法被调用来通知编辑会话的完成。

通常情况下,并没有必要创建CellEditorListener。然而,当创建一个TreeCellEditor(或是任意的CellEditor),管理其监听器列表并且在需要的时候通知这些监听器是必需的。幸运的是,这是借助于AbstractCellEditor为我们进行自动管理的。

创建更好的复选框节点编辑器

当配合JTree使用时,使用DefaultCellEditor类所提供的JCheckBox编辑器并不是一个很好的选择。尽管编辑器可以被包装进DefaultTreeCellEditor来获得相应的树图标,我们不能在复选框内显示文本(也就是除了true或是false)。其他的文本字符串也可以显示在树中,但是一旦一个节点被编辑,被编辑器节点的文本标签只能是true或是false。

要使得具有文本标签的可编辑复选框作为树的单元编辑器,我们必须自己创建。完整的过程涉及到创建三个类-用于树中每个节点的数据模型,渲染自定义数据结构的树单元渲染器以及实际的编辑器-外加一个将他们连接在一起的测试程序。

注意,在这里创建的渲染器与编辑器只支持用于编辑叶子节点的类复选框数据。如果我们要支持非叶节点的复选框,我们需要移除检测叶节点的代码。

创建CheckBoxNode类

我们所要创建的第一个类用于处理树中叶节点的数据模型。我们可以使用与JCheckBox类相同的数据模型,但是这个数据模型包含我们并不需要额外的节点信息。我们所需要的信息仅是节点的被选中状态与其文本标签。列表17-9包含了这个类的基本定义,其中包含用于状态与标签的setter与getter方法。其他类的构建则没有这么容易。

package swingstudy.ch17;

public class CheckBoxNode {

    String text;
    boolean selected;

    public CheckBoxNode(String text, boolean selected) {
        this.text = text;
        this.selected = selected;
    }

    public boolean isSelected() {
        return selected;
    }

    public void setSelected(boolean newValue) {
        selected = newValue;
    }

    public String getText() {
        return text;
    }

    public void setText(String newValue) {
        text = newValue;
    }

    public String toString() {
        return getClass().getName()+"["+text+"/"+selected+"]";
    }
}

创建CheckBoxNodeRenderer类

渲染器包含两部分。对于非叶节点,我们可以使用DefaultTreeCellRenderer,因为这些节点本来不是CheckBoxNode元素。对于CheckBoxNode类型的叶节点的渲染器,我们需要将数据结构映射为相应的渲染器。因为这些节点包含一个选中状态与文本标签,JCheckBox可以作为叶节点的很好渲染器。

这两部分中比较容易解释的是非叶子节点的渲染器。在这个例子中,如通常一样,他简单的配置一个DefaultTreeCellRenderer;而并不做任何特殊的事情。

叶子节点的渲染器需要更多的工作。在配置任何节点之前,我们需要使其看起来像是默认渲染哭喊。构造函数需要渲染器的观感所提供的必须的字体与各种颜色,从而保证两个渲染器看起来比较相似。

树单元渲染器CheckBoxNodeRenderer类的定义显示在列表17-10中。

package swingstudy.ch17;

import java.awt.Color;
import java.awt.Component;
import java.awt.Font;

import javax.swing.JCheckBox;
import javax.swing.JTree;
import javax.swing.UIManager;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeCellRenderer;

public class CheckBoxNodeRenderer implements TreeCellRenderer {

    private JCheckBox leafRenderer = new JCheckBox();
    private DefaultTreeCellRenderer nonLeafRenderer = new DefaultTreeCellRenderer();
    Color selectionBorderColor, selectionForeground, selectionBackground, textForeground, textBackground;

    protected JCheckBox getLeafRenderer() {
        return leafRenderer;
    }

    public CheckBoxNodeRenderer() {
        Font fontValue;
        fontValue = UIManager.getFont("Tree.font");
        if(fontValue != null) {
            leafRenderer.setFont(fontValue);
        }
        Boolean booleanValue = (Boolean)UIManager.get("Tree.drawsFocusBorderAroundIcon");
        leafRenderer.setFocusPainted((booleanValue != null) && (booleanValue.booleanValue()));

        selectionBorderColor = UIManager.getColor("Tree.selectionBorderColor");
        selectionForeground = UIManager.getColor("Tree.selectionForeground");
        selectionBackground = UIManager.getColor("Tree.selectionBackground");
        textForeground = UIManager.getColor("Tree.textForeground");
        textBackground = UIManager.getColor("Tree.textBackground");
    }

    @Override
    public Component getTreeCellRendererComponent(JTree tree, Object value,
            boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
        // TODO Auto-generated method stub

        Component returnValue;

        if(leaf) {
            String stringValue = tree.convertValueToText(value, selected, expanded, leaf, row, false);
            leafRenderer.setText(stringValue);
            leafRenderer.setSelected(false);

            leafRenderer.setEnabled(tree.isEnabled());

            if(selected) {
                leafRenderer.setForeground(selectionForeground);
                leafRenderer.setBackground(selectionBackground);
            }
            else {
                leafRenderer.setForeground(textForeground);
                leafRenderer.setBackground(textBackground);
            }

            if((value != null) && (value instanceof DefaultMutableTreeNode)) {
                Object userObject = ((DefaultMutableTreeNode)value).getUserObject();
                if(userObject instanceof CheckBoxNode) {
                    CheckBoxNode node = (CheckBoxNode)userObject;
                    leafRenderer.setText(node.getText());
                    leafRenderer.setSelected(node.isSelected());
                }
            }
            returnValue = leafRenderer;
        }
        else {
            returnValue = nonLeafRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
        }
        return returnValue;
    }

}

注意,getLeafRenderer()方法是我们在编辑器中所需要的助手方法。

创建CheckBoxNodeEditor类

CheckBoxNodeEditor类是创建更好的复选框编辑器的最后一部分。他作为TreeCellEditor实现,允许我们支持其叶子节点数据是CheckBoxNode类型的树的编辑。TreeCellEditor接口是CellEditor实现的扩展,所以我们必须实现两个接口的方法。我们不能扩展DefaultCellEditor或是DefaultTreeCellEditor,因为他们会要求我们使用他们所提供的JCheckBox编辑器实现,而不是我们在这里所创建的新编辑器。然而,我们可以扩展AbstractCellEditor,并且添加必需的TreeCellEditor接口实现。AbstractCellEditor为我们管理CellEditorListener对象列表,并且具有依据停止或是关闭编辑来通知监听器列表的方法。

因为编辑器承担渲染器的角色,我们需要使用前面的CheckBoxNodeRenderer来获得基本的渲染器外观。这将保证编辑器的外观与渲染器的外观类似。因为叶节点的渲染器是JCheckBox,这可以完美的使得我们可以修改节点状态。编辑器JCheckBox将会是活动的并且是可修改的,从而允许用户由选中状态修改为非选中状态,以及相反的操作。如果编辑器是标准的DefaultTreeCellRenderer,我们需要管理选中变化的创建。

现在已经设置了类的层次结构,所要检测的第一个方法是CellEditor的public Object getCellEditorValue()方法。这个方法的目的就是将存储在节点编辑器的数据转换为存储在节点中的数据。用户界面会在他确定用户已经成功的修改了编辑器中的数据之后调用这个方法来获得编辑器的值。在这个方法中,每次我们需要创建一个新对象。否则,相同的代码会在树中出现多次,使得所有的节点与与最后一个编辑的节点的渲染器相同。要将编辑器转换为数据模型,需要向编辑器询问其当前标签与选中状态是什么,然后创建并返回新节点。

public Object getCellEditorValue() {
  JCheckBox checkbox = renderer.getLeafRenderer();
  CheckBoxNode checkBoxNode =
    new CheckBoxNode(checkbox.getText(), checkbox.isSelected());
  return checkBoxNode;
}

注意,直接访问树中的节点并更新并不是编辑器的工作。getCellEditorValue()方法会返回相应的节点对象,从而用户界面可以通知树的任何变化。

如果我们要自己实现CellEditor接口,我们也需要自己管理CellEditorListener列表。我们需要使用addCellEditorListener()与removeCellEditorListener()方法管理列表,并且提供通知接口中每个方法的监听器列表的方法。但是,因为我们要派生AbstractCellEditor,我们并没有必要自己做这些事情。我们只需要知道为了在合适的时候通知监听器列表,该类提供了fireEditingCanceled()与fireEditingStopped()方法。

下一个CellEditor方法,cancelCellEditing(),会在树中的一个新节点被选中时调用,表明前一个选中的编辑过程已经被停止,并且中间的更新已经中止。这个方法能够做任何事情,例如销毁编辑器所需要的中间对象。然而,这个方法应该做的就是调用fireEditingCanceled();这可以保证所注册的CellEditorListener对象会得到关闭的通知。AbstractCellEditor会为我们完成这些工作。除非我们需要做一些临时操作,否则没有必要重写这一行为。

CellEditor接口的stopCellEditing()方法返回boolean。这个方法被调用来检测当前节点的编辑是否可以停止。如果需要进行一些验证来确认编辑是否可以停止,我们需要在这里进行检测。对于这个例子中的CheckBoxNodeEditor,并没有验证检测的必要。所以,编辑总是可以停止 ,使得方法总是返回true。

当我们希望编辑器停止编辑时,我们可以调用fireEditingStopped()方法。例如,如果编辑器是一个文本域,在文本域中按下Enter可以作为停止编辑的信号。在JCheckBox编辑器的例子中,选中可以作为停止编辑器的信号。如果没有调用fireEditingStopped()方法,树的数据模型就不会被更新。

要在JCheckBox选中之后停止编辑,向其关联一个ItemListener。

ItemListener itemListener = new ItemListener() {
  public void itemStateChanged(ItemEvent itemEvent) {
    if (stopCellEditing()) {
      fireEditingStopped();
    }
  }
};
editor.addItemListener(itemListener);

我们要了解的CellEditor接口的下一个方法是public boolean isCellEditable(EventObject event)。这个方法返回一个boolean来表明事件源的节点是否是可编辑的。要确定某一事件是否发生在一个特定的节点上,我们需要一个到编辑器被使用的树的引用。我们可以将这一需要添加到编辑器的构造函数。

要确定在事件过程中在某一个特定的位置上是哪一个节点,我们可以向树请求到事件位置的节点的路径。这个路径被作为TreePath对象返回,我们会在本章稍后进行探讨。树路径的最后一个组件就是事件发生的特定节点。这就是我们必须检测来确定他是否可编辑的节点。如果他是可编辑的,方法会返回true;如果是不可编辑的,则会返回false。在这里要创建的树的例子中,如果节点是叶节点则是可编辑的,并且他包含CheckBoxNode数据。

JTree tree;
public CheckBoxNodeEditor(JTree tree) {
  this.tree = tree;
}
public boolean isCellEditable(EventObject event) {
  boolean returnValue = false;
  if (event instanceof MouseEvent) {
    MouseEvent mouseEvent = (MouseEvent)event;
    TreePath path = tree.getPathForLocation(mouseEvent.getX(), mouseEvent.getY());
    if (path != null) {
      Object node = path.getLastPathComponent();
      if ((node != null) &&  (node instanceof DefaultMutableTreeNode)) {
        DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode)node;
        Object userObject = treeNode.getUserObject();
        returnValue = ((treeNode.isLeaf()) && (userObject instanceof CheckBoxNode));
      }
    }
  }
  return returnValue;
}

CellEditor接口的shouldSselectCell()方法允许我们决定一个节点是否是可选择的。对于这个例子中的编辑器,所有可编辑的单元都应被选中。然而,这个方法允许我们查看特定的节点以确定他是否是可选中的。默认情况下,AbstractCellEditor会为这个方法返回true。

其他的方法,getTreeCellEditorComponent()来自于TreeCellEditor接口。我们需要一个到CheckBoxNodeRenderer的引用来获取并将其用作编辑器。除了仅是传递所有的参数以外还有两个小小的变化。编辑器应总是被选中并且具有输入焦点。这简单的强制两个参数总是为true。当节点被选中时,背景会被填充。当获得焦点时,在UIManager.get(“Tree.drawsFocusBorderAroundIcon”)返回true时会有边框环绕编辑器。

CheckBoxNodeRenderer renderer = new CheckBoxNodeRenderer();
public Component getTreeCellEditorComponent(JTree tree, Object value,
    boolean selected, boolean expanded, boolean leaf, int row) {
  // Editor always selected / focused
  return renderer.getTreeCellRendererComponent(tree, value, true, expanded, leaf,
    row, true);
}

列表17-11将所有的内容组合在一起,构成了完整的CheckBoxNodeEditor类的源码。

package swingstudy.ch17;

import java.awt.Component;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseEvent;
import java.util.EventObject;

import javax.swing.AbstractCellEditor;
import javax.swing.JCheckBox;
import javax.swing.JTree;
import javax.swing.event.ChangeEvent;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreePath;

public class CheckBoxNodeEditor extends AbstractCellEditor implements
        TreeCellEditor {

    CheckBoxNodeRenderer renderer = new CheckBoxNodeRenderer();

    ChangeEvent changeEvent = null;

    JTree tree;

    public CheckBoxNodeEditor(JTree tree) {
        this.tree = tree;
    }
    @Override
    public Object getCellEditorValue() {
        // TODO Auto-generated method stub
        JCheckBox checkbox = renderer.getLeafRenderer();
        CheckBoxNode checkBoxNode = new CheckBoxNode(checkbox.getText(), checkbox.isSelected());
        return checkBoxNode;
    }

    public boolean isCellEditable(EventObject event) {
        boolean returnValue = false;
        if(event instanceof MouseEvent) {
            MouseEvent mouseEvent = (MouseEvent)event;
            TreePath path = tree.getPathForLocation(mouseEvent.getX(), mouseEvent.getY());
            if(path != null) {
                Object node = path.getLastPathComponent();
                if((node != null ) && (node instanceof DefaultMutableTreeNode)) {
                    DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode)node;
                    Object userObject = treeNode.getUserObject();
                    returnValue = ((treeNode.isLeaf()) && (userObject instanceof CheckBoxNode));
                }
            }
        }
        return returnValue;
    }
    @Override
    public Component getTreeCellEditorComponent(JTree tree, Object value,
            boolean selected, boolean expanded, boolean leaf, int row) {
        // TODO Auto-generated method stub
        Component editor = renderer.getTreeCellRendererComponent(tree, value, true, expanded, leaf, row, true);

        // Editor always selected / focused
        ItemListener itemListener = new ItemListener() {
            public void itemStateChanged(ItemEvent event) {
                if(stopCellEditing()) {
                    fireEditingStopped();
                }
            }
        };

        if(editor instanceof JCheckBox) {
            ((JCheckBox)editor).addItemListener(itemListener);
        }
        return editor;
    }

}

注意,在树节点中并没有对数据的直接修改。修改节点并不是编辑器的角色。编辑器仅是获得新节点值,并且使用getCellEditorValue()方法返回。

创建测试程序

列表17-12中的测试由创建CheckBoxNode元素的基础构成。除了创建树数据,树必须有渲染器以及与渲染器相关联的编辑器,并且是可编辑的。

package swingstudy.ch17;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.util.Vector;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTree;

public class CheckBoxNodeTreeSample {

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

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

                CheckBoxNode accessibilityOptions[] = {
                        new CheckBoxNode("Move System caret with focus/selection changes", false),
                        new CheckBoxNode("Always exand alt text for images", true)
                };

                CheckBoxNode browsingOptions[] = {
                    new CheckBoxNode("Notify when downloads complete", true),
                    new CheckBoxNode("Disabel script debugging", true),
                    new CheckBoxNode("Use AutoComplete", true),
                    new CheckBoxNode("Browse in a new process", false)
                };

                Vector<CheckBoxNode> accessVector = new NamedVector<CheckBoxNode>("Accessibility", accessibilityOptions);
                Vector<CheckBoxNode> browseVector = new NamedVector<CheckBoxNode>("Browsing", browsingOptions);

                Object rootNodes[] = {accessVector, browseVector };
                Vector<Object> rootVector = new NamedVector<Object>("Root", rootNodes);
                JTree tree = new JTree(rootVector);

                CheckBoxNodeRenderer renderer = new CheckBoxNodeRenderer();
                tree.setCellRenderer(renderer);

                tree.setCellEditor(new CheckBoxNodeEditor(tree));
                tree.setEditable(true);

                JScrollPane scrollPane = new JScrollPane(tree);
                frame.add(scrollPane, BorderLayout.CENTER);
                frame.setSize(300, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

运行这个程序并且选择CheckBoxNode会打开编辑器。在编辑器被打开之后,再一次选择编辑器会使得树中节点的状态发生变化。编辑器会保持使能状态直到另一个不同的树节点被选中。图17-16显示了使用中的编辑器的示例。

Swing_17_16.png

Swing_17_16.png

使用树节点

当我们创建一个JTree,树中任意位置的对象类型可以是任意的Object。并没有要求树的节点实现任何接口或是继承任何类。然而,Swing组件库提供一对接口与一类来处理树节点。树的默认数据模型,DefaultTreeModel,使用这些接口与类。然而,树数据类型接口,TreeModel,允许任意的数据类型做为树的节点。

树的基本接口是TreeNode,他定义了描述只读,父子聚合关系的一系列方法。TreeNode的扩展是MutableTreeNode接口,这个接口允许我们编程实现连接节点并且在每一个节点存储信息。实现这两个接口的类是DefaultMutableTreeNode类。除了实现两个接口的方法以外,该类还提供了一个方法集合用于遍历树并且查询各种节点的状态。

记住,尽管有这些节点对象可用,但是很多的工作仍然无需这些接口与类就可以完成,正如本章前面所示。

TreeNode接口

TreeNode接口描述了树单独部分的一个可能定义。他为TreeNode的一个实现,DefaultTreeModel类,用来存储到描述一棵树层次数据的引用。这个接口可以使得我们确定当前节点的父节点是哪一个节点,以及获取关于节点集合的信息。当父节点为null时,此节点就是树的根。

public interface TreeNode {
  // Properties
  public boolean getAllowsChildren();
  public int getChildCount();
  public boolean isLeaf();
  public TreeNode getParent();
  // Other methods
  public Enumeration children();
  public TreeNode getChildAt(int childIndex);
  public int getIndex(TreeNode node);
}

注意,通常情况下,只有非叶节点允许有子节点。然而,安全限制也许会限制非叶子节点具有子节点,或者是至少限制子节点的显示。想像一个目录树,其中我们并没有某个特定目录的读取权限。尽管该目录并不是叶子节点,他也不能有子节点,因为我们并没有查找其子节点的权限。

MutableTreeNode接口

尽管TreeNode接口允许我们获取关于树节点层次结构的信息,但是他并不允许我们创建这个层次结构。TreeNode只是提供我们访问只读树层次结构的能力。另一方面,MutableTreeNode接口允许我们创建这个层次并且在树中的特定节点存储信息。

public interface MutableTreeNode implements TreeNode {
  // Properties
  public void setParent(MutableTreeNode newParent);
  public void setUserObject(Object object);
  // Other methods
  public void insert(MutableTreeNode child, int index);
  public void remove(MutableTreeNode node);
  public void remove(int index);
  public void removeFromParent();
}

当创建树节点的层次结构时,我们可以创建子节点并将其添加到父节点或是创建父节点并且添加子节点。要将一个节点关联到你节点,我们使用setParent()方法设置其父节点。使用insert()方法可以使得我们将子节点添加到父节点。insert()方法的参数包含一个索引参数。索引表示子节点集合中的位置。索引是由零开始的,所以索引为零将会把节点添加为树的第一个子节点。将节点添加到为最后一个子节点,而不是第一个,需要我们使用getChildCount()方法查询节点有多少个子节点,然后加1:

mutableTreeNode.insert(childMutableTreeNode, mutableTreeNode.getChildCount()+1);

至少对于下面将要描述了DefaultMutableTreeNode类来说,setParent()将节点设置子节点的父节点,尽管他并没有子节点作为父节点的一个孩子。换句话说,我们不要自己调用setParent()方法;调用insert()方法,而这个方法会相应的设置父节点。

注意,insert()方法不允许循环祖先,其中子节点被添加为父节点的祖先节点。如果这样做,就会抛出IllegalArgumentException。

DefaultMutableTreeNode类

DefaultMutableTreeNode类提供了MutableTreeNode接口的实现(其实现了TreeNode接口)。当我们由一个Hashtable,数组或是Vector构造函数创建树时,JTree会自动将节点创建为DefaultMutableTreeNode类型集合。另一方面,如果我们希望自己创建节点,我们需要为我们树中的每一个节点创建一个DefaultMutableTreeNode的类型实例。

创建DefaultMutableTreeNode

有三个构造函数可以用来创建DefaultMutableTreeNode实例:

public DefaultMutableTreeNode()
DefaultMutableTreeNode node = new DefaultMutableTreeNode();
public DefaultMutableTreeNode(Object userObject)
DefaultMutableTreeNode node = new DefaultMutableTreeNode("Node");
public DefaultMutableTreeNode(Object userObject, boolean allowsChildren)
DefaultMutableTreeNode node = new DefaultMutableTreeNode("Node", false);

存储在每一个节点中的信息被称之为用户对象。当没有通过构造函数进行指定时,用户对象为null。另外,我们可以指定一个节点是否允许具有子节点。

构建DefaultMutableTreeNode层次

构建DefaultMutableTreeNode类型的节点层次需要创建一个DefaultMutableTreeNode类型的实例,为其孩子创建节点,然后连接他们。在使用DefaultMutableTreeNode直接创建层次之前,首先我们来看一下如何使用新的NamedVector类来创建四个节点的树:一个根节点以及三个叶节点。

Vector vector = new NamedVector("Root", new String[]{ "Mercury", "Venus", "Mars"} );
JTree tree = new JTree(vector);

当JTree获得一个Vector作为其构造函数参数时,树为根节点创建一个DefaultMutableTreeNode,然后为Vector中的每个元素创建一个,使得每一个元素节点成为根节点的子节点。不幸的是,根节点的数据并不是我们所指定的Root,而是没有显示的root。

相反,如果我们希望使用DefaultMutableTreeNode来手动创建一个棵树的节点,或者是我们希望显示根节点,则需要更多的一些代码行,如下所示:

DefaultMutableTreeNode root = new DefaultMutableTreeNode("Root");
DefaultMutableTreeNode mercury = new DefaultMutableTreeNode("Mercury");
root.insert(mercury, 0);
DefaultMutableTreeNode venus = new DefaultMutableTreeNode("Venus");
root.insert(venus, 1);
DefaultMutableTreeNode mars = new DefaultMutableTreeNode("Mars");
root.insert(mars, 2);
JTree tree = new JTree(root);

除了使用MutableTreeNode中的insert()方法来将一个节点关联到父节点,DefaultMutableTreeNode还有一个add()方法可以自动的将子节点添加到尾部,而不是需要提供索引。

DefaultMutableTreeNode root = new DefaultMutableTreeNode("Root");
DefaultMutableTreeNode mercury = new DefaultMutableTreeNode("Mercury");
root.add(mercury);
DefaultMutableTreeNode venus = new DefaultMutableTreeNode("Venus");
root.add(venus);
DefaultMutableTreeNode mars = new DefaultMutableTreeNode("Mars");
root.add(mars);
JTree tree = new JTree(root);

前面的两个代码块都可以创建如图17-17所示的树。

Swing_17_17.png

Swing_17_17.png

如果我们并不需要根节点并且希望我们使用NamedVector作为树根节点的行为,我们可以使用下面的代码来实现:

String elements[] = { "Mercury", "Venus", "Mars"} ;
JTree tree = new JTree(elements);

DefaultMutableTreeNode属性

如表17-5所示,DefaultMutableTreeNode有22个属性。大多数的属性都是只读的,使得我们可以确定关于树节点位置与关系的信息。userObject属性包含特定节点的数据,该节点是在节点被创建时提供给DefaultMutableTreeNode的节点。userObjectPath属性包含一个用户对象数组,由根(索引0)到当前节点(可以为根)。

Swing_table_17_5_1.png

Swing_table_17_5_1.png

Swing_table_17_5_2.png

Swing_table_17_5_2.png

查询节点关系

DefaultMutableTreeNode类提供了若干方法来确定两个节点之间的关系。另外,我们可以使用下面的方法来确定两个节点是否共享相同的父节点:

  • isNodeAncestor(TreeNode aNode):如果aNode是当前节点划当前节点的父节点则返回true。这会迭代检测getParent()方法直到aNode或遇到null为止。
  • isNodeChild(Tree aNode):如果当前节点为aNode的父节点则返回true。
  • isNodeDescendant(DefaultMutableTreeNode aNode):如果当前节点为aNode或是aNode的祖先节点时返回true。
  • isNodeRelated(DefaultMutableTreeNode aNode):如果当前节点与aNode共享相同的根节点则返回true。
  • isNodeSibling(TreeNode aNode):如果两个节点共享相同的父节点则返回true。

每个方法都返回一个boolean值,表明节点之间的关系是否存在。

如果两个节点相关,我们可以请求树的根查找共享的祖先节点。然而,这个祖先节点也许并不是树中最近的祖先节点。如果一个普通的节点位于树中的较低层次,我们可以使用public TreeNode getSharedAncestor(DefaultMutableTreeNode aNode)方法来获得其较近祖先节点。如果由于两个节点不在同一棵树中而不存在,则会返回null。

遍历树

TreeNode接口与DefaultMutableTreeNode类提供了若干遍历特定节点以下所有节点的方法。给定一个特定的TreeNode,我们可以通过每个节点的children()方法遍历到每一个子孙节点,包括初始节点。给定一个特定的DefaultMutableTreeNode,我们可以通过getNextNode()与getPreviousNode()方法查找所有的子孙节点直到没有额外的节点为止。下面的代码片段演示了在给定一个起始节点的情况下使用TreeNode的children()方示来遍历整个树。

public void printDescendants(TreeNode root) {
  System.out.println(root);
  Enumeration children = root.children();
  if (children != null) {
    while(children.hasMoreElements()) {
      printDescendants((TreeNode)children.nextElement());
    }
  }
}

尽管TreeNode的DefaultMutableTreeNode实现允许我们通过getNextNode()与getPreviousNode()方法来遍历树,但是这些方法效率低下,是应该避免使用的。相反,使用DefaultMutableTreeNode的特殊方法来生成节点所有子节点的Enumeration。在了解特定的方法之前,图17-18显示了一个要遍历的简单树。

Swing_17_18.png

Swing_17_18.png

图17-18有助于我们理解DefaultMutableTreeNode的三个特殊方法。这些方法允许我们使用下面的三种方法之五来遍历树,这些方法中的每一个都是public并且返回一个Enumeration:

  • preOrderEnumeration():返回节点的Enumeration,类似于printDescendants()方法。Enumeration中的第一个节点是节点本身。接下来的节点是节点的第一个子节点,然后是第一个子节点的第一个子节点,依次类推。一旦发现没有子节点的叶子节点,其父节点的下一个子节点会被放入Enumeration中,并且其子节点会被添加到相应的列表中,直到没有节点。由图17-18中的树根开始,遍历将会得到以下列顺序出现的节点的Enumeration:root, New York, Mets, yankess, Rangers, Footabll, Ginats, Jets, Bills, Boston, Red Sox, Celtics, Bruins, Denver, Rockies, Avalanche, Broncos。
  • depthFirstEnumeration()与postOrderEnumeration():返回一个与preOrderEnumeration()行为相反的Enumeration。与首先包含当前节点然后添加子节点不同,这些方法首先添加子节点然后将当前节点添加到Enumeration。对于图17-18中的树,这会生成下列顺序的Enumeration:Mets,Yankees,Rangers,Giants,Jets,Bills,Football,New York,Red Sox,Celtics,Bruins,Boston,Rockies,Avalanche,Broncos,Denver,root。
  • breadthFirstEnumeration():返回一个按层添加的节点的Enumeration。对于图17-18中的树,Enumeration的顺序如下:root,New York,Boston,Denver,Mets,Yankees,Rangers,Football,Red Sox,Celtics,Bruins,Rockies,Avalanche,Broncos,Giants,Jets,Bills。

但是还有一个问题:我们如何获得起始节点?当然,第一个节点可以被选为用户动作的结果,或者是我们可以向TreeModel查询根节点。我们稍后就会探讨TreeModel,但是下面显示了获取根节点的源码。因为TreeNode是唯一可以在树中进行排序的对象可能类型,TreeModel的getRoot()方法会返回一个对象。

TreeModel model = tree.getModel();
Object rootObject = model.getRoot();
if ((rootObject != null) && (rootObject instanceof DefaultMutableTreeNode)) {
  DefaultMutableTreeNode root = (DefaultMutableTreeNode)rootObject;
  ...
}

JTree.DynamicUtilTreeNodes类

JTree包含一个内联类,JTree.DynamicUtilTreeNode,树使用这个类来为我们的对创建节点。DynamicUtilTreeNode是DefaultMutableTreeNode的一个子类,该类只有在我们需要时才会创建子节点。当我们展开父节点或者是我们在树中遍历时会需要子节点。尽管我们通常并没有直接使用这个类,我们也许会发现他的用处。为了进行演示,下面的示例使用Hashtable来为树创建节点。在树的根部并没有可见的节点(使用root的userObject属性设置),根节点有一个Root属性。

DefaultMutableTreeNode root = new DefaultMutableTreeNode("Root");
Hashtable hashtable = new Hashtable();
hashtable.put ("One", args);
hashtable.put ("Two", new String[]{"Mercury", "Venus", "Mars"});
Hashtable innerHashtable = new Hashtable();
Properties props = System.getProperties();
innerHashtable.put (props, props);
innerHashtable.put ("Two", new String[]{"Mercury", "Venus", "Mars"});
hashtable.put ("Three", innerHashtable);
JTree.DynamicUtilTreeNode.createChildren(root, hashtable);
JTree tree = new JTree(root);

上面所列的代码创建了一个与图17-2中的TreeArraySample程序相同的树节点。然而,树中第一层次的结点顺序不同。这是因为在这个示例中节点位于Hashtable中,而不是如TreeArraySample一样位于Vector中。第一层次的树元素是以Hashtable返回的Enumeration的顺序被添加的,而不是按着添加到Vector中的顺序。

Swing_17_19.png

Swing_17_19.png

TreeModel接口

TreeModel接口描述了基本的JTree数据模型结构。他描述了父子聚合关系,允许任何的对象成为父节点或是子节点。树有一个根节点,而所有其他的节点都是这个节点的后代。除了返回关于不同节点的信息以外,模型要求实现类管理TreeModelListener对象列表,从而当模型中的节点发生变化时可以得到通知。其他的方法,valueForPathChanged(),用来提供修改特定位置节点内容的方法。

public interface TreeModel {
  // Properties
  public Object getRoot();
  // Listeners
  public void addTreeModelListener(TreeModelListener l);
  public void removeTreeModelListener(TreeModelListener l);
  // Instance methods
  public Object getChild(Object parent, int index);
  public int getChildCount(Object parent);
  public int getIndexOfChild(Object parent, Object child);
  public boolean isLeaf(Object node);
  public void valueForPathChanged(TreePath path, Object newValue);
}

DefaultTreeModel类

JTree自动创建一个DefaultTreeModel实例来存储其数据模型。DefaultTreeModel类提供了一个在每个节点上使用TreeNode实现的TreeModel接口的实现。

除了实现TreeModel接口的方法并且管理TreeModelListener对象的列表以外,DefaultTreeModel类还添加了一些有用的方法:

  • public void insertNodeInto(MutableTreeNode child, MutableParentNode parent, index int):将子节点添加到父节点的子节点集合中的索引位置。
  • public void removeNodeFromParent(MutableTreeNode node):使得节点由树中移除。
  • public void nodeChanged(TreeNode node):通知模型节点已经发生变化。
  • public void nodesChanged(TreeNode node, int childIndices[]):通知模型节点的子节点已经发生变化。
  • public void nodeStructureChanged(TreeNode node):如果节点及子节点已经发生变化则通知模型。
  • public void nodesWereInserted(TreeNode node, int childIndices[]):通知模型节点作为树节点的子节点被插入。
  • public void nodesWereRemoved(TreeNode node, int childIndices[], Object removedChildren[]):通知模型子节点已经被由树中移除并且在方法调用中包含节点作为参数。
  • public void reload()/public void reload(TreeNode node):通知模型节点已经发生了复杂的修改并且由根节点以下或是特定节点以下的模型应重新载入。

第一对方法用于直接由树中添加或是移除节点。其他的方法用于当树节点被修改时通知树模型。如果我们不使用前两个方法的一个由树模型中插入或是移除节点,则我们要负责调用第二个集合中的方法。

TreeModelListener接口与TreeModelEvent类

TreeModel使用TreeModelListener来报告模型的变化。当TreeModel发送一个TreeModelEvent,所注册的监听器就会得到通知。接口包括当节点被插入,移除或是修改时的通知方法,以及当这些操作中的一个或是全部依次完成时的捕获方法。

public interface TreeModelListener implements EventListener {
  public void treeNodesChanged(TreeModelEvent treeModelEvent);
  public void treeNodesInserted(TreeModelEvent treeModelEvent);
  public void treeNodesRemoved(TreeModelEvent treeModelEvent);
  public void treeStructureChanged(TreeModelEvent treeModelEvent);
}

TreeSelectionModel接口

除了所有的树支持用于排序节点的数据模型,用于显示节点的渲染器,以及用于编辑的编辑器以外,还有一个名为TreeSelectionModel的用于树元素选取操作的数据模型。TreeSelectionModel接口包含用来描述到选定节点的选定路径集合的方法。每一个路径存储在TreePath中,其中包含由根对象到选定节点的树节点的路径。我们会在稍后探讨TreePath类。

public interface TreeSelectionModel {
  // Constants
  public final static int CONTIGUOUS_TREE_SELECTION;
  public final static int DISCONTIGUOUS_TREE_SELECTION;
  public final static int SINGLE_TREE_SELECTION;
  // Properties
  public TreePath getLeadSelectionPath();
  public int getLeadSelectionRow();
  public int getMaxSelectionRow();
  public int getMinSelectionRow();
  public RowMapper getRowMapper();
  public void setRowMapper(RowMapper newMapper);
  public int getSelectionCount();
  public boolean isSelectionEmpty();
  public int getSelectionMode();
  public void setSelectionMode(int mode);
  public TreePath getSelectionPath();
  public void setSelectionPath(TreePath path);
  public TreePath[] getSelectionPaths();
  public void setSelectionPaths(TreePath paths[]);
  public int[] getSelectionRows();
  // Listeners
  public void addPropertyChangeListener(PropertyChangeListener listener);
  public void removePropertyChangeListener(PropertyChangeListener listener);
  public void addTreeSelectionListener(TreeSelectionListener listener);
  public void removeTreeSelectionListener(TreeSelectionListener listener);
  // Other methods
  public void addSelectionPath(TreePath path);
  public void addSelectionPaths(TreePath paths[]);
  public void clearSelection();
  public boolean isPathSelected(TreePath path);
  public boolean isRowSelected(int row);
  public void removeSelectionPath(TreePath path);
  public void removeSelectionPaths(TreePath paths[]);
  public void resetRowSelection();
}

TreeSelectiomModel接口支持三种选择模式,每一种模型由一个类常量指定:CONTIGUOUS_TREE_SELECTION, DISCONTIGUOUS_TREE_SELECTION或是SINGLE_TREE_SELECTION。当选择模式是CONTIGUOUS_TREE_SELECTION时,只有彼此相连的节点才会被同时选中。DISCONTIGUOUS_TREE_SELECTION模式意味着并没有同时选中的限制。而另一种选择模式,SIGNLE_TREE_SELECTION,每次只能选中一个节点。如果我们不希望任何内容被选中,可以使用null设置。这会使用受保护的JTree.EmptySelectionModel类。

注意,用于选择多个节点的按键是特定于观感类型的。尝试使用Ctrl或中Shift键组合来选择多个节点。

除了修改选择模式以外,其他的方法允许我们监视选择牟属性。有些这些方法会使用行数,而有时这些方法会使用TreePath对象。选择模型使用RowMapper来为我们将行映射到路径。抽象的AbstractLayoutCache类提供了一个RowMapper接口的基本实现,并且由FixedHeightLayoutCache与VariableHeightLayoutCache类进行特例化。我们并不需要访问或是修改RowMapper或是其实现。要将行映射到路径(或是将路径映射到行),我们仅需要请求树即可。

DefaultTreeSelectionModel类

DefaultTreeSelectionModel类提供了TreeSelectionModel接口的实现,这个实现初始时使用DISCONTIGUOUS_TREE_SELECTION模式并且支持所有三种选择模式。这个类引入了一些自己的方法用来获取监听器列表;其他的方法仅是实现了所有的TreeSelectionModel接口方法,包括访问表17-6中所列的11个属性的方法。另外,DefaultTreeSelectionModel重写了Object的clone()方法,从而可以Cloneable。

Swing_table_17_6.png

Swing_table_17_6.png

使用TreeSelectionModel的主要原因在于修改模型的选择模式。例如,下面的两行代码将模式修改为单选模式:

TreeSelectionModel selectionModel = tree.getSelectionModel();
selectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);

如果我们对找出选定路径感兴趣,我们可以直接请求JTree。我们并不需要由模型获取选定路径。

TreeSelectionListener接口与TreeSelectionEvent类

当树中的选定节点集合变化时,就会生成一个TreeSelectionEvent并且TreeSelectionModel所注册的TreeSelectionListener对象会得到通知。TreeSelectionListener可以注册到JTree或是直接注册到TreeSelectionModel。该接口定义如下:

public interface TreeSelectionListener implements EventListener {
  public void valueChanged(TreeSelectionEvent treeSelectionEvent);
}

TreePath类

我们要探讨的最后一个类就是TreePath。在本章前面的例子中我们已经多次使用这个类。他描述了一个由根节点到另一个节点映射路径的只读节点集合,这里的根可以是一个子树的根也可以是整棵树的根。尽管有两个构造函数可以用来创建TreePath对象,我们通常仅其作为方法的返回值进行处理。我们也可以通过使用public TreePath pathByAddingChild(Object child)方法向已存在的TreePath添加元素来创建新的路径。

TreePath可以作为是一个Object数组,其中数组的第一个元素是树的根,而最后一个元素被最后的路径组件。在这两者之间是连接他们的组件。通常情况下,数组的元素是TreeNode类型。然而,因为TreeModel支持任意类型的对象,TreePath的path属性被定义为Object节点数组。表17-7列出了四个TreePath属性。

Swing_table_17_7.png

Swing_table_17_7.png

为了更好的理解TreePath,我们重用图17-18所示的树遍历示例,如图17-20所示。

Swing_17_20.png

Swing_17_20.png

Additional Expansion Events

还有两个可以注册到JTree监听器需要探讨:一个TreeExpansionListner与一个TreeWillExapndListener。

TreeExpansionaListener接口与TreeExpansionEvent类

如果我们对确定一个树节点何时展开与折叠,我们可以向树注册一个TreeExpansionListner。在父节点被展开或是折叠之后所注册的监听器就会得到通知。

public interface TreeExpansionListener implements EventListener {
  public void treeCollapse(TreeExpansionEvent treeExpansionEvent);
  public void treeExpand(TreeExpansionEvent treeExpansionEvent);
}

每一个方法都有一个TreeExpansionEvent作为其参数。TreeExapnsionEvent类只有一个用于获取到展开或是折叠节点路径的方法:public TreePath getPath()。

TreeWillExpandListener接口与ExpandVetoException类

JTree支持TreeWillExpandListener的注册,其定义如下:

public interface TreeWillExpandListener implements EventListener {
  public void treeWillCollapse(TreeExpansionEvent treeExpansionEvent)
    throws ExpandVetoException;
  public void treeWillExpand(TreeExpansionEvent treeExpansionEvent)
    throws ExpandVetoException;
}

这两个方法签名类似于TreeExpansionListener,并且他们都抛出ExpandVetoException。在父节点展开或是折叠之前所注册的监听器会得到通知。如果监听器不希望展开或是折叠发生,监听器可以抛出异常拒绝请求,阻止节点打开或是关闭。

为了演示TreeWillExpandListneer,下面的代码不会允许sports节点在默认的数据模型中展开或是colors节点折叠。

TreeWillExpandListener treeWillExpandListener = new TreeWillExpandListener() {
  public void treeWillCollapse(TreeExpansionEvent treeExpansionEvent)
      throws ExpandVetoException {
    TreePath path = treeExpansionEvent.getPath();
    DefaultMutableTreeNode node =
      (DefaultMutableTreeNode)path.getLastPathComponent();
    String data = node.getUserObject().toString();
    if (data.equals("colors")) {
      throw new ExpandVetoException(treeExpansionEvent);
    }
  }
  public void treeWillExpand(TreeExpansionEvent treeExpansionEvent)
      throws ExpandVetoException {
    TreePath path = treeExpansionEvent.getPath();
    DefaultMutableTreeNode node =
      (DefaultMutableTreeNode)path.getLastPathComponent();
    String data = node.getUserObject().toString();
    if (data.equals("sports")) {
      throw new ExpandVetoException(treeExpansionEvent);
    }
  }
};

不要忘记使用类似下面的代码将监听器注册到树中:

tree.addTreeWillExpandListener(treeWillExpandListener)

小结

在本章中,我们了解了与JTree组件使用相关的多个类。我们了解了使用TreeCellRenderer接口与DefaultTreeCellRenderer实现的树节点渲染。我们深入了使用TreeCellEditor接口,DefaultCellEditor与DefaultTreeCellEditor实现的树节点编辑。

在审视了如何显示与编辑树之后,我们了解了用于手动创建树对象的TreeNode接口,MutableTreeNode接口,与DefaultMutableTreeNode类。我们探讨了用于存储树数据模型的的TreeModel接口与DefaultTreeModel实现,以及用于存储树选择模型的TreeSelectionModel接口与DefaultTreeSelectionModel实现。

另外,我们了解了用于各种树类的事件相关类以及用于描述节点连接路径的TreePath。

在第18章,我们将会探讨javax.swing.table包及其用于JTable组件的多个类。

Tables

在第17章中,我们深入了解了Swing的JTree组件。在本章中,我们将会探讨JTable组件的细节。该组件是用于以网络的形式显示二维数据的标准Swing组件。

Intoducing Tables

图18-1显示了JTable的一个简单示例。我们可以看到这个示例包含日本字体。为了能够看到本章示例程序中的Kanji表意文字,我们需要安装必须的日本字体。然而,在没有配置我们的环境来显示日本字体的情况下这些示例也可以正常工作,但是我们并不能看到表意文字,相反我们会看到问号或是方框,这依据于我们的平台而定。

Swing_18_1.png

Swing_18_1.png

类似于JTree组件,JTable组件依赖于各种用于其内部工作的支持类。对于JTable,支持类位于javax.swing.table包中。JTable中的单元可以通过行,列,行与列,或是单个单元来选定。当前 的ListSelectionModel设置负责控制表格的选定。

表格中不同单元格的显示是TableCellRenderer的职责;DefaultCellRenderer以JLabel子类的形式提供了一个TableCellRenderer接口的实现。

管理存储在单元格中的数据是通过TableModel接口实现来实现的。AbstractTableModel提供了提供了一个不具有数据存储功能的接口实现的基础。通过对比,DefaultTableModel封装了TableModel接口并且使用Vector对象用于数据存储。如果我们需要一个不同的存储类型而不是DefaultTableModel提供的类型,则我们需要扩展AbstractTableModel;例如,如果我们已经在我们自己的数据结构中存储了数据。

TableColumnModel接口与此接口的DefaultTableColumnModel实现将表格的数据作为一系列的列来管理。他们配合TableColumn类使用从而为单个列的管理提供更多的灵活性。例如,我们可以以不同于在JTable中的显示顺序来在TableModel中存储数据列。TableColumnModel管得另一个ListSelectionModel来控制表格列的选定。

在每一列的顶部是一个列头。默认情况下,TableColumn类依据JTableHeader类来渲染一个文本列头。然而,我们必须将JTable嵌入到一个滚动面板中来查看默认列头。

JTable中的单元格是可编辑的。如果一个单元格是可编辑的,编辑如何作用依赖于TableCellEditor实现,例如DefaultCellEditor实现,其扩展了AbstractCellEditor。另外,并不存在用于处理单个行的类。行必须通过单元格来处理。在幕后,JTable使用SizeSequence实用类来处理高度变化的行;我们并不需要自己来处理。

JTable组件所用的元素之间还存在一些其他的关系。这些关系会在本章稍后的特定接口与类中进行探讨。

要展示JTable元素如何组合在一起,请参看图18-2。

Swing_18_2.png

Swing_18_2.png

JTable类

首先我们来了解JTable类,他为我们提供了以表格的形式显示数据的方法。(参看图18-1与18-2)。

创建JTable

我们可以有七种不同的方法来创建JTable。这些构造函数可以使得我们使用多种数据源来创建表格。

public JTable()
JTable table = new JTable();
public JTable(int rows, int columns)
JTable table = new JTable(2, 3);
public JTable(Object rowData[][], Object columnNames[])
Object rowData[][] = { { "Row1-Column1", "Row1-Column2", "Row1-Column3"},
  { "Row2-Column1", "Row2-Column2", "Row2-Column3"} };
Object columnNames[] = { "Column One", "Column Two", "Column Three"};
JTable table = new JTable(rowData, columnNames);
public JTable(Vector rowData, Vector columnNames)
Vector rowOne = new Vector();
rowOne.addElement("Row1-Column1");
rowOne.addElement("Row1-Column2");
rowOne.addElement("Row1-Column3");
Vector rowTwo = new Vector();
rowTwo.addElement("Row2-Column1");
rowTwo.addElement("Row2-Column2");
rowTwo.addElement("Row2-Column3");
Vector rowData = new Vector();
rowData.addElement(rowOne);
rowData.addElement(rowTwo);
Vector columnNames = new Vector();
columnNames.addElement("Column One");
columnNames.addElement("Column Two");
columnNames.addElement("Column Three");
JTable table = new JTable(rowData, columnNames);
public JTable(TableModel model)
TableModel model = new DefaultTableModel(rowData, columnNames);
JTable table = new JTable(model);
public JTable(TableModel model, TableColumnModel columnModel)
// Swaps column order
TableColumnModel columnModel = new DefaultTableColumnModel();
TableColumn firstColumn = new TableColumn(1);
firstColumn.setHeaderValue(headers[1]);
columnModel.addColumn(firstColumn);
TableColumn secondColumn = new TableColumn(0);
secondColumn.setHeaderValue(headers[0]);
columnModel.addColumn(secondColumn);
JTable table = new JTable(model, columnModel);
public JTable(TableModel model, TableColumnModel columnModel,
  ListSelectionModel selectionModel)
// Set single selection mode
ListSelectionModel selectionModel = new DefaultListSelectionModel();
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
JTable table = new JTable(model, columnModel, selectionModel);

无参数的构造函数会创建一个没有行与列的表格。第二个构造函数带有两个参数来创建一个具有行与列的空表。

注意,由JTable构造函数所创建的单元格是可编辑的,而不是只读的。要在代码中修改其内容,只需要调用JTable的public void setValueAt(Object value, int row, int column)方法。

当我们的数据已经位于一个特定的结构形式中时,接下来的两个方法就会十分有用。例如,如果我们的数据位于数组或是Vector对象中时,我们可以创建一个JTable而不需要创建我们自己的TableModel。一个两行三列的表格可以使用数组 { { “Row1-Column1”, “Row1-Column2”, “Row1-Column3”}, { “Row2-Column1”, “Row2-Column2”, “Row2-Column3”} }来创建,并使用另一个数组来存储表头名字。类似的数据结构对于基于向量的构建函数也是必须的。

其余的三个构造函数使用JTable特定的数据结构。如果忽略三个参数中的任意一个,则会使用默认的设置。例如,如果我们没有指定TableColumnModel,则会使用默认实现DefaultTableColumnModel,并且会使用TableModel的列顺序来自动填充显示顺序。如果忽略选择模型,则ListSelectionModel会全使用多行选择模型,这就意味着非连续行而不是列可以被选中。

滚动JTable组件

类似于其他的需要更多可用空间的组件,JTable组件实现了Scrollable接口并且应放置在一个JScrollPane中。当JTable对于可用的屏幕状态过大时,滚动条会出现在JScrollPane中,并且列头的名字会出一在每一列的上方。图18-3显示了图18-1中的表没有位于JScrollPane中的显示结果。注意,列头与滚动条都没有出现。这意味着我们不能确定数据的意义,也不能滚动到未显示的行。

Swing_18_3.png

Swing_18_3.png

所以,我们所创建的每一个表格需要通过类似于下面的代码来将其放置在JScrollPane中:

JTable table = new JTable(...);
JScrollPane scrollPane = new JScrollPane(table);

手动放置JTable视图

当位于JScrollPane中的JTable被添加到窗口时,表格会自动显示在表格位置,所以第一行与第一列出现在左上角。如果我们需要将位置调整为原点,我们可以将视窗位置设置回点(0,0)。

为了滚动的目的,依据滚动条的方向,块增长量是视窗的可见宽度与高度。对于水平滚动是100像素,而对于垂直滚动则是单个行的高度。图18-4显示了这些增量的可视化表示。

Swing_18_4.png

Swing_18_4.png

移除列头

如前所述,将JTable放在JScrollPane中会自动为不同的列名生成列头标签。如果我们不需要列头,我们可以使用多种方法来移除。图18-5显示了一个没有列头的表格的示例。

Swing_18_5.png

Swing_18_5.png

移除列头最简单的方法就是提供一个空字符串作为列头名。使用前面七个构造函数列表中的第三个JTable构造函数,就会将三个列名替换为”“空字符串。

Object rowData[][] = {{"Row1-Column1", "Row1-Column2", "Row1-Column3"},
  {"Row2-Column1", "Row2-Column2", "Row2-Column3"}};
Object columnNames[] = { "", "", ""};
JTable table = new JTable(rowData, columnNames);
JScrollPane scrollPane = new JScrollPane(table);

因为这种移除列头的方法同时也移除了不同列的描述,也许我们会希望另一种隐藏列头的方法。最简单的方法就是我们告诉JTable我们并不需要表格头:

table.setTableHeader(null);

我们也可以通过继承JTable类并且覆盖受保护的configureEnclosingScrollPane()方法来移除列头,或者是告诉每一个TableColumn其列头值为空。这些是实现相同任务更为复杂的方法。

注意,调用scrollPane.setColumnHeaderView(null)方法并不清除列头。相反,他会使得JScrollPane使用默认的列头。

JTable属性

如表18-1所示,JTable有许多属性,共计40个。这40个属性是对由JComponent,Container与Component类继承所得属性的补充。

Swing_table_18_1_1.png

Swing_table_18_1_1.png

Swing_table_18_1_2.png

Swing_table_18_1_2.png

Swing_table_18_1_3.png

Swing_table_18_1_3.png

注意,行的高度并不是固定的。我们可以使用public void setRowHeight(int row, int rowHeight)方法来修改单个行的高度。

大多数的JTable属性以下三类中的一种:显示设置,选择设置以及自动尺寸调整设置。

显示设置

表18-1中第一个属性子集合允许我们设置各种JTable显示选项。除了由Component继承的foreground与background属性以外,我们可以修改选择前景(selectionForeground)与背景(selectionBackground)颜色。我们可以控制显示哪一个网格线(showGrid)及其颜色(gridColor)。intercellSpacing属性设置处理表格单元之间的额外空间。

选择模式

我们可以使用JTable三种不同的选择模式类型中的一种。我们可以一次选择一行表格元素,一次选择一列表格元素,或是一次选择一个单元格。这三种设置是通过rowSelectionAllowed,columnSelectionAllowed以及cellSelectionEnabled属性来控制的。初始时,仅允许行选择模式。因为默认的ListSelectionModel位于多选模式,我们可以一次选中多行。如果我们不喜欢多选模式,我们可以修改JTable的selectionMode属性,从而使得JTable的行与列选择模式相应的发生变化。当同时允许行选择与列选择时,就会允许单元格选择。

如果我们对JTable的行或是列是否被选中感兴趣,我们可以查询JTable的下列六个属性:selectedColumnCount, selectedColumn, selectedColumns, selectedRowCounts, selectedRow以及selectedRows。ListSelectionModel类为不同的选择模式提供相应的常量。ListSelectionModel接口与DefaultListSelectionModel类已经在第13章中的JList组件信息中进行探讨。他们被用来描述JTable组件中的行与列。他们具有三个设置:

  • MULTIPLE_INTERVAL_SELECTION (the default)

•SINGLE_INTERVAL_SELECTION •SINGLE_SELECTION

JTable对于行与列具有独立的选择模式。行选择模式被存储在JTable中的selectionModel属性中。列选择模式被存储在TableColumnModel属性。设置JTable的selectionMode属性会为两个独立的JTable选择模式设置选择模式。

一旦设置了选择模式并且用户与组件进行交互,我们可以向选择模型询问发生了什么,或是更确切的,用户选择了什么。表18-2列出了使用DefaultListSelectionModel的可用属性。

Swing_table_18_2.png

Swing_table_18_2.png

如果我们对于了解何时发生选择事件感兴趣,则我们可以向ListSelectionModel注册一个ListSelectionListener。ListSelectionListener在第13中JList组件中进行演示了。

注意,所有的表格索引都是由0开始的。所以第一个可见的列是第0列。

尺寸自动调整模式

JTable属性的最后一个子集处理JTable的列尺寸调整行为。当JTable位于一个尺寸变化的列或是窗口中时,则其如何响应呢?表18-3显示了JTable所支持的五个设置。

Swing_table_18_3.png

Swing_table_18_3.png

列表18-1演示了当调整表格列时每一种设置如何响应。

package swingstudy.ch18;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;

import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;

public class ResizeTable {

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

        final Object rowData[][] = {
                {"1", "one", "ichi - \u4E00", "un", "I"},
                {"2", "two", "ni -\u4E8C", "deux", "II"},
                {"3", "three", "san - \u4E09", "trois", "III"},
                {"4", "four", "shi - \u56DB", "quatre", "IV"},
                {"5", "five", "go - \u4E94", "cinq", "V"},
                {"6", "six", "roku - \u516D", "treiza", "VI"},
                {"7", "seven", "shichi - \u4E03", "sept", "VII"},
                {"8", "eight", "hachi - \u516B", "huit", "VIII"},
                {"9", "nine", "kyu - \u4E5D", "neur", "IX"},
                {"10", "ten", "ju - \u5341", "dix", "X"}
        };

        final String columnNames[] = {"#", "English", "Japanese", "French", "Roman"};

        Runnable runner = new Runnable() {
            public void run() {
                final JTable table= new JTable(rowData, columnNames);
                JScrollPane scrollPane = new JScrollPane(table);

                String modes[] = {"Resize All Columns", "Resize Last Column", "Resize Next Column", "Resize Off", "Resize Susequent Columns"};

                final int modeKey[] = {
                    JTable.AUTO_RESIZE_ALL_COLUMNS,
                    JTable.AUTO_RESIZE_LAST_COLUMN,
                    JTable.AUTO_RESIZE_NEXT_COLUMN,
                    JTable.AUTO_RESIZE_OFF,
                    JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS
                };

                JComboBox resizeModeComboBox = new JComboBox(modes);
                int defaultMode = 4;
                table.setAutoResizeMode(modeKey[defaultMode]);
                resizeModeComboBox.setSelectedIndex(defaultMode);
                ItemListener itemListener = new ItemListener() {
                    public void itemStateChanged(ItemEvent e) {
                        JComboBox source = (JComboBox)e.getSource();
                        int index = source.getSelectedIndex();
                        table.setAutoResizeMode(modeKey[index]);
                    }
                };
                resizeModeComboBox.addItemListener(itemListener);

                JFrame frame = new JFrame("Resizing Table");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                frame.add(resizeModeComboBox, BorderLayout.NORTH);
                frame.add(scrollPane, BorderLayout.CENTER);

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

        EventQueue.invokeLater(runner);
    }

}

图18-6显示了程序初始时的显示。变化JComboBox,从而我们就可以修改列的尺寸调整行为。

Swing_18_6.png

Swing_18_6.png

渲染表格单元

默认情况下,表格数据的渲染是通过JLabel完成的。存储在表格中的值被作为文本字符串进行渲染。同时也为Date与Number子类等类安装了的额外的默认渲染器,但是他们并没有被激活。我们将会在本章稍后的章节中了解如何激活这些渲染器。

使用TableCellRenderer接口与DefaultTableCellRenderer类

TableCellRenderer接口定义了一个唯一的方法。

public interface TableCellRenderer {
  public Component getTableCellRendererComponent(JTable table, Object value,
    boolean isSelected, boolean hasFocus, int row, int column);
}

通过使用指定给getTableCellRendererComponent()方法的信息,则会创建合适的渲染器组件并且使用其特定的方法来显示JTable的相应内容。“合适”意味着反映我们决定显示的表格单元状态的渲染器,例如当我们需要区别显示选中的表格单元与未选中的表格单元,或者是当表格单元获得输入焦点时,我们希望选中的单元如何显示等。

要查看一个简单的演示,如图18-7所示,其中依据渲染器所在的行显示了不同的颜色。

Swing_18_7.png

Swing_18_7.png

用于生成图18-7示例的自定义渲染器的代码显示在列表18-2中。

package swingstudy.ch18;

import java.awt.Color;
import java.awt.Component;

import javax.swing.JTable;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;

public class EvenOddRenderer implements TableCellRenderer {

    public static final DefaultTableCellRenderer DEFAULT_RENDERER = new DefaultTableCellRenderer();

    @Override
    public Component getTableCellRendererComponent(JTable table, Object value,
            boolean isSelected, boolean hasFocus, int row, int column) {
        // TODO Auto-generated method stub
        Component renderer = DEFAULT_RENDERER.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
        Color foreground, background;
        if(isSelected) {
            foreground = Color.YELLOW;
            background = Color.GREEN;
        }
        else {
            if(row%2==0) {
                foreground = Color.BLUE;
                background = Color.WHITE;
            }
            else {
                foreground = Color.WHITE;
                background = Color.BLUE;
            }
        }
        renderer.setForeground(foreground);
        renderer.setBackground(background);
        return renderer;
    }

}

表格的渲染器可以为单个类或是特定的列而安装。要将渲染器安装为JTable的默认渲染器,换句话说,对于Object.class,使用类似下面的代码:

TableCellRenderer renderer = new EvenOddRenderer();
table.setDefaultRenderer(Object.class, renderer);

一旦安装,EvenOddRenderer将会用于其类不具有特定渲染器的任意列。TableModel的public Class getColumnClass()方法负责返回用作特定列中所有表格单元渲染器的类。DefaultTableModel为所有表格单元返回Object.class;所以,EvenOddRenderer将会用于所有的表格单元。

使用EvenOddRenderer来生成图18-7示例的示例程序显示在列表18-3中。

package swingstudy.ch18;

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

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.TableCellRenderer;

public class RendererSample {

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

        final Object rows[][] = {
                {"one", "ichi - \u4E00"},
                {"two", "ni - \u4E8C"},
                {"three", "san - \u4E09"},
                {"four", "shi - \u56DB"},
                {"fiv", "go - \u4E94"},
                {"six", "roku - \u516D"},
                {"seven", "shichi - \u4E03"},
                {"eight", "hachi - \u516B"},
                {"nine", "kyu - \u4E5D"},
                {"ten", "ju - \u5341"}
        };

        final Object headers[] = {"English", "Japanese"};

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Renderer Sample");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                JTable table = new JTable(rows, headers);
                TableCellRenderer renderer = new EvenOddRenderer();
                table.setDefaultRenderer(Object.class, renderer);
                JScrollPane scrollPane = new JScrollPane(table);
                frame.add(scrollPane, BorderLayout.CENTER);
                frame.setSize(300, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

使用工具提示

默认情况下,我们的表格单元将会显示我们配置其显示的工具提示文本。与JTree组件不同,我们并不需要手动向表格注册ToolTipManager。然而,如果我们的表格不显示工具提示文本,如果我们使用类似下面的代码来取消ToolTipManager的注册,表格的响应就会更为迅速:

// Explicitly
ToolTipManager.sharedInstance().unregisterComponent(aTable);
// Implicitly
yourTable.setToolTipText(null);

处理JTable事件

并没有我们可以直接注册到JTable的JTable事件。要确定某件事情何时发生,我们必须注册到JTable的模型类:TableModel,TableColumnModel或是ListSelectionModel。

自定义JTable观感

每一个可安装的Swing观感都提供了不同的JTable外观与默认的UIResource值设置集合。图18-8显示了预安装的观感类型Motif,Windows与Ocean的JTable组件外观。在图所示的三个观感中,第三行是高亮显示的,而第一列的颜色显示正在编辑状态。

Swing_18_8.png

Swing_18_8.png

JTable可用的UIResource相关的属性集合显示在表18-4中。JTable组件有21个不同的属性。

Swing_table_18_4_1.png

Swing_table_18_4_1.png

Swing_table_18_4_2.png

Swing_table_18_4_2.png

TableMode接口

现在我们已经了解了JTable组件的基础,现在我们可以了解其内部是如何管理数据元素的了。他是借助于实现了TableModel接口的类来完成的。

TableModel接口定义了JTable查询列头与表格单元值,并且当表格可编辑时修改单元值所需要的框架。其定义如下:

public interface TableModel {
  // Listeners
  public void addTableModelListener(TableModelListener l);
  public void removeTableModelListener(TableModelListener l);
  // Properties
  public int getColumnCount();
  public int getRowCount();
  // Other methods
  public Class getColumnClass(int columnIndex);
  public String getColumnName(int columnIndex);
  public Object getValueAt(int rowIndex, int columnIndex);
  public boolean isCellEditable(int rowIndex, int columnIndex);
  public void setValueAt(Object vValue, int rowIndex, int columnIndex);
}

AbstractTableModel类

AbstractTableModel类提供了TableModel接口的基本实现。他管理TableModelListener列表以及一些TableModel方法的默认实现。当我们派生这个类时,我们所需要提供的就是实际列与行的计数以及表格模型中的特定值(getValueAt())。列名默认为为如A,B,C,...,Z,AA,BB之类的标签,并且数据模型是只读的,除非isCellEditable()被重写。

如果我们派生AbstractTableModel并且使得数据模型是可编辑的,那么我们就要负责调用AbstractTableModel中的fireXXX()方法来保证当数据模型发生变化时TableModelListener对象都会得到通知:

public void fireTableCellUpdated(int row, int column);
public void fireTableChanged(TableModelEvent e);
public void fireTableDataChanged();
public void fireTableRowsDeleted(int firstRow, int lastRow);
public void fireTableRowsInserted(int firstRow, int lastRow);
public void fireTableRowsUpdated(int firstRow, int lastRow);
public void fireTableStructureChanged();

当我们需要创建一个JTable时,为了重用已有的数据结构而派生AbstractTableModel并不常见。这个数据结构通常是来自JDBC查询的结果,但是并没有限制必须是这种情况。为了进行演示,下面的匿名类定义显示了我们如何将一个数据看作一个AbstractTableModel:

TableModel model = new AbstractTableModel() {
  Object rowData[][] = {
    {"one",   "ichi"},
    {"two",   "ni"},
    {"three", "san"},
    {"four",  "shi"},
    {"five",  "go"},
    {"six",   "roku"},
    {"seven", "shichi"},
    {"eight", "hachi"},
    {"nine",  "kyu"},
    {"ten",   "ju"}
  };
  Object columnNames[] = {"English", "Japanese"};
  public String getColumnName(int column) {
    return columnNames[column].toString();
  }
  public int getRowCount() {
    return rowData.length;
  }
  public int getColumnCount() {
    return columnNames.length;
  }
  public Object getValueAt(int row, int col) {
    return rowData[row][col];
  }
};
JTable table = new JTable(model);
JScrollPane scrollPane = new JScrollPane(table);

指定固定的JTable列

现在我们已经了解了TableModel与AbstractTableModel是如何描述数据的基础了,现在我们可以创建一个JTable了,其中一些列是固定的,而另一些不是。要创建不滚动的列,我们需要将第二个表格放在JScrollPane的行头视图中。然后,当用户垂直滚动表格时,两个表格就会保持同步。两个表格需要共享他们的ListSelectionModel。 这样,当一个表格中的一行被选中时,另一个表格中的行也会自动被选中。图18-9显示了具有一个固定列与四个滚动列的表格。

Swing_18_9.png

Swing_18_9.png

生成图18-9示例的源代码显示在列表18-4中。

package swingstudy.ch18;

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

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JViewport;
import javax.swing.ListSelectionModel;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableModel;

public class FixedTable {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        final Object rowData[][] = {
                {"1", "one", "ichi", "un", "I", "\u4E00"},
                {"2", "two", "ni", "deux", "II", "\u4E8C"},
                {"3", "three", "san", "trois", "III", "\u4E09"},
                {"4", "four", "shi", "quatre", "IV", "\u56DB"},
                {"5", "five", "go", "cinq", "V", "\u4E94"},
                {"6", "six", "roku", "treiza", "VI", "\u516D"},
                {"7", "seven", "shichi", "sept", "VII", "\u4E03"},
                {"8", "eight", "hachi", "huit", "VIII", "\u516B"},
                {"9", "nine", "kyu", "neur", "IX", "\u4E5D"},
                {"10", "ten", "ju", "dix", "X", "\u5341"}
        };

        final String columnNames[] = {
                "#", "English", "Japanese", "French", "Roman", "Kanji"
        };

        final TableModel fixedColumnModel = new AbstractTableModel() {
            public int getColumnCount() {
                return 1;
            }

            public String getColumnName(int column) {
                return columnNames[column];
            }

            public int getRowCount() {
                return rowData.length;
            }

            public Object getValueAt(int row, int column) {
                return rowData[row][column];
            }
        };

        final TableModel mainModel = new AbstractTableModel() {
            public int getColumnCount() {
                return columnNames.length-1;
            }

            public String getColumnName(int column) {
                return columnNames[column+1];
            }

            public int getRowCount() {
                return rowData.length;
            }

            public Object getValueAt(int row, int column) {
                return rowData[row][column+1];
            }
        };

        Runnable runner = new Runnable() {
            public void run() {
                JTable fixedTable = new JTable(fixedColumnModel);
                fixedTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);

                JTable mainTable = new JTable(mainModel);
                mainTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);

                ListSelectionModel model = fixedTable.getSelectionModel();
                mainTable.setSelectionModel(model);

                JScrollPane scrollPane = new JScrollPane(mainTable);
                Dimension fixedSize = fixedTable.getPreferredSize();
                JViewport viewport = new JViewport();
                viewport.setView(fixedTable);
                viewport.setPreferredSize(fixedSize);
                viewport.setMaximumSize(fixedSize);
                scrollPane.setCorner(JScrollPane.UPPER_LEFT_CORNER, fixedTable.getTableHeader());
                scrollPane.setRowHeaderView(viewport);

                JFrame frame = new JFrame("Fixed Column Table");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(scrollPane, BorderLayout.CENTER);
                frame.setSize(300, 150);
                frame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

激活默认的表格单元渲染器

在前面的章节中,我们提到,JTable为Date与Number类提供了默认渲染器。现在我们了解一下AbstractTableModel类并且了解如何激活这些渲染器。

TableModel的public Class getColumnClass(int column)方法为数据模型中的列返回类类型。如果JTable类为这个特定类安装了特殊的渲染器,则会使用这个渲染器来显示这个类。默认情况下,TableModel的AbstractTableModel(以及DefaultTableModel)实现会为所有的事情返回Object.class。AbstractTableModel类并不会尝试聪明的猜测什么在列中。然而,如果我们知道数据模型中的特定列总是数字,日期或是其他的类,我们可以使得数据模型返回类类型。这就会允许JTable尝试更为聪明并且使用更好的渲染器。

表18-5显示了JTable的预安装的渲染器。例如,如果我们有一个满是数字的表格或是有一个数字列,我们可以重写getColumnClass()来相应的列返回Number.class;我们的数字将会右对齐而不是左对齐。对于日期,为Date类使用默认渲染器会产生更好的观感以及本地化输出。

Swing_table_18_5.png

Swing_table_18_5.png

图18-10显示了激活渲染器之前与之后的样子。

Swing_18_10.png

Swing_18_10.png

我们可以选择为列硬编码类名或是使得getColumnClass()方法通用并且在列元素上调用getClass()方法。将下面的代码添加到AbstractTableModel实现中将会使得JTable使用其默认渲染器。这个实现假定特定列的所有实体是同一个类类型。

public Class getColumnClass(int column) {
  return (getValueAt(0, column).getClass());
}

DefaultTableModel类

DefaultTableModel类是AbstractTableModel的子类,他为存储提供了自己的Vector数据。数据模型中的所有内容在内部都是存储在向量中的即使当数据初始时是数组的一部分也是如此。换句话说,如果我们已经将我们的数据放在一个适当的数据结构中,则不要使用DefaultTableModel。创建一个使用该数据结构的AbstractTableModel,而不要使用DefaultTableModel为我们转换数据结构。

创建DefaultTableModel

有六个构造函数可以用来创建DefaultTableModel:

public DefaultTableModel()
TableModel model = new DefaultTableModel()
public DefaultTableModel(int rows, int columns)
TableModel model = new DefaultTableModel(2, 3)
public DefaultTableModel(Object rowData[][], Object columnNames[])
Object rowData[][] = {{"Row1-Column1", "Row1-Column2", "Row1-Column3"},
  {"Row2-Column1", "Row2-Column2", "Row2-Column3"}};
Object columnNames[] = {"Column One", "Column Two", "Column Three"};
TableModel model = new DefaultTableModel(rowData, columnNames);

public DefaultTableModel(Vector rowData, Vector columnNames)
Vector rowOne = new Vector();
rowOne.addElement("Row1-Column1");
rowOne.addElement("Row1-Column2");
rowOne.addElement("Row1-Column3");
Vector rowTwo = new Vector();
rowTwo.addElement("Row2-Column1");
rowTwo.addElement("Row2-Column2");
rowTwo.addElement("Row2-Column3");
Vector rowData = new Vector();
rowData.addElement(rowOne);
rowData.addElement(rowTwo);
Vector columnNames = new Vector();
columnNames.addElement("Column One");
columnNames.addElement("Column Two");
columnNames.addElement("Column Three");
TableModel model = new DefaultTableModel(rowData, columnNames);

public DefaultTableModel(Object columnNames[], int rows)
TableModel model = new DefaultTableModel(columnNames, 2);
public DefaultTableModel(Vector columnNames, int rows)
TableModel model = new DefaultTableModel(columnNames, 2);

其中四个构造函数直接映射到JTable构造函数,而其余的两个则允许我们由一个列头集合创建一个具有固定行数的空表格。一旦我们创建了DefaultTableModel,我们就可以将传递给JTable构造函数来创建实际的表格,然后将这个表格放在JScrollPane中。

填充DefaultTableModel

如果我们选择使用DefaultTableModel,我们必须使用JTable要显示的数据来进行填充。除了填充数据结构的基本例程以外,还有一些移除数据或是替换整个内容的额外方法:

下面的方法允许我们添加列:

public void addColumn(Object columnName);
public void addColumn(Object columnName, Vector columnData);
public void addColumn(Object columnName, Object columnData[ ]);

使用下面的方法来添加行:

public void addRow(Object rowData[ ]);
public void addRow(Vector rowData);

下面的方法可以插入行:

public void insertRow(int row, Object rowData[ ]);
public void insertRow(int row, Vector rowData);

这个方法可以移除行:

public void removeRow( int row);

最后,我们可以使用下面的方法来替换内容:

public void setDataVector(Object newData[ ][ ], Object columnNames[ ]);
public void setDataVector(Vector newData, Vector columnNames);

DefaultTableModel属性

除了由AbstractTableModel继承的rowCount与columnCount属性以外,DefaultTableModel还有两个其他的属性,如表18-6所示。设置rowCount属性可以使得我们按照我们的意愿扩大或是缩小表格尺寸。如果我们正在扩展模型,其他的行会保持为空。

Swing_table_18_6.png

Swing_table_18_6.png

创建一个稀疏的表格模型

默认的表格模型实现用于填满数据的表格,而不是用于由大多数空表格单元的组成的数据表。当表格中的单元大部分为空时,DefaultTableModel的默认数据结构就会学浪费大量的空间。以为每一个位置创建一个Point为代价,我们可以创建一个使用HashMap的稀疏表格模型。列表18-5演示了这种实现。

package swingstudy.ch18;

import java.awt.Point;
import java.util.HashMap;
import java.util.Map;

import javax.swing.table.AbstractTableModel;

public class SparseTableModel extends AbstractTableModel {

    private Map<Point, Object> lookup;
    private final int rows;
    private final int columns;
    private final String headers[];

    public SparseTableModel(int rows, String columnHeaders[]) {
        if((rows<0) || (columnHeaders == null)) {
            throw new IllegalArgumentException("Invalida row count/columnHeaders");
        }
        this.rows = rows;
        this.columns = columnHeaders.length;
        headers = columnHeaders;
        lookup = new HashMap<Point, Object>();
    }
    @Override
    public int getRowCount() {
        // TODO Auto-generated method stub
        return rows;
    }

    @Override
    public int getColumnCount() {
        // TODO Auto-generated method stub
        return columns;
    }

    public String getColumnName(int column) {
        return headers[column];
    }

    @Override
    public Object getValueAt(int row, int column) {
        // TODO Auto-generated method stub
        return lookup.get(new Point(row, column));
    }

    public void setValueAt(Object value, int row, int column) {
        if((rows<0) || (columns<0)) {
            throw new IllegalArgumentException("Invalid row/column setting");
        }
        if((row<rows) && (column<columns)) {
            lookup.put(new Point(row, column), value);
        }
    }
}

测试这个示例涉及到创建并填充模型,如下面的代码所示:

String headers[] = { "English", "Japanese"};
TableModel model = new SparseTableModel(10, headers);
JTable table = new JTable(model);
model.setValueAt("one", 0, 0);
model.setValueAt("ten", 9, 0);
model.setValueAt("roku - \ u516D", 5, 1);
model.setValueAt("hachi - \ u516B", 8, 1);

使用TableModelListener监听JTable事件

如果我们需要动态更新我们的表格数据,我们可以使用TableModelListener来确定数据何时发生变化。这个接口由一个可以告诉我们表格数据何时发生变化的方法构成。

public interface TableModelListener extends EventListener {
  public void tableChanged(TableModelEvent e);
}

在TableModelListener得到通知以后,我们可以向TableModelEvent查询所发生的事件的类型以及受到影响的行与列的范围。表18-7显示了我们可以查询的TableModelEvent的属性。

Swing_table_18_7.png

Swing_table_18_7.png

事件类型可以是TableModeleEvent三个类型常量中的一个:INSERT, UPDATE或是DELETE。

如果TableModelEvent的column属性设置为ALL_COLUMNS,那么数据模型中所有的列都会受到影响。如果firstRow属性为HEADER_ROW,则意味着表格头发生了变化。

排序JTable元素

JTable组件并没有内建的排序支持。然而,却经常需要这一特性。排序并不需要改变数据模型,但是他却需要改变JTable所具有的数据模型视图。这种改变类型是通过装饰者模式来描述的,在这种模式中我们维护到数据的相同的API,但是向视图添加排序功能。装饰者设计模式的设计如下:

  1. Component:组件定义了将要装饰的服务接口。
  2. ConcreteComponent:具体组件是将要装饰的对象。
  3. Decorator:装饰者是到具体组件的一个抽象封装;他维护服务接口。
  4. ConcreteDecorator(s)[A,B,C,...]:具体装饰者对象通过添加装饰功能扩展装饰者,然而维护相同的编程接口。他们将服务请求重定向到由抽象超类所指向的具体组件。

注意,java.io包的流是装饰者模式的示例。各种过滤器流向基本的流类添加功能并且维护相同的访问API。

在表格排序这个特定例子中,只需要Component,ConcreteComponent与Decorator,因为只有一个具体装饰者。Component是TableModel接口,ConcreteComponent是实际的模型,而Decorator是排序模型。

为了排序,我们需要维护一个真实数据到排序数据的一个映射。由用户接口,我们必须允许用户选择一个列头标签来激活特定列的排序。

要使用排序功能,我们告诉自定义TableSorter类关于我们数据模型的情况,装饰这个模型,并且由装饰模型而不是原始模型创建一个JTable。要通过点击列头标签来激活排序,我们需要调用TableHeaderSorter类的install()方法,如下面的TableSorter类的源码所示:

TableSorter sorter = new TableSorter(model);
JTable table = new JTable(sorter);
TableHeaderSorter.install(sorter, table);

TableSorter类的主要源码显示在列表18-6中。他扩展了TableMap类,该类显示在列表18-7中。TableSorter类是所有动作所在的位置。该类执行排序并且通知其他类数据已经发生变化。

package swingstudy.ch18;

import java.sql.Date;
import java.util.Vector;

import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;

public class TableSorter extends TableMap implements TableModelListener {

    int indexes[] = new int[0];
    Vector sortingColumns = new Vector();
    boolean ascending = true;

    public TableSorter() {

    }

    public TableSorter(TableModel model) {
        setModel(model);
    }

    public void setModel(TableModel model) {
        super.setModel(model);
        reallocateIndexes();
        sortByColumn(0);
        fireTableDataChanged();
    }

    public int compareRowsByColumn(int row1, int row2, int column) {
        Class type = model.getColumnClass(column);
        TableModel data = model;

        // check for nulls
        Object o1 = data.getValueAt(row1, column);
        Object o2 = data.getValueAt(row2, column);

        // if both values are null return 0
        if(o1 == null && o2 == null) {
            return 0;
        }
        else if(o1 == null) { // define null less than everything
            return -1;
        }
        else if(o2 == null) {
            return 1;
        }

        if(type.getSuperclass() == Number.class) {
            Number n1 = (Number)data.getValueAt(row1, column);
            double d1 = n1.doubleValue();
            Number n2 = (Number)data.getValueAt(row1, column);
            double d2 = n2.doubleValue();

            if(d1<d2) {
                return -1;
            }
            else if(d1>d2) {
                return 1;
            }
            else {
                return 0;
            }
        }
        else if(type == String.class) {
            String s1 = (String)data.getValueAt(row1, column);
            String s2 = (String)data.getValueAt(row2, column);
            int result = s1.compareTo(s2);

            if(result < 0)
                return -1;
            else if(result > 0)
                return 1;
            else return 0;
        }
        else if(type == java.util.Date.class) {
            Date d1 = (Date)data.getValueAt(row1, column);
            long n1 = d1.getTime();
            Date d2 = (Date)data.getValueAt(row2, column);
            long n2 = d2.getTime();

            if(n1 < n2)
                return -1;
            else if(n1 > n2)
                return 1;
            else
                return 0;
        }
        else if(type == Boolean.class) {
            Boolean bool1 = (Boolean)data.getValueAt(row1, column);
            boolean b1 = bool1.booleanValue();
            Boolean bool2 = (Boolean)data.getValueAt(row2, column);
            boolean b2 = bool2.booleanValue();

            if(b1 == b2) {
                return 0;
            }
            else if(b1) // define false < true
                return 1;
            else
                return -1;
        }
        else {
            Object v1 = data.getValueAt(row1, column);
            String s1 = v1.toString();
            Object v2 = data.getValueAt(row2, column);
            String s2 = v2.toString();
            int result = s1.compareTo(s2);

            if(result < 0)
                return -1;
            else if(result > 0)
                return 1;
            else
                return 0;
        }
    }

    public int compare(int row1, int row2) {
        for(int level=0, n=sortingColumns.size(); level<n;level++) {
            Integer column = (Integer)sortingColumns.elementAt(level);
            int result = compareRowsByColumn(row1, row2, column.intValue());
            if(result != 0) {
                return (ascending ? result : -result);
            }
        }
        return 0;
    }

    public void reallocateIndexes() {
        int rowCount = model.getRowCount();
        indexes = new int[rowCount];
        for(int row=0; row<rowCount; row++) {
            indexes[row] = row;
        }
    }

    @Override
    public void tableChanged(TableModelEvent e) {
        // TODO Auto-generated method stub
        super.tableChanged(e);
        reallocateIndexes();
        sortByColumn(0);
        fireTableStructureChanged();
    }

    public void checkModel() {
        if(indexes.length != model.getRowCount()) {
            System.err.println("Sorter not informed of a change in model.");
        }
    }

    public void sort() {
        checkModel();
        shuttlesort((int[])indexes.clone(), indexes, 0, indexes.length);
        fireTableDataChanged();
    }

    public void shuttlesort(int from[], int to[], int low, int high) {
        if(high-low<2) {
            return ;
        }
        int middle = (low+high)/2;
        shuttlesort(to, from, low, middle);
        shuttlesort(to, from, middle, high);

        int p = low;
        int q = middle;

        for(int i=low; i<high; i++) {
            if(q>=high || (p<middle && compare(from[p], from[q]) <= 0)) {
                to[i] = from[p++];
            }
            else {
                to[i] = from[q++];
            }
        }
    }

    private void swap(int first, int second) {
        int temp = indexes[first];
        indexes[first] = indexes[second];
        indexes[second] = temp;
    }

    public Object getValueAt(int row, int column) {
        checkModel();
        return model.getValueAt(indexes[row], column);
    }

    public void setValueAt(Object aValue, int row, int column) {
        checkModel();
        model.setValueAt(aValue, row, column);
    }

    public void sortByColumn(int column) {
        sortByColumn(column, true);
    }

    public void sortByColumn(int column, boolean ascending) {
        this.ascending = ascending;
        sortingColumns.removeAllElements();
        sortingColumns.addElement(new Integer(column));
        sort();
        super.tableChanged(new TableModelEvent(this));
    }
}

显示在列表18-7中的TableMap类作为一个代理,将调用传递给相应的TableModel类。他是列表18-6中的TableSorter类的超类。

package swingstudy.ch18;

import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableModel;

public class TableMap extends AbstractTableModel implements TableModelListener {

    TableModel model;

    public TableModel getModel() {
        return model;
    }

    public void setModel(TableModel model) {
        if(this.model != null) {
            this.model.removeTableModelListener(this);
        }
        this.model = model;
        if(this.model != null) {
            this.model.addTableModelListener(this);
        }
    }

    public Class getcolumnClass(int column) {
        return model.getColumnClass(column);
    }

    @Override
    public int getRowCount() {
        // TODO Auto-generated method stub
        return ((model == null)? 0 : model.getRowCount());
    }

    @Override
    public int getColumnCount() {
        // TODO Auto-generated method stub
        return ((model == null)? 0 :model.getColumnCount());
    }

    public String getColumnName(int column) {
        return model.getColumnName(column);
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        // TODO Auto-generated method stub
        return model.getValueAt(rowIndex, columnIndex);
    }

    public void setValueAt(Object value, int row, int column) {
        model.setValueAt(value, row, column);
    }

    public boolean isCellEditable(int row, int column) {
        return model.isCellEditable(row, column);
    }

    @Override
    public void tableChanged(TableModelEvent e) {
        // TODO Auto-generated method stub
        fireTableChanged(e);
    }

}

排序例程的安装需要MouseListener的注册,如列表18-8所示,从而表格头中的选择会触发排序处理。通常的鼠标点击是升序排列;Shift点击为降序排列。

package swingstudy.ch18;

import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.JTable;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableColumnModel;

public class TableHeaderSorter extends MouseAdapter {

    private TableSorter sorter;
    private JTable table;

    private TableHeaderSorter() {

    }

    public static void install(TableSorter sorter, JTable table) {
        TableHeaderSorter tableHeaderSorter = new TableHeaderSorter();
        tableHeaderSorter.sorter = sorter;
        tableHeaderSorter.table = table;
        JTableHeader tableHeader = table.getTableHeader();
        tableHeader.addMouseListener(tableHeaderSorter);
    }

    public void mouseClicked(MouseEvent event) {
        TableColumnModel columnModel = table.getColumnModel();
        int viewColumn = columnModel.getColumnIndexAtX(event.getX());
        int column = table.convertColumnIndexToModel(viewColumn);
        if(event.getClickCount() == 1 && column != -1) {
            System.out.println("Sorting ...");
            int shiftPressed = (event.getModifiers() & InputEvent.SHIFT_MASK);
            boolean ascending = (shiftPressed == 0);
            sorter.sortByColumn(column, ascending);
        }
    }
}

TableColumnModel接口

TableColumnModel是那些位于背后并且不需要太多注意的接口之一。其基本作用就是管理当前通过JTable显示的列集合。除非触发去做其他一些事情,当JTable被创建时,组件就会由数据模型创建一个默认的列模型,指明显示列顺序与数据模型中的顺序相同。

当在设置JTable的数据模型之前将JTable的autoCreateColumnsFromModele属性设置为true,则TableColumnModel会自动被创建。另外,如果当前的设置需要重置,我们可以手动告诉JTable来创建默认的TableColumnModel。public void createDefaultColumnsFromModel()方法会为我们完成创建工作,并将新创建的对象赋给JTable的TableColumnModel。

既然所有都是为我们自动完成的,我们为什么需要了解TableColumnModel呢?通常,只有当我们不喜欢默认生成的TableModel或是我们需要手动移动一些内容时,我们需要使用这个接口。除了维护一个TableColumn对象集合,TableColumnModel管理第二个ListSelectionModel,从而允许用户由表格中选择列与行。

在我们深入默认实现之前我们先来了解一下该接口的定义:

public interface TableColumnModel {
  // Listeners
  public void addColumnModelListener(TableColumnModelListener l);
  public void removeColumnModelListener(TableColumnModelListener l);
  // Properties
  public int getColumnCount();
  public int getColumnMargin();
  public void setColumnMargin(int newMargin);
  public Enumeration getColumns();
  public boolean getColumnSelectionAllowed();
  public void setColumnSelectionAllowed(boolean flag);
  public int getSelectedColumnCount();
  public int[ ] getSelectedColumns();
  public ListSelectionModel getSelectionModel();
  public void setSelectionModel(ListSelectionModel newModel);
  public int getTotalColumnWidth();
  // Other methods
  public void addColumn(TableColumn aColumn);
  public TableColumn getColumn(int columnIndex);
  public int getColumnIndex(Object columnIdentifier);
  public int getColumnIndexAtX(int xPosition);
  public void moveColumn(int columnIndex, int newIndex);
  public void removeColumn(TableColumn column);
}

DefaultTableColumnModel类

DefaultTableColumnModel类定义了系统所用的TableColumnModel接口的实现。他在JTable内通过跟踪空白,宽度,选择与数量来描述TableColumn对象的一般外观。表18-8显示了用于访问DefaultTableColumnModel设置的9个属性。

Swing_table_18_8.png

Swing_table_18_8.png

除了类属性,我们可以使用下面的方法通过TableColumn类来添加,移除与移动列,我们会在稍后进行讨论。

public void addColumn(TableColumn newColumn);
public void removeColumn(TableColumn oldColumn);
public void moveColumn(int currentIndex, int newIndex);

使用TableColumnModelListener监听JTable事件

也许我们通过TableColumnModel要做的事情之一就是使用TableColumnModelListener来监听TableColumnModelEvent对象。监听器会得到列的添加,移除,移动或是选择,或是列空白变化的通知,如前面的接口定义所示。注意,当事件发生时不同的方法并不同有全部接收TableColumnModelEvent对象。

public interface TableColumnModelListener extends EventListener {
  public void columnAdded(TableColumnModelEvent e);
  public void columnMarginChanged(ChangeEvent e);
  public void columnMoved(TableColumnModelEvent e);
  public void columnRemoved(TableColumnModelEvent e);
  public void columnSelectionChanged(ListSelectionEvent e);
}

因为监听器定义标明了事件类型,TableColumnModelEvent定义只定义了变化所影响的列的范围,如表18-9所示。

Swing_table_18_9.png

Swing_table_18_9.png

要查看TableColumnModelListener的演示,我们可以向我们的TableColumnModel对象关联一个监听器:

TableColumnModel columnModel = table.getColumnModel();
columnModel.addColumnModelListener(...);

在列表18-9中我们可以看到这样的监听器。他除了输出信息以外并没有做其他的事情。然而我们可以用其来确定不同事情的发生。

TableColumnModelListener tableColumnModelListener =
    new TableColumnModelListener() {
  public void columnAdded(TableColumnModelEvent e) {
    System.out.println("Added");
  }
  public void columnMarginChanged(ChangeEvent e) {
    System.out.println("Margin");
  }
  public void columnMoved(TableColumnModelEvent e) {
    System.out.println("Moved");
  }
  public void columnRemoved(TableColumnModelEvent e) {
    System.out.println("Removed");
  }
  public void columnSelectionChanged(ListSelectionEvent e) {
    System.out.println("Selected");
  }
};

当然我们需要编写一些代码来引出特定的事件。

package swingstudy.ch18;

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

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableColumnModelListener;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;

public class ColumnModelSample {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        final Object rows[][] = {
                {"one", "ichi - \u4E00"},
                {"two", "ni - \u4E8C"},
                {"three", "san - \u4E09"},
                {"four", "shi - \u56DB"},
                {"five", "go - \u4E94"},
                {"six", "roku - \u516D"},
                {"seven", "shichi - \u4E03"},
                {"eight", "kachi - \u516B"},
                {"nine", "kyu - \u4E5D"},
                {"ten", "ju - \u5341"}
        };

        final Object headers[] = {"English", "Japanese"};

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Scrollless Table");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                JTable table = new JTable(rows, headers);

                TableColumnModelListener tableColumnModelListener = new TableColumnModelListener() {
                    public void columnAdded(TableColumnModelEvent e) {
                        System.out.println("Added");
                    }

                    public void columnMarginChanged(ChangeEvent e) {
                        System.out.println("Margin");
                    }

                    public void columnMoved(TableColumnModelEvent e) {
                        System.out.println("Moved");
                    }

                    public void columnRemoved(TableColumnModelEvent e) {
                        System.out.println("Removed");
                    }

                    public void columnSelectionChanged(ListSelectionEvent e) {
                        System.out.println("Selection Changed");
                    }
                };

                TableColumnModel columnModel = table.getColumnModel();
                columnModel.addColumnModelListener(tableColumnModelListener);

                columnModel.setColumnMargin(12);

                TableColumn column = new TableColumn(1);
                columnModel.addColumn(column);

                JScrollPane pane = new JScrollPane(table);
                frame.add(pane, BorderLayout.CENTER);
                frame.setSize(300, 150);
                frame.setVisible(true);
            }
        };

        EventQueue.invokeLater(runner);
    }

}

TableColumn类

TableColumn是另一个很重要的幕后类。Swing表格由一个列集合构成,而列由表格单元构成。这个列中的每一个是通过TableColumn实例来描述的。TableColumn类的每一个实例存储相应的编辑器,渲染器,名字与尺寸信息。然后TableColumn对象被组合进TableColumnModel来构成当前要由JTable显示的列集合。在这里有一个有用的技巧,如果我们不希望某一列被显示,我们就将其TableColumn由TableColumnModel中移除,但是将保留在TableModel中。

创建TableColumn

如果我们选择自己来创建我们的TableColumn,我们可以使用以下四个构造函数中的一个。他们是通过添加构造函数参数来级联的。

public TableColumn()
TableColumn column = new TableColumn()
public TableColumn(int modelIndex)
TableColumn column = new TableColumn(2)
public TableColumn(int modelIndex, int width)
TableColumn column = new TableColumn(2, 25)
public TableColumn(int modelIndex, int width, TableCellRenderer
  renderer, TableCellEditor editor)
TableColumn column = new TableColumn(2, 25, aRenderer, aEditor)

如果没有参数,例如列表中的第一个构造函数,我们就会获得一个空列,其具有默认宽度(75像素),默认编辑器,以及默认渲染器。modelIndex参数允许我们指定我们希望TableColumn在JTable中显示TableModel中的哪一列。如果我们不喜欢默认的设置,我们也可以指定宽度,渲染器,或是编辑器。如是我们喜欢其中的一个而不喜欢其他的,我们也可以为渲染器或是编辑器指定null。

TableColumn属性

列表18-10列出了TableColumn的12个属性。这些属性可以使得我们在初始的构造参数集合以外自定义列。大多数时候,我们可以基于TableModel配置所有的事情。然而,我们仍然可以通过TableColumn类来自定义单个列。除了监听器列表,所有的属性都是绑定的。

Swing_table_18_10.png

Swing_table_18_10.png

注意,如果列所有的默认头渲染器headerRenderer为null:TableCellRenderer headerRenderer = table.getTableHeader().getDefaultRenderer();则默认渲染器不会由getHeaderRenderer()方法返回。

在列头中使用图标

默认情况下,表格的头渲染器显示文本或是HTML。尽管我们可以使用HTML获得多行文本或是图片,但是有时我们希望在头中显示通常的Icon对象,如图18-11中的示例所示。要实现这一目的,我们必须修改头的渲染器。头渲染器只是另一个TableCellRenderer。

要创建一个可以显示图片的灵活渲染器,要使得渲染器将value数据看作为JLabel,而不是使用value来填充JLabel。列表18-11显示一个这样的渲染器,用于创建图18-11中的程序。

package swingstudy.ch18;

import java.awt.Component;

import javax.swing.JComponent;
import javax.swing.JTable;
import javax.swing.table.TableCellRenderer;

public class JComponentTableCellRenderer implements TableCellRenderer {

    @Override
    public Component getTableCellRendererComponent(JTable table, Object value,
            boolean isSelected, boolean hasFocus, int row, int column) {
        // TODO Auto-generated method stub
        return (JComponent)value;
    }

}

图18-11显示了这个渲染器如何使用DiamondIcon显示Icon。示例程序的源码显示在列表18-12中。

package swingstudy.ch18;

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

import javax.swing.Icon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;

import swingstudy.ch04.DiamondIcon;

public class LabelHeaderSample {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        final Object rows[][] = {
                {"one", "ichi - \u4E00"},
                {"two", "ni - \u4E8C"},
                {"three", "san - \u4E09"},
                {"four", "shi - \u56DB"},
                {"five", "go - \u4E94"},
                {"six", "roku - \u516D"},
                {"seven", "shichi - \u4E03"},
                {"eight", "kachi - \u516B"},
                {"nine", "kyu - \u4E5D"},
                {"ten", "ju - \u5341"}
        };

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Label Header");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                String headers[] = {"English", "Japanese"};
                JTable table = new JTable(rows, headers);
                JScrollPane scrollPane = new JScrollPane(table);

                Icon redIcon = new DiamondIcon(Color.RED);
                Icon blueIcon = new DiamondIcon(Color.BLUE);

                Border headerBorder = UIManager.getBorder("TableHeader.cellBorder");

                JLabel blueLabel = new JLabel(headers[0], blueIcon, JLabel.CENTER);
                blueLabel.setBorder(headerBorder);
                JLabel redLabel = new JLabel(headers[1], redIcon, JLabel.CENTER);
                redLabel.setBorder(headerBorder);

                TableCellRenderer renderer =  new JComponentTableCellRenderer();

                TableColumnModel columnModel = table.getColumnModel();

                TableColumn column0 = columnModel.getColumn(0);
                TableColumn column1 = columnModel.getColumn(1);

                column0.setHeaderRenderer(renderer);
                column0.setHeaderValue(blueLabel);

                column1.setHeaderRenderer(renderer);
                column1.setHeaderValue(redLabel);

                frame.add(scrollPane, BorderLayout.CENTER);
                frame.setSize(300, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

JTableHeader类

每一个JTableHeader实例表示所有不同列的头集合中的一个。JTableHeader对象集合放置在JScrollPane中列头视图中。

我们很少需要直接使用JTableHeader。然而我们可以配置列头的某些特征。

创建JTableHeader

JTableHeader有两个属性。一个使用默认的TableColumnModel,而另一个需要显式指定模型。

public JTableHeader()
JComponent headerComponent = new JTableHeader()
public JTableHeader(TableColumnModel columnModel)
JComponent headerComponent = new JTableHeader(aColumnModel)

JTableHeader属性

如表18-11所示,JTableHeader有十个不同的属性。这些属性可以使得我们配置用户可以通过特定的列头做什么或是列头如何显示。

Swing_table_18_11.png

Swing_table_18_11.png

在表格头中使用工具提示

默认情况下,如果我们为表格头设置工具提示,所有的列头都会共享相同的工具提示文本。要为特定的列指定工具提示文本,我们需要创建或是获取渲染器,然后为渲染器设置工具提示。对于单个的单元也是如些。图18-12显示了这种定制结果显示的样子。

Swing_18_12.png

Swing_18_12.png

图18-12中定制的源码显示在列表18-13中。除非我们在前面设置了列头,并没有必要首先检测特定列的头是null。

JLabel headerRenderer = new DefaultTableCellRenderer();
String columnName = table.getModel().getColumnName(0);
headerRenderer.setText(columnName);
headerRenderer.setToolTipText("Wave");
TableColumnModel columnModel = table.getColumnModel();
TableColumn englishColumn = columnModel.getColumn(0);
englishColumn.setHeaderRenderer((TableCellRenderer)headerRenderer);

自定义JTableHeader观感

JTableHeaderUIResource相关属性的可用集合显示在表18-12中。五个属性用于控制头渲染器的颜色,字体与边框。

Swing_table_18_12.png

Swing_table_18_12.png

编辑表格单元

编辑JTable单元与编辑JTree单元基本上是相同的。事实上,默认的表格单元编辑器,DefaultCellEditor,同时实现了TableCellEditor与TreeCellEditor接口,从而使得我们可以为表格与树使用相同的编辑器。

点击可编辑器的单元将会使得单元处理框架模式。(所需要的点击次数依赖于编辑器的类型。)所有单元的默认编辑器是JTextField。尽管这对于许多数据类型可以工作得很好,但是对于其他的许多数据类型却并不合适。所以,我们或者不支持非文本信息的编辑或者是为我们的JTable设置特殊的编辑器。对于JTable,我们可以为一个特定的类类型或是列注册一个编辑器。然后,当表格在多个相应类型的单元上运行时,则会使用所需要的编辑器。

注意,当没有安装特殊的编辑器时,则会使用JTextField,尽管他对于内容并不合适。

TableCellEditor接口与DefaultCellEditor类

TableCellEditor接口定义了JTable获取编辑器所必须的方法。TableCellEditor的参数列表与TableCellRenderer相同,所不同的是hasFocused参数。因为单元正在被编辑,已知他已经具有输入焦点。

public interface TableCellEditor extends CellEditor {
  public Component getTableCellEditorComponent(JTable table, Object value,
    boolean isSelected, int row, int column);
}

正如第17章所描述的,DefaultCellEditor提供了接口的实现。他提供了JTextField作为一个编辑器,JCheckBox作为另一个编辑器,而JComboBox作为第三个编辑器。

如表格18-13所示,在大多数情况下,默认编辑器为JTextField。如果单元数据可以由一个字符串转换而成或是转换成一个字符串,类提供了一个具有String参数的构造函数,编辑器提供了数据的文本表示作为初始编辑值。然后我们可以编辑内容。

Swing_table_18_13.png

Swing_table_18_13.png

创建一个简单的单元编辑器

作为修改JTable中非String单元的简单示例,我们可以为用户提供一个固定的颜色选择集合。然后当用户选择颜色时,我们可以向表格模型返回相应的Color值。DefaultCellEditor为这种情况提供了一个JComboBox。在配置JComboBox的ListCellRenderer正确的显示颜色之后,我们就会有一个TableCellEditor用于选择颜色。图18-13显示了可能显示结果。

Swing_18_13.png

Swing_18_13.png

提示,任何时候我们都可以重新定义所有的选项,我们可以通过DefaultCellEditor将JComboBox作为我们的编辑器。

列表18-14显示了表示了图18-13中所示的用于Color列事件的TableCellRenderer类以及JComboBox TableCellEditor的ListCellRenderer。由于这两个渲染器组件的相似性,他们的定义被组合在一个类中。

package swingstudy.ch18;

import java.awt.Color;
import java.awt.Component;

import javax.swing.DefaultListCellRenderer;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JTable;
import javax.swing.ListCellRenderer;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;

import swingstudy.ch04.DiamondIcon;

public class ComboTableCellRenderer implements ListCellRenderer,
        TableCellRenderer {

    DefaultListCellRenderer listRenderer = new DefaultListCellRenderer();
    DefaultTableCellRenderer tableRenderer = new DefaultTableCellRenderer();

    public void configureRenderer(JLabel renderer, Object value) {
        if((value != null) && (value instanceof Color)) {
            renderer.setIcon(new DiamondIcon((Color)value));
            renderer.setText("");
        }
        else {
            renderer.setIcon(null);
            renderer.setText((String)value);
        }
    }
    @Override
    public Component getTableCellRendererComponent(JTable table, Object value,
            boolean isSelected, boolean hasFocus, int row, int column) {
        // TODO Auto-generated method stub
        tableRenderer = (DefaultTableCellRenderer)tableRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
        configureRenderer(tableRenderer, value);
        return tableRenderer;
    }

    @Override
    public Component getListCellRendererComponent(JList list, Object value,
            int index, boolean isSelected, boolean cellHasFocus) {
        // TODO Auto-generated method stub
        listRenderer = (DefaultListCellRenderer)listRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
        configureRenderer(listRenderer, value);
        return listRenderer;
    }

}

为了演示新的组合渲染器的使用以及显示一个简单的表格单元编辑器,显示在列表18-15中的程序创建了一个数据模型,其中一个列为Color。在两次安装渲染器并且设置表格单元编辑器以后,就可以显示表格并且Color可以被编辑。

package swingstudy.ch18;

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

import javax.swing.DefaultCellEditor;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableColumn;
import javax.swing.table.TableModel;

public class EditableColorColumn {

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

        Runnable runner = new Runnable() {
            public void run() {
                Color choices[] = {Color.RED, Color.ORANGE, Color.YELLOW, Color.GREEN, Color.BLUE, Color.MAGENTA};
                ComboTableCellRenderer renderer = new ComboTableCellRenderer();
                JComboBox comboBox = new JComboBox(choices);
                comboBox.setRenderer(renderer);
                TableCellEditor editor = new DefaultCellEditor(comboBox);

                JFrame frame = new JFrame("Editable Color Table");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                TableModel model = new ColorTableModel();
                JTable table = new JTable(model);

                TableColumn column = table.getColumnModel().getColumn(3);
                column.setCellRenderer(renderer);
                column.setCellEditor(editor);

                JScrollPane scrollPane = new JScrollPane(table);
                frame.add(scrollPane, BorderLayout.CENTER);
                frame.setSize(400, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

列表18-16显示在这个示例以及下个示例中所用的表格模型。

package swingstudy.ch18;

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

import javax.swing.DefaultCellEditor;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableColumn;
import javax.swing.table.TableModel;

public class EditableColorColumn {

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

        Runnable runner = new Runnable() {
            public void run() {
                Color choices[] = {Color.RED, Color.ORANGE, Color.YELLOW, Color.GREEN, Color.BLUE, Color.MAGENTA};
                ComboTableCellRenderer renderer = new ComboTableCellRenderer();
                JComboBox comboBox = new JComboBox(choices);
                comboBox.setRenderer(renderer);
                TableCellEditor editor = new DefaultCellEditor(comboBox);

                JFrame frame = new JFrame("Editable Color Table");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                TableModel model = new ColorTableModel();
                JTable table = new JTable(model);

                TableColumn column = table.getColumnModel().getColumn(3);
                column.setCellRenderer(renderer);
                column.setCellEditor(editor);

                JScrollPane scrollPane = new JScrollPane(table);
                frame.add(scrollPane, BorderLayout.CENTER);
                frame.setSize(400, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

创建复杂的单元编辑器

尽管前面的例子演示了如何以列表框TableCellEditor的方向用户提供一个确定的选项集合,但是提供一个JColorChooser作为选项似乎是更好的选择(至少,在颜色选择中是如此)。当定义我们自己的TableCellEditor时,我们必须实现单一的TableCellEditor方法来获得相应的组件。我们必须同时实现CellEditor的七个方法,因为他们管理并通知一个CellEditorListener对象列表,同时控制一个单元何时可以编辑。以一个AbstractCellEditor子类作为起点会使得定义我们自己的TableCellEditor更为简单。

通过扩展AbstractCellEditor类,只有CellEditor方法中的getCellEditorValue()方法需要为编辑器进行自定义。实现上述步骤并且提供了一个JButton,当点击整个编辑器组件时弹出JColorChooser。列表18-17显示了自定义编辑器的源码。

package swingstudy.ch18;

import java.awt.Color;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.AbstractCellEditor;
import javax.swing.JButton;
import javax.swing.JColorChooser;
import javax.swing.JTable;
import javax.swing.table.TableCellEditor;

import swingstudy.ch04.DiamondIcon;

public class ColorChooserEditor extends AbstractCellEditor implements
        TableCellEditor {

    private JButton delegate = new JButton();

    Color savedColor;

    public ColorChooserEditor() {
        ActionListener actionListener = new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                Color color = JColorChooser.showDialog(delegate, "Color Chooser", savedColor);
                ColorChooserEditor.this.changeColor(color);
            }
        };
        delegate.addActionListener(actionListener);
    }
    @Override
    public Object getCellEditorValue() {
        // TODO Auto-generated method stub
        return savedColor;
    }

    public void changeColor(Color color) {
        if(color != null) {
            savedColor = color;
            delegate.setIcon(new DiamondIcon(color));
        }
    }
    @Override
    public Component getTableCellEditorComponent(JTable table, Object value,
            boolean isSelected, int row, int column) {
        // TODO Auto-generated method stub
        changeColor((Color)value);
        return delegate;
    }

}

图18-14显示了ColorChooserEditor的运行结果。

Swing_18_14.png

Swing_18_14.png

使用新的ColorChooserEditor的示例程序显示在列表18-18中。示例程序重用了前面显示在列表18-16中的ColorTableModel数据模型。设置ColorChooserEditor简单的涉及为相应的列设置TableCellEditor。

package swingstudy.ch18;

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

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableColumn;
import javax.swing.table.TableModel;


public class ChooserTableSample {

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

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Editable Color Table");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                TableModel model =  new ColorTableModel();
                JTable table = new JTable(model);

                TableColumn column = table.getColumnModel().getColumn(3);

                ComboTableCellRenderer renderer = new ComboTableCellRenderer();
                column.setCellRenderer(renderer);

                TableCellEditor editor = new ColorChooserEditor();
                column.setCellEditor(editor);

                JScrollPane scrollPane = new JScrollPane(table);
                frame.add(scrollPane, BorderLayout.CENTER);
                frame.setSize(400, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

打印表格

JDK5.0的一个新特性也是最容易使用一个特性:打印表格功能。通过简单的JTable的public boolean print() throws PrinterException方法,我们就可以在打印机在多面上打印一个大表格。甚至是如果我们不喜欢将表格适应整页纸的宽度的默认行为,我们可以在多个页面上扩展列。

为了演示这种行为,列表18-19使用基本的JTable示例代码来生成图18-1,向表格添加更多的行,并且添加一个打印按钮。

package swingstudy.ch18;

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

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;

public class TablePrint {

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

        final Object rows[][] = {
                {"1", "ichi - \u4E00"},
                {"2", "ni -\u4E8C"},
                {"3", "san - \u4E09"},
                {"4", "shi - \u56DB"},
                {"5", "go - \u4E94"},
                {"6", "roku - \u516D"},
                {"7", "shichi - \u4E03"},
                {"8", "hachi - \u516B"},
                {"9", "kyu - \u4E5D"},
                {"10","ju - \u5341"},
                {"1", "ichi - \u4E00"},
                {"2", "ni -\u4E8C"},
                {"3", "san - \u4E09"},
                {"4", "shi - \u56DB"},
                {"5", "go - \u4E94"},
                {"6", "roku - \u516D"},
                {"7", "shichi - \u4E03"},
                {"8", "hachi - \u516B"},
                {"9", "kyu - \u4E5D"},
                {"10","ju - \u5341"},
                {"1", "ichi - \u4E00"},
                {"2", "ni -\u4E8C"},
                {"3", "san - \u4E09"},
                {"4", "shi - \u56DB"},
                {"5", "go - \u4E94"},
                {"6", "roku - \u516D"},
                {"7", "shichi - \u4E03"},
                {"8", "hachi - \u516B"},
                {"9", "kyu - \u4E5D"},
                {"10","ju - \u5341"}
        };
        final Object headers[] = {"English", "Japanese"};

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Table Printing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                final JTable table = new JTable(rows, headers);
                JScrollPane scrollPane = new JScrollPane(table);
                frame.add(scrollPane, BorderLayout.CENTER);
                JButton button = new JButton("Print");
                ActionListener printAction =  new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        try {
                            table.print();
                        }
                        catch(PrinterException pe) {
                            System.out.println("Error printing: "+pe.getMessage());
                        }
                    }
                };
                button.addActionListener(printAction);
                frame.add(button, BorderLayout.SOUTH);
                frame.setSize(300, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

在点击打印Print按钮之后,会向用户提示一个经典的打印机选择对话框,如图18-15所示。

Swing_18_15.png

Swing_18_15.png

在用户点击打印对话框中的打印按钮之后,打印开始。会显示一个类似于图18-16中所示的对话框。

Swing_18_16.png

Swing_18_16.png

确实,很容易使用JDK 5.0打印多页表格。print()方法会返回一个boolean值,从而我们可以发现用户是否关闭了操作。

对于查找更多打印操作控制的用户,JTable具有多个重载的print()方法版本。类似于简单的print()方法,他们都抛出PrinterException。

其中一个print()版本会允许我们指定打印模式:

public boolean print(JTable.PrintModel printMode)

JTable.PrintModel参数是一个FIT_WIDTH与NORMAL的枚举。当使用无参数的print()版本没有指定时,则默认为FIT_WIDTH。

另一个版本的方法允许我们指定页眉与页脚:

public boolean print(JTable.PrintMode printMode, MessageFormat headerFormat,
  MessageFormat footerFormat)

MessageFormat来自于java.text包。页眉与页脚格式化字符串的一个参数是页数。要显示页数,在我们的格式化字符串我们希望显示页数的地方包含{0}。两个都会显示在页面的中间,而页眉使用稍大一些的字符。为了演示,将列表18-19中的print()方法调用改为下面的形式:

MessageFormat headerFormat = new MessageFormat("Page {0}");
MessageFormat footerFormat = new MessageFormat("- {0} -");
table.print(JTable.PrintMode.FIT_WIDTH, headerFormat, footerFormat);

最后一个print()版本是一个综合版本,使得我们在不显示打印机对话框的情况配置默认打印机所需要的属性,例如要打印多少份。

public boolean print(JTable.PrintMode printMode, MessageFormat headerFormat,
MessageFormat footerFormat, boolean showPrintDialog,
PrintRequestAttributeSet attr, boolean interactive)

对于我们不希望用户与打印机交互的情况下,可以考虑使用最后一个版本。

小结

在本章中我们探讨了JTable组件的内部细节。我们了解了如何为JTable自定义TableModel,TableColumnModel与ListSelectionModel。我们深入了不同的表格模型的抽象与具体实现。另外,我们探讨了各种表格模型的内部元素,例如TableColumn与JTableHeader类。我们同时了解了如何通过提供一个自定义的TableCellRenderer与TableCellEditor来自定义JTable的显示与编辑。最后,我们了解了通过print()方法打印表格。

在第19章中,我们将会探讨JFC/Swing组件集合的拖拽体系结构。

拖放支持

拖放支持允许在一个程序中或是屏幕上的某个区域中高亮显示某些内容,选中他,并将其重新分配到另一个程序或是屏幕上的某个区域中。例如,在Microsoft Word中,我们可以选中一段并拖动到一个新位置。

随着Java的发展,在新版本中不仅有打印支持,同时也有拖放支持。拖放支持的最主要变化发生在J2SE 1.4版本中。以前版本中的拖放支持极难使用,特别是对于复杂类型的拖放行为更是如此。JDK 5.0版本添加了一些bug修正并且加强了拖放支持。

我们可以使用三种方法在我们的程序中实现拖放支持:

  1. 对于具有内建支持的组件,只需要使用参数true调用其setDragEnabled()方法将其激活即可。这些组件包括JColorChooser,JFileChooser,JList,JTable,JTree以及JTextComponent的所有子类,除了JPasswordField。
  2. 对于没有内建支持的组件,我们通常需要为该组件配置TransferHandler。
  3. 我们可以直接使用java.awt.dnd包中的类。多亏了内建支持与可配置性,这种方法很少使用。

内建拖放支持

表19-1显示了为拖放支持提供了内建支持的组件。初始时,支持拖放操作的组件只有放操作被激活,但是在调用组件的setDragEnabled(true)方法之后,我们也可以激活拖操作,如果支持拖放操作。Java平台的拖放功能使用底层的java.awt.datatransfer包来移动数据。这个包中的类允许我们描述要移动的数据。

Swing_table_19_1.png

Swing_table_19_1.png

注意,出于安全原因,我们不能拖拽JPasswordField组件中的文本。

对于JColorChooser组件,我们拖拽的是java.awt.Color对象。另一个相对的便是JFileChooser,在其中我们拖拽java.io.File对象并将其放在目标中。如果拖放目标不支持File对象的使用,则会拖放表示路径的字符串。

作为一个简单的渲染,列表19-1显示了在一个屏幕上有两个JColorChooser组件的程序。在两个选择器上调用setDragEnabled(true)调用,所以我们可以在两个组件之间使用最少的代码来拖拽颜色。

package swingstudy.ch19;

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

import javax.swing.JColorChooser;
import javax.swing.JFrame;

public class DoubleColor {

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

        Runnable runner = new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Double Color Chooser");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                JColorChooser left = new JColorChooser();
                left.setDragEnabled(true);
                frame.add(left, BorderLayout.WEST);

                JColorChooser right = new JColorChooser();
                right.setDragEnabled(true);
                frame.add(right, BorderLayout.EAST);

                frame.pack();
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

图19-1显示了在拖拽一些颜色之后的效果。拖拽区域是底部的预览面板。放操作并不会修改每一个颜色选择器右边的最近列表,并且我们可以在不同的颜色选择器面板之间执行放操作。

Swing_19_1.png

Swing_19_1.png

TransferHandler类

拖放的神奇是由于java.swing.TransferHandler类,由J2SE 1.4版本引入。也许我们会问,他有什么神奇之处呢?通过这个类,我们可以选择在拖放操作中我们希望传递哪些属性。

当我们在某个组件上调用setDragEnabled(true)来支持拖拽特性,组件会向所安装的TransferHandler询问传递哪些内容。如果我们不喜欢正在传递的默认对象,我们可以调用组件的setTransferhandler()方法,并传递相应的参数进行替换。当我们希望激活没有内建拖放支持的组件的拖放特性时,我们也可以调用setTransferHandler()方法。

TransferHandler类只有一个公开构造函数:

public TransferHandler(String property)

构造函数的参数表示我们希望传递的组件的属性。换句话说,我们指定JavaBeans组件属性作为拖放操作的可传递对象。

例如,要传递JLabel的文本标签,我们可以使用下面的代码:

JLabel label = new JLabel("Hello, World");
label.setTransferHandler(new TransferHandler("text"));

因为JLabel并没有setDragEnabled()方法,我们必须告诉组件如何开始拖拽。通常情况下,这需要我们按下鼠标按钮,所以我们需要向按钮添加了一个MouseListener。当我们通知TransferHandler来exportAsDrag()时,这会为组件激活拖拽操作。

MouseListener listener = new MouseAdapter() {
  public void mousePressed(MouseEvent me) {
    JComponent comp = (JComponent)me.getSource();
    TransferHandler handler = comp.getTransferHandler();
    handler.exportAsDrag(comp, me, TransferHandler.COPY);
  }
};
button.addMouseListener(listener);

当放操作发生时-在这个例子中是释放鼠标-默认行为是放下使用TransferHandler所注册的内容。

列表19-2演示了为JLabel激活拖放操作的示例。

package swingstudy.ch19;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
import javax.swing.TransferHandler;

public class DragLabel {

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

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

                JLabel label = new JLabel("Hello, World");
                label.setTransferHandler(new TransferHandler("text"));
                MouseListener listener = new MouseAdapter() {
                    public void mousePressed(MouseEvent event) {
                        JComponent comp = (JComponent)event.getSource();
                        TransferHandler handler = comp.getTransferHandler();
                        handler.exportAsDrag(comp, event, TransferHandler.COPY);
                    }
                };

                label.addMouseListener(listener);
                frame.add(label, BorderLayout.SOUTH);

                JTextField text = new JTextField();
                frame.add(text, BorderLayout.NORTH);

                frame.setSize(300, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

图19-2显示了拖拽操作中的程序。注意鼠标是如何变化来指示操作的。

Swing_19_2.png

Swing_19_2.png

如果我们并不想要拖拽JLabel的文本,而是希望拖拽前景色,则只需要修改程序中setTransferHandler()一行:

label.setTransferHandler(new TransferHandler("foreground"));

然后,假定我们有某个位置来放置颜色,如列表19-1中的示例程序,那么我们就将前景色由标签拖拽到JColorChooser,并且将颜色由JColorChooser拖拽到JLabel。因为TransferHandler被注册为组件的特定属性,并不需要显示的代码来处理放操作。相反,传递给处理器构造函数属性的设置方法会得到变化的通知。

图像拖放支持

如果我们希望传输的不仅是简单的属性,我们需要创建一个Transferable接口的实现,这个接口位于java.awt.datatransfer包中。Transferable实现通常意味着通过剪切板传输,但是我们的实现是TransferHandler的子类,我们可以用其拖放对象。Transferable接口的三个方法显示如下:

public interface Transferable{
  public DataFlavor[] getTransferDataFlavors();
  public boolean isDataFlavorSupported(DataFlavor);
  public Object getTransferData(java.awt.datatransfer.DataFlavor)
    throws UnsupportedFlavorException, IOException;
}

使用这个接口的通常程序是传输图像。JLabel或是JButton所公开的属性是javax.swing.Icon对象,而不是java.awt.Image对象。虽然我们可以Java程序内部或是跨越Java程序传输Icon对象,然而另一个更有用的行为是向外部对象传输Image对象,例如Paint Shop Pro或是Photoshop。

要创建一个可传输的图像对象,ImageSelection类,我们必须实现三个Transferable接口方法并且重写TransferHandler的四个方法:getSourceActions(),canImport(),createTransferable()与importData()。

注意,用于传输字符串的类名为StringSelection。

getSourceActions()方法需要报告我们将要支持哪些动作。默认情况下,当通过构造函数设置属性或是当不可用为Transferable.NONE时则为TransferHandler.COPY操作。因为ImageSelection类隐式使用Icon属性来获取组件的图像,只需要使得这个方法返回TransferHandler.COPY即可:

public int getSourceActions(JComponent c) {
  return TransferHandler.COPY;
}

还有一个TransferHandler.MOVE操作,但是通常我们并不希望图像由复制的地方删除。

我们向canImport()方法传递一个组件或是一个DataFlavor对象的数组。我们需要验证组件是被支持的并且数组中的一个flavor匹配所支持的集合:

private static final DataFlavor flavors[] = {DataFlavor.imageFlavor};
...
public boolean canImport(JComponent comp, DataFlavor flavor[]) {
  if (!(comp instanceof JLabel) && !(comp instanceof AbstractButton)) {
    return false;
  }
  for (int i=0, n=flavor.length; i<n; i++) {
    for (int j=0, m=flavors.length; j<m; j++) {
      if (flavor[i].equals(flavors[j])) {
        return true;
      }
    }
  }
  return false;
}

createTransferable()方法返回一个到Transferable实现的引用。当剪切板粘贴操作被执行时,或者是当拖拽时执行放操作,Transferable对象将会得到通知来获得所传输的对象。

public Transferable createTransferable(JComponent comp) {
  // Clear
  image = null;
  if (comp instanceof JLabel) {
    JLabel label = (JLabel)comp;
    Icon icon = label.getIcon();
    if (icon instanceof ImageIcon) {
      image = ((ImageIcon)icon).getImage();
      return this;
    }
  } else if (comp instanceof AbstractButton) {
    AbstractButton button = (AbstractButton)comp;
    Icon icon = button.getIcon();
    if (icon instanceof ImageIcon) {
      image = ((ImageIcon)icon).getImage();
      return this;
    }
  }
  return null;
}

当数据被放入组件或是由剪切板粘贴时会调用importData()方法。他有两个参数:粘贴剪切板数据的JComponent与借助于Transferable对象的剪切板数据。假定方法接由一个为Java平台所支持的格式,与传输处理器相关联的组件会获得一个要显示的新图像。

the Java platform, the component associated with the transfer handler gets a new image to display.
public boolean importData(JComponent comp, Transferable t) {
  if (comp instanceof JLabel) {
    JLabel label = (JLabel)comp;
    if (t.isDataFlavorSupported(flavors[0])) {
      try {
        image = (Image)t.getTransferData(flavors[0]);
        ImageIcon icon = new ImageIcon(image);
        label.setIcon(icon);
        return true;
      } catch (UnsupportedFlavorException ignored) {
      } catch (IOException ignored) {
      }
    }
  } else if (comp instanceof AbstractButton) {
    AbstractButton button = (AbstractButton)comp;
    if (t.isDataFlavorSupported(flavors[0])) {
      try {
        image = (Image)t.getTransferData(flavors[0]);
        ImageIcon icon = new ImageIcon(image);
        button.setIcon(icon);
        return true;
      } catch (UnsupportedFlavorException ignored) {
      } catch (IOException ignored) {
      }
    }
  }
  return false;
}

将所有的代码与Transferable接口的三个实现方法组合在一起就构成了列表19-3。

package swingstudy.ch19;

import java.awt.Image;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;

import javax.swing.AbstractButton;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.TransferHandler;

public class ImageSelection extends TransferHandler implements Transferable {

    private static final DataFlavor flavors[] = {DataFlavor.imageFlavor};

    private Image image;

    public int getSourceActions(JComponent c) {
        return TransferHandler.COPY;
    }

    public boolean canImport(JComponent comp, DataFlavor flavor[]) {
        if(!(comp instanceof JLabel) && !(comp instanceof AbstractButton)) {
            return false;
        }
        for(int i=0, n=flavor.length; i<n; i++) {
            for(int j=0, m=flavors.length; j<m; j++) {
                if(flavor[i].equals(flavors[j])) {
                    return true;
                }
            }
        }
        return false;
    }

    public Transferable createTransferable(JComponent comp) {
        // clear
        image = null;

        if(comp instanceof JLabel) {
            JLabel label = (JLabel)comp;
            Icon icon = label.getIcon();
            if(icon instanceof ImageIcon) {
                image = ((ImageIcon)icon).getImage();
                return this;
            }
        }
        else if(comp instanceof AbstractButton) {
            AbstractButton button = (AbstractButton)comp;
            Icon icon = button.getIcon();
            if(icon instanceof ImageIcon) {
                image = ((ImageIcon)icon).getImage();
                return this;
            }
        }
        return null;
    }

    public boolean importData(JComponent comp, Transferable t) {
        if(comp instanceof JLabel) {
            JLabel label = (JLabel)comp;
            if(t.isDataFlavorSupported(flavors[0])) {
                try {
                    image = (Image)t.getTransferData(flavors[0]);
                    ImageIcon icon = new ImageIcon(image);
                    label.setIcon(icon);
                    return true;
                }
                catch(UnsupportedFlavorException ignored) {

                }
                catch(IOException ignored){

                }
            }
        }
        else if(comp instanceof AbstractButton) {
            AbstractButton button = (AbstractButton)comp;
            if(t.isDataFlavorSupported(flavors[0])) {
                try {
                    image = (Image)t.getTransferData(flavors[0]);
                    ImageIcon icon = new ImageIcon(image);
                    button.setIcon(icon);
                    return true;
                }
                catch(UnsupportedFlavorException ignored) {

                }
                catch(IOException ignored) {

                }
            }
        }
        return false;
    }

    @Override
    public DataFlavor[] getTransferDataFlavors() {
        // TODO Auto-generated method stub
        return flavors;
    }

    @Override
    public boolean isDataFlavorSupported(DataFlavor flavor) {
        // TODO Auto-generated method stub
        return flavors[0].equals(flavor);
    }

    @Override
    public Object getTransferData(DataFlavor flavor)
            throws UnsupportedFlavorException, IOException {
        // TODO Auto-generated method stub
        if(isDataFlavorSupported(flavor)) {
            return image;
        }
        return null;
    }

}

要测试这个类,我们需要使用可拖放的JLabel与AbstractButton子类创建一个程序。这个程序基本上与显示在列表19-2中的程序相同,但是只有一个与位于屏幕中间的图像相关联的JLabel。

package swingstudy.ch19;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.TransferHandler;

public class DragImage {

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

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

                Icon icon = new ImageIcon("dog.jpg");
                JLabel label = new JLabel(icon);
                label.setTransferHandler(new ImageSelection());
                MouseListener listener = new MouseAdapter() {
                    public void mousePressed(MouseEvent event) {
                        JComponent comp = (JComponent)event.getSource();
                        TransferHandler handler = comp.getTransferHandler();
                        handler.exportAsDrag(comp, event, TransferHandler.COPY);
                    }
                };
                label.addMouseListener(listener);
                frame.add(new JScrollPane(label), BorderLayout.CENTER);

                frame.setSize(300, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

图19-3显示程序运行的结果。

Swing_19_3.png

Swing_19_3.png

小结

Swing中的拖放支持是丰富且多样的。我们可以自由的获取标准组件的多个行为。如果我们需要更多的行为,我们可以更深入一步直到我们获得所需要的属性。

通常情况下,我们并不需要深入java.awt.dnd中的所有方法,例如DragSourceDragEvent,DragSourceDropEvent,或是DropTargetDragEvent。他们就在那里并且在幕后完成工作,但是我们并不需要担心这些。相反,拖放支持通常委托给与要拖拽的组件属性相关联的TransferHandler。只需要在组件上调用setDragEnabled(true)方法,从而我们就准备好一切。我们也可以为其他的项目设置拖放支持,例如图像,但是需要创建一个Transferable接口的一个实现。

在下一章中,我们将会探讨Swing可插拨的观感体系结构。我们将会了解如何在不改变程序代码的情况下自定义我们的界面。

可插拨的观感体系结构

在第19章中,我们探讨了Swing的拖放支持。在本章中,我们将会深入我们使用Swing组件库时可用的可插拨的观感体系结构。

Swing组件的所有方面都是基于Java的。所以不存在原生代码,AWT组件集合也是如此。如果我们不喜欢组件的方式,我们可以对其进行修改,并且我们可以有多种实现方法。

抽象的LookAndFeel类是特定观感的根类。每一个可安装的观感类,正如UIManager.LookAndFeelInfo类所描述的,必须是LookAndFeel类的子类。LookAndFeel子类描述了特定观感Swing组件的默认外观。

当前已安装的观感类集合是由UIManager类提供的,他同时管理特定LookAndFeel的所有的组件的默认显示属性。这些显示属性是在UUIDefaluts散列表中管理的。这些显示属性或者以空的UIResource或是UI委托进行标记,所以是ComponentUI类的子类。依据他们的用法,这些属性可以存储为UIDefaults.LazyValue对象UIDefaults.ActiveValue对象。

LookAndFeel类

抽象LookAndFeel类的实现描述了每一个Swing组件如何显示以及用户如何与他们进行交互。每一个Swing组件的外观是由一个UI委托来控制的,他同时承担了MVC体系结构中视图与控制器的角色。每一个预定义的观感类及其相关联的UI委托类都包含在各自的包中。当配置当前观感时,我们可以使用一个预定义的观感类或是创建我们自己的类。当我们创建我们自己的观感时,我们可以在已存在的观感类上进行构建,例如BasicLookAndFeel类及其UI委托,而不是从零创建所有的UI委托。图20-1显示了预定义类的层次结构。

Swing_20_1.png

Swing_20_1.png

每一个观感类有六个属性,如表20-1所示。

Swing_table_20_1.png

Swing_table_20_1.png

这些属性是只读的并且大部分描述了观感。然而defaults属性有一些不同。一旦我们获得其UIDefaults值,我们就可以直接通过其方法修改其状态。另外,LookAndFeel的UIDefaults可以通过UIManager直接访问与修改。

nativeLookAndFeel属性可以使得我们确定某个特定的观感实现是否是用户操作系统的原生观感。例如,WindowsLookAndFeel对于运行在Microsoft Windows操作系统上的任何系统而言都是原生的。suppportedLookAndFeel属性可以告诉我们某一个特定观感的实现是否可以使用。对于WindowsLookAndFeel实现,这个特定的属性只会为Microsoft Windows操作系统所支持。相应的,MacLookAndFeel实现只为MacOS计算机所支持。MotifLookAndFeel与MetalLookAndFeel并没有固定为特定操作系统的原生观感。

列出已安装的观感类

要确定在我们的计算机上安装了哪些观感类,我们可以向UIManager查询,如列表20-1所示。UIManager有一个UIManager.LookAndFeelInfo[] getInstalledLookAndFeels()方法可以返回一个对象数组,这个对象可以提供所有已安装观感类的文本名字(public String getName())与类名(public String getClassName())。

package swingstudy.ch19;

import javax.swing.UIManager;

public class ListPlafs {

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

        UIManager.LookAndFeelInfo plaf[] = UIManager.getInstalledLookAndFeels();
        for(int i=0, n=plaf.length; i<n; i++) {
            System.out.println("Name: "+plaf[i].getName());
            System.out.println("  Class name: "+plaf[i].getClassName());
        }
    }

}

运行这个程序也许会生成下面的输出。我们当前的系统配置与/或未来Swing库版本的变化都会导致不同的结果。

Name: Metal

 Class name: javax.swing.plaf.metal.MetalLookAndFeel

Name: CDE/Motif

 Class name: com.sun.java.swing.plaf.motif.MotifLookAndFeel

Name: Windows

 Class name: com.sun.java.swing.plaf.windows.WindowsLookAndFeel

注意,Ocean本身并不是一个观感。相反,他是Metal观感的一个内建主题。这个主题恰好为Metal的默认主题。

改变当前观感

一旦我们知道在我们的系统上有哪些可用的观感类,我们就可以使得我们的程序使用其中一个观感类。UIManager有两个重载的setLookAndFeel()方法来修改已安装的观感类:

public static void setLookAndFeel(LookAndFeel newValue) throws
  UnsupportedLookAndFeelException
public static void setLookAndFeel(String className) throws
  ClassNotFoundException, InstantiationException, IllegalAccessException,
  UnsupportedLookAndFeelException

尽管第一个版本看起来是更为合理的选择,然后第二个却是更为经常使用的版本。当我们使用UIManager.getInstalledLookAndFeels()方法请求已安装的观感类时,我们以字符串的形式获得对象的类名,而不是对象实例。由于改变观感时可能发生的异常,我们需要将setLookAndFeel()调用放在一个try/catch块中。如果我们为一个已存在的窗口改变观感,我们需要使用SwingUtilities的public static void updateComponentTreeUI(Component rootComponent)方法来告诉组件更新其外观。如果还没有创建组件,则没有这样的必要。

下面的代码演示了如何改变观感:

try {
  UIManager.setLookAndFeel(finalLafClassName);
  SwingUtilities.updateComponentTreeUI(frame);
} catch (Exception exception) {
  JOptionPane.showMessageDialog (
    frame, "Can't change look and feel",
    "Invalid PLAF", JOptionPane.ERROR_MESSAGE);
}

图20-2演示了通过JComboBox或是JButton组件在运行时改变观感的示例程序。通常情况下,我们并不允许用户改变观感;我们也许只是希望在启动时设置观感。

Swing_20_2.png

Swing_20_2.png

列表20-2显示了图20-2中示例程序的源码。

package swingstudy.ch19;

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

import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;

public class ChangeLook {

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

        Runnable runner = new Runnable() {
            public void run() {
                final JFrame frame = new JFrame("Change Look");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        Object source = event.getSource();
                        String lafClassName = null;
                        if(source instanceof JComboBox) {
                            JComboBox comboBox = (JComboBox)source;
                            lafClassName = (String)comboBox.getSelectedItem();
                        }
                        else if(source instanceof JButton) {
                            lafClassName = event.getActionCommand();
                        }
                        if(lafClassName != null) {
                            final String finalLafClassName = lafClassName;
                            Runnable runner = new Runnable() {
                                public void run() {
                                    try {
                                        UIManager.setLookAndFeel(finalLafClassName);
                                        SwingUtilities.updateComponentTreeUI(frame);
                                    }
                                    catch(Exception exception) {
                                        JOptionPane.showMessageDialog(frame, "Can't change look and fee", "INvalid PLAF", JOptionPane.ERROR_MESSAGE);
                                    }
                                }
                            };
                            EventQueue.invokeLater(runner);
                        }
                    }
                };

                UIManager.LookAndFeelInfo looks[] = UIManager.getInstalledLookAndFeels();

                DefaultComboBoxModel model = new DefaultComboBoxModel();
                JComboBox comboBox = new JComboBox(model);

                JPanel panel = new JPanel();

                for(int i=0, n=looks.length; i<n; i++){
                    JButton button = new JButton(looks[i].getName());
                    model.addElement(looks[i].getClassName());
                    button.setActionCommand(looks[i].getClassName());
                    button.addActionListener(actionListener);
                    panel.add(button);
                }

                comboBox.addActionListener(actionListener);

                frame.add(comboBox, BorderLayout.NORTH);
                frame.add(panel, BorderLayout.SOUTH);
                frame.setSize(350, 150);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

注意,实际的观感变化是在EventQueue.invokeLater()调用中实现的。这是必需的,因为当前事件的处理在我们可以改变观感之前必须完成,并且变化必须发生在事件队列上。

除了编程改变当前的观感之外,我们可以由命令行使用一个新观感启动程序。只需要将swing.defaultlaf系统属性设置为观感类名。例如,下面的启动行将会启动ChangeLook程序,使用Motif作为初始观感。

java -Dswing.defaultlaf=com.sun.java.swing.plaf.motif.MotifLookAndFeel ChangeLook

如果我们希望每次程序启动时具有不同的观感,我们可以在具有相关设置的Java运行库(默认为jre)目录下创建一个文件,swing.properties。swing.properties文件需要位于Java运行库的lib目录下。例如,下面的配置行会使得初始观感总是Motif,除非通地编程或是由命令行改变。

swing.defaultlaf=com.sun.java.swing.plaf.motif.MotifLookAndFeel

除了swing.defaultlatf设置以外,swing.properties文件还支持一些其他的设置,如表20-2所示。每一个属性都会允许我们覆盖预定义观感设置的默认设置。在其他的设置中,auxiliary与multiplexing观感类支持可访问性。我们将会在本章稍后进行讨论。

Swing_table_20_2.png

Swing_table_20_2.png

提示,swing.installedlafs与swing.auxiliarylaf属性设置是以逗号分隔的已安装观感类列表。

我们也许已经注意到图20-1中类层次结构中所示的Synth类并没有列在已安装的观感类默认集合中。Synth需要另一个配置文件;他并不是我们可以在运行时在没有定义自定义外观的情况切换的配置。这个基本观感类为自定义提供了框架。我们将会在本章稍后的内容中了解如何使用Synth观感。

当Windows XP风格并不适合用户平台或是设置了swing.noxp系统属性时可以使用WindowsClassicLookAndFeel。

自定义当前观感

在第3章中,我们了解了MVC体系结构以及Swing组件如何将视图与控制器组合为UI委托。现在我们将会深入Swing组件的UI委托。基本来说,如果我们不喜欢Swing组件的显示,我们可以通知UIManager来修改,然后他就不会向以前那样显示。

UIManager类

当我们需要创建一个Swing组件时,UIManager类扮演代理的角色来获得当前已安装观感的信息。这样如果我们要安装一个新的观感或是修改已有的观感,我们不需要直接通知Swing组件;我们只需要通知UIManager。

在前面章节中每一个组件的讨论是通守列出通过UIManager可以改变的所有设置的方式来实现的。另外,本书的附录提供了一个JDK 5.0所有可用设置的字母列表。一旦我们知道我们修改的设置的属性字符串,我们就可以调用public Object UIManager.put(Object key, Object value)方法,这个方法会修改属性设置并返回以前的设置(如果存在)。例如,下面的代码行将JButton组件的背景色设置为红色。在我们将新设置放入UIManager类的查找表以后,以后所创建的组件将会使用新的值,Color.RED。

UIManager.put(“Button.background”, Color.RED);

一旦我们将新设置放入UIManager的查找表中以后,当我们创建新的Swing组件时就会使用新的设置。旧组件不会自动更新;如果我们希望他们单个更新,我们必须调用他们的public void updateUI()方法(或是调用updateComponentTreeUI()方法来更新一个组件的整个窗口)。如果我们正在创建我们自己的组件,或者我们只是关心一个不同组件属性的当前设置,我们可以通过表20-3中所列出的方法向UIManager查询。

Swing_table_20_3.png

Swing_table_20_3.png

除了getUI()方法以外,每一个方法都有一个接受用于本地化支持的Locale参数的第二个版本。

除了defaults属性以外,当我们调用不同的put()与get()方法会使用该属性,UIManager类有八个类级别的属性。这些属性列在表20-4中,其中包括具有两个不同设置方法的两个用于lookAndFeel的项。

Swing_table_20_4.png

Swing_table_20_4.png

systemLookAndFeelClassName属性允许我们确定哪一个特定的观感类名适合于用户的操作系统。crossPlatformLookAndFeelClassName属性使得我们可以确定默认情况下哪一个类名表示跨平台观感:java.swing.plaf.metal.MetalLookAndFeel。初始时,lookAndFeelDefaults属性与defaults属性是相同的。当我们要对观感进行修改时,我们使用defaults属性。这样,预定义观感的设置就不会发生改变。

UIManager.LookAndFeelInfo类

当我们向UIManager查询已安装的观感类列表时,我们会返回一个UIManager.LookAndFeelInfo对象的数组。由这个数组我们可以获得观感的描述性名字(LookAndFeel实现的name属性),以及实现的类名字。如表20-5所示,这两个设置是只读的。

Swing_table_20_5.png

Swing_table_20_5.png

UIDefaults类

LookAndFeel类以及UIManager使用一个特殊的UIDefaults散列表来管理依赖于观感的Swing组件属性。这种特殊的行为在于当一个新设置通过put()方法放入散列表时,就会生成一个PropertyChangeEvent并且所注册的PropertyChangeListener对象就会得到通知。BasicLookAndFeel类的大部分都会在合适的时间将UI委托注册到所感兴趣的属性变化事件。

如果我们需要一次改变多个属性,我们可以使用public void putDefaults(Object keyValueList[])方法,这个方法会引起一次事件通知事件。通过putDefaults()方法,键/值对会位于一维数组中。例如,要使得按钮的默认背景色为粉色而前景色为洋红色,我们可以使用下面的代码:

Object newSettings[] = {"Button.background", Color.PINK,
                        "Button.foreground", Color.MAGENTA};
UIDefaults defaults = UIManager.getDefaults();
defaults.putDefaults(newSettings);

因为UIDefaults是Hashtable的子类,我们可以通过使用Enumeration在所有的键或值上进行循环来获得所有的已安装设置。要简化事情,列表20-3给出一个列出了存储在JTable中属性的示例程序。这个程序重用了多个第18章中的排序类。

package swingstudy.ch19;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Enumeration;
import java.util.Vector;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JToolBar;
import javax.swing.SwingUtilities;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.table.AbstractTableModel;

import swingstudy.ch18.TableHeaderSorter;
import swingstudy.ch18.TableSorter;

public class ListProperties {

    static class CustomTableModel extends AbstractTableModel {
        Vector<Object> keys = new Vector<Object>();
        Vector<Object> values = new Vector<Object>();
        private static final String columnNames[] = {"Property String", "Value"};

        public int getColumnCount() {
            return columnNames.length;
        }

        public String getColumnName(int column) {
            return columnNames[column];
        }

        public int getRowCount() {
            return keys.size();
        }

        public Object getValueAt(int row, int column) {
            Object returnValue = null;
            if(column == 0) {
                returnValue = keys.elementAt(row);
            }
            else if(column == 1) {
                returnValue = values.elementAt(row);
            }
            return returnValue;
        }

        public synchronized void uiDefaultsUpdate(UIDefaults defaults) {
            Enumeration newKeys = defaults.keys();
            keys.removeAllElements();
            while(newKeys.hasMoreElements()) {
                keys.addElement(newKeys.nextElement());
            }
            Enumeration newValues = defaults.elements();
            values.removeAllElements();
            while(newValues.hasMoreElements()) {
                values.addElement(newValues.nextElement());
            }
            fireTableDataChanged();
        }
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Runnable runner = new Runnable() {
            public void run() {
                final JFrame frame = new JFrame("List Properties");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                final CustomTableModel model = new CustomTableModel();
                model.uiDefaultsUpdate(UIManager.getDefaults());
                TableSorter sorter = new TableSorter(model);

                JTable table = new JTable(sorter);
                TableHeaderSorter.install(sorter, table);

                table.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);

                UIManager.LookAndFeelInfo looks[] = UIManager.getInstalledLookAndFeels();

                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        final String lafClassName = event.getActionCommand();
                        Runnable runner = new Runnable() {
                            public void run() {
                                try {
                                    UIManager.setLookAndFeel(lafClassName);
                                    SwingUtilities.updateComponentTreeUI(frame);
                                    model.uiDefaultsUpdate(UIManager.getDefaults());
                                }
                                catch(Exception exception) {
                                    JOptionPane.showMessageDialog(frame, "Can't change look and feel", "Invalid PLAF", JOptionPane.ERROR_MESSAGE);
                                }
                            }
                        };
                        EventQueue.invokeLater(runner);
                    }
                };

                JToolBar toolbar = new JToolBar();
                for (int i=0, n=looks.length; i<n; i++) {
                    JButton button = new JButton(looks[i].getName());
                    button.setActionCommand(looks[i].getClassName());
                    button.addActionListener(actionListener);
                    toolbar.add(button);
                }

                frame.add(toolbar, BorderLayout.NORTH);
                JScrollPane scrollPane = new JScrollPane(table);
                frame.add(scrollPane, BorderLayout.CENTER);
                frame.setSize(400, 400);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

图20-3显示了程序的运行结果。

Swing_20_3.png

Swing_20_3.png

提示,要将属性重置为当前安装观感的默认值,则将其设置为null。这将会使得组件由观感获取原始默认值。

UIResource接口

预定义的观感类的UIDefaults设置使用一个特殊的标记接口,UIResource,从而可以使得UI委托确定默认值是否被覆盖。如果我们将一个特定的设置改变为一个新值(例如,将Button.background设置修改变Color.PINK),那么当已安装的观感变化时UIManager不会替换这个设置。调用setBackground(Color.PINK)也同样如此。当观感发生变化时,只有实现了UIResource接口的特定属性的值才会发生变化。

javax.swing.plaf包中包含许多实现了UIResource接口的类。例如,ColorUIResource类将Color对象看作为UIResource元素。列表20-6列出了所有自定义已安装观感可用的UUIResource组件。

Swing_table_20_6_1.png

Swing_table_20_6_1.png

|Swing\_table\_20\_6\_2.png| |Swing\_table\_20\_6\_3.png| |Swing\_table\_20\_6\_4.png|

下面的代码演示了使用ColorUIResource类来将按钮的背景色设置为一个当已安装的观感变化时将会发生变化的值。

Color background = new ColorUIResource(Color.PINK);
UIManager.put("Button.background", background);

如果封装的ColorUIResource构造函数调用,颜色就会在观感变化之后仍然保持不变。

UIDefaults.ActionValue, UIDefaults.LazyValue与UIDefaults.ProxyLazyValue类

除了实现在UIResouce接口以外,UIDefaults查询表中的元素如果实现了UIDefaults的内联类LazyValue或是ActiveValue,则他们就是延迟的或是活动的。例如,因为Color与Dimension对象并不是非常资源敏感的,当这样的一个元素被放入UIDefaults表中时,则Color与Dimension就会被创建并且立即放入查询表中-这就是称之为是活动的。相对的,在类似于Icon这样的资源例子中,而且特别是ImageIcon,我们希望延迟创建并载入图标类直到需要他时-这就称之为延迟。我们也许希望使其成为延迟的另一个元素例子就是对于每一个JList组件都需要一个单独的渲染器的ListCellRenderer。因为我们并不知道我们需要多少渲染器以或是将要安装哪一个渲染器,我们可以将创建时机延迟并且在我们请求时获得当前渲染器的一个唯一版本。

下面我们来了解一下LookAndFeel的public Object makeIcon(Class baseClass, String imageFile)方法。为了处理图标图像文件的延迟加载,LookAndFeel类会自动为载入一个Icon创建一个LzyValue类。因为图像文件将会在稍后载入,我们需要向图标加载器提供图标图像文件(baseClass)以及文件名(imageFile)的位置。

Object iconObject = LookAndFeel.makeIcon(this.getClass(), "World.gif");
UIManager.put("Tree.leafIcon", iconObject);

接下来我们了解一个UIDefaults.LazyValue定义并且创建DiamondIcon的延迟版本。

public interface UIDefaults.LazyValue {
  public Object createValue(UIDefaults table);
}

在实现了LazyValue接口的类中,他们的构造函数将会保存通过createValue()接口方法传递给实际构造函数的信息。为了有助于创建自定义的延迟值,UIDefaults.ProxyLazyValue类提供了一个保存所传递信息的方法。有四种方法来使用ProxyLazyValue来延迟对象创建,而每一个方法都会使用反射来创建实际的对象,由构造函数的参数获取特定的信息:

  1. public UIDefaults.ProxyLazyValue(String className):如果对象创建使用无参数的构造函数,只需要传递类名作为参数。
  2. public UIDefaults.ProxyLazyValue(String className, String method):如果对象创建将会使用无需参数的工厂方法,则传递工厂方法以及类名。
  3. public UIDefaults.ProxyLazyValue(String className, Object[] arguments):如果对象创建将会使用需要参数的构造函数,则向ProxyLazyValue构造函数传递类名与参数数组。
  4. public UIDefaults.ProxyLazyValue(String lcassName, String method, Object[] arguments):如里对象创建使用需要参数的工厂方法,则传递工厂方法名以及类名与参数组件。

对于将要创建的延迟DiamondIcon,我们将需要传递由颜色,选中状态与维度构成的状态信息。

要测试延迟DiamondIcon,我们可以将UIDefaults.ProxyLazyValue的实例关联到Tree.openIcon设置,如下所示:

Integer fifteen = new Integer(15);
Object lazyArgs[] = new Object[] { Color.GREEN, Boolean.TRUE, fifteen, fifteen} ;
Object lazyDiamond = new UIDefaults.ProxyLazyValue("DiamondIcon", lazyArgs);
UIManager.put("Tree.openIcon", lazyDiamond);

结合前面将Tree.leafIcon设置修改为World.gif图标的变化以及使用默认树数据模型,所生成的树如图20-4所示。

Swing_20_4.png

Swing_20_4.png

package swingstudy.ch19;

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

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.LookAndFeel;
import javax.swing.UIDefaults;
import javax.swing.UIManager;

public class LazySample {

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

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

                Object iconObject = LookAndFeel.makeIcon(LazySample.class, "World.gif");
                UIManager.put("Tree.leafIcon", iconObject);

                Integer fifteen = new Integer(15);
                Object lazyArgs[] = new Object[] {Color.GREEN, Boolean.TRUE, fifteen, fifteen};
                Object lazyDiamond = new UIDefaults.ProxyLazyValue("DiamondIcon", lazyArgs);
                UIManager.put("Tree.openIcon", lazyDiamond);

                JTree tree = new JTree();
                JScrollPane scrollPane = new JScrollPane(tree);

                frame.add(scrollPane, BorderLayout.CENTER);
                frame.setSize(200, 200);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

与延迟值不同,活动值类似于实例创建工厂。每次通过UIManager的get()方法请求一个值时,则会创建并返回一个新实例。接口方法与UIDefaults.LazyValue的接口方法相同;只有接口的名字是不同的。

public interface UIDefaults.ActiveValue {
  public Object createValue(UIDefaults table);
}

为了进行演示,列表20-5定义了一个构建JLabel组件的工厂。标签文本将作为显示创建了多个标签的计数器。每次createValue()方法被调用时,则会创建一个新JLabel。

package swingstudy.ch19;

import javax.swing.JLabel;
import javax.swing.UIDefaults;

public class ActiveLabel implements UIDefaults.ActiveValue {
    private int counter = 0;

    public Object createValue(UIDefaults defaults) {
        JLabel label = new JLabel(""+counter++);
        return label;
    }
}

为了创建组件,我们需要使用UIManager.put()方法安装ActiveLabel类。一旦这个类被安装,每次调用UIManager的get()方法都会导致创建一个新的组件。

UIManager.put(LABEL_FACTORY, new ActiveLabel());
...
JLabel label = (JLabel)UIManager.get(LABEL_FACTORY);

图20-5显示了使用中的组件。当每次按钮被点击时,则会调用UIManager.get()方法,并且组件被添加到屏幕。

Swing_20_5.png

Swing_20_5.png

列表20-6显示了图20-5所示的示例程序的源码。

package swingstudy.ch19;

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

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

public class ActiveSample {

    private static final String LABEL_FACTORY = "LabelFactory";
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

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

                UIManager.put(LABEL_FACTORY, new ActiveLabel());

                final JPanel panel = new JPanel();

                JButton button = new JButton("Get");

                ActionListener actionListener = new ActionListener() {
                    public void actionPerformed(ActionEvent event) {
                        JLabel label = (JLabel)UIManager.get(LABEL_FACTORY);
                        panel.add(label);
                        panel.revalidate();
                    }
                };
                button.addActionListener(actionListener);

                frame.add(panel, BorderLayout.CENTER);
                frame.add(button, BorderLayout.SOUTH);
                frame.setSize(200, 200);
                frame.setVisible(true);
            }
        };
        EventQueue.invokeLater(runner);
    }

}

注意,有一个特殊的延迟类用于InputMap延迟:UIDefaults.LazyInputMap类。

使用客户端属性

如果修改所有UIManager已知的UIResource属性仍不能为我们提供我们所需要的观感,一些UI委托类可以为我们提供隐藏于API视图之外的他们自己的自定义功能。这些自定义功能是作为客户端属性来提供的,并且可以通过JComponent的两个方法来访问:public final Object getClientProperty(Object key)与public final void putClientProperty(Object key, Object value)。记住这里的key与value是Object类型的。虽然通常情况下key是一个String而value是一个任意类型的对象,key也可是一个任意类型的对象。

客户端属性的目的是作为特定观感的组件的属性。无需通过继承观感委托通过一对getter/setter方示来公开属性,get/put客户端属性提供了到私有实例级别查询表的访问来存储新的属性设置。另外,当对UIDefaults进行修改时,修改组件的客户端属性会通知所注册的组件的属性变化监听器。

大多数特定的客户端属性已在本书中相关的组件部分进行探讨。表20-7提供了一个用于所有可配置的客户端属性的资源。左边的例显示了除了包名以外属性所用于的类。中间一列显示了属性名,其中包含所用的原始文本与可用的类常量。右边一列包含了来存储属性名的类类型。如果类类型是一个String,则会提供一个可用值列表。

|Swing\_table\_20\_7\_1.png| |Swing\_table\_20\_7\_2.png| |Swing\_table\_20\_7\_3.png| |Swing\_table\_20\_7\_4.png| |Swing\_table\_20\_7\_5.png| |Swing\_table\_20\_7\_6.png|

注意,表20-7中的大多数属性都是为特定的委托实现在内部所用的,而我们不需要使用他们。其他的一些属性,例如桌面管理器的拖拽模式,是在JDK新版本发布之前保存API不变的来添加功能的中间方法。

为了演示客户端属性的使用,下面的两行代码将JToolBar.isRoolover属性修改为Boolean.TRUE。其他的工具栏也许并不希望这个属性设置为Boolean.TRUE,所以将这个属性设置保持为Boolean.FALSE。

JToolBar toolbar = new JToolBar();
toolbar.putClientProperty("JToolBar.isRollover", Boolean.TRUE);

创建新的UI委托

有时修改Swing组件的某些UIResource元素并不足以获得我们所希望的外观或是行为。当出现这种情况时,我们需要为组件创建一个新的UI委托。每一个Swing组件都有控制其MVC体系统结构中的视图与控制器方面的UI委托。

表20-8提供了一个Swing组件,描述每一个组件的UI委托的类以及预定义观感类的特定实现的列表。例如,调用JToolBar的getUIClassID()方法将会返回ToolBarUI的UI委托的类ID字符串。然后如果我们使用UIManager.get(“ToolBarUI”)调用向UIManager查询当前已安装的观感的该UI委托的特定实现,则会返回抽象的ToolBarUI的实现。所以,如果我们要为JToolBar组件开发我们自己的观感,我们必须创建一个抽象ToolBarUI类的实现。

|Swing\_table\_20\_8\_1.png| |Swing\_table\_20\_8\_2.png| |Swing\_table\_20\_8\_3.png| |Swing\_table\_20\_8\_4.png|

注意,JWindow,JFrame与JApplet这样的类都是重量级组件,因而缺少UI委托。

第13章中的PopupComboSample示例演示了新的UI委托的创建。列表20-7稍微修改了自定义的ComboBoxUI片段,其中显示下拉菜单的普通向下按钮被替换为右箭头。

package swingstudy.ch20;

import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.basic.BasicArrowButton;
import javax.swing.plaf.basic.BasicComboBoxUI;

public class MyComboBoxUI extends BasicComboBoxUI {

    public static ComponentUI createUI(JComponent c) {
        return new MyComboBoxUI();
    }

    protected JButton createArrowButton() {
        JButton button = new BasicArrowButton(BasicArrowButton.EAST);
        return button;
    }
}

要使用新的UI委托,我们只需要创建这个类并且使用setUI()方法将其与组件相关联。

JComboBox comboBox = new JComboBox(labels);
comboBox.setUI((ComboBoxUI)MyComboBoxUI.createUI(comboBox));

修改第13的PopupComboSample示例使其显示两个组合框,自定义的ComboBoxUI在上面而另一个在下面,则会产生图20-6所示的结果。

Swing_20_6.png

Swing_20_6.png

列表20-8显示了生成了图20-6的更新的源码。

package swingstudy.ch20;

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

import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.plaf.ComboBoxUI;

public class PopupComboSample {

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

        Runnable runner = new Runnable() {
            public void run() {
                String labels[] = {"Chardonnay", "Sauvignon", "Riesling", "Cabernet",
                        "Zinfandel", "Merlot", "Pinot Noir", "Sauvignon Blanc", "Syrah",
                        "Gewurztraminer"
                };
                JFrame frame = new JFrame("Popup JComboBox");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                JComboBox comboBox = new JComboBox(labels);
                comboBox.setMaximumRowCount(5);
                comboBox.setUI((ComboBoxUI)MyComboBoxUI.createUI(comboBox));
                frame.add(comboBox, BorderLayout.NORTH);

                JComboBox comboBox2= new JComboBox(labels);
                frame.add(comboBox2, BorderLayout.SOUTH);

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

}

如果我们要为所有的组件使用这个新的UI委托,我们可以在创建组件之前使得UIManager知道这个委托,而不是在创建之后手动调用setUI()。在列表20-8的示例中,我们可以添加下面的代码行:

UIManager.put("ComboBoxUI", "MyComboBoxUI")

如果我们这样做,两个组合框就会看起来相同。

UI委托的实际创建是间接完成的,如图20-7所示。组件的构造函数调用会向UIManager查询UI委托类。UIManager在在其默认属性UIDefaults对象中维护委托列表。当UIDefaults为委托所需要时,他会返回到组件来查询需要哪个委托。在查找到相应的委托实现以外,UIDefaults对象会告诉ComponentUI来创建组件,从而导致创建实际的UI委托类。一旦UI委托被创建,他就需要为特定的模型状态进行配置。

Swing_20_7.png

Swing_20_7.png

创建新的观感

除非公司要求我们自定义所有的内容来提供唯一的体验,通常并不需要从头创建一个完整的新观感。通常,开发者通过提供一些自定义的UI委托来对现在的观感进行小量的修改。然而如果我们确实希望创建一个新的观感类,我们只需要创建一个LookAndFeel类的子类。我们仍然必须提供UI委托,但是现在这些类可以由Swing组件隐藏,因为他们的使用将并不会直接为javax.swing组件类所知。

在非Windows机器上使用WindowsLookAndFeel

为了演示新的观感类的创建,让我们创建一个封装了Windows UI委托的平台需求的观感实现。通过简单的重写public boolean isSupportedLookAndFeel()方法来返回true,我们就可以为Windows观感类移除平台需求。

注意,Java许可证禁止发布移除了Windows观感类平台需求的程序。所以只要我们不发布他,就可以使用这个这里的示例。

列表20-9中的类定义显示了创建一个新的观感实现是多么的简单。

package swingstudy.ch20;

import com.sun.java.swing.plaf.windows.WindowsLookAndFeel;

public class MyWindows extends WindowsLookAndFeel {

    public String getID() {
        return "MyWindows";
    }

    public String getName() {
        return "MyWindows Look and Feel";
    }

    public String getDescription() {
        return "The MyWindows Look and Feel";
    }

    public boolean isNativeLookAndFeel() {
        return false;
    }

    public boolean isSupportedLookAndFeel() {
        return true;
    }
}

如果我们在一个非Windows机器上使用这个Swing类,我们可以使得观感成为Windows观感。只需要将我们的观感设置为MyWindows并且使得观感类文件可用。类文件只需要在我们的CLASSPATH中可用并且使用下面的命令行启动:

java -Dswing.defaultlaf=MyWindows ClassFile

为了使得Windows观感变化可以正常工作,我们需要在MyWindows目录结构的icons子目录中提供观感所用的图标文件。表20-9列出了与预定义的观感类型相对应的图标。MyWindows观感需要所有的Windows图像文件。

注意,尽管Ocean只是Metal的主题,他却提供了他自己的图像集合。

|Swing\_table\_20\_9\_1.png| |Swing\_table\_20\_9\_2.png| |Swing\_table\_20\_9\_3.png| |Swing\_table\_20\_9\_4.png| |Swing\_table\_20\_9\_5.png| |Swing\_table\_20\_9\_6.png|

注意,至少JOptionPane消息类型的图像在所有的观感中都是需要的。通常他们名为Error.gif,Inform.gif,Question.gif与Warn.gif,尽管并不是绝对需要。

如果我们不希望跨过Widnows观感的“本地”需求,我们可以安装单独的UI委托,例如下面的代码将会为JButton组件使用Windows UI委托:

UIManager.put("ButtonUI","com.sun.java.swing.plaf.windows.WindowsButtonUI")

添加UI委托

创建一个具有自定义UI委托的新观感需要创建一个LookAndFeel类的子类。更可能的情况是,我们将会创建一个BasicLookAndFeel类或是另一个预定义观感类的子类,然后为其中的一些组件提供我们的委托。

如果我们继承BasicLookAndFeel类,则他会有一个public void initClassDefaults(UIDefaults table)方法,可以重写这个方法来安装我们自己的委托。只需要将委托放在观感的UIDefaults表中,而不是放在希望使用这个新委托的我们程序中。

列表20-10中的MetalLookAndFeel的扩展将前面定义的MyComboBoxUI委托作为ComboBoxUI委托添加到观感中。随着我们定义更多的自定义组件,我们可以使用相似的方法进行添加。

package swingstudy.ch20;

import javax.swing.UIDefaults;
import javax.swing.plaf.metal.MetalLookAndFeel;

public class MyMetal extends MetalLookAndFeel {

    public String getID() {
        return "MyMetal";
    }

    public String getName() {
        return "MyMetal Look and Feel";
    }

    public String getDescription() {
        return "The MyMetal Look and Feel";
    }

    public boolean isNativeLookAndFeel() {
        return false;
    }

    public boolean isSupportedLookAndFeel() {
        return true;
    }

    protected void initClassDefaults(UIDefaults table) {
        super.initClassDefaults(table);
        table.put("ComboBoxUI", "MyComboBoxUI");
    }
}

使用Metal主题

Metal观感类(javax.swing.plaf.metal.MetalLookAndFeel)提供了定义颜色,字体以及由UIManager管理的所有的UIDefaults默认设置的方法。通过允许用户修改主题,他们就可以通过开发者的最少工作来获得所喜欢的颜色或是字体尺寸。通过开发合适的主题,我们无需创建新的观感类就可以很容易的定制接口或是手动将新的设置插入到UIDefaults中。

MetalTheme类

表20-10列出了通过MetalTheme类可用的49个不同的属性。各种primary与secondary属性是抽象并且必须在子类中实现。在其他的属性中,以Font结尾的六个属性-controlTextFont,menuTextFont,subTextFont,systemTextFont,userTextFont与windowTextFont-也是抽象并且必须在子类中实现。其余的属性,在默认情况下,重用11个primary/secondary值中的一个用于他们的设置。

|Swing\_table\_20\_10\_1.png| |Swing\_table\_20\_10\_2.png| |Swing\_table\_20\_10\_3.png| |Swing\_table\_20\_10\_4.png|

DefaultMetalTheme与OceanTheme类

与类名相反,DefaultMetalTheme类并不是默认的Metal主题;默认主题为OceanTheme。DefaultMetalTheme称其自身为Steel主题并且分别使用蓝色与灰色用于primary与secondary设置。OceanTheme,称之为Ocean,对于背景使用浅蓝色调色板。

要使用Steel主题而不是Ocean主题,我们需要将swing.metalTheme系统属性设置为steel,如下所示:

java –Dswing.metalTheme=steel ClassName

大多数人更喜欢Ocean的新外观,但是为了兼容仍然可以使用Steel。

如果我们创建我们自己的Metal主题,我们需要继承OceanTheme或是DefaultMetalTheme,然后通过设置MetalLookAndFeel类的表述currentTheme属性将自定义主题安装为我们的主题。

MetalTheme myTheme = new MyTheme();
MetalLookAndFeel.setCurrentTheme(myTheme);

由于MetalTheme的大多数定制都是与字体和颜色相关的,public void addCustomEntriesToTable(UIDefaults table)方法允许我们重写Metal观感的默认UIDefaults设置。所以,不仅主题自定义Swing组件的字体与颜色,而他们还可以自定义Swing组件中许多UIResource相关的属性。

下面的代码演示了如何为特定的主题设置两个滚动条设置。记住,当合适的时候要使用UIResource标记这些设置,并且不要忘记使用我们超类实现来初始化table参数。

public void addCustomEntriesToTable(UIDefaults table) {
  super.addCustomEntriesToTable(table);
  ColorUIResource thumbColor = new ColorUIResource(Color.MAGENTA);
  table.put("Scrollbar.thumb", thumbColor);
  table.put("ScrollBar.width", new Integer(25));
}

MetalWorks系统demo是由JDK安装随着自定义主题的例子而提供的。他所定义的主题由一个属性文件读取主题颜色设置。无需每次我们要修改我们程序主题的时候创建一个新的类文件,我们可以在运行时由文件读取。

name=Charcoal
primary1=33,66,66
primary2=66,99,99
primary3=99,99,99
secondary1=0,0,0
secondary2=51,51,51
secondary3=102,102,102
black=255,255,255
white=0,0,0

图20-8显示了Metalworks演示程序中所用的Charcoal主题。图20-9显示了他所定义的Presentation主题。

|Swing\_20\_8.png| |Swing\_20\_9.png|

使用Auxiliary观感

Swing提供了多个观感,可以通过MutliLookAndFeel或是通过swing.properties文件中swing.plaf.multiplexingplaf属性指定在任意时刻激活。当安装了多个观感类时,只有一个观感会可见并在屏幕上绘制。其余的版本被称之为auxiliary观感并且与可访问选项相关联,例如屏幕读取器。另一个辅助观感就是日志记录器,他会记录那些与日志文件交互的组件。

Auxiliary观感类是通过使用swing.properties文件中的swing.auxiliarylaf属性使用运行环境注册的。如果指定了多个类,则各项需要通过逗号进行分隔。除了使用属性文件,我们可以通过调用UIManager的public static void addAuxiliaryLookAndFeel(LookAndFeel lookAndFeel)方法在程序中安装观感。一旦安装,多元观感就会为所有已安装的观感类自动创建并管理UI委托。

要确定安装了哪一个辅助观感类,我们可以通过UIManager的public static LookAndFeel[] getAuxiliaryLookAndFeels()方法进行查询。这会返回实际LookAndFeel对象的数组,与通过getInstalledLookAndFeels()方法返回的UIManager.LookAndFeelInfo数组不同。

SynthLookAndFeel类

Synth观感是一个完全丰满的观感,并不是Metal,Windows或Motif的主题扩展。尽管此观感并不使用UIResource表,该类由一个空的绘图开始并且由一个XML文件中读取完整的定义。

配置Synth

Synth观感的配置类似如下的样子:

SynthLookAndFeel synth = new SynthLookAndFeel();
Class aClass = SynthSample.class;
InputStream is = aClass.getResourceAsStream("config.xml");
synth.load(is, aClass);
UIManager.setLookAndFeel(synth);

那么配置文件config.xml中的内容到底是什么呢?在我们的配置文件中,我们指定了我们希望在我们的程序中所用的特定组件如何显示。这通常称之为skining我们的程序,或是创建一个自定义的皮肤。通过简单的修改XML文件,我们程序的整个外观就会发生变化;并不需要编程实现。

DTD文件可以由 http://java.sun.com/j2se/1.5.0/docs/api/javax/swing/plaf/synth/doc-files/synth.dtd获取。文件格式在 http://java.sun.com/j2se/1.5.0/docs/api/javax/swing/plaf/synth/doc-files/synthFileFormat.html进行完全描述。解析器并不验证,在有工具帮助自动化过程之前,我们需要小心处理XML文件的创建。

在Synth中有许多可用的配置选项,但是基本的XML概念用来定义style并将其bind到组件。