【问题标题】:How to design undo & redo in text editor?如何在文本编辑器中设计撤消和重做?
【发布时间】:2025-12-27 06:50:12
【问题描述】:

我项目的一部分是编写一个文本编辑器,用于输入一些规则、编译我的应用程序并运行它。编写编译器已结束并发布测试版。在最终版本中,我们必须在文本编辑器中添加撤消和重做。我使用一个文件并定期保存它以供文本编辑器使用。如何为我的文本编辑器设计撤消和重做?文件持久化结构有什么变化?

【问题讨论】:

  • 你应该看看*.com/questions/49755/…
  • 如果我在编辑器中对 a.txt 进行了更改,然后关闭您的编辑器,然后再次打开并加载 a.txt,我应该能够撤消上一次会话中的更改吗?
  • @lins314159:当然没有,我认为世界上没有应用程序有这个功能
  • 很确定我已经看到一些用这个做广告的软件。只需将您已经存储在内存中的内容写入文件并使用一些控制文件来指示哪个撤消文件用于哪个文本文件。需要对其进行管理,以确保您最终不会遇到一堆不再有用的撤消文件。

标签: java text-editor undo undo-redo redo


【解决方案1】:

您可以通过两种方式做到这一点:

  • 保留编辑器状态列表和列表中的指针; undo 将指针向后移动并恢复那里的状态,redo 向前移动,做某事会丢弃指针之外的所有内容并将状态作为新的顶部元素插入;
  • 不保留状态,而是保留动作,这要求对于每个动作,您都有一个反动作来撤消该动作的影响

在我的(图表)编辑器中,有四个级别的状态变化:

  • 动作片段:这些是更大动作的一部分,不能单独撤消或重做 (例如移动鼠标)
  • 动作:一个或多个动作片段,形成有意义的变化,可以撤消或重做, 但未反映在已编辑的文档中,因为在磁盘上已更改 (例如选择元素)
  • 文档更改:更改已编辑文档的一项或多项操作,因为它将保存到磁盘 (例如更改、添加或删除元素)
  • 文档保存:文档的当前状态显式保存到磁盘 - 此时我的编辑器会丢弃撤消历史记录,因此您无法通过保存撤消操作

