如何在Java中实现简单的撤销/重做操作?

16

我已经创建了一个 XML 编辑器,但是在最后一阶段遇到了困难:添加撤销/重做功能。

我只需要为用户向 JTree 添加元素、属性或文本时添加撤销/重做功能。

虽然我还是个新手,但今天在学校里,我试图(未成功地)创建两个堆栈对象[],称为 undo 和 redo,并将执行的操作添加到它们中去。

例如,我有:

Action AddElement() {

// some code
public void actionPerformed(ActionEvent e) {

                    performElementAction();
                }
}

performElementAction实际上只是向JTree添加一个元素。

我想添加一种方法将此操作添加到撤销栈中。是否有一种简单的方式只是undo.push(整个操作执行)或类似的东西?


看一下命令模式,它的用途包括实现撤销/重做功能。 - Óscar López
1
请务必查看内置的撤销支持;我从未使用过它,也找不到Swing教程,但这里有管理器。 - Nate W.
3个回答

19

TL;DR: 通过实现 Command(第233页)和 Memento(第283页)模式,您可以支持撤消和重做操作。(设计模式 - Gamma等人)。

Memento模式

这个简单模式允许您保存对象的状态。只需在新类中包装对象,并在其状态更改时进行更新。

public class Memento
{
    MyObject myObject;
    
    public MyObject getState()
    {
        return myObject;
    }
    
    public void setState(MyObject myObject)
    {
        this.myObject = myObject;
    }
}

命令模式

命令模式存储原始对象(我们想要支持撤销/重做的对象)和备忘录对象,在撤销时需要使用备忘录对象。此外,定义了两种方法:

  1. execute:执行命令
  2. unExecute:撤销命令

代码:

public abstract class Command
{
    MyObject myObject;
    Memento memento;
    
    public abstract void execute();
    
    public abstract void unExecute();
}

他们定义了逻辑“操作”,扩展命令(例如插入):

public class InsertCharacterCommand extends Command
{
    //members..

    public InsertCharacterCommand()
    {
        //instantiate 
    }

    @Override public void execute()
    {
        //create Memento before executing
        //set new state
    }

    @Override public void unExecute()
    {
        this.myObject = memento.getState()l
    }
}

应用这些模式:

最后一步是定义撤销/重做的行为。核心思想是存储一个命令堆栈,作为命令历史记录的列表。为了支持重做,每当应用撤销命令时,可以保留第二指针。请注意,每当插入新对象时,其当前位置之后的所有命令都会被删除;这通过下面定义的deleteElementsAfterPointer方法实现:

private int undoRedoPointer = -1;
private Stack<Command> commandStack = new Stack<>();

private void insertCommand()
{
    deleteElementsAfterPointer(undoRedoPointer);
    Command command =
            new InsertCharacterCommand();
    command.execute();
    commandStack.push(command);
    undoRedoPointer++;
}

private void deleteElementsAfterPointer(int undoRedoPointer)
{
    if(commandStack.size()<1)return;
    for(int i = commandStack.size()-1; i > undoRedoPointer; i--)
    {
        commandStack.remove(i);
    }
}

  private void undo()
{
    Command command = commandStack.get(undoRedoPointer);
    command.unExecute();
    undoRedoPointer--;
}

private void redo()
{
    if(undoRedoPointer == commandStack.size() - 1)
        return;
    undoRedoPointer++;
    Command command = commandStack.get(undoRedoPointer);
    command.execute();
}

结论:

这个设计的强大之处在于你可以添加任意数量的命令(通过扩展Command类),例如RemoveCommandUpdateCommand等。此外,相同的模式适用于任何类型的对象,使得该设计在不同的使用场景下具有可重用性可修改性


你会如何限制撤销/重做步骤的数量?无法调整堆栈大小。 - Atlas2k
@Atlas2k,你可以设置一个阈值(例如int threshhold = 30),然后根据当前堆栈大小删除所有“旧”的命令。例如,如果你调用了insertCommand()并且你的commandStack.size()大于30,则删除第一个元素(commandStack.remove(0))。主要思想是将堆栈限制在一定的阈值大小内。是的,这是一个更复杂的用例,但绝对可行 :) - Menelaos Kotsollaris
我按照这个链接设置了我的最大堆栈大小:http://ntsblog.homedev.com.au/index.php/2010/05/06/c-stack-with-maximum-limit/ - Atlas2k

1
你需要在Command接口中定义undo(),redo()操作,同时与execute()一起使用。
例子:
interface Command {

    void execute() ;

    void undo() ;

    void redo() ;
}

在您的ConcreteCommand类中定义一个状态。根据执行execute()方法后的当前状态,您需要决定是否将命令添加到撤销堆栈或重做堆栈,并相应地做出决策。

阅读这篇撤销重做命令文章以获得更好的理解。


0
我会尝试创建一个Action类,其中包含一个继承自Action的AddElementAction类。 AddElementAction可以有Do()和Undo()方法,根据需要添加/删除元素。然后,您可以保留两个操作的堆栈以进行撤消/重做,并在弹出顶部元素之前调用其Do()/Undo()方法。

请参阅javax.swing.undo.UndoManager - Nate W.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接