画图应用程序中的撤销/重做命令模式

5
我希望在一个小的绘画应用程序中实现撤销/重做。看起来命令模式非常适合这种情况,但我不确定如何最好地实现它。
据我了解,每个命令都需要包含以下内容:
1. 用于重做的绘画操作的详细信息(例如,线条->起始和结束点,自由形式线条->GeneralPath) 2. 更改前组件的状态以进行撤消。在这种情况下,将是受命令影响的区域的小快照图像。
根据我的理解,每个命令都需要“原子性”或自包含,具有所有撤销/重做该操作所需的信息。

不幸的是,这需要存储比我最初预期的更多的信息。对于一条线,我们还必须考虑用于最初绘制它的ColorStrokeRenderingHints等因素。这使得我的“简单小命令”变得更加笨重,需要更多样板代码(每个都将成为可序列化的bean1)。

出于内存保护的原因(大多数情况下),我希望在命令的规范上“作弊”。也许可以在每100次更新时备份整个绘图区域,但否则不存储任何已更改的图像部分,并仅为每个新绘制操作重建最后(最多)100个命令。但是,要确保在每个部分绘制之前Graphics对象的状态正确是有问题的-这部分可能需要一条线,但是RenderingHints在4个命令之前发生了改变,而Color在98个命令之前发生了改变,而Stroke在最后227个命令中保持不变。

追求更节省内存的命令似乎使“原子性”模式彻底被抛弃。这反过来会导致难以确定可能影响渲染的最早命令。
我应该:
- 寻找新的模式吗? - 尝试通过调整模式来实现我的特殊需求吗? - 把所有这些都扔进垃圾桶里,因为这是过早的优化,并用最简单(也是最消耗内存)的方式编写符合定义的命令模式的代码?
更新
1. “每个都将是可序列化的bean” 经过第二次考虑,不是这样的。我进行了一些检查,发现一个Graphics2D(它整洁地封装了许多绘图时使用的参数)不可序列化。此外,BasicStroke是可序列化的,但未存储笔画的粗细。我可以创建许多属性的可序列化版本,但这似乎会导致更多的代码,因此我将放弃该规范。我只会尝试在运行时存储对BufferedImage的引用。

1
也许你应该使用备忘录模式? - white
@white 我需要进一步研究Memento模式,但似乎Memento对象基本上填充了命令模式中的Command对象的角色,并且每个Memento都需要存储组件在引用所做出的更改之前的“整个状态”。因此,我认为这会导致我面临存储每个操作原子性的相同问题。 - Andrew Thompson
2个回答

3

我建议使用命令模式,并尝试一种朴素的解决方案(即最耗费内存的解决方案)。对于某些图形操作,甚至需要在命令对象中保留整个图像的副本(例如,考虑滤镜)。这也是专业图像编辑应用程序中常见的问题,它们通常有一个记忆最后几个命令的内存或步骤限制。如果内存消耗真的很大,您可以考虑将命令历史记录中最老的条目交换到文件系统中。我认为用户不会介意等待一秒钟直到更改被撤消。


这是一个常见的问题,即使在专业的图像编辑应用程序中也是如此。这是一个非常好的观点。如果这个问题对于专业绘画应用程序的编码人员来说变得“太难”,那么似乎我几乎没有可靠地实现它的机会。 - Andrew Thompson
我想至少等待24小时才接受任何答案,以鼓励不同的观点(看表)。时间限制已经过去,我猜想“没有更多答案”意味着人们认为你们两个已经涵盖了它。我选择了你的答案。虽然@stemm的答案提供了一些有趣的优化命令模式的思路,但解决方案似乎归结为“要么按照现有的命令模式实现,要么证明你比一些非常聪明的人更聪明,并发明一种新的模式”。 <open secret>我不是那么聪明。</open secret> ;) - Andrew Thompson
@AndrewThompson 不要太谦虚 ;) - Adam Dyga

1
也许最好不要在命令中存储整个图像的副本,而是只存储受命令更改的区域的副本。当然,这并非万无一失的解决方案。

“但仅存储区域的副本”是已经在考虑之中的(“小快照图像”),并将在我编写最终命令时使用。感谢您的回复。 - Andrew Thompson
@Andrew Thompson,另外,在看了你的屏幕截图后,我得出结论,存储快照时进行一些无损压缩会更有效率。 - stemm
“在存储快照时进行无损压缩可能会更有效。” 这是个好主意。虽然会增加一些 CPU 开销,但可以节省大量内存。 - Andrew Thompson
@AndrewThompson 你甚至可以创建一个算法来计算一组最小边界矩形(MBRs),覆盖变化的区域。这应该会使快照的大小更小。 - Adam Dyga

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接