撤销和重做实现-第一部分(单对象状态变化)

Posted 火柴盒zhang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了撤销和重做实现-第一部分(单对象状态变化)相关的知识,希望对你有一定的参考价值。

        本文是从英文的网站翻译,用作自己的整理和记录,有修改调整。水平有限,欢迎指正。原文地址:地址

一、引言

        这是关于用C#编写多级撤销和重做实现的系列文章的第一部分。本系列展示了针对同一问题的三种方法的撤销/重做实现,以及我们如何使用这些方法实现不同场景的撤销/恢复。这些方法使用单对象状态变化、命令模式和备忘录模式。

        正如我们所知道的,撤销/重做没有通用的解决方案,撤销/重做的实现对于每个应用程序都具有定制化部分。因此,本系列文章的每个部分首先讨论了该模式下的设计思路,然后在实例程序中展示了如何实现。该系列,撤销/重做操作使用三种不同的设计方式,因此您可以将每个模式实现与其他方法实现进行比较,并选择最适合您需求的方法。在每个部分文章中我们还讨论了每种方法的优缺点。

二、Undo/Redo实现的基本思路

        我们知道,应用程序在每次操作后都会改变其状态。当应用程序操作运行时,它会改变其状态。因此,如果我们想Undo撤销,我们必须回到以前的状态。因此,为了能够恢复到以前的状态,我们需要存储应用程序运行时的状态。为了支持Redo重做,我们必须从当前状态转到下一个状态。

        要实现Undo/Redo,我们必须存储应用程序的状态,并且必须转到上一状态进行撤销,转至下一状态进行重做。因此,我们必须维护应用程序的状态以支持Undo/Redo。为了在三种设计方法中维护应用程序的状态,我们使用了两个堆栈。一个堆栈包含撤消操作的状态,第二个堆栈包含重做操作的状态。撤消操作弹出撤消堆栈以获取以前的状态,并将以前的状态设置为应用程序。同样,重做操作弹出重做堆栈以获得下一个状态,并为应用程序设置下一状态。

       现在我们知道,实现撤销-重做操作就是在应用程序的每个操作之后保持状态。现在的问题是,这种方法如何保持状态?在单对象状态这种方法中,单个操作的更改保留在对象中,其中某些属性对于该操作作为状态是冗余的。

         单对象的操作变化的实现是保持一个对象,因为这里的单对象被用来持有所有类型的操作数据,所有该对象的属性是沉余,过剩的。

三、单对象状态实现路径

     这里的单对象代表应用程序中所有操作的所有更改。因此,当你决定使用一个持有状态操作的对象时 ,你只需要设置你关系的对象属性,其他的属性不需要关心、使用。例如,一个应用程序中有两个操作;高度变化和宽度变化。因此,这里的对象包含两个属性:高度和宽度。执行高度变化方法后,你仅仅只设置更改对象的高度属性,其他属性时没有使用的。 

四、单对象状态通用思路

        在以下步骤中讨论了如何使用单对象通用设计思路:

4.1 第一步

        首先确定要支持 Undo/Redo 的操作。然后,确定在哪个容器中支持撤消/重做,以及要支持撤消/恢复的对象。

4.2 第二步

        然后确定需要保存的属性,以便进一步处理每个撤消/重做操作。

4.3 第三步

        然后创建一个类(例如:ChangeRepresentationObject),该类包含支持所有操作的撤消/重做的所有属性。还可以创建一个操作类型枚举,它将表示所有操作。此操作类型枚举将是ChangeRepresentationObject类的一部分。

4.4 第四步

        然后创建一个名为UndoRedo的类,该类包含两个类型为ChangeRepresentationObject的堆栈。一个堆栈用于撤消操作,另一个操作id用于重做操作。该类将实现以下接口:

interface IUndoRedo
  
      void Undo(int level);
      void Redo(int level);
      void InsertObjectforUndoRedo(ChangeRepresentationObject dataobject);
  

4.5 第五步

        然后实现方法:Undo、Redo、InsertObjectforUndoRedo。

4.5.1 在每次撤消操作中:

  • 首先,您将检查撤消Undo堆栈是否为空。如果为空,则返回,否则继续。
  • 如果不是,则pop 弹出ChangeRepresentationObject并将其push 推送到重做堆栈。
  • 检查操作类型 action type。
  • 然后,根据动作类型action type,使用ChangeRepresentationObject属性执行撤消操作。

