撤销和重做实现-第一部分(单对象状态变化)
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示例应用程序支持四种操作:对象插入、对象删除、对象移动和对象调整大小,并有两种几何对象:矩形和多边形。它使用画布作为容器来包含这些几何对象。
现在,在本系列文章中,我们将看到如何在这四个操作中提供撤销/重做支持。
- 第一部分,展示了使用单对象
- 第二部分,使用命令模式
- 第三部分,用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 缺点
可维护性低。表示此方法的对象包含额外信息,因为这里使用单个对象来保存所有类型的操作数据。例如,对于移动,我们应该只保留移动相关数据,对于调整大小,我们应该仅保留调整大小相关数据。因此,我们保留了冗余数据。随着操作数量的增加,冗余度增加。这不是一个好的面向对象设计。
以上是关于撤销和重做实现-第一部分(单对象状态变化)的主要内容,如果未能解决你的问题,请参考以下文章