Sun的Swing包包含一个支持我们程序中撤销操作支持的实用程序。他允许我们支持修改我们数据状态的撤销与重做操作。尽管这个框架是Swing包层次结构的一部分,他却可以用在任意的程序中,而不仅是基于组件的程序。
Undo框架位于javax.swing.undo包中,包含五个类,两个接口以及两个异常。要支持Undo框架,在javax.swing.event包中还有一个相关的癌与事件。在其顶部是UndoableEdit接口。这个接口构成了用于封装使用Command设计模式可以撤销或是重做的操作。
可撤销命令的根实现类是AbstractUndoableEdit类。不要叫这个类名迷惑我们,他并不是抽象的。根命令的子命令是CompundEdit与StateEdit命令类。
CompoundEdit类可以使得我们组合多个可撤销的操作,其中一些可撤销的操作则是存储状态变化的StateEdit对象。Swing文本组件会在他们的内容发生变化时创建DefaultDocumentEvent命令。这个命令是CompundEdit的子类,同时也是AbstractDocument的内联类。另一个封装的命令是UndoManager,他是CompundEdit的子类。
UndoManager通过承担UndoableEditListener的角色并且响应UndoableEditEvent的创建来管理可编辑对象上的编辑操作。当UndoableEdit不可撤销时,则会抛出CannotUndoException。另外,当一个UndoableEdit不可以重做时,则会抛出CannotRedoException。
如查我们希望创建支持撤销与重做操作的对象,对象需要实现StateEditable接口,并且他们可以使用UndoableEditSupport类来帮助管理UndoableEdit对象的列表。
在深入单个Undo框架的片段细节之前,我们先来了解一下如何在Swing文本组件中使用Undo框架。如果这些就是我们所需要的,我们就不需要理解其他的工作了。
Swing文本组件已经支持了必须的撤销与重做功能。我们并不需要使用UndoManager来进行管理并且通知管理器何时撤销/重做。
作为一个示例,考虑一个包含一个JTextArea与两个用于撤销与重做的工具栏按钮,如图21-1所示。
要使得图21-1中的JTextArea支持撤销操作,我们必须将一个UndoableEditListener关联到组件的Document。我们需要做的就是将UndoManager作为我们的监听器。首先,我们创建管理器,然后关联。
UndoManager manager = new UndoManager();
textArea.getDocument().addUndoableEditListener(manager);
一旦管理器被关联到JTextArea的文档,他就会监听文本域内容的所有变化。因为每一个Swing文本组件都有一个Document数据模型,我们可以直接将UndoManager与这些组件的文档相关联。
在将管理器关联到文本组件之后,我们必须提供一些方法来通知管理器撤销或是重做某一操作。通常这是通过菜单选择或是工具栏按钮选择来实现的。在图21-1中,这是借助于JToolBar上的按钮来实现的,而另一个按钮用于实现重做。对于Undo按钮,我们希望管理撤销一个操作。所以,按钮的ActionListener应该调用UndoManager的public void undo()方法。Redo按钮的ActionListener应该调用管理器的public void redo()方法。undo()方法与redo()方法都会抛出必须处理的异常。
因为Undo与Redo按钮所必须的功能对于所用的管理器都是相同的,我们将会在列表21-1中包含一个助手类UndoManagerHelper来为我们创建Action对象。这些对象可以为JMenuBar,JToolBar,或是其他可以使用ActionListener响应用于处理撤销与重做操作的组件所用。我们必须向助手类请求Action,然后将Action关联到相应的组件。例如,下面的五行代码将会获取前面所创建的UndoManager,并且为JToolBar添加必要的按钮:
JToolBar toolbar = new JToolBar();
JButton undoButton = new JButton(UndoManagerHelper.getUndoAction(manager));
toolbar.add(undoButton);
JButton redoButton = new JButton(UndoManagerHelper.getRedoAction(manager));
toolbar.add(redoButton);
使用Swing文本组件的撤销功能就是如此简单。UndoManagerHelper类的定义显示在列表21-1中。如果我们不喜欢默认的按钮标签(显示在图21-1中),还有一些支持自定义的其他方法。另外,如果在撤销或是重做过程中抛出异常,则会弹出一个警告信息。警告信息与弹出容器标题也是可以自定义的。
package swingstudy.ch21;
import java.awt.Component;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JOptionPane;
import javax.swing.UIManager;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;
public class UndoManagerHelper {
public static Action getUndoAction(UndoManager manager, String label) {
return new UndoAction(manager, label);
}
public static Action getUndoAction(UndoManager manager) {
return new UndoAction(manager, (String)UIManager.get("AbstractUndoableEdit.undoText"));
}
public static Action getRedoAction(UndoManager manager, String label) {
return new RedoAction(manager, label);
}
public static Action getRedoAction(UndoManager manager) {
return new RedoAction(manager, (String)UIManager.get("AbstractUndoableEdit.redoText"));
}
private abstract static class UndoRedoAction extends AbstractAction {
UndoManager undoManager = new UndoManager();
String errorMessage = "Cannot undo";
String errorTitle = "Undo Problem";
protected UndoRedoAction(UndoManager manager, String name) {
super(name);
undoManager = manager;
}
public void setErrorMessage(String newValue) {
errorMessage = newValue;
}
public void setErrorTitle(String newValue) {
errorTitle = newValue;
}
protected void showMessage(Object source) {
if (source instanceof Component) {
JOptionPane.showMessageDialog((Component)source, errorMessage, errorTitle, JOptionPane.WARNING_MESSAGE);
}
else {
System.err.println(errorMessage);
}
}
}
public static class UndoAction extends UndoRedoAction {
public UndoAction(UndoManager manager, String name) {
super(manager, name);
setErrorMessage("Cannot undo");
setErrorTitle("Undo Problem");
}
public void actionPerformed(ActionEvent event) {
try {
undoManager.undo();
}
catch(CannotUndoException cannotUndoException) {
showMessage(event.getSource());
}
}
}
public static class RedoAction extends UndoRedoAction {
public RedoAction(UndoManager manager, String name) {
super(manager, name);
setErrorMessage("Cannot redo");
setErrorTitle("Redo Problem");
}
public void actionPerformed(ActionEvent event) {
try {
undoManager.redo();
}
catch(CannotRedoException cannotRedoException) {
showMessage(event.getSource());
}
}
}
}
图21-1中所示示例的其余源代码显示在列表21-2中。借助于新创建的UndoManagerHelper类,在Swing文本组件中使用Undo框架的最大困难已经得到了极大的简化。
package swingstudy.ch21;
import java.awt.BorderLayout;
import java.awt.EventQueue;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JToolBar;
import javax.swing.undo.UndoManager;
public class UndoSample {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Runnable runner = new Runnable() {
public void run() {
JFrame frame = new JFrame("Undo Sample");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JTextArea textArea = new JTextArea();
JScrollPane scrollPane = new JScrollPane(textArea);
UndoManager manager = new UndoManager();
textArea.getDocument().addUndoableEditListener(manager);
JToolBar toolbar = new JToolBar();
JButton undoButton = new JButton(UndoManagerHelper.getUndoAction(manager));
toolbar.add(undoButton);
JButton redoButton = new JButton(UndoManagerHelper.getRedoAction(manager));
toolbar.add(redoButton);
frame.add(toolbar, BorderLayout.NORTH);
frame.add(scrollPane, BorderLayout.CENTER);
frame.setSize(300, 150);
frame.setVisible(true);
}
};
EventQueue.invokeLater(runner);
}
}
如果我们只是希望在Swing文本组件中使用Undo框架,我们可以跳过本章的其他部分。相反,如果我们希望在其他的组件中使用该框架,或者甚至是在非组件设置中使用,我们需要阅读其余部分,我们将会在余下的内容中深入探讨框架的内部实现。
javax.swing.undo包的撤销功能实现了Command设计模式,其包括如下组成部分:
如果我们要Swing文本组件之外使用Undo框架,Document元素将会为特定于我们客户程序的接收者所替代。我们需要创建我们自己的UndoableEdit接口实现来承担模式中Concrete Command的任务。无需直接实现接口,我们只需要继承AbstractUndoableEdit类来封装关于我们命令的特定信息。
Command设计模式十分强大。无论我们在模式中使用哪种命令,我们都可以设置功能,例如用于执行自动测试的宏,因为调用会顺序执行这些命令。