21.4 Undo框架组件
我们已经实际了解了Swing文本组件中的Undo框架并且审视了Command设计模式。下面我们来了解一下框架的单独部分。
21.4.1 UndoableEdit接口
第一个Undo框架组成部分就是UndoableEdit接口,其定义如下:
public interface UndoableEdit { // Properties public String getPresentationName(); public String getRedoPresentationName(); public boolean isSignificant(); public String getUndoPresentationName(); // Other Methods public boolean addEdit(UndoableEdit anEdit); public boolean canRedo(); public boolean canUndo(); public void die(); public void redo() throws CannotRedoException; public boolean replaceEdit(UndoableEdit anEdit); public void undo() throws CannotUndoException; }
这个接口定义了应该支持撤销与重做功能的对象所要完成的操作。除了描述所支持的操作,接口隐式定义了可撤销的操作可以位于的三个状态,如图21-2所示。
状态之间的转换关系如下:
可撤销状态:当UndoableEdit命令第一次被创建时,操作位于可撤销状态。die()方法的目的就在于在垃圾收集器决定清理并且将命令置于完成状态之前释放UndoableEdit的资源。调用undo()方法或者会抛出一个CannotUndoException或者使得命令撤销完成并且状态变化可重做。调用redo()方法或者会抛出CannotRedoException或是使得命令再次完成并且状态保持为可撤销状态。
可重做状态:当操作位于可重做状态时,命令已经撤销完成。调用die()方法会释放所有的资源并且将命令置于完成状态。调用undo()方法或者抛出CannotUndoException或是使得命令再次撤销完成并且状态保持为可重做状态。调用redo()方法或者抛出CannotRedoException或是使得命令重做完成,并且将状态返回可撤销状态。
完成状态:当操作位于完成状态时,调用undo(),redo()或是die()方法会使得操作保持为完成状态。
某些状态变化并不会发生;然而,所有的状态变化都被支持。特殊情况留给我们正在使用Command。例如,如果功能有意义,Microsoft Word会允许我们持续重做上一个命令,例如格式化段落或是输入段落。
21.4.2 AbstractUndoableEdit类
AbstractUndoableEdit类为UndoableEdit接口的所有方法提供了默认实现。尽管由其名字我们也许会猜测该类是一个抽象类,事实上并不是。然而,开发者会使用该类的子类,而不是直接使用该类的实例。
默认情况下,AbstractUndoableEdit命令是significant(其isSignificant()会返回true)。我们在significance属性设置上所设置的意义依赖于我们命令的使用。另外,类限制了可撤销状态变化的重复。除非通过子类重写,如果我们尝试在可撤销状态中重做或在可重做状态中撤销时会抛出异常。该类并不支持添加或是替换UndoableEdit操作。
undoPresentationName与redoPresentationName属性的默认表现名字为Undo与Redo。这可以通过查找表21-1中的UIResource相关属性查到。对于presentationName属性并没有默认值。子类应该至少提供表现名来提供有意义的名字,而不是默认设置。
21.4.3 CompoundEdit类
CompoundEdit类使得我们可以将多个可撤销操作组合为一个可撤销操作。例如,也许我们希望将输入一个完整单词的所有的按键组合为一个单个的CompoundEdit命令。这可以使得我们连续撤销在多个位置所输入的完整单词。如果没有组合单个的按键,重做上一个命令仅是重做上一次按键。
CompoundEdit类使用一个只读的inProgress属性来报告一个命令是否仍然被组合。初始时,该属性为true。当位于过程中时,可以使用addEdit(UndoableEdit)将其他的命令添加到组合命令中。要标识命令集合的结束,我们可以调用end()方法。只在我们组合所有的命令之后才可以撤销或是重做。如图21-3所示。
使用CompundEdit,如果我们undo()编辑,所有添加的命令都会被撤销。redo()也是如此:集合中的所有命令都会被重做。
21.4.4 UndoManager类
UndoManager类是一个跟踪编辑命令历史的特殊CompoundEdit子类,主要用于整个程序。管理器可以跟踪的可撤销命令数目是由一个可配置的limit属性来定义的,其初始值为100.
当isInProgress()方法对于特定的CompoundEdit报告true时,UndoManager在某种程度上类似于退步的CompoundEdit,其中单个命令可以撤销完成或是重做完成。一旦调用end()方法,UndoManager的作用类似于CompoundEdit,但是没有撤销或是重做单个编辑命令的功能。另外,UndoManager还有另外一个状态-Undoable或是Redoable-用于管理器已经撤销完成至少一个命令,仍然可以撤销更多命令,但是同时可以重做已撤销完成的命令。
除了能够直接使用addEdit()方法添加可编辑的操作外,管理同时承担UndoableEditListener的任务。当UndoableEditEvent发生时,监听器使用addEdit()方法将事件的UndoableEdit命令添加到管理器。另外,我们可以使用public void discardAllEdits()方法来清除编辑队列。在管理器接收end()方法以外,序列回到图21-3所示的状态,只剩下图表中底部所显示的三个状态(Undoable,Redoable,Done)。整个序列如图21-4所示。
记住,特定的undo()与redo()调用可以抛出异常。另外,当我们请求UndoManager撤销或是重做一个编辑命令时,请求会撤销(或是重做)至到上一个有意义命令的所有命令。
对于某些用户来说UndoManager到CompoundEdit的转换也许会感到迷惑。这个转换可以使得我们有另一个UndoManager用于特定的子操作,也就是一旦完成就会变为一个CompundEdit,并传递给主UndoManager。
]21.4.5 UndoableEditListener接口与UndoableEditEvent类
UndoManager实现了UndoableEditListener接口,从而当可撤销的操作发生时,他可以得到通知。该监听器的定义如下:
public interface UndoableEditListener extends EventListener { public void undoableEditHappened(UndoableEditEvent undoableEditEvent); }
当一个可撤销的命令发生时,UndoableEditListener使用UndoableEditEvent来通知所有感兴趣的对象。UndoableEditEvent类包含一个属性,edit,他会返回事件的UndoableEdit对象:public UndoableEdit getEdit()。
在Swing相关的所有包中,只有AbstractDocument类(定义在Document接口中)带有内建的添加这些监听器的支持。当创建我们自己的支持可撤销操作的类时,我们需要借助于UndoableEditSupport类来维护我们自己的监听器列表,如下所述。
21.4.6 UndoableEditSupport类
UndoableEditSupport类似于PropertyChangeSupport与VetoableChangeSupport的JavaBean相关类。所有这三个类会管理一个特定类型的监听器列表。在UndoableEditSupport类的例子中,监听器的类型为UnableEditListener。我们使用public void addUndoableListener(UndoableEditListener)添加监听器,使用public void removeUndoableListener(UndoableEditListener)移除监听器。
当我们希望通知一个UndoableEdit操作发生时,我们调用public void postEdit(UndoableEdit)方法,他会创建一个UndoableEditEvent并且调用每一个监听器的undoableEditHappened()方法。
该使用的基本框架显示在列表21-3中。通常情况下,我们将可撤销的事件关联到某些其他操作。在这个示例中,他被关联到ActionEvent发生的时刻,从而所有注册的ActionListener对象需要得到通知。
package swingstudy.ch21; import java.awt.AWTEventMulticaster; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.event.UndoableEditListener; import javax.swing.undo.AbstractUndoableEdit; import javax.swing.undo.UndoableEditSupport; public class AnUndoableComponent { UndoableEditSupport undoableEditSupport = new UndoableEditSupport(this); ActionListener actionListenerList = null; public void addActionListener(ActionListener actionListener) { actionListenerList = AWTEventMulticaster.add(actionListener, actionListenerList); } public void removeActionListener(ActionListener actionListener) { actionListenerList = AWTEventMulticaster.remove(actionListener, actionListenerList); } public void addUndoableEditListener(UndoableEditListener undoableEditListener) { undoableEditSupport.addUndoableEditListener(undoableEditListener); } public void removeUndoableEditListener(UndoableEditListener undoableEditListener) { undoableEditSupport.removeUndoableEditListener(undoableEditListener); } protected void fireActionPerformed(ActionEvent event) { if(actionListenerList != null) { actionListenerList.actionPerformed(event); } // Need to create your custom type of undoable operation undoableEditSupport.postEdit(new AbstractUndoableEdit()); } }
21.4.7 一个完整的可撤销程序示例
现在我们已经了解了Swing Undo框架的主要类,下面我们来看一下定义自定义可撤销类的完整示例。这个可撤销类是一个绘制面板,其中每一次鼠标点击定义了一个要在区域中绘制的点。图21-5显示了在撤销操作之前与之后的可绘制面板。
列表21-4中的主程序看起来与前面列表21-2中的在Swing文本组件中支持撤销与重做操作的示例相同。我们只需要简单的的创建一个UndoManager来管理可撤销的操作并且将其关联到可撤销的对象。在这里Undo框架的使用与此相同,所不同的是可撤销的对象是将要创建的UndoableDrawingPanel类。
package swingstudy.ch21; import java.awt.BorderLayout; import java.awt.EventQueue; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JToolBar; import javax.swing.undo.UndoManager; public class UndoDrawing { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Runnable runner = new Runnable() { public void run() { JFrame frame = new JFrame("Drawing Sample"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); UndoableDrawingPanel drawingPanel = new UndoableDrawingPanel(); UndoManager manager = new UndoManager(); drawingPanel.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(drawingPanel, BorderLayout.CENTER); frame.setSize(300, 150); frame.setVisible(true); } }; EventQueue.invokeLater(runner); } }
UndoableDrawingPanel类是一个依据区域内的点集合在其内部绘制多边形的组件。当鼠标被释放时新点就被添加到多边形中。如果我们不希望组件支持可撤销的操作,则除了收集用于绘制面板的点以外我们不需要做任何事情。
为使得面板支持可撤销的操作,他需要做两件事情:
- 他必须包含一个UndoableEditListener对象列表。这可以很容易的通过UndoableEditSupport类实现,如前面的列表21-3所示。
- 第二个任务需要创建一个UndoableEdit对象,依据状态变化将其发送到注册的监听器。因为绘制面板的状态是多连形,这个属性必须在绘制类中公开。
列表21-5显示了UndoableDrawingPanel类的定义。这个类不需要特别解释。当定义一个可撤销的类时需要记住的一件重要事情就是可撤销的事情必须在组件状态变化之前创建。(UndoableEdit接口的实现类,UndoableDrawEdit,在稍后的列表21-6中显示。)
package swingstudy.ch21;
import java.awt.Graphics; import java.awt.Polygon; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener;
import javax.swing.JPanel; import javax.swing.event.UndoableEditListener; import javax.swing.undo.UndoableEditSupport;
public class UndoableDrawingPanel extends JPanel {
UndoableEditSupport undoableEditSupport = new UndoableEditSupport(this); Polygon polygon = new Polygon();
public UndoableDrawingPanel() { MouseListener mouseListener = new MouseAdapter() { public void mouseReleased(MouseEvent event) { undoableEditSupport.postEdit(new UndoableDrawEdit(UndoableDrawingPanel.this)); polygon.addPoint(event.getX(), event.getY()); repaint(); } }; addMouseListener(mouseListener); }
public void addUndoableEditListener(UndoableEditListener undoableEditListener) { undoableEditSupport.addUndoableEditListener(undoableEditListener); }
public void removeUndoableEditListener(UndoableEditListener undoableEditListener) { undoableEditSupport.removeUndoableEditListener(undoableEditListener); }
public void setPolygon(Polygon newValue) { polygon = newValue; repaint(); }
public Polygon getPolygon() { Polygon returnValue; if(polygon.npoints == 0) { returnValue = new Polygon(); } else { returnValue = new Polygon(polygon.xpoints, polygon.ypoints, polygon.npoints); } return returnValue; }
protected void paintComponent(Graphics g) { super.paintComponent(g); g.drawPolygon(polygon); } }
当定义自定义的UndoableEdit接口实现时,我们可以选择实现完整的接口,或是我们可以继承AbstractUndoableEdit类并且重写相应的方法。更为通常的情况是,我们只是继承AbstractUndoableEdit。尽管要重写的最小方法就是undo()与redo(),但是我们也可以选择重写getPresentationName()方法来为可撤销的操作提供一个更好的名字。
因为Command设计需要Concrete Command(也就是UndoableEdit实现)调用操作,构造函数必须保存使得操作可撤销所必需的信息。在绘制面板的例子中,我们需要保存到面板及其当前多边形的引用。那么当请求操作撤销时,原始的多边形可以重新载入。要支持重做与撤销操作,undo()方法必须同时保存新多边形;否则,redo()操作将不会知道如何将事物回复到以前状态。这听起来需要大量的工作,但是实际上并不是这样。UndoableEdit实现的完整类定义显示在列表21-6中。
package swingstudy.ch21; import java.awt.BorderLayout; import java.awt.EventQueue; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JToolBar; import javax.swing.undo.UndoManager; public class UndoDrawing { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Runnable runner = new Runnable() { public void run() { JFrame frame = new JFrame("Drawing Sample"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); UndoableDrawingPanel drawingPanel = new UndoableDrawingPanel(); UndoManager manager = new UndoManager(); drawingPanel.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(drawingPanel, BorderLayout.CENTER); frame.setSize(300, 150); frame.setVisible(true); } }; EventQueue.invokeLater(runner); } }
这就是全部的代码。最后两个类使得UndoDrawing示例类可以工作。当创建我们自己的可撤销类时,我们需要继承一个不可撤销的类,然后添加必要的支持使其可撤销。另外,我们需要定义一个UndoableEdit实现来支持我们的特定类。