• No results found

Undoing Commands

In document Leanpub.more.Coding.in.Delphi.2015 (Page 94-98)

The Command Pattern also makes providing undo functionality possible. To do so, your command should know how to undo itself, and your application should maintain a stack of commands that have been executed, allowing you to call that undo functionality in reverse order.

To make this happen, our command interface adds another method called – not surprisingly –Undo:

type

IPointCommand = interface

['{5D792581-9D05-4A52-BE44-4EB0CB0D3B3B}']

procedure Execute;

procedure Undo;

end;

Now, the interface will be able to represent a class that can Execute its command, and then Undo that command when asked to do so.

In order to demonstrate this, I’m going to use a VCL application that draws and “un-draws” little red dots.

Here are the steps to get going:

1. Create a new VCL application.

2. Drop aTPanel, clear itsCaptionproperty, and then set itsAlignproperty toalTop. 3. Drop aTPaintBoxon the main area of the form and set itsAlignproperty toalClient. 4. Place twoTButtonson theTPanel, giving one theCaptionof “Undo”, and the other “Clear”.

5. Place aTLabelon the Panel.

6. Align them all up and make them look pretty. You should get something that looks like this:

Next, create a separate unit and name ituPointCommand.pas. First, we’ll add the aboveIPointCommand

interface to this unit. Then, we’ll add an implementation of IPointCommand:

type

IPointCommand = interface

['{5D792581-9D05-4A52-BE44-4EB0CB0D3B3B}']

procedure Execute;

procedure Undo;

end;

TPointPlacerCommand = class(TInterfacedObject, IPointCommand) private

FCanvas: TCanvas;

FPoint: TPoint;

public

constructor Create(aCanvas: TCanvas; aPoint: TPoint);

procedure Execute;

procedure Undo;

end;

const

CircleDiameter = 15;

constructor TPointPlacerCommand.Create(aCanvas: TCanvas; aPoint: TPoint);

begin

inherited Create;

FCanvas := aCanvas;

FPoint := aPoint;

end;

procedure TPointPlacerCommand.Execute;

var

FOriginalColor: TColor;

begin

FOriginalColor := FCanvas.Brush.Color;

try

FCanvas.Brush.Color := clRed;

FCanvas.Pen.Color := clRed;

FCanvas.Ellipse(FPoint.X, FPoint.Y, FPoint.X + CircleDiameter, FPoint.Y + CircleDiameter);

finally

FCanvas.Brush.Color := FOriginalColor;

FCanvas.Pen.Color := FOriginalColor;

end;

end;

procedure TPointPlacerCommand.Undo;

begin

FCanvas.Ellipse(FPoint.X, FPoint.Y, FPoint.X + CircleDiameter, FPoint.Y + CircleDiameter);

end;

Notice that the command does all the work of painting on the canvas that is passed to it. The constructor takes aTCanvasand aTPoint. Those are stored away so that the command can remember them. In theExecute

command, it draws a small red dot on theCanvas. TheUndocommand simply paints over the same spot with the original color of the canvas.

Now, let’s go back to the main form. First, we’ll need a way to collect and manage all the dots we plan on placing on theTPaintBoxvia a mouse click. We’ll also want to be able to remember them in reverse order, so that we can undo them in the reverse order that they were laid down. And what else but a stack will do the job?

We’ll dig into our bag of tricks, otherwise known as Spring for Delphi, and use itsIStackimplementation.

Thus, we’ll declare this variable asprivate:

FDots: IStack<IPointCommand>;

and we’ll initialize it in theOnCreateevent of the form:

procedure TUndoExampleForm.FormCreate(Sender: TObject);

begin

FDots := TCollections.CreateStack<IPointCommand>;

end;

Next up we’ll create anIPointCommandevery time theOnMouseUpevent occurs:

procedure TUndoExampleForm.PaintBox1MouseUp(Sender: TObject; Button:

TMouseButton; Shift:

TShiftState; X, Y: Integer);

var

LCommand: IPointCommand;

begin

LCommand := TPointPlacerCommand.Create(PaintBox1.Canvas, TPoint.Create(X, Y));

LCommand.Execute;

FDots.Push(LCommand);

UpdateLabel;

end;

Here, we create a new command, call theExecute method that paints a dot on theTPaintBox, and then push it onto the stack for later use during theUndoprocess.UpdateLabelis a simple method that updates theTLabelthat outputs the total number of dots on the screen.

procedure TUndoExampleForm.UpdateInfo;

begin

Label1.Caption := 'Number of Dots: ' + FDots.Count.ToString();

end;

Now for the “why we are here” part: the actual undoing of the dots. Remember that the commands are saved in the stack, and that they remember where they are located. So undoing things is pretty easy. We provide an

OnClickevent handler for the Undo button:

procedure TUndoExampleForm.btnUndoClick(Sender: TObject);

begin

if FDots.Count > 0 then begin

FDots.Pop.Undo;

end;

UpdateLabel;

end;

First we check to see if there are any dots, and if so, we justPopthe top one – that is, the last dot we put down – off the stack. We then call theUndomethod for the command so that the last dot we drew is covered up by the original color of the canvas.

The Clear button does the same thing for all of the dots:

procedure TUndoExampleForm.btnClearClick(Sender: TObject);

begin

if FDots.Count > 0 then begin

One final thing – any good Window needs to be able to paint itself on demand, and so we provide anOnPaint

handler for the paintbox which simply calls theExecutemethod of each Dot.

procedure TUndoExampleForm.PaintBox1Paint(Sender: TObject);

var

LDot: IPointCommand;

begin

for LDot in FDots do begin

LDot.Execute;

end;

end;

And that is about it. Run the application, and you should see a red dot wherever you click the mouse on the paintbox. Hit the “Undo” button, and they will be “undone” in reverse order. Hitting the “Clear” button will make them all go away. If you want to see the undoing at work, click a whole bunch on the same basic spot, and then undo them. You should see portions of the dots that are painted over when the dots on top of them are removed.

The form really only knows about IPointCommand – we would probably use a Factory or a Dependency Injection container to actually create an instance of TPointPlacerCommandfor us so the form could be ignorant of any implementation. TheIPointCommandinterface gives us theExecutemethod, which paints a dot, and theUndomethod that knows how to un-paint a dot. Thus, we basically have separated the calling of commands from the execution of the actual code while providing undo capabilities. Pretty slick. Again – separation of concerns makes for clean, maintainable code.

In document Leanpub.more.Coding.in.Delphi.2015 (Page 94-98)