4.5.2在每个重做操作中:

  • 首先,您将检查RedoStack堆栈是否为空。如果为空,则返回,否则继续。
  • 如果不是,则弹出ChangeRepresentationObject并将其推到撤消堆栈。。
  • 检查操作类型。 action type。
  • 然后,根据动作类型,使用ChangeRepresentationObject属性执行重做操作。

    在InsertObjectforUndoRedo操作中,您只需将数据对象插入撤消堆栈并清除重做堆栈。

4.6 第六步

        然后,在执行每个操作之前,调用InsertObjectforUndoRedo方法以支持所有这些操作的撤消/重做。当从UI中单击Undo时,只需调用UndoRedo类的Undo方法;当从UI单击Redo时,只需要调用UndoRedo类的Redo方法。

五、程序实现

        这里,一个简单的WPF绘图应用程序被用作撤消/重做操作的示例。这个WPF示例应用程序支持四种操作:对象插入、对象删除、对象移动和对象调整大小,并有两种几何对象:矩形和多边形。它使用画布作为容器来包含这些几何对象。

        现在,在本系列文章中,我们将看到如何在这四个操作中提供撤销/重做支持。

  1.   第一部分,展示了使用单对象
  2.   第二部分,使用命令模式
  3.   第三部分,用Memento模式

        下面讨论了使用单对象方法的示例应用程序的撤消/重做实现:

5.1 第一步

        我们将确定支持撤消/重做的操作。以下是我们支持撤销/重做的四个操作。它们是:对象插入、对象删除、对象移动、对象调整大小。我们打算支持矩形和椭圆对象的撤消/重做,这里的容器是画布。

5.2 第二步

        现在,我们将确定需要保存的参数,以便进一步处理撤消/重做。几何对象移动其边距变化,因此为了支持对象移动的撤消/重做,我们需要保存边距。对象调整大小时,会更改其高度、宽度和边距。因此,为了支持对象大小调整的撤消/重做,我们需要保存高度、宽度和边距。为了支持插入和删除的撤消/重做,我们将保留几何对象的引用。

5.3 第三步

         现在我们将创建ChangeRepresentationObject类,它包含边距、高度、宽度、动作类型和几何对象引用,以支持所有操作的撤消/重做。在这里,几何对象引用被保留,以在我们想要对其进行撤消/重做时获得引用。还需要创建一个动作类型枚举,它将表示插入、删除、移动和调整大小操作。此操作类型枚举用作ChangeRepresentationObject的一部分。

public enum ActionType

       Delete = 0,
       Move = 1,
       Resize = 2,
       Insert = 3


public class ChangeRepresentationObject

    public ActionType Action;
    public Point Margin;
    public double Width;
    public double height;
    public FrameworkElement UiElement;

5.4 第四步 和 第五步

        我们创建了一个名为UndoRedo的类,它包含两个类型为ChangeRepresentationObject的堆栈。一个堆栈用于撤消操作,另一个操作id用于重做操作。类代码如下所示:

