如何实现Undo 功能 ( Design Pattern 讨论 )(200分)

  • 主题发起人 主题发起人 cheka
  • 开始时间 开始时间
C

cheka

Unregistered / Unconfirmed
GUEST, unregistred user!

许多应用软件都允许用户撤销作,比方说各类文本还有图形编辑器,
那么如何在自己的程序当中也实现类似的Undo 呢?
比方说有一个Form, 上面有一个CheckBox , 一个Edit, 一个颜色选择框
我们把一次操作定义为:
对CheckBox的选择 + Edit中文本的改变 + 颜色选择框中的改变。
每完成一次操作,允许用户返回到操作前的状态,比如
初始状态
ChekcBox : false 一次操作 ChekcBox : true
Edit : Hello ----------> Edit : World
ColorList: Red ColorList: Blue
要求Undo 后又回到初始状态。
有兴趣的请参与讨论,以上的需求可能在讨论过程中会变得更为复杂,
但是从简单的实现开始,我们可以一起见证Pattern是怎样炼成的。
 
遗憾
我不会
帮不了你
提前一下吧
 

这并不是一个实际问题,标准答案(但并不一定唯一)其实就
在《Design Pattern》一书当中。
在直接利用书中所给的Pattern之前,通过讨论来逐步完善一个
设计应该是很有趣也是很有裨益的
 
设立堆栈,然后把每步操作记录到里面,undo 时执行相反的操作。
 
用堆栈是个好办法,但是堆栈里面具体应该存什么呢,如果存的是操作的话,
undo时未必能从操作推出操作之前的状态,特别以一些图像处理程序来说,
对一个图像执行模糊操作之后,undo时是没法进行“反模糊”的。
 
具体问题具体分析。可逆操作保存操作步骤应该是一个好办法,对于不可逆操作或者是
太复杂的操作,可以完全保存原始状态。一般的图像程序不会允许无限 undo,有限的
undo 可以使用队列。如果允许无限 undo,应当使用二级存储器。
 
图像处理时的undo, 可以用保存图片到硬盘的办法来实现,这个我做过,肯定可行。
而且我做的是无限次undo,也可历史回溯, 最近才改了一个bug(图片保存路径有错了)
用堆栈是错的, 堆栈是后进先出的,肯定不可行。
应该用可变长的队列,队列的总长度随操作的执行而增长。这样既可以undo
也可以redo, 还可以任意跳转到其中任何一步。
文本编辑的undo也可以这样做,只不过,每执行一步编辑操作后,都需要将当前操作
记录下来(例如记录当前光标所有位置,具体执行的操作等,具体怎么记录操作,你恐
怕要好好地研究一下),并保存到一个足够大的数组中。

nCurrentOperationSN : integer;
//当前动作的序号, 用于记录当前历史记录
nMaxOperationSN : integer;
//当前动作的序号, 用于记录当前历史记录
proceduredo
SthForUndo(sThisAction :string);
//为undo做准备
procedure TFormMain.DoSthForUndo(sThisAction :string);
//为undo做准备
begin
SaveCurrentBitmap(Image1.Picture.Bitmap, sCurrentFile , sThisAction);
MainMenu1.Items.Items[1].Items[0].Caption :='恢复 '+sThisAction;
MenuProcess;
end;

procedure TFormMain.MenuProcess;
begin
if nCurrentOperationSN>1 then
MainMenu1.Items.Items[1].Items[0].Caption :='恢复'+sHistory[nCurrentOperationSN-1];
if nCurrentOperationSN=1000 then
MainMenu1.Items.Items[1].Items[0].Caption :='恢复'+sHistory[1];
if nCurrentOperationSN < nMaxOperationSN then
MainMenu1.Items.Items[1].Items[1].Enabled :=true
else
MainMenu1.Items.Items[1].Items[1].Enabled :=false;
if nCurrentOperationSN<=0 then
//置灰菜单
begin
MainMenu1.Items.Items[1].Items[0].Enabled :=false;
MainMenu1.Items.Items[1].Items[2].Enabled :=false;
end
else
begin
MainMenu1.Items.Items[1].Items[0].Enabled :=true;
MainMenu1.Items.Items[1].Items[2].Enabled :=true;
end;

end;

procedure TFormMain.mnuUndoClick(Sender: TObject);
{var
myFormMain : TFormMain;
begin
myFormMain := TFormMain.Create(Self);
try
myFormMain.Show;
myFormMain.TrayIcon.Active :=false;
finally
// myFormMain.Free;
end;
}
var
Bitmap: TBitmap;
begin
with NewBMPFormdo
begin
ActiveControl := WidthEdit;
try
if Image1.Picture.Graphic.Width>0 then
WidthEdit.Text := IntToStr(Image1.Picture.Graphic.Width);
if Image1.Picture.Graphic.Height>0 then
HeightEdit.Text := IntToStr(Image1.Picture.Graphic.Height);
except
end;
if ShowModal <> idCancel then
begin
Bitmap := nil;
try
Bitmap := TBitmap.Create;
Bitmap.Width := StrToInt(WidthEdit.Text);
Bitmap.Height := StrToInt(HeightEdit.Text);
Image1.Picture.Graphic := Bitmap;
// sCurrentFile := EmptyStr;
finally
Bitmap.Free;
end;
end;
end;

end;

