【问题标题】:Can't get my head around implementing an Undo/Redo functionality, should I use a Stack?无法实现撤消/重做功能,我应该使用堆栈吗?
【发布时间】:2011-10-15 07:03:04
【问题描述】:

我现在有点困惑,我猜是有这样的日子。

我需要为表单实现撤消和重做功能。为简单起见,假设我只保存被修改的控件以及它离开 Focus 时的值。

我如何以一种让我在“时间线”中来回切换的方式保存这些信息。

我曾考虑过使用 Stack,但在测试我的小演示时,我有一个轻微的动脉瘤,我就在这里。

需要代码,不是真的,但会有所帮助。我对需要实现的算法更感兴趣。有什么建议吗?

【问题讨论】:

    标签: c# winforms stack undo redo


    【解决方案1】:

    我会使用 IUndoableAction 接口。这些实现可以存储他们需要完成和撤消的任何数据。那么是的,我会用一个堆栈来保存它们。

    interface IUndoableAction
    {
        void Do();
        void Undo();
    }
    Stack<IUndoableAction> Actions;
    

    每种动作都会实现 Do 和 Undo 方法。

    那么,某处就会有这两种方法:

        void PerformAction(IUndoableActionaction)
        {
            Actions.Push(action);
            action.Do();
        }
    
        void Undo()
        {
            var action = Actions.Pop();
            action.Undo();
        }
    

    至于在动作类中存储什么,一些动作可以只存储旧值。但是,一旦我有一个操作来交换电子表格中的两行。我没有在两行中存储每个单元格的值——我只是存储了行索引,以便可以将它们交换回来。如果您为每个操作存储所有状态,则很容易填满大量内存。

    那么你也需要一个重做堆栈,当你撤消一个动作时,它会被压入重做堆栈。执行新操作时需要清除重做堆栈,因此事情不会乱序。

    【讨论】:

      【解决方案2】:

      如果您将“更改”压入堆栈,并且撤消时从中弹出“更改”,则堆栈是完美的。然后,您将弹出的更改推送到另一个表示重做的堆栈中。在未来的某个时候,希望在保存时,你清除两个堆栈。

      实际上并没有那么简单,因为您需要记录更改的类型,了解旧值和新值等。因此,当您从撤消堆栈中弹出时,您弹出的内容必须描述先前的值是什么,并且它被设置为什么控件。

      对于重做堆栈,它需要了解新值是什么以及它去了哪里。但是,是的,两个堆栈的想法是自制撤消重做的良好开端。

      基于业务对象的撤销的一个很好的例子是 CSLA.NET,它有 UndoableBase

      http://www.lhotka.net/cslanet/

      http://www.koders.com/csharp/fidCF6AB2CF035B830FF6E40AA22C8AE7B135BE1FC0.aspx?s=serializationinfo

      但是,这会记录对象状态的快照,因此它会比基于表单的概念更先进。但是,CSLA.NET 提供完整的数据绑定支持,因此从 UndoableBase 继承的数据绑定对象自然会支持 UI 中的撤消(而不是重做)。

      【讨论】:

      • @Sergio 是的,当您进行更改时,您将其推送到撤消堆栈,当人们撤消操作时,您从撤消堆栈弹出并推送到重做堆栈。如果他们执行除重做以外的任何操作,则清除重做堆栈,因为您可能会得到无效状态。
      • @Adam,我不喜欢两个堆栈的想法。当用户在撤消后进行“新更改”时,您认为这不会有问题。我认为此时重做列表将被清除。因此,我会亲自尝试使用列表和指针
      • @musefan 我认为这已成为个人品味的问题。堆栈是一个完全有效的容器选择,您无需了解您使用它们的位置。列表也是一个不错的选择,但您可以通过了解您当前在该列表中的位置来了解上下文。
      • 两种方法我都做过,我更喜欢使用两个堆栈。当用户执行新操作并且您需要清除重做堆栈时,RedoStack.Clear()while (UndoList.Count &gt; UndoPointer) UndoList.RemoveAt(UndoList.Count - 1); 更简单、更易读、更明显正确。它还可以轻松启用和禁用撤消和重做按钮——CanUndo 就像UndoStack.Any() 一样简单,CanRedo 就像RedoStack.Any()
      • @Joe,好点 - 我会尽量记住这一切,以备不时之需
      【解决方案3】:

      可能最直接的方法是使用撤消/重做堆栈组合。

      另一种方法是拥有一个数组或动作列表,只需递增/递减指向数组中索引的指针。当一个动作被撤销时,索引向后移动一个,当一个动作被重做时,索引向前移动一个。 这样做的好处是,您不需要为每个操作执行“弹出然后推送”序列。

      需要考虑的事项:

      • 如果您撤消几次,然后执行一个操作,所有 必须消除重做操作。
      • 在尝试执行撤消/重做之前,请务必检查边界并确保有可用于撤消/重做的操作。

      【讨论】:

        【解决方案4】:

        是的,您将使用堆栈。有几种方法可以做到这一点;阅读这些参考资料:

        http://en.wikipedia.org/wiki/Command_pattern

        http://en.wikipedia.org/wiki/Memento_pattern

        各有优缺点。

        【讨论】:

        • 我认为对于我的用例(UI 值变化等等),使用 Memento 模式更有意义。另外,我喜欢这部电影,并且在编码时感觉很好。
        • 我真的更喜欢 UI 的命令模式,原因有很多(例如内存更少,UI 与数据模型更好的分离,以及将多个值更改合并到一个步骤中的能力......还有它减少了 UI 和 Data 之间的耦合...)但这可能只是个人喜好。
        • 是的,这是一部很棒的电影! :) Memento 很容易实现,因为您只需要复制数据模型而不是创建命令对象。但如果数据集很大(内存限制),这并不总是一种选择。另一方面,如果执行每个命令的时间很长,那么 memento 可能是最好的选择。在您的应用程序中,我猜这并不重要,除非您正在制作图像编辑器。
        猜你喜欢
        • 2021-06-04
        • 2017-03-16
        • 2019-08-22
        • 1970-01-01
        • 2012-02-08
        • 1970-01-01
        • 2016-03-31
        • 2011-05-01
        • 1970-01-01
        相关资源
        最近更新 更多