实例:
我们将构建一个简单的程序来演示Swing的undo/redo机制。下面列出我们的applet小程序,允许用户在一个JList组件中添加和删除元素。当用户添加或者删除一个原色,这个程序储存相关操作的效果--在一个AddEdit 或者RemoveEdit对象中--以便以后进行undo操作。
废话少说,让我们开始code!
下面这些类组成这个简单的程序:
下面我们从AddEdit开始
001 class AddEdit extends AbstractUndoableEdit {
002
003 private Object element_;
004
005 private int index_;
006
007 private DefaultListModel model_;
008
009 public AddEdit(DefaultListModel model, Object element, int index) {
010 model_=model;
011
012 element_ = element;
013
014 index_=index;
015 }
016
017 public void undo() throws CannotUndoException {
018
019 model_.removeElementAt(index_);
020
021 }
022
023 public void redo() throws CannotRedoException {
024 model_.insertElementAt(element_,index_);
025 }
026
027 public boolean canUndo() { return true; }
028
029 public boolean canRedo() { return true; }
030
031 public String getPresentationName() { return "Add"; }
032
034 }
在构造函数(009-015)中我们保存了一个添加动作进行取消/重做需要的所有信息,其中包括:
- 添加到list中的元素
- 元素在list中的索引
- 动作的接受者List的model本身:list默认的model是DefaultListModel,它是一个类似与Vector的接口,可用来访问JList组件
Undo()方法(017-021)从list中删除这个元素,而redo()(023-025)方法把它重新插入。getPresentationName()方法返回被undo和redo的名字。需要注意的是:如果你的类继承自AbstractUndoableEdit,Swing将通过在getPresentationName()的返回值前加上”undo”和”redo”的方法来处理getUndoPresentationName()和getRedoPresentationName()方法的返回值。
现在让我们来测试添加操作。下面的片段将Action对象添加到add按钮上。Action是Swing的一个新的接口,用来实现UI的控制器。也就是说,一个action能够被直接添加到一个工具栏(作为一个新按钮)或者一个菜单(作为一个新的菜单项)。当这个action的一个属性发生改变(比如:变为可操作或者不可操作),UI的组件被通知并改变自身状态与Action保持一致。比如:当CutAction变成不可操作,Cut工具栏按钮和Cut菜单选项都同时变为不可操作。无论如何,Action接口继承ActionListener接口来处理ActionEvent事件,并且Action接口是自描述的。
当添加如JToolBar或者JMenu这样的组件到一个支持Action的容器当中时,Action项目被用来决定以何种方式来生成这个组件,并且被自动注册到UI事件中。容器将新的组件注册为action的一个PropertyListener。
AbstractAction是一个实现Action接口的具体类,它提供了所有方法的缺省实现。在这个例子中,我们只使用Action的Listener。
001 private class AddAction extends AbstractAction {
002
003 public void actionPerformed(ActionEvent evt) {
005 // always add to the end of the JList
006 int NumOfElements = elementModel_.getSize();
007 // however, give the element its ID number
008 Object element = new String("Foo " + _lastElementID);
009
010 // record the effect
011 UndoableEdit edit = new AddEdit(elementModel_,
012 element, NumOfElements);
013 // perform the operation
014 elementModel_.addElement(element);
015
016 // notify the listeners
017 undoSupport_.postEdit(edit);
017
018 // increment the ID
019 _lastElementID ++ ;
020
021 }
022
023 }
AddAction类:
- 添加一个新的元素 (008)
- 添加一个新的AddEdit对象并且传给它三个参数:ListModel(action的接受者),新元素的索引,新元素本身。 (011)
- 执行实际添加操作 (014)
- 通过调用undoSupport对象的postEdit方法通知undo listeners (017)
注意:AddAction类是undo小程序的一个私有内部类。这个方式保证它可以直接访问此程序的私有变量成员(比如:undoSupport_)。一般来说,我倾向于将actions定义为使用它们的类的内部类,这样做是既防止了接口体系膨胀也避免了打破封装(通过暴露对象的内部结果--举个例子:一个外部的添加动作的action可能会想要能够操作ListModel)
连接到UI
当然,为了一个简单的undo系统,你很可能愿意注册一个UndoManager类作为UndoableEvents事件的唯一listener,提供一个只提供一般undo标签并且调用manager类的undo()方法的UndoAction
无论如何,为了使UI更健壮,你一定会愿意为用户提供取消最后一次操作的功能。
下面的代码片断展示了如何制作一个UndoAdaptor类,用来更新支持undo的组件的状态使其恢复到undo历史列表中的最新状态(在这个例子中是undo和redo按钮)。
001 private class UndoAdaptor implements UndoableEditListener {
002
003 public void undoableEditHappened (UndoableEditEvent evt) {
004
005 UndoableEdit edit = evt.getEdit();
006
007 undoManager_.addEdit(edit);
008
009 refreshUndoRedo();
010 }
011 }
这个UndoAdaptor在程序装入时被注册到UndoableEditSupport。每次一个可undo的事件发生,这个适配器会做以下动作:
- 从事件中取出操作 (005)
- 添加到UndoManager中(007)
- 刷新undo相关的GUI状态(009)
一个代替的方法是通过实现一个UndoManager的子类并且覆写addEdit()方法以便自动刷新用户界面。(即每次add后在addEdit()方法中刷新界面),不用在客户端代码中刷新
下面是refreshUndoRedo()方法:
001 public void refreshUndoRedo() {
002
003 //refresh undo
004
005 undoBtn_.setText(undoManager_.getUndoPresentationName());
006 undoBtn_.setEnabled(undoManager_.canUndo());
007
008 // refresh redo
009
010 redoBtn_.setText(undoManager_.getRedoPresentationName());
011 redoBtn_.setEnabled(undoManager_.canRedo());
012
013 }
这个方法刷新执行undo和redo后的用户界面。并从undoManager中取得目前编辑状态信息。
The Undo 操作
当用户点击Undo按钮,undo action被调用,如下所示:
001 private class UndoAction extends AbstractAction {
002
003 public void actionPerformed(ActionEvent evt ) {
004
005 undoManager_.undo();
006
007 refreshUndoRedo();
008 }
009 }
所有的管理undo历史列表的细节都由UndoManager类来管理。Undo操作只是简单的调用manager类的undo()方法并刷新程序的界面。Redo的代码与此类似,这里就不列出了。
将各部分串起来
在程序的构造函数中,我们建立这个系统:
001 public UndoPanel () {
002 // construct the actions
003 ActionListener undoAction = new undoAction();
004 ActionListener redoAction = new redoAction();
005
006 // register the listener
007 undoBtn_ = new JButton("undo");
008 undoBtn_.addActionListener(undoAction);
009 redoBtn_ = new JButton("redo");
010 redoBtn_.addActionListener(redoAction);
011
012
013 // initialize the undo.redo system
014 undoManager_= new UndoManager();
015 undoSupport_ = new UndoableEditSupport();
016 undoSupport_.addUndoableEditListener(new UndoAdapter());
017
018 }
总结:
Command模式在保存用户的行为在单独的类中非常有用。它允许我们将undo/redo工具的实现局部化,这样单独的类可执行和取消自身状态的改变。这个方法极大的简化了代码的维护;当我们改变一个操作的行为,undo/redo操作的代码几乎完全独立与发布给用户的接口,使客户代码不依赖于undo/redo机制的实现细节。
在应用程序中支持undo/redo功能,使客户在学习如何操作程序时更有信心。这篇文章展示了使用Swing来实现这样的功能是如何简单。对于开发者来说,它可能只是一个小小的进步,但对于如何创建完善友好的应用来说,这确实是一个巨大的进步。