2007. november 25., vasárnap
Undo Redo using Commands
Problem/Question/Abstract:
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.
unit UndoRedoCommand;
interface
uses
Classes, SysUtils;
type
IUndoRedoCommand = interface(IUnknown)
['{D84BFD00-8396-11D6-B4FA-000021D960D4}']
procedure Execute;
procedure Redo;
procedure Undo;
end;
IUndoRedoCommandGroup = interface(IUndoRedoCommand)
['{9169AE00-839B-11D6-B4FA-000021D960D4}']
function GetUndoRedoCommands: TInterfaceList;
property UndoRedoCommands: TInterfaceList read GetUndoRedoCommands;
end;
TUndoRedoCommandGroup = class(TInterfacedObject, IUndoRedoCommandGroup,
IUndoRedoCommand)
private
FList: TInterfaceList;
FCanRedo: Boolean;
public
constructor Create;
destructor Destroy; override;
procedure Execute;
function GetUndoRedoCommands: TInterfaceList;
procedure Redo;
procedure Undo;
property UndoRedoCommands: TInterfaceList read GetUndoRedoCommands;
end;
TUndoRedoManager = class(TObject)
private
FRedoList: TInterfaceList;
FUndoList: TInterfaceList;
FTransactLevel: Integer;
FTransaction: IUndoRedoCommandGroup;
function GetCanRedo: Integer;
function GetCanUndo: Integer;
public
constructor Create;
destructor Destroy; override;
procedure BeginTransaction;
procedure EndTransaction;
procedure ExecCommand(const AUndoRedoCommand: IUndoRedoCommand);
procedure Redo(RedoCount: Integer = 1);
procedure Undo(UndoCount: Integer = 1);
property CanRedo: Integer read GetCanRedo;
property CanUndo: Integer read GetCanUndo;
end;
implementation
{
**************************** TUndoRedoCommandGroup *****************************
}
constructor TUndoRedoCommandGroup.Create;
begin
inherited Create;
FList := TInterfaceList.Create;
end;
destructor TUndoRedoCommandGroup.Destroy;
begin
FList.Free;
inherited Destroy;
end;
procedure TUndoRedoCommandGroup.Execute;
var
I: Integer;
begin
for I := 0 to FList.Count - 1 do
(FList[I] as IUndoRedoCommand).Execute;
end;
function TUndoRedoCommandGroup.GetUndoRedoCommands: TInterfaceList;
begin
Result := FList;
end;
procedure TUndoRedoCommandGroup.Redo;
var
I: Integer;
begin
if FCanRedo then
begin
for I := 0 to FList.Count - 1 do
(FList[I] as IUndoRedoCommand).Redo;
FCanRedo := False;
end
else
raise
Exception.Create('Must call TUndoRedoCommandGroup.Undo before calling Redo.');
end;
procedure TUndoRedoCommandGroup.Undo;
var
I: Integer;
begin
if FCanRedo then
raise Exception.Create('TUndoRedoCommandGroup.Undo already called');
for I := FList.Count - 1 downto 0 do
(FList[I] as IUndoRedoCommand).Undo;
FCanRedo := True;
end;
{
******************************* TUndoRedoManager *******************************
}
constructor TUndoRedoManager.Create;
begin
inherited Create;
FRedoList := TInterfaceList.Create;
FUndoList := TInterfaceList.Create;
end;
destructor TUndoRedoManager.Destroy;
begin
FRedoList.Free;
FUndoList.Free;
inherited Destroy;
end;
procedure TUndoRedoManager.BeginTransaction;
begin
Inc(FTransactLevel);
if FTransactLevel = 1 then
FTransaction := TUndoRedoCommandGroup.Create;
end;
procedure TUndoRedoManager.EndTransaction;
begin
Dec(FTransactLevel);
if (FTransactLevel = 0) then
begin
if FTransaction.UndoRedoCommands.Count > 0 then
begin
FRedoList.Clear;
FUndoList.Add(FTransaction);
end;
FTransaction := nil;
end
else if FTransactLevel < 0 then
raise
Exception.Create('Unmatched TUndoRedoManager.BeginTransaction and EndTransaction');
end;
procedure TUndoRedoManager.ExecCommand(const AUndoRedoCommand:
IUndoRedoCommand);
begin
BeginTransaction;
try
FTransaction.UndoRedoCommands.Add(AUndoRedoCommand);
AUndoRedoCommand.Execute;
finally
EndTransaction;
end;
end;
function TUndoRedoManager.GetCanRedo: Integer;
begin
Result := FRedoList.Count;
end;
function TUndoRedoManager.GetCanUndo: Integer;
begin
Result := FUndoList.Count;
end;
procedure TUndoRedoManager.Redo(RedoCount: Integer = 1);
var
I: Integer;
Item: IUndoRedoCommand;
RedoLast: Integer;
begin
if FTransactLevel <> 0 then
raise Exception.Create('Cannot Redo while in Transaction');
// Index of last redo item
RedoLast := FRedoList.Count - RedoCount;
if RedoLast < 0 then
RedoLast := 0;
for I := FRedoList.Count - 1 downto RedoLast do
begin
Item := FRedoList[I] as IUndoRedoCommand;
FRedoList.Delete(I);
FUndoList.Add(Item);
Item.Redo;
end;
end;
procedure TUndoRedoManager.Undo(UndoCount: Integer = 1);
var
I: Integer;
Item: IUndoRedoCommand;
UndoLast: Integer;
begin
if FTransactLevel <> 0 then
raise Exception.Create('Cannot undo while in Transaction');
// Index of last undo item
UndoLast := FUndoList.Count - UndoCount;
if UndoLast < 0 then
UndoLast := 0;
for I := FUndoList.Count - 1 downto UndoLast do
begin
Item := FUndoList[I] as IUndoRedoCommand;
FUndoList.Delete(I);
FRedoList.Add(Item);
Item.Undo;
end;
end;
end.
Feliratkozás:
Megjegyzések küldése (Atom)
Nincsenek megjegyzések:
Megjegyzés küldése