Author: William Egge
There are 2 ways to do undo - redo, one is with state, the other is using commands.
This artical explains using commands and provides full source code implementation
of a TUndoRedoManager
Answer:
This article will cover
Command
Requirements of a command
Command Stack
Undo redo manager
Command grouping
Full source code implementation
A command is simply an object that implements an action in the system, for example
in a paint program a command may be a line command, or a circle command, or a
rectangle command, and so on. In order to implement command based undo redo you
must design your editing to use command objects.
Because we want to undo and redo the effects of commands, the commands themselves
must be able to undo and redo their own action as well as execute the initial
action.
The primary methods of a command is
Execute
Undo
Redo
You may wonder why there is a seprate Redo instead of simply reusing the Execute
method. This is because the redo implementation may be different than the Execute.
For example, if this were a paint command. The Execute may choose the brush and
follow some algorithm to draw some sort of gradual transparent circle. The redo
could simply copy a image of the results of the paint rather than painting again.
In any case, if this functionality is not needed then simply call the Execute
method from within your Redo method.
Ok, so now we have one command. We need to remember the sequence of commands so we
can have multilevel undo and redo. This is the command stack.
When you undo, you take the last command and call its undo method. The next time
you undo, you call the undo method of the 2nd command from the top and so on.
When you redo, you call the redo method of the last command that you called undo
on. To simplify this we create 2 lists, an undo list and a redo list and
encapsulate these with an undo manager.
For the undoredo manager, we give it 3 methods.
ExecuteCommand(Command)
Undo
Redo
Internally the UndoRedoManager will maintain 2 lists of commands, Undo and Redo
Here is the full sequence:
Execute a command by passing it to the ExecuteCommand method, internally the
UndoRedoManager will call the Execute method of the command and then add the
command to the top of the Undo list.
Calling undo, the manager will take the last command in the undo list, call its
undo method and then remove the command from the undo list and add it to the redo
list.
Calling redo will do the reverse of undo, it will take the last command from the
redo list, call its redo method, then remove it from the redo list and add it to
the top of the undo list
Now, the next time ExecuteCommand is called, we must prune the redo list... delete
all commands in it.
Sometimes, or most of the time, you will execute a bunch of commands as a single
group. Calling undo and redo should undo and redo this entire group and not the
individual commands within it one at a time. An example might be some wizard that
did a lot of things, you would want to undo and redo this as one group.
I'll add 2 methods to the UndoRedoManager
BeginTransaction
EndTransaction
All commands executed between calls to BeginTransaction and EndTransaction will be
stored as one group. You should be allowed to make nested calls to BeginTransaction
and EndTransaction.
Using inheritence, this can be easy to implement. We make a command group class
that inherits from the Command, that way the manager acts as if it is working with
single commands.
Below is the Full source code of a working UndoRedoManager along with interfaces
for IUndoRedoCommand and IUndoRedoCommandGroup. Note: I think a lot of people
associate delphi interfaces with ActiveX or COM and then think that interfaces ARE
ActiveX or COM. This is not true, you can create classes that implement interfaces
and those classes do not have any implementation of ActiveX or COM. They do not
require registering and all the things that go with COM or ActiveX. You should
keep in mind that interfaces are reference counted, they are freed when there are
not more references.
1 unit UndoRedoCommand;
2
3 interface
4 uses
5 Classes, SysUtils;
6
7 type
8 IUndoRedoCommand = interface(IUnknown)
9 ['{D84BFD00-8396-11D6-B4FA-000021D960D4}']
10 procedure Execute;
11 procedure Redo;
12 procedure Undo;
13 end;
14
15 IUndoRedoCommandGroup = interface(IUndoRedoCommand)
16 ['{9169AE00-839B-11D6-B4FA-000021D960D4}']
17 function GetUndoRedoCommands: TInterfaceList;
18 property UndoRedoCommands: TInterfaceList read GetUndoRedoCommands;
19 end;
20
21 TUndoRedoCommandGroup = class(TInterfacedObject, IUndoRedoCommandGroup,
22 IUndoRedoCommand)
23 private
24 FList: TInterfaceList;
25 FCanRedo: Boolean;
26 public
27 constructor Create;
28 destructor Destroy; override;
29 procedure Execute;
30 function GetUndoRedoCommands: TInterfaceList;
31 procedure Redo;
32 procedure Undo;
33 property UndoRedoCommands: TInterfaceList read GetUndoRedoCommands;
34 end;
35
36 TUndoRedoManager = class(TObject)
37 private
38 FRedoList: TInterfaceList;
39 FUndoList: TInterfaceList;
40 FTransactLevel: Integer;
41 FTransaction: IUndoRedoCommandGroup;
42 function GetCanRedo: Integer;
43 function GetCanUndo: Integer;
44 public
45 constructor Create;
46 destructor Destroy; override;
47 procedure BeginTransaction;
48 procedure EndTransaction;
49 procedure ExecCommand(const AUndoRedoCommand: IUndoRedoCommand);
50 procedure Redo(RedoCount: Integer = 1);
51 procedure Undo(UndoCount: Integer = 1);
52 property CanRedo: Integer read GetCanRedo;
53 property CanUndo: Integer read GetCanUndo;
54 end;
55
56 implementation
57
58 {
59 **************************** TUndoRedoCommandGroup *****************************
60 }
61
62 constructor TUndoRedoCommandGroup.Create;
63 begin
64 inherited Create;
65 FList := TInterfaceList.Create;
66 end;
67
68 destructor TUndoRedoCommandGroup.Destroy;
69 begin
70 FList.Free;
71 inherited Destroy;
72 end;
73
74 procedure TUndoRedoCommandGroup.Execute;
75 var
76 I: Integer;
77 begin
78 for I := 0 to FList.Count - 1 do
79 (FList[I] as IUndoRedoCommand).Execute;
80 end;
81
82 function TUndoRedoCommandGroup.GetUndoRedoCommands: TInterfaceList;
83 begin
84 Result := FList;
85 end;
86
87 procedure TUndoRedoCommandGroup.Redo;
88 var
89 I: Integer;
90 begin
91 if FCanRedo then
92 begin
93 for I := 0 to FList.Count - 1 do
94 (FList[I] as IUndoRedoCommand).Redo;
95
96 FCanRedo := False;
97 end
98 else
99 raise
100 Exception.Create('Must call TUndoRedoCommandGroup.Undo before calling Redo.');
101 end;
102
103 procedure TUndoRedoCommandGroup.Undo;
104 var
105 I: Integer;
106 begin
107 if FCanRedo then
108 raise Exception.Create('TUndoRedoCommandGroup.Undo already called');
109
110 for I := FList.Count - 1 downto 0 do
111 (FList[I] as IUndoRedoCommand).Undo;
112
113 FCanRedo := True;
114 end;
115
116 {
117 ******************************* TUndoRedoManager *******************************
118 }
119
120 constructor TUndoRedoManager.Create;
121 begin
122 inherited Create;
123 FRedoList := TInterfaceList.Create;
124 FUndoList := TInterfaceList.Create;
125 end;
126
127 destructor TUndoRedoManager.Destroy;
128 begin
129 FRedoList.Free;
130 FUndoList.Free;
131 inherited Destroy;
132 end;
133
134 procedure TUndoRedoManager.BeginTransaction;
135 begin
136 Inc(FTransactLevel);
137 if FTransactLevel = 1 then
138 FTransaction := TUndoRedoCommandGroup.Create;
139 end;
140
141 procedure TUndoRedoManager.EndTransaction;
142 begin
143 Dec(FTransactLevel);
144 if (FTransactLevel = 0) then
145 begin
146 if FTransaction.UndoRedoCommands.Count > 0 then
147 begin
148 FRedoList.Clear;
149 FUndoList.Add(FTransaction);
150 end;
151 FTransaction := nil;
152 end
153 else if FTransactLevel < 0 then
154 raise
155 Exception.Create('Unmatched TUndoRedoManager.BeginTransaction and
156 EndTransaction'
157 end;
158
159 procedure TUndoRedoManager.ExecCommand(const AUndoRedoCommand:
160 IUndoRedoCommand);
161 begin
162 BeginTransaction;
163 try
164 FTransaction.UndoRedoCommands.Add(AUndoRedoCommand);
165 AUndoRedoCommand.Execute;
166 finally
167 EndTransaction;
168 end;
169 end;
170
171 function TUndoRedoManager.GetCanRedo: Integer;
172 begin
173 Result := FRedoList.Count;
174 end;
175
176 function TUndoRedoManager.GetCanUndo: Integer;
177 begin
178 Result := FUndoList.Count;
179 end;
180
181 procedure TUndoRedoManager.Redo(RedoCount: Integer = 1);
182 var
183 I: Integer;
184 Item: IUndoRedoCommand;
185 RedoLast: Integer;
186 begin
187 if FTransactLevel <> 0 then
188 raise Exception.Create('Cannot Redo while in Transaction');
189
190 // Index of last redo item
191 RedoLast := FRedoList.Count - RedoCount;
192 if RedoLast < 0 then
193 RedoLast := 0;
194
195 for I := FRedoList.Count - 1 downto RedoLast do
196 begin
197 Item := FRedoList[I] as IUndoRedoCommand;
198 FRedoList.Delete(I);
199 FUndoList.Add(Item);
200 Item.Redo;
201 end;
202 end;
203
204 procedure TUndoRedoManager.Undo(UndoCount: Integer = 1);
205 var
206 I: Integer;
207 Item: IUndoRedoCommand;
208 UndoLast: Integer;
209 begin
210 if FTransactLevel <> 0 then
211 raise Exception.Create('Cannot undo while in Transaction');
212
213 // Index of last undo item
214 UndoLast := FUndoList.Count - UndoCount;
215 if UndoLast < 0 then
216 UndoLast := 0;
217
218 for I := FUndoList.Count - 1 downto UndoLast do
219 begin
220 Item := FUndoList[I] as IUndoRedoCommand;
221 FUndoList.Delete(I);
222 FRedoList.Add(Item);
223 Item.Undo;
224 end;
225 end;
226
227 end.
|