撤消/重做实施
给我一些思考如何实现撤消/重做function – 就像我们在文本编辑器中所做的那样。 我应该使用哪些algorithm以及我可以读取哪些algorithm。 谢谢。
我知道关于撤消types的两个主要部门
- 保存状态:撤消的一种types是您实际保存历史状态的地方。 在这种情况下,发生什么事情是在每一点上,你一直在保存状态的一些位置的内存。 当你想进行撤消时,只需将当前状态换掉,并以已经存在的状态进行交换。 例如,Adobe Photoshop中的“历史logging”或“Google Chrome”中重新打开的closures标签就是这样做的。
- 生成状态:另一个类别不是维护状态,而是记住操作是什么。 当你需要撤消时,你需要做一个特定行动的逻辑反向。 举一个简单的例子,当你在一些支持undo的文本编辑器中做一个Ctrl + B时,它被记为一个Bold操作。 现在每个动作都是其逻辑反转的映射。 所以,当你做一个Ctrl + Z的时候 ,它从一个逆动作表中查找,并且发现撤消动作是一个Ctrl + B。 这是执行,你得到你以前的状态。 所以,在这里你以前的状态并没有存储在内存中,而是在你需要的时候生成的。
对于文本编辑器来说,用这种方式生成状态并不是计算密集型的,但对于像Adobe Photoshop这样的程序来说,它可能计算量太大或根本不可能。 例如,对于模糊操作,您将指定一个去模糊操作,但是由于数据已经丢失,所以无法使您达到原始状态。 所以,根据情况 – 逻辑逆动作的可能性及其可行性,您需要在这两个大类之间进行select,然后按照您的要求执行。 当然,有可能有一个适合你的混合策略。
另外,有时候,像在Gmail中一样,有时候可以撤销时间,因为发送邮件的操作从来不是首先完成的。 所以,你并不是在那里“撤销”,你只是“不”行动本身。
我从头开始编写两个文本编辑器,它们都采用了非常原始的撤销/重做function。 “原始”是指这个function非常容易实现,但是在非常大的文件(例如>> 10 MB)中是不经济的。 但是,这个系统非常灵活, 例如,它支持无限级别的撤消。
基本上,我定义了一个像
type TUndoDataItem = record text: /array of/ string; selBegin: integer; selEnd: integer; scrollPos: TPoint; end;
然后定义一个数组
var UndoData: array of TUndoDataItem;
然后,该数组的每个成员都指定文本的已保存状态。 现在,在文本的每个编辑(字符键向下,向下空格,删除键,剪切/粘贴,鼠标移动的select等),我(重新)启动一个计时器(说)一秒钟。 触发时,定时器将当前状态保存为UndoData
数组的新成员。
撤消(Ctrl + Z)时,我将编辑器恢复到UndoData[UndoLevel - 1]
状态, UndoData[UndoLevel - 1]
减1。 默认情况下, UndoLevel
等于UndoData
数组最后一个成员的索引。 在重做(Ctrl + Y或Shift + Ctrl + Z)上,我将编辑器恢复到状态UndoData[UndoLevel + 1]
并将UndoLevel
加1。 当然,如果在UndoLevel
不等于UndoData
数组的长度(减1)时触发编辑定时器,那么在UndoLevel
之后清除此数组的所有项目,这在Microsoft Windows中是很常见的(但是如果我使用Emacs,则更好回忆正确 – 微软Windows方法的缺点是,如果你撤销了很多的改变,然后意外地编辑了缓冲区,那么这个前所未有的内容(永久性的)将永久丢失。 你可能想跳过这个数组的减less。
例如,在一个不同types的程序中,一个图像编辑器,可以应用相同的技术,但是当然,使用完全不同的UndoDataItem
结构。 更高级的方法,不需要太多的内存,只保存撤销级别之间的变化 (也就是说,不是保存“alpha \ nbeta \ gamma”和“alpha \ nbeta \ ngamma \ ndelta”,你可以保存“alpha \ nbeta \ ngamma”和“ADD \ ndelta”,如果你看到我的意思)。 在非常大的文件中,与文件大小相比,每次更改都很小,这将大大减less撤消数据的内存使用量,但实现起来更为棘手,并且可能更容易出错。
有几种方法可以做到这一点,但是你可以开始看Command模式 。 使用命令列表将其移回(撤消)或转发(重做)通过您的操作。 在C#中的一个例子可以在这里find。
有点迟了,但这里有:你特指文本编辑器,下面解释一个algorithm,可以适应你正在编辑的任何东西。 所涉及的原则是保留一个行动/指令列表,可以自动重新创build你所做的每一个变化。 不要对原始文件进行修改(如果不是空的话),保留作为备份。
保留对原始文件所做的更改的前后链接列表。 这个列表被间歇地保存到一个临时文件中,直到用户实际上保存更改:当发生这种情况时,将更改应用到新文件,复制旧文件并同时应用更改; 然后将原始文件重命名为备份,并将新文件的名称更改为正确的名称。 (您可以保留已保存的更改列表,或者删除它,并用随后的更改列表replace。)
链表中的每个节点都包含以下信息:
- 更改types:您可以插入数据,也可以删除数据:“更改”数据表示
delete
后跟insert
- 文件中的位置:可以是偏移量或行/列对
- 数据缓冲区:这是与动作有关的数据; 如果
insert
它是insert
的数据; 如果delete
,delete
的数据。
要实现“ Undo
,您可以使用“当前节点”指针或索引,从链接列表尾部开始工作:在insert
更改的位置,执行删除操作,但不更新链接列表; 并且它是delete
地方,您可以从链接列表缓冲区中的数据中插入数据。 对用户的每个“撤消”命令执行此操作。 Redo
将“当前节点”指针向前移动,并根据节点执行更改。 如果用户在撤消之后对代码进行了修改,则将“当前节点”指示符之后的所有节点删除到尾部,并将尾部设置为等于“当前节点”指示符。 然后用户的新改变被插入到尾部之后。 这就是它。
我只有两分钱就是你想用两个堆栈来跟踪操作。 每次用户执行某些操作时,程序都应该将这些操作放在“执行”堆栈上。 当用户想撤销这些操作时,只需将操作从“执行”堆栈popup到“重新调用”堆栈。 当用户想要重做这些操作时,从“重新调用”堆栈中popup项并将其推回“已执行”堆栈。
希望能帮助到你。
你可以学习一个现有的撤销/重做框架的例子,第一个Google命中是在codeplex(对于.NET) 。 我不知道这比其他框架好还是差,其中有很多。
如果您的目标是在您的应用程序中具有撤销/重做function,那么您可以select一个适合您的应用程序的现有框架。
如果你想学习如何构build你自己的撤销/重做,你可以下载源代码,看看模式和细节如何连线。
纪念图案是为此而制作的。
在自己实现这个之前,请注意这是很常见的,并且代码已经存在 – 例如,如果您使用.Net编码,则可以使用IEditableObject 。
如果行动是可逆的。 例如添加1,让玩家移动等,看看如何使用命令模式来实现撤销/重做 。 按照链接你会发现如何做到这一点的详细的例子。
如果没有,按照@Lazer的说明使用Saved State 。