Articles   Members Online:
-Article/Tip Search
-News Group Search over 21 Million news group articles.
-Delphi/Pascal
-CBuilder/C++
-C#Builder/C#
-JBuilder/Java
-Kylix
Member Area
-Home
-Account Center
-Top 10 NEW!!
-Submit Article/Tip
-Forums Upgraded!!
-My Articles
-Edit Information
-Login/Logout
-Become a Member
-Why sign up!
-Newsletter
-Chat Online!
-Indexes NEW!!
Employment
-Build your resume
-Find a job
-Post a job
-Resume Search
Contacts
-Contacts
-Feedbacks
-Link to us
-Privacy/Disclaimer
Embarcadero
Visit Embarcadero
Embarcadero Community
JEDI
Links
How to use Undo Redo using Commands Turn on/off line numbers in source code. Switch to Orginial background IDE or DSP color Comment or reply to this aritlce/tip for discussion. Bookmark this article to my favorite article(s). Print this article
09-Nov-03
Category
Algorithm
Language
Delphi 5.x
Views
168
User Rating
No Votes
# Votes
0
Replies
0
Publisher:
DSP, Administrator
Reference URL:
DKB
			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.


			
Vote: How useful do you find this Article/Tip?
Bad Excellent
1 2 3 4 5 6 7 8 9 10

 

Advertisement
Share this page
Advertisement
Download from Google

Copyright © Mendozi Enterprises LLC