繁体   English   中英

撤消/重做实施[关闭]

[英]Undo/Redo implementation [closed]

给我一些关于如何实现撤消/重做功能的想法——就像我们在文本编辑器中所做的那样。 我应该使用什么算法以及我可以阅读什么。 谢谢。

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

  • 保存状态:一类撤消是您实际保存历史状态的地方。 在这种情况下,发生的情况是,在每一点上,您都将状态保存在内存的某个位置。 当您想要撤消时,您只需换出当前状态并换入内存中已经存在的状态。 例如,这就是在 Adob​​e Photoshop 中使用历史记录或在谷歌浏览器中重新打开关闭的标签的方式。

替代文字

  • 生成状态:另一个类别是无需维护状态本身,只需记住操作是什么。 当您需要撤消时,您需要对该特定操作进行逻辑反转。 举一个简单的例子,当你在一些支持撤销的文本编辑器中按 Ctrl + B时,它被记住为一个粗体动作。 现在每个动作都是其逻辑反转的映射。 因此,当您执行Ctrl + Z 时,它会从反向操作表中查找并发现撤消操作再次是Ctrl + B。 执行该操作,您将获得之前的状态。 所以,这里你之前的状态没有存储在内存中,而是在你需要的时候生成。

对于文本编辑器,以这种方式生成状态并不是计算密集型的,但对于像 Adob​​e Photoshop 这样的程序来说,它可能是计算密集型的,或者根本不可能。 例如 - 对于模糊操作,您将指定去模糊操作,但这永远无法让您回到原始状态,因为数据已经丢失。 因此,根据情况 - 逻辑逆操作的可能性及其可行性,您需要在这两个大类之间进行选择,然后按照您想要的方式实施它们。 当然,可以有适合您的混合策略。

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

我从头开始编写了两个文本编辑器,它们都采用了非常原始的撤消/重做功能形式。 通过“原始”,我的意思是该功能很容易实现,但是在非常大的文件(比如>> 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数组的长度(减一)时触发编辑定时器,我会在UndoLevel之后清除该数组的所有项,这在 Microsoft Windows 平台上很常见(但 Emacs 更好,如果我没记错的话——Microsoft Windows 方法的缺点是,如果您撤消了大量更改,然后不小心编辑了缓冲区,则先前的内容(未修改的内容)将永久丢失)。 您可能希望跳过这种减少数组的步骤。

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

有几种方法可以做到这一点,但您可以开始查看命令模式 使用命令列表在您的操作中后退(撤消)或前进(重做)。 可以在 此处找到 C# 中的示例。

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

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

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

  • 更改类型:插入数据或删除数据:“更改”数据意味着deleteinsert
  • 文件中的位置:可以是偏移量或行/列对
  • 数据缓冲区:这是与动作相关的数据; 如果insert ,则是插入的数据; 如果delete ,被删除的数据。

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

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

希望能帮助到你。

Memento 图案就是为此而制作的。

在自己实现之前,请注意这是很常见的,并且代码已经存在 - 例如,如果您在 .Net 中编码,则可以使用IEditableObject ,而在 javascript 世界中,您可以使用不可变库,例如immer.js

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

例如,Memento旨在保持对象的状态以便稍后恢复。 出于优化目的,此纪念品应尽可能小。

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

基于这两个概念,您可以编写基本的撤消/重做历史记录,例如以下用 TypeScript 编写的代码(从前端库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.shift();
            }

            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 文档页面上详细介绍了此类示例。

编辑:

请注意,存在多种撤消/重做算法。 我详细介绍了经典的线性。 对于代码编辑器,研究人员提出了“选择性撤销算法” (研究文章) 用于协作编辑的撤消/重做算法也是特定的(研究文章)

如果动作是可逆的。 例如添加 1,让玩家移动等等,看看如何使用命令模式来实现 undo/redo 按照链接,您将找到有关如何执行此操作的详细示例。

如果没有,请按照@Lazer 的说明使用Saved State

你可以研究一个现有的撤销/重做框架的例子,第一个谷歌命中是关于codeplex (for .NET) 我不知道这是否比任何其他框架更好或更差,有很多这样的框架。

如果您的目标是在您的应用程序中具有撤消/重做功能,您不妨选择一个看起来适合您的应用程序类型的现有框架。
如果您想学习如何构建自己的撤消/重做,您可以下载源代码并查看这两种模式以及如何连接的详细信息。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM