24

我需要为我的窗口应用程序(如 powerpoint 之类的编辑器)实现撤消/重做框架工作,应该遵循的最佳实践是什么,如何处理我的对象的所有属性更改以及它在 UI 上的反映。

4

5 回答 5

39

有两种经典模式可供使用。第一个是备忘录模式,用于存储完整对象状态的快照。这可能比命令模式更加系统密集,但它允许非常简单地回滚到较旧的快照。您可以将快照存储在磁盘上,例如 PaintShop/PhotoShop,或者将它们保存在内存中以用于不需要持久性的较小对象。你所做的正是这个模式的设计目的,所以它应该比其他人建议的命令模式更符合要求。

另外,需要注意的是,因为它不需要您使用互惠命令来撤消以前完成的操作,这意味着任何潜在的单向功能[例如散列或加密]都无法使用互惠来轻松撤消只需回滚到较旧的快照,仍然可以非常简单地撤消命令。

同样正如所指出的,命令模式可能占用较少的资源,所以我承认在特定情况下:

  • 有一个大的对象状态需要持久化和/或
  • 没有破坏性的方法和
  • 可以非常简单地使用互惠命令来逆转所采取的任何行动

命令模式可能更适合[但不一定,这将在很大程度上取决于情况]。在其他情况下,我会使用备忘录模式。

我可能会避免使用两者的混搭,因为我倾向于关心将在我身后出现并维护我的代码的开发人员,以及我对雇主的道德责任是让这个过程变得简单和便宜可能的。我看到这两种模式的混搭很容易成为无法维护的不适感,维护成本很高。

于 2009-12-16T16:40:16.253 回答
26

这里有三种可行的方法。备忘录模式(快照)、命令模式和状态差异。它们都有优点和缺点,这实际上取决于您的用例、您正在使用的数据以及您愿意实施的内容。

如果您可以摆脱它,我会选择 State Diffing,因为它结合了内存减少与易于实现和可维护性

我将引用一篇描述这三种方法的文章(参考下文)。

请注意,文中提到的 VoxelShop 是开源的。所以你可以在这里看看命令模式的复杂性: https ://github.com/simlu/voxelshop/tree/develop/src/main/java/com/vitco/app/core/data/history

以下是文章的摘录。但是,我确实建议您完整阅读它。


纪念品图案

在此处输入图像描述

每个历史状态都存储一个完整副本。一个动作创建一个新状态,一个指针用于在状态之间移动以允许撤消和重做。

优点

  • 实施独立于应用的操作。一旦实现,我们就可以添加操作而不必担心破坏历史。
  • 快速推进到历史中的预定义位置。当在当前和期望的历史位置之间应用的动作在计算上很昂贵时,这很有趣。

缺点

  • 与其他方法相比,内存要求可能要高得多。
  • 如果快照很大,加载时间可能会很慢。

命令模式

在此处输入图像描述

类似于备忘录模式,但不是存储完整状态,而是仅存储状态之间的差异。差异存储为可以应用和不应用的操作。引入新动作时,需要执行应用和取消应用。

优点

  • 内存占用很小。我们只需要将更改存储到模型中,如果这些更改很小,那么历史堆栈也很小。

缺点

  • 我们不能直接去任意位置,而是需要取消应用历史堆栈,直到我们到达那里。这可能很耗时。
  • 每个动作及其相反的动作都需要封装在一个对象中。如果你的行动不是微不足道的,这可能很困难。(反向)操作中的错误确实很难调试,并且很容易导致致命的崩溃。即使是看起来很简单的动作通常也包含大量的复杂性。例如,在 3D 编辑器的情况下,添加到模型的对象需要存储添加的内容、当前选择的颜色、覆盖的内容、镜像模式是否激活等。
  • 当动作没有简单的反向时,例如在模糊图像时,实现起来可能具有挑战性并且需要大量内存。

状态差异

在此处输入图像描述

与命令模式类似,但通过简单地对状态进行异或操作来独立于操作存储差异。引入新动作不需要任何特殊考虑。

优点

  • 实施独立于应用的操作。一旦添加了历史功能,我们就可以添加操作而不必担心破坏历史。
  • 内存要求通常比快照方法低得多,并且在很多情况下与命令模式方法相当。然而,这在很大程度上取决于所应用的操作类型。例如,使用命令模式反转图像的颜色应该非常便宜,而状态区分将保存整个图像。相反,当绘制一条自由格式的长线时,如果命令模式方法将每个像素的历史条目链接起来,它可能会使用更多内存。

缺点/限制

  • 我们不能直接去任意位置,而是需要取消应用历史堆栈,直到我们到达那里。
  • 我们需要计算状态之间的差异。这可能很昂贵。
  • 根据您的数据模型,实现模型状态之间的异或差异可能很难实现。

参考:

https://www.linkedin.com/pulse/solving-history-hard-problem-lukas-siemon

于 2017-08-23T15:28:20.200 回答
6

经典的做法是遵循命令模式

