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

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

一、引言

        这是关于用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 缺点

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现撤销操作的一种常用方式是使用命令模式。具体实现步骤如下: 1. 定义命令对象 首先,定义一个命令对象,该对象包含一个`execute`方法和一个`undo`方法,`execute`方法用于执行命令,`undo`方法用于撤销命令。例如: ```js class Command { constructor(execute, undo) { this.execute = execute; this.undo = undo; } } ``` 2. 定义命令历史记录 定义一个命令历史记录,用于存储所执行的命令。例如: ```js class CommandHistory { constructor() { this.commands = []; this.currentCommandIndex = -1; } execute(command) { command.execute(); this.commands.push(command); this.currentCommandIndex++; } undo() { if (this.currentCommandIndex >= 0) { const command = this.commands[this.currentCommandIndex]; command.undo(); this.currentCommandIndex--; } } redo() { if (this.currentCommandIndex < this.commands.length - 1) { this.currentCommandIndex++; const command = this.commands[this.currentCommandIndex]; command.execute(); } } } ``` 3. 创建命令对象并执行 创建一个命令对象,将其添加到命令历史记录中,并执行该命令。例如: ```js const command = new Command(() => { // execute command }, () => { // undo command }); const commandHistory = new CommandHistory(); commandHistory.execute(command); ``` 4. 撤销命令 使用命令历史记录的`undo`和`redo`方法来撤销命令。例如: ```js // 撤销上一步操作 commandHistory.undo(); // 上一步操作 commandHistory.redo(); ``` 以上是使用命令模式实现撤销操作的一种常见方式。需要注意的是,命令模式实现撤销操作只适用于命令式操作,而不适用于函数式操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值