撤消/重做实施

时间:2010-08-22 11:56:23

标签: algorithm design-patterns

给我一​​些想法如何实现撤消/重做功能 - 就像我们在文本编辑器中一样。 我应该使用哪些算法以及我可能阅读的内容。感谢。

10 个答案:

答案 0 :(得分:69)

我知道撤消类型的两个主要部分

  • 保存状态: 一类撤消是实际保存历史状态的地方。在这种情况下,发生的是在每个点上你继续将状态保存在某个内存位置。当你想要撤销时,你只需要换掉当前状态并交换已经存在于内存中的状态。例如,这是在Adobe Photoshop中使用历史记录或在Google Chrome中重新打开已关闭的标签页的方式。

alt text

  • 生成状态: 另一类是在那里,而不是维持状态本身,你只记得行动是什么。当您需要撤消时,您需要对该特定操作进行逻辑反转。举个简单的例子,当你做一个<大骨节病>控制 + <大骨节病> B'/骨节病>在支持撤消的一些文本编辑器,才想起为粗体行动。现在,每个动作都是其逻辑反转的映射。所以,当你做了<大骨节病>控制 + <大骨节病> Z ^ ,它从一个相反的动作表中查找,发现这些撤消操作是<大骨节病>控制 + < kbd> B 再次。这是执行,你得到你以前的状态。所以,这里你以前的状态没有存储在内存中,而是在需要时生成。

对于文本编辑器,以这种方式生成状态并不是计算密集型,但对于像Adobe Photoshop这样的程序,它可能计算量太大或者根本不可能。例如 - 对于模糊操作,您将指定去模糊操作,但由于数据已丢失,因此无法使您进入原始状态。因此,根据情况 - 逻辑逆向操作的可能性及其可行性,您需要在这两大类中进行选择,然后以您希望的方式实现它们。当然,有可能采用适合您的混合策略。

此外,有时候,就像在Gmail中一样,可以进行限时撤消,因为动作(发送邮件)从未在第一时间完成。所以,你没有“撤消”那里,你只是“不做”行动本身。

答案 1 :(得分:14)

我从头开始编写了两个文本编辑器,它们都采用了非常原始的撤消/重做功能。 “原始”,我的意思是功能很容易实现,但在非常大的文件中(例如&gt;&gt;&gt; 10 MB)是不经济的。但是,该系统非常灵活;例如,它支持无限级别的撤销。

基本上,我定义了一个像

这样的结构
type
  TUndoDataItem = record
    text: /array of/ string;
    selBegin: integer;
    selEnd: integer;
    scrollPos: TPoint;
  end;

然后定义一个数组

var
  UndoData: array of TUndoDataItem;

然后,此数组的每个成员都指定文本的已保存状态。现在,在每次编辑文本时(字符键向下,退格键,删除键,剪切/粘贴,鼠标移动选择等),我(重新)启动一个(比如说)一秒的计时器。触发时,计时器将当前状态保存为UndoData数组的新成员。

在撤消(Ctrl + Z)时,我将编辑器恢复到状态UndoData[UndoLevel - 1]并将UndoLevel减少一个。默认情况下,UndoLevel等于UndoData数组的最后一个成员的索引。在重做(Ctrl + Y或Shift + Ctrl + Z)上,我将编辑器恢复到状态UndoData[UndoLevel + 1]并将UndoLevel增加一。当然,如果在UndoLevel不等于UndoData数组的长度(减1)时触发编辑计时器,我会在UndoLevel之后清除此数组的所有项目,因为在Microsoft Windows中很常见(但是Emacs更好,如果我没记错的话 - Microsoft Windows方法的缺点是,如果你撤消了很多更改然后意外地编辑了缓冲区,那么preveous内容(unid)是永久性的丢失)。您可能希望跳过此数组的减少。

