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文件。
要实现多次的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文件。
上一篇: 历史上项羽和刘邦义结金兰过?
下一篇: 我来跟你道别