您可以封装任何使用命令执行操作的对象,并使用 Undo() 方法让它执行相反的操作。您将所有操作存储在堆栈中,以便轻松地倒带。

于 2009-12-16T16:38:07.973 回答
2

看看命令模式。您必须将对模型的每个更改封装到单独的命令对象中。

于 2009-12-16T16:39:41.963 回答
0

我编写了一个非常灵活的系统来跟踪更改。我有一个绘图程序,它实现了两种类型的更改:

  • 添加/删除形状
  • 形状的属性变化

基类:

public abstract class Actie
{
    public Actie(Vorm[] Vormen)
    {
        vormen = Vormen;
    }

    private Vorm[] vormen = new Vorm[] { };
    public Vorm[] Vormen
    {
        get { return vormen; }
    }

    public abstract void Undo();
    public abstract void Redo();
}

添加形状的派生类:

public class VormenToegevoegdActie : Actie
{
    public VormenToegevoegdActie(Vorm[] Vormen, Tekening tek)
        : base(Vormen)
    {
        this.tek = tek;
    }

    private Tekening tek;
    public override void Redo()
    {
        tek.Vormen.CanRaiseEvents = false;
        tek.Vormen.AddRange(Vormen);
        tek.Vormen.CanRaiseEvents = true;
    }

    public override void Undo()
    {
        tek.Vormen.CanRaiseEvents = false;
        foreach(Vorm v in Vormen)
            tek.Vormen.Remove(v);
        tek.Vormen.CanRaiseEvents = true;
    }
}

用于删除形状的派生类:

public class VormenVerwijderdActie : Actie
{
    public VormenVerwijderdActie(Vorm[] Vormen, Tekening tek)
        : base(Vormen)
    {
        this.tek = tek;
    }

    private Tekening tek;
    public override void Redo()
    {
        tek.Vormen.CanRaiseEvents = false;
        foreach(Vorm v in Vormen)
            tek.Vormen.Remove(v);
        tek.Vormen.CanRaiseEvents = true;
    }

    public override void Undo()
    {
        tek.Vormen.CanRaiseEvents = false;
        foreach(Vorm v in Vormen)
            tek.Vormen.Add(v);
        tek.Vormen.CanRaiseEvents = true;
    }
}

属性更改的派生类:

public class PropertyChangedActie : Actie
{
    public PropertyChangedActie(Vorm[] Vormen, string PropertyName, object OldValue, object NewValue)
        : base(Vormen)
    {
        propertyName = PropertyName;
        oldValue = OldValue;
        newValue = NewValue;
    }

    private object oldValue;
    public object OldValue
    {
        get { return oldValue; }
    }

    private object newValue;
    public object NewValue
    {
        get { return newValue; }
    }

    private string propertyName;
    public string PropertyName
    {
        get { return propertyName; }
    }

    public override void Undo()
    {
        //Type t = base.Vorm.GetType();
        PropertyInfo info = Vormen.First().GetType().GetProperty(propertyName);
        foreach(Vorm v in Vormen)
        {
            v.CanRaiseVeranderdEvent = false;
            info.SetValue(v, oldValue, null);
            v.CanRaiseVeranderdEvent = true;
        }
    }
    public override void Redo()
    {
        //Type t = base.Vorm.GetType();
        PropertyInfo info = Vormen.First().GetType().GetProperty(propertyName);
        foreach(Vorm v in Vormen)
        {
            v.CanRaiseVeranderdEvent = false;
            info.SetValue(v, newValue, null);
            v.CanRaiseVeranderdEvent = true;
        }
    }
}

每次Vormen = 提交到更改的项目数组。它应该像这样使用:

堆栈声明:

Stack<Actie> UndoStack = new Stack<Actie>();
Stack<Actie> RedoStack = new Stack<Actie>();

添加新形状(例如点)

VormenToegevoegdActie vta = new VormenToegevoegdActie(new Vorm[] { NieuweVorm }, this);
UndoStack.Push(vta);
RedoStack.Clear();

删除选定的形状

VormenVerwijderdActie vva = new VormenVerwijderdActie(to_remove, this);
UndoStack.Push(vva);
RedoStack.Clear();

登记财产变更

PropertyChangedActie ppa = new PropertyChangedActie(new Vorm[] { (Vorm)e.Object }, e.PropName, e.OldValue, e.NewValue);
UndoStack.Push(ppa);
RedoStack.Clear();

最后撤消/重做动作

public void Undo()
{
    Actie a = UndoStack.Pop();
    RedoStack.Push(a);
    a.Undo();
}

public void Redo()
{
    Actie a = RedoStack.Pop();
    UndoStack.Push(a);
    a.Redo();
}

我认为这是实现撤消重做算法的最有效方法。例如,看看我网站上的这个页面:DrawIt

我在 Tekening.cs 文件的第 479 行左右实现了撤消重做内容。您可以下载源代码。它可以由任何类型的应用程序实现。

于 2016-01-12T22:40:07.037 回答