在第20章中,我们通过探讨可插拨的观感体系结构支持了解了如何自定义基于Swing的程序。在本章中,我们将会探讨由作为Swing包一部分的JFC所提供的Undo框架。
Sun的Swing包包含一个支持我们程序中撤销操作支持的实用程序。他允许我们支持修改我们数据状态的撤销与重做操作。尽管这个框架是Swing包层次结构的一部分,他却可以用在任意的程序中,而不仅是基于组件的程序。
21.1 使用Undo框架
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框架。如果这些就是我们所需要的,我们就不需要理解其他的工作了。
21.2 配合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框架,我们可以跳过本章的其他部分。相反,如果我们希望在其他的组件中使用该框架,或者甚至是在非组件设置中使用,我们需要阅读其余部分,我们将会在余下的内容中深入探讨框架的内部实现。
21.3 Command设计模式
javax.swing.undo包的撤销功能实现了Command设计模式,其包括如下组成部分:
- Command:UndoableEdit接口定义了用于执行撤销与重做操作的接口。
- Concrete Command:AbstractUndoableEdit类的实例,或者是更为特定的子类,实现了必须的Command接口。他们将命令绑定到接收者(Document)来修改其内容。
- Client:在Swing文本组件的例子中,Document执行了实际的AbstractUndoableEdit子类的创建,默认为AbstractDocument.DefaultDocumentEvent。
- Invoker:UndoManager扮演UndoableEdit命令调用者的角色。通常情况下,其他的参与者会通知调用者何时执行调用。然而,调用者会通知特定的UndoableEdit实例何时撤销或是重做命令。
- Receiver:Document是实际的AbstractUndoabelEdit子类中命令的接收者。他知道如何执行请求。
如果我们要Swing文本组件之外使用Undo框架,Document元素将会为特定于我们客户程序的接收者所替代。我们需要创建我们自己的UndoableEdit接口实现来承担模式中Concrete Command的任务。无需直接实现接口,我们只需要继承AbstractUndoableEdit类来封装关于我们命令的特定信息。
Command设计模式十分强大。无论我们在模式中使用哪种命令,我们都可以设置功能,例如用于执行自动测试的宏,因为调用会顺序执行这些命令。