【讨论】:

    【解决方案2】:

    读一本书Design Patterns: Elements of Reusable Object-Oriented Software。据我所知,有一个很好的例子。

    【讨论】:

      【解决方案3】:

      这是the command pattern 的工作。

      【讨论】:

        【解决方案4】:

        哇,真是巧合——我在最后一个小时里在我的所见即所得文本编辑器中实现了撤消/重做:

        基本思想是要么将文本编辑器的全部内容保存在一个数组中,要么将上次编辑的差异保存在一个数组中。

        在重要点更新此数组,即每隔几个字符(检查每次按键的内容长度,如果超过 20 个字符不同,则创建一个保存点)。还可以更改样式(如果是富文本)、添加图像(如果允许的话)、粘贴文本等。您还需要一个指针(只是一个 int 变量)来指向数组中的哪个项目是当前状态编辑)

        使数组具有设定的长度。每次添加保存点时,将其添加到数组的开头,并将所有其他数据点向下移动一个。 (一旦你有这么多的保存点,数组中的最后一项就会被遗忘)

        当用户按下撤消按钮时,检查编辑器当前的内容是否与最近一次保存的相同(如果不是,则用户自上次保存点以来进行了更改,所以保存当前编辑器的内容(因此可以重做),使编辑器等于最后一个保存点,并使指针变量= 1(数组中的第二项)。如果它们相同,则没有进行任何更改从上一个保存点开始,所以你需要撤消到之前的点。为此,增加指针值+1,并使编辑器的内容=指针的值。

        要重做,只需将指针值减 1 并加载数组的内容(确保检查是否已到达数组末尾)。

        如果用户在撤消后进行编辑,则将指向值数组单元格向上移动到单元格 0,并将其余部分向上移动相同的量(一旦他们进行了不同的编辑,您就不想重做其他内容)。

        另一个主要的捕获点 - 确保仅在文本编辑器的内容实际发生更改时才添加保存点(否则您会得到重复的保存点,并且看起来撤消对用户没有任何作用。

        我无法帮助您了解 Java 细节,但我很乐意回答您的任何其他问题,

        尼哥

        【讨论】:

          【解决方案5】:

          您可以将您的操作建模为commands,将其分为两个堆栈。一个用于撤消,另一个用于重做。您可以compose 您的命令来创建更多高级命令,例如当您想要撤消宏的操作时;或者,如果您想将单个单词或短语的单个击键组合在一个操作中。

          编辑器中的每个操作(或重做操作)都会生成一个新的撤消命令,该命令会进入撤消堆栈(并且还会清除重做堆栈)。每个撤消操作都会生成相应的重做命令,并进入重做堆栈。

          正如derekerdmann 在 cmets 中所提到的,您还可以将撤消和重做命令组合成一种类型的命令,该命令知道如何撤消和重做其操作。

          【讨论】:

          • 相反,您可以创建一个知道如何撤消和重做自身的命令;当您撤消操作时,将其从撤消堆栈中弹出,调用撤消操作,然后将其移至重做堆栈。当您调用重做时,将顶部从重做堆栈中弹出,调用重做操作,并将其压入撤消堆栈。这使您不必一直创建新对象;你只需重新洗牌周围现有的。
          【解决方案6】:

          基本上有两种好方法:

          • “命令”设计模式

          • 在不可变对象上使用 OO,其中一切都只是由不可变对象组成的不可变对象,由不可变对象组成(这不太常见,但如果正确完成,则非常优雅)

          使用面向对象而不是不可变对象而不是简单的命令或简单的撤消/重做的优势在于,您无需考虑太多:无需“撤消”操作的效果,也无需“重播”所有命令。您所需要的只是一个指向大量不可变对象的指针。

          由于对象是不可变的,所有“状态”都可以非常轻量级,因为您可以缓存/重用处于任何状态的大多数对象。

          “OO over immutable objects”是一颗纯粹的宝石。再过 10 年可能不会成为主流; )

          P.S:在不可变对象上执行 OO 还惊人地简化了并发编程。

          【讨论】:

          • “OO over immutable objects”技术听起来很有趣。你能提供我可以查看的任何参考资料吗?我假设您使用 OO 来代表“面向对象”?谢谢。
          • 哦,我想我在撤消不可变对象时发现了一些东西:*.com/questions/1812978/…
          【解决方案7】:

          这是一个显示 SWT 如何支持撤消/重做操作的 sn-p。将其作为实际示例(或直接使用,如果您的编辑器基于 SWT):

          SWT Undo Redo

          【讨论】:

            【解决方案8】:

            如果您不想要任何花哨的东西,您可以添加UndoManager。每次添加或删除文本时,Document 都会触发 UndoableEdit。要撤消和重做每个更改,只需在 UndoManager 中调用这些方法即可。

            这样做的缺点是 UndoManager 会在用户每次输入内容时添加一个新编辑,因此输入“apple”将为您留下 5 个编辑,一次可撤消一个。对于我的文本编辑器,我为编辑编写了一个包装器,它存储除了文本更改和偏移之外的时间,以及一个UndoableEditListener,如果两者之间只有很短的时间,它会将新编辑连接到以前的编辑它们(0.5 秒对我来说效果很好)。

            这适用于一般编辑,但在完成大量替换时会导致问题。如果您有一个包含 5000 个“apple”实例的文档,并且您想将其替换为“orange”,那么您最终会得到 5000 个编辑,所有这些都存储“apple”、“orange”和一个偏移量。为了减少使用的内存量,我将其视为普通编辑的一个单独案例,而是存储“apple”、“orange”和一个 5000 个偏移量的数组。我还没有开始应用它,但我知道当多个字符串匹配搜索条件(例如,不区分大小写的搜索、正则表达式搜索)时,它会引起一些麻烦。

            【讨论】:

            • 一种更简单的方法是比较内容更改前后的长度,并且仅在自上次更改超过 20 个字符时才存储编辑已保存编辑。