Author: William Egge
Do you need to implement undo and redo in your application? Here is a simple
method, with source, that does the job for small data (up to 20 or 100K in memory)
Do you need to implement undo and redo in your application? Here is a simple
method, with source, that does the job for small data (up to 20 or 100K in memory)
Answer:
There are 2 methods of Undo-Redo that I know of. The first is saving the current
state of the system into a list before it is modified. There would be a GetState
and SetState method of your editor. The second method is to store commands, where
each command can undo and redo itself.
Saving state is a good choice when your editor data is small such as 10 to 20K and
your editor has many capabilities. Saving state is a simple solution. If you are
doing image editing then you could get by with using a file to store your undo and
redo information. A vector graphics editor would be a good choice here because
vectors do not need much storage space.
The more complex solution of storing commands requires much more coding but is
nessesary when your editor edits large amounts of data and storing its state would
be too time consuming. A word processor is an example.
I have coded an Undo-Redo State class.. here is how it works. There is the main
class that holds the state snapshots (TUndoRedoState), then there is the interface
"IState" that has 2 methods, GetState and SetState. I implemented this by making my
editor form implement the IState interface.
The main class is created and passed the IState interface. Calling Undo and Redo
makes calls to GetState and SetState. If you do not like the way I use an interface
then you can easily change the class to accept method pointers to some GetState and
SetState method, but I prefer the Interface.
1 {
2 Author William Egge, egge@eggcentric.com
3 http://www.eggcentric.com
4
5 Download this working example at http://www.eggcentric.com/UndoRedoState.htm
6
7 This is a demo of using TUndoRedoState.
8 Created June 13, 2001
9
10 Enjoy!
11 }
12 unit Frm_UndoRedoExample;
13
14 interface
15
16 uses
17 Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
18 StdCtrls, Buttons, ExtCtrls, UndoRedoState, _State;
19
20 type
21 // Make this form implement the IState interface to be used
22 // by the UndoRedoState object.
23 TForm_UndoRedoExample = class(TForm, IState)
24 FDrawSurface: TImage;
25 FRedoBtn: TSpeedButton;
26 FUndoBtn: TSpeedButton;
27 FDirections: TLabel;
28 procedure Ev_FormCreate(Sender: TObject);
29 procedure Ev_FUndoBtnClick(Sender: TObject);
30 procedure Ev_FRedoBtnClick(Sender: TObject);
31 procedure Ev_FDrawSurfaceMouseDown(Sender: TObject; Button: TMouseButton;
32 Shift: TShiftState; X, Y: Integer);
33 procedure Ev_FDrawSurfaceMouseMove(Sender: TObject; Shift: TShiftState; X,
34 Y: Integer);
35 procedure Ev_FDrawSurfaceMouseUp(Sender: TObject; Button: TMouseButton;
36 Shift: TShiftState; X, Y: Integer);
37 procedure Ev_FormDestroy(Sender: TObject);
38 private
39 { Private declarations }
40 FUndoRedo: TUndoRedoState;
41 FMouseDown: Boolean;
42 public
43 { Public declarations }
44 // Methods that implement the IState interface
45 procedure GetState(S: TStream);
46 procedure SetState(S: TStream);
47 end;
48
49 var
50 Form_UndoRedoExample: TForm_UndoRedoExample;
51
52 implementation
53
54 {$R *.DFM}
55
56 procedure TForm_UndoRedoExample.GetState(S: TStream);
57 begin
58 FDrawSurface.Picture.Bitmap.SaveToStream(S);
59 end;
60
61 procedure TForm_UndoRedoExample.SetState(S: TStream);
62 begin
63 FDrawSurface.Picture.Bitmap.LoadFromStream(S);
64 end;
65
66 procedure TForm_UndoRedoExample.Ev_FormCreate(Sender: TObject);
67 begin
68 // Create a bitmap to draw on
69 with FDrawSurface.Picture.Bitmap do
70 begin
71 Width := FDrawSurface.Width;
72 Height := FDrawSurface.Height;
73 end;
74
75 // Create the UndoRedo object, this form implements the state interface
76 FUndoRedo := TUndoRedoState.Create(Self);
77 end;
78
79 procedure TForm_UndoRedoExample.Ev_FUndoBtnClick(Sender: TObject);
80 begin
81 FUndoRedo.Undo;
82 end;
83
84 procedure TForm_UndoRedoExample.Ev_FRedoBtnClick(Sender: TObject);
85 begin
86 FUndoRedo.Redo;
87 end;
88
89 procedure TForm_UndoRedoExample.Ev_FDrawSurfaceMouseDown(Sender: TObject;
90 Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
91 begin
92 // It is possible to get 2 mouse down events with no mouse up event, but rarely
93 // Get out when this happens and let mouse up reset it to false.
94 if FMouseDown then
95 Exit;
96
97 FMouseDown := True;
98 FUndoRedo.BeginModify;
99
100 // Set our start point where you first click
101 FDrawSurface.Canvas.MoveTo(X, Y);
102 end;
103
104 procedure TForm_UndoRedoExample.Ev_FDrawSurfaceMouseMove(Sender: TObject;
105 Shift: TShiftState; X, Y: Integer);
106 begin
107 // Draw
108 if FMouseDown then
109 FDrawSurface.Canvas.LineTo(X, Y);
110 end;
111
112 procedure TForm_UndoRedoExample.Ev_FDrawSurfaceMouseUp(Sender: TObject;
113 Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
114 begin
115 // Finished Editing
116 if FMouseDown then
117 begin
118 FUndoRedo.EndModify;
119 FMouseDown := False;
120 end;
121 end;
122
123 procedure TForm_UndoRedoExample.Ev_FormDestroy(Sender: TObject);
124 begin
125 FUndoRedo.Free;
126 end;
127
128 end.
Full Source of UndoRedoState.pas and _State.pas:
2 units:
129 unit _State;
130
131 interface
132 uses
133 Classes;
134
135 type
136 IState = interface
137 procedure GetState(S: TStream);
138 procedure SetState(S: TStream);
139 end;
140
141 implementation
142
143 end.
144
145 [ver 2, update: fixed problem where setting state the stream needed to be set back
146 to position 0 before calling setState]
147
148 unit UndoRedoState;
149 {
150 Author William Egge
151 egge@eggcentric.com
152 http://www.eggcentric.com
153 }
154
155 interface
156 uses
157 _State, Classes, SysUtils;
158
159 // A value of 0 for MaxMemoryUsage means unlimited (default).
160 type
161 TUndoRedoState = class
162 private
163 FState: IState;
164 FUndoRedoList: TList;
165 FModifyCount: Integer;
166 FUndoPos: Integer;
167 FTailState: TStream;
168 FMaxMemoryUsage: LongWord;
169 FCurrMemUsage: LongWord;
170 function CreateCurrentState: TStream;
171 procedure SetMaxMemoryUsage(const Value: LongWord);
172 procedure TruncToMem;
173 public
174 constructor Create(AState: IState);
175 property MaxMemoryUsage: LongWord read FMaxMemoryUsage write SetMaxMemoryUsage;
176 procedure BeginModify;
177 procedure EndModify;
178 procedure Undo;
179 procedure Redo;
180 destructor Destroy; override;
181 end;
182
183 implementation
184
185 { TUndoRedoState }
186
187 procedure TUndoRedoState.BeginModify;
188 var
189 I: Integer;
190 S: TStream;
191 begin
192 Inc(FModifyCount);
193 if FModifyCount = 1 then
194 begin
195 for I := FUndoRedoList.Count - 1 downto FUndoPos + 1 do
196 begin
197 S := FUndoRedoList[I];
198 Dec(FCurrMemUsage, S.Size);
199 FUndoRedoList.Delete(I);
200 S.Free;
201 end;
202 S := CreateCurrentState;
203 Inc(FCurrMemUsage, S.Size);
204 FUndoRedoList.Add(S);
205 FUndoPos := FUndoRedoList.Count - 1;
206 if FTailState <> nil then
207 begin
208 Dec(FCurrMemUsage, FTailState.Size);
209 FreeAndNil(FTailState);
210 end;
211 TruncToMem;
212 end;
213 end;
214
215 constructor TUndoRedoState.Create(AState: IState);
216 begin
217 Assert(AState <> nil, 'AState should not be nil for '
218 + '"TUndoRedoState.Create(AState: IState)"');
219
220 inherited Create;
221 FState := AState;
222 FUndoRedoList := TList.Create;
223 FUndoPos := -1;
224 end;
225
226 function TUndoRedoState.CreateCurrentState: TStream;
227 begin
228 Result := TMemoryStream.Create;
229 try
230 FState.GetState(Result);
231 except
232 Result.Free;
233 raise;
234 end;
235 end;
236
237 destructor TUndoRedoState.Destroy;
238 var
239 I: Integer;
240 begin
241 FState := nil;
242 for I := 0 to FUndoRedoList.Count - 1 do
243 TObject(FUndoRedoList[I]).Free;
244
245 FTailState.Free;
246
247 inherited Destroy;
248 end;
249
250 procedure TUndoRedoState.EndModify;
251 begin
252 Assert(FModifyCount > 0, 'TUndoRedoState.EndModify: EndModify was called '
253 + 'more times than BeginModify');
254
255 Dec(FModifyCount);
256 end;
257
258 procedure TUndoRedoState.Redo;
259 var
260 FRedoPos: Integer;
261 S: TStream;
262 begin
263 Assert(FModifyCount = 0, 'TUndoRedoState.Redo: should not be called while '
264 + 'modifying');
265
266 if (FUndoRedoList.Count > 0) and (FUndoPos < (FUndoRedoList.Count - 1)) then
267 begin
268 FRedoPos := FUndoPos + 2;
269 if FRedoPos > (FUndoRedoList.Count - 1) then
270 begin
271 FTailState.Position := 0;
272 FState.SetState(FTailState);
273 Dec(FCurrMemUsage, FTailState.Size);
274 FreeAndNil(FTailState);
275 end
276 else
277 begin
278 S := FUndoRedoList[FRedoPos];
279 S.Position := 0;
280 FState.SetState(S);
281 end;
282 Inc(FUndoPos);
283 end;
284 end;
285
286 procedure TUndoRedoState.SetMaxMemoryUsage(const Value: LongWord);
287 begin
288 FMaxMemoryUsage := Value;
289 end;
290
291 procedure TUndoRedoState.TruncToMem;
292 var
293 S: TStream;
294 begin
295 if (FMaxMemoryUsage > 0) and (FCurrMemUsage > FMaxMemoryUsage) then
296 begin
297 while (FUndoRedoList.Count > 0) and (FCurrMemUsage > FMaxMemoryUsage) do
298 begin
299 S := FUndoRedoList[0];
300 FUndoRedoList.Delete(0);
301 Dec(FCurrMemUsage, S.Size);
302 Dec(FUndoPos);
303 S.Free;
304 end;
305
306 if (FUndoRedoList.Count = 0) and (FCurrMemUsage > FMaxMemoryUsage) then
307 if FTailState <> nil then
308 begin
309 Dec(FCurrMemUsage, FTailState.Size);
310 FreeAndNil(FTailState);
311 end;
312 end;
313 end;
314
315 procedure TUndoRedoState.Undo;
316 var
317 S: TStream;
318 begin
319 Assert(FModifyCount = 0, 'TUndoRedoState.Undo: should not be called while '
320 + 'modifying');
321
322 if FUndoPos >= 0 then
323 begin
324 if FUndoPos = (FUndoRedoList.Count - 1) then
325 begin
326 FTailState := CreateCurrentState;
327 Inc(FCurrMemUsage, FTailState.Size);
328 end;
329 S := FUndoRedoList[FUndoPos];
330 S.Position := 0;
331 Dec(FUndoPos);
332 FState.SetState(S);
333 TruncToMem;
334 end;
335 end;
336
337 end.
Component Download: http://www.eggcentric.com/UndoRedoState.zip
|