2004. január 4., vasárnap
Undo - Redo using State (update 2)
Problem/Question/Abstract:
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.
{
Author William Egge, egge@eggcentric.com
http://www.eggcentric.com
Download this working example at http://www.eggcentric.com/UndoRedoState.htm
This is a demo of using TUndoRedoState.
Created June 13, 2001
Enjoy!
}
unit Frm_UndoRedoExample;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, Buttons, ExtCtrls, UndoRedoState, _State;
type
// Make this form implement the IState interface to be used
// by the UndoRedoState object.
TForm_UndoRedoExample = class(TForm, IState)
FDrawSurface: TImage;
FRedoBtn: TSpeedButton;
FUndoBtn: TSpeedButton;
FDirections: TLabel;
procedure Ev_FormCreate(Sender: TObject);
procedure Ev_FUndoBtnClick(Sender: TObject);
procedure Ev_FRedoBtnClick(Sender: TObject);
procedure Ev_FDrawSurfaceMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure Ev_FDrawSurfaceMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
procedure Ev_FDrawSurfaceMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure Ev_FormDestroy(Sender: TObject);
private
{ Private declarations }
FUndoRedo: TUndoRedoState;
FMouseDown: Boolean;
public
{ Public declarations }
// Methods that implement the IState interface
procedure GetState(S: TStream);
procedure SetState(S: TStream);
end;
var
Form_UndoRedoExample: TForm_UndoRedoExample;
implementation
{$R *.DFM}
procedure TForm_UndoRedoExample.GetState(S: TStream);
begin
FDrawSurface.Picture.Bitmap.SaveToStream(S);
end;
procedure TForm_UndoRedoExample.SetState(S: TStream);
begin
FDrawSurface.Picture.Bitmap.LoadFromStream(S);
end;
procedure TForm_UndoRedoExample.Ev_FormCreate(Sender: TObject);
begin
// Create a bitmap to draw on
with FDrawSurface.Picture.Bitmap do
begin
Width := FDrawSurface.Width;
Height := FDrawSurface.Height;
end;
// Create the UndoRedo object, this form implements the state interface
FUndoRedo := TUndoRedoState.Create(Self);
end;
procedure TForm_UndoRedoExample.Ev_FUndoBtnClick(Sender: TObject);
begin
FUndoRedo.Undo;
end;
procedure TForm_UndoRedoExample.Ev_FRedoBtnClick(Sender: TObject);
begin
FUndoRedo.Redo;
end;
procedure TForm_UndoRedoExample.Ev_FDrawSurfaceMouseDown(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
// It is possible to get 2 mouse down events with no mouse up event, but rarely
// Get out when this happens and let mouse up reset it to false.
if FMouseDown then
Exit;
FMouseDown := True;
FUndoRedo.BeginModify;
// Set our start point where you first click
FDrawSurface.Canvas.MoveTo(X, Y);
end;
procedure TForm_UndoRedoExample.Ev_FDrawSurfaceMouseMove(Sender: TObject;
Shift: TShiftState; X, Y: Integer);
begin
// Draw
if FMouseDown then
FDrawSurface.Canvas.LineTo(X, Y);
end;
procedure TForm_UndoRedoExample.Ev_FDrawSurfaceMouseUp(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
// Finished Editing
if FMouseDown then
begin
FUndoRedo.EndModify;
FMouseDown := False;
end;
end;
procedure TForm_UndoRedoExample.Ev_FormDestroy(Sender: TObject);
begin
FUndoRedo.Free;
end;
end.
Full Source of UndoRedoState.pas and _State.pas:
2 units:
unit _State;
interface
uses
Classes;
type
IState = interface
procedure GetState(S: TStream);
procedure SetState(S: TStream);
end;
implementation
end.
[ver 2, update: fixed problem where setting state the stream needed to be set back to position 0 before calling setState]
unit UndoRedoState;
{
Author William Egge
egge@eggcentric.com
http://www.eggcentric.com
}
interface
uses
_State, Classes, SysUtils;
// A value of 0 for MaxMemoryUsage means unlimited (default).
type
TUndoRedoState = class
private
FState: IState;
FUndoRedoList: TList;
FModifyCount: Integer;
FUndoPos: Integer;
FTailState: TStream;
FMaxMemoryUsage: LongWord;
FCurrMemUsage: LongWord;
function CreateCurrentState: TStream;
procedure SetMaxMemoryUsage(const Value: LongWord);
procedure TruncToMem;
public
constructor Create(AState: IState);
property MaxMemoryUsage: LongWord read FMaxMemoryUsage write SetMaxMemoryUsage;
procedure BeginModify;
procedure EndModify;
procedure Undo;
procedure Redo;
destructor Destroy; override;
end;
implementation
{ TUndoRedoState }
procedure TUndoRedoState.BeginModify;
var
I: Integer;
S: TStream;
begin
Inc(FModifyCount);
if FModifyCount = 1 then
begin
for I := FUndoRedoList.Count - 1 downto FUndoPos + 1 do
begin
S := FUndoRedoList[I];
Dec(FCurrMemUsage, S.Size);
FUndoRedoList.Delete(I);
S.Free;
end;
S := CreateCurrentState;
Inc(FCurrMemUsage, S.Size);
FUndoRedoList.Add(S);
FUndoPos := FUndoRedoList.Count - 1;
if FTailState <> nil then
begin
Dec(FCurrMemUsage, FTailState.Size);
FreeAndNil(FTailState);
end;
TruncToMem;
end;
end;
constructor TUndoRedoState.Create(AState: IState);
begin
Assert(AState <> nil, 'AState should not be nil for '
+ '"TUndoRedoState.Create(AState: IState)"');
inherited Create;
FState := AState;
FUndoRedoList := TList.Create;
FUndoPos := -1;
end;
function TUndoRedoState.CreateCurrentState: TStream;
begin
Result := TMemoryStream.Create;
try
FState.GetState(Result);
except
Result.Free;
raise;
end;
end;
destructor TUndoRedoState.Destroy;
var
I: Integer;
begin
FState := nil;
for I := 0 to FUndoRedoList.Count - 1 do
TObject(FUndoRedoList[I]).Free;
FTailState.Free;
inherited Destroy;
end;
procedure TUndoRedoState.EndModify;
begin
Assert(FModifyCount > 0, 'TUndoRedoState.EndModify: EndModify was called '
+ 'more times than BeginModify');
Dec(FModifyCount);
end;
procedure TUndoRedoState.Redo;
var
FRedoPos: Integer;
S: TStream;
begin
Assert(FModifyCount = 0, 'TUndoRedoState.Redo: should not be called while '
+ 'modifying');
if (FUndoRedoList.Count > 0) and (FUndoPos < (FUndoRedoList.Count - 1)) then
begin
FRedoPos := FUndoPos + 2;
if FRedoPos > (FUndoRedoList.Count - 1) then
begin
FTailState.Position := 0;
FState.SetState(FTailState);
Dec(FCurrMemUsage, FTailState.Size);
FreeAndNil(FTailState);
end
else
begin
S := FUndoRedoList[FRedoPos];
S.Position := 0;
FState.SetState(S);
end;
Inc(FUndoPos);
end;
end;
procedure TUndoRedoState.SetMaxMemoryUsage(const Value: LongWord);
begin
FMaxMemoryUsage := Value;
end;
procedure TUndoRedoState.TruncToMem;
var
S: TStream;
begin
if (FMaxMemoryUsage > 0) and (FCurrMemUsage > FMaxMemoryUsage) then
begin
while (FUndoRedoList.Count > 0) and (FCurrMemUsage > FMaxMemoryUsage) do
begin
S := FUndoRedoList[0];
FUndoRedoList.Delete(0);
Dec(FCurrMemUsage, S.Size);
Dec(FUndoPos);
S.Free;
end;
if (FUndoRedoList.Count = 0) and (FCurrMemUsage > FMaxMemoryUsage) then
if FTailState <> nil then
begin
Dec(FCurrMemUsage, FTailState.Size);
FreeAndNil(FTailState);
end;
end;
end;
procedure TUndoRedoState.Undo;
var
S: TStream;
begin
Assert(FModifyCount = 0, 'TUndoRedoState.Undo: should not be called while '
+ 'modifying');
if FUndoPos >= 0 then
begin
if FUndoPos = (FUndoRedoList.Count - 1) then
begin
FTailState := CreateCurrentState;
Inc(FCurrMemUsage, FTailState.Size);
end;
S := FUndoRedoList[FUndoPos];
S.Position := 0;
Dec(FUndoPos);
FState.SetState(S);
TruncToMem;
end;
end;
end.
Component Download: http://www.eggcentric.com/UndoRedoState.zip
Feliratkozás:
Megjegyzések küldése (Atom)
Nincsenek megjegyzések:
Megjegyzés küldése