public partial class UnDoRedo : IUndoRedo
  
      private Stack<ChangeRepresentationObject> _UndoActionsCollection =
                  new Stack<ChangeRepresentationObject>();
      private Stack<ChangeRepresentationObject> _RedoActionsCollection =
                  new Stack<ChangeRepresentationObject>();

      #region IUndoRedo Members

      public void Undo(int level)
      
          for (int i = 1; i <= level; i++)
          
              if (_UndoActionsCollection.Count == 0) return;

              ChangeRepresentationObject Undostruct = _UndoActionsCollection.Pop();
              if (Undostruct.Action == ActionType.Delete)
              
                  Container.Children.Add(Undostruct.UiElement);
                  this.RedoPushInUnDoForDelete(Undostruct.UiElement);
              
              else if (Undostruct.Action == ActionType.Insert)
              
                  Container.Children.Remove(Undostruct.UiElement);
                  this.RedoPushInUnDoForInsert(Undostruct.UiElement);
              
              else if (Undostruct.Action == ActionType.Resize)
              
                  if (_UndoActionsCollection.Count != 0)
                  
                      Point previousMarginOfSelectedObject = new Point
          (((FrameworkElement)Undostruct.UiElement).Margin.Left,
                          ((FrameworkElement)Undostruct.UiElement).Margin.Top);
                      this.RedoPushInUnDoForResize(previousMarginOfSelectedObject,
          Undostruct.UiElement.Width,
                          Undostruct.UiElement.Height, Undostruct.UiElement);
                      Undostruct.UiElement.Margin = new Thickness
          (Undostruct.Margin.X, Undostruct.Margin.Y, 0, 0);
                      Undostruct.UiElement.Height = Undostruct.height;
                      Undostruct.UiElement.Width = Undostruct.Width;
                  
              
              else if (Undostruct.Action == ActionType.Move)
              
                  Point previousMarginOfSelectedObject = new Point
          (((FrameworkElement)Undostruct.UiElement).Margin.Left,
                      ((FrameworkElement)Undostruct.UiElement).Margin.Top);
                  this.RedoPushInUnDoForMove(previousMarginOfSelectedObject,
                          Undostruct.UiElement);
                  Undostruct.UiElement.Margin = new Thickness
          (Undostruct.Margin.X, Undostruct.Margin.Y, 0, 0);
              
          
      

      public void Redo(int level)
      
          for (int i = 1; i <= level; i++)
          
              if (_RedoActionsCollection.Count == 0) return;

              ChangeRepresentationObject Undostruct = _RedoActionsCollection.Pop();
              if (Undostruct.Action == ActionType.Delete)
              
                  Container.Children.Remove(Undostruct.UiElement);
                  this.PushInUnDoForDelete(Undostruct.UiElement);
              
              else if (Undostruct.Action == ActionType.Insert)
              
                  Container.Children.Add(Undostruct.UiElement);
                  this.PushInUnDoForInsert(Undostruct.UiElement);
              
              else if (Undostruct.Action == ActionType.Resize)
              
                  Point previousMarginOfSelectedObject = new Point
          (((FrameworkElement)Undostruct.UiElement).Margin.Left,
                      ((FrameworkElement)Undostruct.UiElement).Margin.Top);
                  this.PushInUnDoForResize(previousMarginOfSelectedObject,
                      Undostruct.UiElement.Width,
                      Undostruct.UiElement.Height, Undostruct.UiElement);
                  Undostruct.UiElement.Margin = new Thickness
          (Undostruct.Margin.X, Undostruct.Margin.Y, 0, 0);
                  Undostruct.UiElement.Height = Undostruct.height;
                  Undostruct.UiElement.Width = Undostruct.Width;
              
              else if (Undostruct.Action == ActionType.Move)
              
                  Point previousMarginOfSelectedObject = new Point
          (((FrameworkElement)Undostruct.UiElement).Margin.Left,
                      ((FrameworkElement)Undostruct.UiElement).Margin.Top);
                  this.PushInUnDoForMove(previousMarginOfSelectedObject,
                          Undostruct.UiElement);
                  Undostruct.UiElement.Margin = new Thickness
              (Undostruct.Margin.X, Undostruct.Margin.Y, 0, 0);
              
          
      

      public void InsertObjectforUndoRedo(ChangeRepresentationObject dataobject)
      
          _UndoActionsCollection.Push(dataobject);
          _RedoActionsCollection.Clear();
      

5.5 第六步

        在执行每个操作之前,请调用InsertObjectforUndoRedo方法。当从UI单击Undo时,我们调用UndoRedo类的Undo方法,当从UI中单击Redo时,我们将调用UndoRedo类的Redo方法。

        在这里,没有明确设置撤消堆栈和重做堆栈的大小,保存的撤消重做状态的数量取决于系统内存。

六、优势和缺点

6.1 优点

        它的优点是实现简单,因为在不知道任何设计模式的情况下,可以实现撤销/重做。

6.2 缺点

        可维护性低。表示此方法的对象包含额外信息,因为这里使用单个对象来保存所有类型的操作数据。例如,对于移动,我们应该只保留移动相关数据,对于调整大小,我们应该仅保留调整大小相关数据。因此,我们保留了冗余数据。随着操作数量的增加,冗余度增加。这不是一个好的面向对象设计。

以上是关于撤销和重做实现-第一部分(单对象状态变化)的主要内容,如果未能解决你的问题,请参考以下文章

撤销和重做实现-第二部分(命令模式)

撤销和重做实现-第二部分(命令模式)

撤销和重做实现-第三部分(备忘录模式)

撤销和重做实现-第三部分(备忘录模式)

B6:命令模式 Command

命令模式-command