procedure TFormMain.mnuNextStepClick(Sender: TObject);
var
bmp : Tbitmap;
sTemp : string;
begin
try
if nCurrentOperationSN >= nMaxOperationSN then
exit;
bmp := TBitmap.Create;
if LoadNextBitmap(bmp, sCurrentFile, sTemp ) then
//载入指定历史图片, 返回当前操作名
begin
image1.Picture.Bitmap.Assign(bmp);
MainMenu1.Items.Items[1].Items[0].Caption :='恢复 '+sHistory[nCurrentOperationSN];
end;
MenuProcess;
except
end;
end;

procedure TFormMain.mnuLastStepClick(Sender: TObject);
var
bmp : Tbitmap;
sTemp : string;
begin
if nCurrentOperationSN <=0 then
exit;
bmp := TBitmap.Create;
if LoadLastBitmap(bmp, sCurrentFile, sTemp ) then
//载入指定历史图片, 返回当前操作名
begin
image1.Picture.Bitmap.Assign(bmp);
MainMenu1.Items.Items[1].Items[0].Caption :='恢复'+sHistory[nCurrentOperationSN];
end;
MenuProcess;
end;




 
我的想法:
1。必须找出操作单元(OPUnit)和操作对象(OPObj)
2。像(Image),每个操作单元和操作对象必须支持照相(takeImage),每个像支持
再生(generateOPUnit,generateOPObj)
3。有一个历史列表(我非常想试试树型History,应该很有意思),列表将记录每个像
4。关于生成像,应该可以应用任何方法,包括全部保存,增量保存,特征保存等
5。可以考虑提供Persistent Interface和Serialization支持
 
谢谢几位的精彩发言, 不过想请JJams_King 解释一下操作单元和操作对象各指什么呢?
目前的大体方案是用一定的数据结构来存储一系列Undo对象,这个对象既可以是当前状态,
也可以是一个操作的逆操作(如果有的话),以求在空间效率和时间效率上尽可能达到最优。
同时这个Undo对象要支持 Serialization。
那么现在有了更高的追求:
1. 设计必须有很高的可复用性,就是说,提供一些接口,各种程序都可以利用我们已有的
Undo代码,从点阵图像处理程序移植到矢量图形编辑器,或从图形编辑程序移植到文本编
辑器,都只需要写相当少量的代码,而大部分与操作历史列表有关的代码都可以无缝的重用。
2. 设计必须有很好的扩展性,比方说一开始是用堆栈(不支持Redo)保存历史数据,然
后改写到队列,再然后也许用JJams_King所提的树形结构可以支持分枝的Undo/Redo,但
是客户代码绝大部分保持不变,最多加一些界面元素,比如一个Redo按钮或是显示分枝的
列表框。
To VRGL: 这确实是memento模式的典型应用,不过我更希望经过大家的讨论来得到一个良好
的设计,这对理解memento的意义也会有很大的帮助,纸上得来终觉浅,绝知此事要躬行,
不是么?
 
我以前看过一篇文章,好象是说利用Windows的API函数可以实现的,
具体怎样实现没有留意,因为我暂时还没有用到这个功能。
 
API里有基本消息可以实现文本输入等的Undo,但是复杂的Undo只能自己来做,如图形操作等。
我的软件是GIS软件,有文本输入、图形编辑等操作,我把数据结构分为操作(Action)和
数据(Data),比如用鼠标在屏幕上画一个多边形,那么“画多边形”就是Action,多边形的
坐标点就是Data。我曾经把思路重点放在Action的描述和记录上,很复杂很烦的,特别是
Action又和数据绑定得很紧,很难搞定,总觉得不合理。后来就抛弃了了Action的描述,只
对Data的状态入栈,这样层次和模块化清晰,每一次Undo只是Data的状态回退,再更新View
而已,系统也稳定。我现在还没有实现完全功能,但我已经按这个思路建立了完成的Undo的
基础代码。
有时很复杂的东西,用最简单的办法实现最可靠。
 
我的想法是这样的:
OPObj是个基类,所有可以操作的对象必须继承。OPObj提供overridable的takeImage方法
这样可以在每次改变后对每个OPObj照相。例如有文本操作,有图形操作,图像操作,那
么可以分别作出三个class,这些class继承并改写takeImage方法,另外他们分别与各自
的实际界面相关联(如:Memo,Picture等)
OPUnit是一个OPObj集合,她也继承OPObj,以后所有操作都以他为单位,而她则负责收
集所有element的像。例如在Edit中输入字符,然后render到画布上某个位置,这时候就
必须把这两个OPObj看成一个单元来操作。
Image是个接口,应该对应每个OPObj实现。实例化后将于一个OPObj相对应,使用这个像
可以把对应的OPObj恢复到照相时候的样子。Image会被保存在History中,所以可以要求
Image是Serializable的。
History保存所有的Image,维护一定的结构。不仅如此,History还必须至少提供一个方
法使得Image可以通过History链时间后退(backward in time)找到所有像(前像)。
另外可以考虑利用外部定义好的persitent接口把History的数据导出存入外存,这时候
History可以使用每个Image的Serialization功能保存这些数据。但是又一个问题就是
从外存力读出History里的Image必须重新关联到一个OPObj上。
只是想肯定有很多问题
 
我以为如果一个编辑器当前编辑的对象如果可以序列化的话,
undo功能实现并不是很难。换个角度讲,保存编辑结果的功能
就可以作为undo的核心,需要考虑的是如何为临时文件命名。
不过这种考虑对于分支undo恐怕不易实现。
 
精彩的争论,不错!
 
To 小猪
需要支持序列化的并不是当前所编辑的对象,而应该是一个封装了编辑状态的对象,
而且对一些简单的Undo功能来说,序列化并不是必要条件。
你以为呢?
 
后退
顶部