在不同类型的程序中,例如,图像编辑器,可以应用相同的技术,但当然,具有完全不同的UndoDataItem结构。一种不需要太多内存的更高级方法是在撤消级别之间仅保存更改(即,而不是保存“alpha \ nbeta \ gamma”和“alpha \ nbeta \” ngamma \ ndelta“,如果你明白我的意思,你可以保存”alpha \ nbeta \ ngamma“和”ADD \ ndelta“。在非常大的文件中,每个更改与文件大小相比较小,这将大大降低撤消数据的内存使用量,但实现起来比较棘手,并且可能更容易出错。

答案 2 :(得分:12)

有几种方法可以做到这一点,但您可以开始查看Command pattern。使用命令列表通过您的操作返回(撤消)或转发(重做)。可以找到C#中的一个示例here

答案 3 :(得分:6)

有点晚了,但是这里有:你特别提到文本编辑器,下面解释了一个算法,可以适应你正在编辑的任何东西。所涉及的原则是保留可以自动化的动作/指令列表,以重新创建您所做的每个更改。不要对原始文件进行更改(如果不是空的),请将其保留为备份。

保留对原始文件所做更改的前向 - 后向链接列表。此列表会间歇性地保存到临时文件中,直到用户实际保存更改:发生这种情况时,您将更改应用于新文件,复制旧文件并同时应用更改;然后将原始文件重命名为备份,并将新文件的名称更改为正确的名称。 (您可以保留已保存的更改列表,也可以将其删除并替换为后续的更改列表。)

链表中的每个节点都包含以下信息:。

  • 更改类型:您要么插入数据,要么删除数据:“更改”数据意味着delete后跟insert
  • 文件中的位置:可以是偏移量或行/列对
  • 数据缓冲区:这是与操作有关的数据;如果insert它是插入的数据; if delete,删除的数据。

要实现Undo,您可以使用“当前节点”指针或索引从链接列表的尾部向后工作:如果更改为insert,则执行删除但不使用更新链表;如果它是delete,您可以从链表缓冲区中的数据中插入数据。为用户的每个“撤消”命令执行此操作。 Redo向前移动'current-node'指针并根据节点执行更改。如果用户在撤消后对代码进行更改,请将“当前节点”指示符后的所有节点删除到尾部,并将尾部设置为等于“当前节点”指示符。然后在尾部之后插入用户的新更改。这就是它。

答案 4 :(得分:5)

我只有两分钱是你想要使用两个堆栈来跟踪操作。每次用户执行某些操作时,程序都应将这些操作放在“已执行”的堆栈上。当用户想要撤消这些操作时,只需将操作从“执行”堆栈弹出到“调用”堆栈。当用户想要重做那些操作时,从“调用”堆栈弹出项目并将它们推回到“已执行”堆栈。

希望它有所帮助。

答案 5 :(得分:2)

您可以研究一个现有撤消/重做框架的示例,第一个Google点击是codeplex (for .NET)。我不知道这是否比任何其他框架更好或更差,它们有很多。

如果您的目标是在应用程序中使用撤消/重做功能,那么您也可以选择一个适合您的应用程序的现有框架。
如果你想学习如何建立你自己的撤销/重做,你可以下载源代码,看看这两种模式以及如何连接的详细信息。

答案 6 :(得分:2)

如果行动是可逆的。例如,添加1,让玩家移动等,请参阅how to use the Command Pattern to implement undo/redo。按照链接,您将找到有关如何执行此操作的详细示例。

如果没有,请按照@Lazer的说明使用 已保存状态

答案 7 :(得分:1)

Memento pattern是为此做的。

在自己实现之前,请注意这很常见,并且代码已经存在 - 例如,如果您在.Net中进行编码,则可以使用IEditableObject

答案 8 :(得分:0)

除了讨论,我还写了一篇博客文章,内容是基于对直观的思考,如何实现UNDO和REDO:http://adamkulidjian.com/undo-and-redo.html

答案 9 :(得分:0)

实现基本撤消/重做功能的一种方法是同时使用记忆和命令设计模式。

例如,

Memento 旨在保持对象的状态以便稍后还原。 为了优化起见,此纪念品应尽可能小。

命令模式将某些指令封装在对象(命令)中,以在需要时执行。

基于这两个概念,您可以编写基本的撤消/重做历史记录,例如以下以TypeScript(extracted and adapted来自前端库Interacto)进行编码的代码。

这种历史依赖于两个堆栈:

  • 可撤消对象的堆栈
  • 可以重做的对象的堆栈

在算法中提供了注释。请注意,在撤消操作中,必须清除重做堆栈!。原因是让应用程序处于稳定状态:如果您回头过去重做所做的某些操作,则您以前的操作当您改变未来时,将不再存在。

export class UndoHistory {
    /** The undoable objects. */
    private readonly undos: Array<Undoable>;

    /** The redoable objects. */
    private readonly redos: Array<Undoable>;

    /** The maximal number of undo. */
    private sizeMax: number;

    public constructor() {
        this.sizeMax = 0;
        this.undos = [];
        this.redos = [];
        this.sizeMax = 30;
    }

    /** Adds an undoable object to the collector. */
    public add(undoable: Undoable): void {
        if (this.sizeMax > 0) {
            // Cleaning the oldest undoable object
            if (this.undos.length === this.sizeMax) {
                this.undos.pop();
            }

            this.undos.push(undoable);
            // You must clear the redo stack!
            this.clearRedo();
        }
    }

    private clearRedo(): void {
        if (this.redos.length > 0) {
            this.redos.length = 0;
        }
    }

    /** Undoes the last undoable object. */
    public undo(): void {
        const undoable = this.undos.pop();
        if (undoable !== undefined) {
            undoable.undo();
            this.redos.push(undoable);
        }
    }

    /** Redoes the last undoable object. */
    public redo(): void {
        const undoable = this.redos.pop();
        if (undoable !== undefined) {
            undoable.redo();
            this.undos.push(undoable);
        }
    }
}

Undoable界面非常简单:

export interface Undoable {
    /** Undoes the command */
    undo(): void;
    /** Redoes the undone command */
    redo(): void;
}

您现在可以编写在应用程序上运行的可撤消命令。

例如(仍然基于Interacto示例),您可以编写如下命令:

export class ClearTextCmd implements Undoable {
   // The memento that saves the previous state of the text data
   private memento: string;

   public constructor(private text: TextData) {}
   
   // Executes the command
   public execute() void {
     // Creating the memento
     this.memento = this.text.text;
     // Applying the changes (in many 
     // cases do and redo are similar, but the memento creation)
     redo();
   }

   public undo(): void {
     this.text.text = this.memento;
   }

   public redo(): void {
     this.text.text = '';
   }
}

您现在可以执行命令并将其添加到UndoHistory实例:

const cmd = new ClearTextCmd(...);
//...
undoHistory.add(cmd);

最后,您可以将撤消按钮(或快捷方式)绑定到此历史记录(与重做相同)。

Interacto documentation page中详细介绍了此类示例。