欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

java 实现undo/redo 三

程序员文章站 2022-06-23 10:25:44
...
三, UndoManager

要实现多次的Undo,Redo,必须要有一个数据结构管理多个UndoableCommand, 这个数据结构可以有多种选择, ArrayList, LinkedList, Stack等都可以。这里用下标访问元素的操作要多一些,所以采用ArrayList。另外,还要考虑多线程的情况,Java Swing 的UndoManager采用的是Vector, 但是有些情况下,操作只是在一个线程上工作,这时候用
ArrayList能获得更好的性能。这里的UndoManager提供了单线程和多线程两种选择。

    private List<UndoableCommand> commandList;

    private UndoManager(List<UndoableCommand> list) {
        commandList = list;
    }

    public static UndoManager getInstance() {
        return new UndoManager(new ArrayList<UndoableCommand>());
    }

    public static UndoManager getSynchronizedInstance() {
        List list = Collections.synchronizedList(new ArrayList<UndoableCommand>());
        return new UndoManager(list);
}

仔细考察commandList,可以发现只有在第一个或者最后一个元素时执行undo() 和redo()时是同一个元素外,其他情况下,执行undo() 和redo()功能是两个不同的但是相邻的元素。而且,第一个元素执行undo() 后整个commandList就不能undo()了,同理,最后一个元素执行redo() 后整个commandList就不能redo()了,其他情况下,commandList是既可以undo() 也可以redo()的。因此,这里采用一个变量 undoIndex 来记住执行undo() 的元素的位置。
undoIndex 的范围在-1(这时commandList不能undo())和最后一个元素下标(这时commandList不能redo())之间, redo() 的元素的位置应该是undoIndex + 1。


  private static final int CAN_NOT_UNDO_INDEX = -1;
  private static final int REDO_UNDO_INDEX_INTERVAL = 1;

    public boolean canUndo() {
        return undoIndex > CAN_NOT_UNDO_INDEX;
    }

    public boolean canRedo() {
        return undoIndex < getLastIndex();
    }

    private int getLastIndex() {
        return commandList.size() - 1;
    }

private int getRedoIndex() {
   return undoIndex + REDO_UNDO_INDEX_INTERVAL;
}

下面是完成UndoManager功能的3个主要function了:

manageCommand(UndoableCommand command) 做3件事
执行传来的UndoableCommand, 把它加到commandList的尾部,并把undoIndex指向这个尾部的位置

       public void manageCommand(UndoableCommand command) {
        command.execute();
        commandList.add(command);
        setUndoIndex(getLastIndex());
    }

undo(): 当commandList不能undo()时调用会抛出异常,如果当前undoIndex的元素可以undo(),则执行这个元素的undo(),undoIndex指向commandList上一个位置,这时还要找到下一次commandList undo() 元素的位置。主要要处理2种情况:可以一起执行的元素要依次执行,使得看上去像一起执行;不能执行undo()的元素要从commandList里删除,微软的WORD就是这样的,你在WORD里先输入一个A,执行撤销,这时再输入B,再执行撤销,你会发现已经不可能撤销到输入A这一步了。

public void undo() {
        if (!canUndo()) {
            throw new CannotUndoException();
        }
        UndoableCommand current = commandList.get(undoIndex);
        undoCommand(current);
        for (int i = undoIndex; i >= 0; i--) {
            UndoableCommand temp = commandList.get(i);
            if (!temp.canUndo()) {
                removeCommand(temp);
            } else if (current.canAppendWith(temp)) {
                undoCommand(temp);
                current = temp;
            } else {
                setUndoIndex(i);
                break;
            }
        }
}
redo(): 处理commandList 的 redo(), 跟undo()处理过程相反,只是相对简单一点。

下面是完整的UndoManager源码:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;

public class UndoManager {

    private static final int CAN_NOT_UNDO_INDEX = -1;
    private static final int REDO_UNDO_INDEX_INTERVAL = 1;
    private List<UndoableCommand> commandList;
    private int undoIndex;

    private UndoManager(List<UndoableCommand> list) {
        commandList = list;
    }

    public static UndoManager getInstance() {
        return new UndoManager(new ArrayList<UndoableCommand>());
    }

    public static UndoManager getSynchronizedInstance() {
        List list = Collections.synchronizedList(new ArrayList<UndoableCommand>());
        return new UndoManager(list);
    }

    public void manageCommand(UndoableCommand command) {
        command.execute();
        commandList.add(command);
        setUndoIndex(getLastIndex());
    }

    private void removeCommand(UndoableCommand command) {
        commandList.remove(command);
        undoIndex--;
    }

    private void undoCommand(UndoableCommand command) {
        command.undo();
        undoIndex--;
    }

    private void redoCommand(UndoableCommand command) {
        command.redo();
        undoIndex++;
    }

    public boolean canUndo() {
        return undoIndex > CAN_NOT_UNDO_INDEX;
    }

    public boolean canRedo() {
        return undoIndex < getLastIndex();
    }

    public void undo() {
        if (!canUndo()) {
            throw new CannotUndoException();
        }
        UndoableCommand current = commandList.get(undoIndex);
        undoCommand(current);
        for (int i = undoIndex; i >= 0; i--) {
            UndoableCommand temp = commandList.get(i);
            if (!temp.canUndo()) {
                removeCommand(temp);
            } else if (current.canAppendWith(temp)) {
                undoCommand(temp);
                current = temp;
            } else {
                setUndoIndex(i);
                break;
            }
        }
    }

    public void redo() {
        if (!canRedo()) {
            throw new CannotRedoException();
        }
        int redoIndex = getRedoIndex();
        UndoableCommand current = commandList.get(redoIndex);
        redoCommand(current);
        for (int i = getRedoIndex(); i < commandList.size(); i++) {
            UndoableCommand temp = commandList.get(i);
            if (temp.canAppendWith(current)) {
                redoCommand(current);
                current = temp;
            } else {
                return;
            }
        }
    }

    public void reset() {
        commandList.clear();
        setUndoIndex(CAN_NOT_UNDO_INDEX);
    }

    private void setUndoIndex(int index) {
        undoIndex = index;
    }

    private int getLastIndex() {
        return commandList.size() - 1;
    }

    private int getRedoIndex() {
        return undoIndex + REDO_UNDO_INDEX_INTERVAL;
    }
}

经测试,这个Undomanager类是可以工作的。

至此,这里提供的UndoableCommand抽象类和UndoManager类,已经封装了一些底层的Undo和Redo状态的切换,当实际项目中需要Undo/Redo功能时,只需要继承UndoableCommand实现自己的Undo/Redo逻辑并让UndoManager来manageCommand(UndoableCommand command),然后由UndoManage undo() 和 redo() 就可以了。

程序中Undo/Redo一般是菜单项或者是工具栏按钮,因此可以在UndoManager的基础上封装一个更为适用的UndoUtility类,它提供 getRedoAction() 和getUndoAction() function。
当使用时:
toolBar.add(undoUtility.getUndoAction());
toolBar.add(undoUtility.getRedoAction());
是不是很简单呢?
在本文的最后一部分,将给出这个类的代码,和本文提到的测试代码,这里先给出这个可无限次Undo/Redo的画,擦除直线的jar文件。