MindStream. How we write software under FireMonkey

A month ago, we decided to write a cross-platform application using FireMonkey. As a direction chosen drawing graphic primitives, with the ability to save and restore data.

We agreed to describe the process of writing the application in detail on Habré.

The articles will demonstrate in practice the use of various techniques, such as: Dependency Injection, factory method, use of contexts, use of controllers, etc. In the near future, it is planned to screw Dunit tests there. DUnit is not currently available for FMX, so you have to come up with something yourself.

We will start with a working prototype which, at the end of the article, will take on this form:



First, teach the program to draw on Canvas'e. The first primitives that we add to the program will be a rectangle and a line.

To do this, place the TImage object on the form, and also add the creation of Bitmap:
procedure TfmMain.FormCreate(Sender: TObject);
begin
 imgMain.Bitmap := TBitmap.Create(400, 400);
 imgMain.Bitmap.Clear(TAlphaColorRec.White);
end;

Procedure for drawing a rectangle:
procedure TfmMain.btnRectClick(Sender: TObject);
begin
 imgMain.Bitmap.Canvas.BeginScene;
 imgMain.Bitmap.Canvas.DrawRect(TRectF.Create(10, 10, 200, 270),
                                30, 60,
                                AllCorners, 100,
                                TCornerType.ctRound);
 imgMain.Bitmap.Canvas.EndScene;
end;

For the line it’s still easier:
 ImgMain.Bitmap.Canvas.BeginScene;
 ImgMain.Bitmap.Canvas.DrawLine(FStartPos, TPointF.Create(X, Y), 1);
 ImgMain.Bitmap.Canvas.EndScene;

The next step is to select the TMyShape shape class from which we will inherit our TLine and TRectangle shapes:
type 
 TMyShape = class
 private
  FStartPoint, FFinalPoint: TPointF;
 public
  Constructor Create(aStartPoint, aFinalPoint: TPointF); overload;
  procedure DrawTo(aCanvas : TCanvas);
  procedure DrawShape(aCanvas : TCanvas); virtual; abstract;
 end;
 TLine = class(TMyShape)
 private
   procedure DrawShape(aCanvas : TCanvas); override;
 end;
 TRectangle = class(TMyShape)
 private
   procedure DrawShape(aCanvas : TCanvas); override;
 end;
procedure TMyShape.DrawTo(aCanvas: TCanvas);
begin
  aCanvas.BeginScene;
  DrawShape(aCanvas);
  aCanvas.EndScene;
end;

As you can see, the DrawTo method is responsible for preparing the canvas for drawing and calls the virtual drawing method for each figure.

Let's create the TDrawness class responsible for storing all the shapes and drawing them:
type
 TDrawness = class
 private
  FShapeList : TObjectList;
    function GetShapeList: TObjectList;
 public
  constructor Create;
  destructor Destroy; override;
  procedure DrawTo(aCanvas : TCanvas);
 property ShapeList : TObjectList read GetShapeList;
 end;

The DrawTo procedure runs through the entire list and calls the corresponding method for each object:
procedure TDrawness.DrawTo(aCanvas: TCanvas);
var
 i : Integer;
begin
 for i:= 0 to FShapeList.Count-1
  do FShapeList[i].DrawTo(aCanvas);
end;

That is, now, every figure that we want to remember must be added to Drawness. For example, the rectangle creation code becomes:
procedure TfmMain.btnRectClick(Sender: TObject);
var
 l_StartPoint, l_FinalPoint: TPointF;
begin
 l_StartPoint := TPointF.Create(StrToFloat(edtStartPointX.Text),
                                StrToFloat(edtStartPointY.Text));
 l_FinalPoint := TPointF.Create(StrToFloat(edtFinalPointX.Text),
                                StrToFloat(edtFinalPointY.Text));
 FDrawness.ShapeList.Add(TRectangle.Create(l_StartPoint, l_FinalPoint));
 FDrawness.ShapeList.Last.DrawTo(imgMain.Bitmap.Canvas);
end;

The last line in the method is necessary for us in order to draw the just added shape.

To draw lines, add a small circle that will be drawn at the start and end point of the line:
type
 TmsPointCircle= class(TMyShape)
 private
   procedure DrawShape(const aCanvas : TCanvas); override;
 end;
procedure TmsPointCircle.DrawShape(const aCanvas: TCanvas);
var
 l_StartPoint, l_FinalPoint: TPointF;
begin
 l_StartPoint.X := FStartPoint.X - 15;
 l_StartPoint.Y := FStartPoint.Y - 15;
 l_FinalPoint.X := FStartPoint.X + 15;
 l_FinalPoint.Y := FStartPoint.Y + 15;
 aCanvas.DrawEllipse(TRectF.Create(l_StartPoint, l_FinalPoint), 1);
end;

The next step is to learn how to add lines only by the second click of the mouse, while doing it in the forehead:
procedure TfmMain.imgMainMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Single);
begin
 FPressed := True;
 FStartPos := TPointF.Create(X, Y);
 if FIsFirstClick then
  FIsFirstClick := False
 else
 begin
  FDrawness.ShapeList.Add(TLine.Create(FStartPos, FLastPoint));
  FDrawness.ShapeList.Last.DrawTo(imgMain.Bitmap.Canvas);
  FIsFirstClick := True;
 end;
 FLastPoint := TPointF.Create(X, Y);
 FDrawness.ShapeList.Add(TmsPointCircle.Create(FStartPos, FLastPoint));
 FDrawness.ShapeList.Last.DrawTo(imgMain.Bitmap.Canvas);
end;

Let's do a little refactoring and add the AddPrimitive method to the TDrawness class:
procedure TmsDrawness.AddPrimitive(const aShape: TmsShape);
begin
 FShapeList.Add(aShape);
end;

And here we apply Dependency Injection. Create a container that will store all types of our shapes. To do this, use the TmsShape metaclass list. We’ll make the container itself Singleton, since we need a list of the types of our shapes in a single instance and add the AddPrimitive method there.
unit msRegisteredPrimitives;
interface
uses
 msShape, Generics.Collections;
type
 RmsShape = class of TmsShape;
 TmsRegistered = TList;
 TmsRegisteredPrimitives  = class
 strict private
  FmsRegistered : TmsRegistered;
  class var FInstance: TmsRegisteredPrimitives;
  constructor Create;
 public
  class function GetInstance: TmsRegisteredPrimitives;
  procedure AddPrimitive(const Value : RmsShape);
 end;
implementation
procedure TmsRegisteredPrimitives.AddPrimitive(const Value: RmsShape);
begin
 FmsRegistered.Add(Value);
end;
constructor TmsRegisteredPrimitives.Create;
 begin
  inherited;
 end;
 class function TmsRegisteredPrimitives.GetInstance: TmsRegisteredPrimitives;
 begin
  If FInstance = nil Then
  begin
   FInstance := TmsRegisteredPrimitives.Create();
  end;
  Result := FInstance;
 end;
end.

An injection will be the registration of each class inherited from TMsShape.
initialization
 TmsRegisteredPrimitives.GetInstance.AddPrimitive(TmsLine);
 TmsRegisteredPrimitives.GetInstance.AddPrimitive(TmsRectangle);
end.

We enter (on FormCreate) a list of our primitives in ComboBox so that it would be more convenient to call them:
for i := 0 to TmsRegisteredPrimitives.GetInstance.PrimitivesCount-1 do
  cbbPrimitives.Items.AddObject(TmsRegisteredPrimitives.GetInstance.Primitives[i].ClassName,
                                TObject(TmsRegisteredPrimitives.GetInstance.Primitives[i]));

Now, through the simplest operation, we can create the primitive that is selected in the ComboBox:
FDrawness.AddPrimitive(RmsShape(cbbPrimitives.items.Objects[cbbPrimitives.ItemIndex]).Create(TPointF.Create(X,Y),TPointF.Create(X+100,Y+100)));

We add the class method IsNeedsSecondClick to the TmsShape object. Which we will redefine in descendants. For lines True, for all others False.

Add a new field to TmsDrawness, which will be responsible for the selected class in ComboBox'e:
 property CurrentClass : RmsShape read FCurrentClass write FCurrentClass;

In this connection, we add to ComboBox.OnChange:
 FDrawness.CurrentClass := RmsShape(cbbPrimitives.items.Objects[cbbPrimitives.ItemIndex]);

Rewrite adding a shape to Drawness:
 ShapeObject := FDrawness.CurrentClass.Create(FStartPos, FLastPoint);
 FDrawness.AddPrimitive(ShapeObject);

Since Drawness is responsible for drawing all the shapes, add the Canvas cleaning method to it:
procedure TmsDrawness.Clear(const aCanvas: TCanvas);
begin
 aCanvas.BeginScene;
 aCanvas.Clear(TAlphaColorRec.Null);
 aCanvas.EndScene;
end;

And rewrite the drawing procedure. Before we start drawing, we will clear the Canvas, and then draw all the objects that are in Drawness.List.
procedure TmsDrawness.DrawTo(const aCanvas: TCanvas; const aOrigin : TPointF);
var
 i : Integer;
begin
 Clear(aCanvas);
 for i:= 0 to FShapeList.Count-1
  do FShapeList[i].DrawTo(aCanvas, aOrigin);
end;

Since we were convinced of the work of the prototype, it’s time to start refactoring and actually build the application architecture.

First, we’ll transfer the creation of the object to the TDrawness.AddPrimitive method and stop creating it on the form.
procedure TmsDrawness.AddPrimitive(const aStart: TPointF; const aFinish: TPointF);
begin
 Assert(CurrentClass <> nil);
 FShapeList.Add(CurrentClass.Create(aStart, aFinish));
end;

The next step is to change the algorithm for creating and adding a new shape. Instead of immediately adding a primitive to the list, we introduce an intermediate object of the TmsShape type. The primitive add code now looks like this:
procedure TmsDrawness.AddPrimitive(const aStart: TPointF; const aFinish: TPointF);
begin
 Assert(CurrentClass <> nil);
 FCurrentAddedShape := CurrentClass.Create(aStart, aFinish);
 FShapeList.Add(FCurrentAddedShape);
end;

Next, we will process the current class whether this class needs a second mouse click to draw.
procedure TmsDrawness.AddPrimitive(const aStart: TPointF; const aFinish: TPointF);
begin
 Assert(CurrentClass <> nil);
 FCurrentAddedShape := CurrentClass.Create(aStart, aFinish);
 FShapeList.Add(FCurrentAddedShape);
 if not FCurrentAddedShape.IsNeedsSecondClick then
 // - если не надо SecondClick, то наш примитив - завершён
  FCurrentAddedShape := nil;
end;

At the same time, change the addition of primitives on the form:
procedure TfmMain.imgMainMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Single);
var
 l_StartPoint : TPointF;
begin
 l_StartPoint := TPointF.Create(X, Y);
 if (FDrawness.CurrentAddedShape = nil) then
 // - мы НЕ ДОБАВЛЯЛИ примитива - надо его ДОБАВИТЬ
  FDrawness.AddPrimitive(l_StartPoint, l_StartPoint)
 else
  FDrawness.FinalizeCurrentShape(l_StartPoint);
 FDrawness.DrawTo(imgMain.Bitmap.Canvas, FOrigin);
end;

So what did we get.
If we need to draw a line, our CurrentAddedShape is nil on the first click. Therefore, we add a primitive with the same points at the beginning and end of the segment.

Further in FDrawness.AddPrimitive we check the current class and since (in the case of the line) it needs a second click we do nothing.

After that we redraw all the objects. Now nothing is drawn with us because the line with the same starting and ending point is simply not drawn.

When the user clicks the mouse a second time, we will check CurrentAddedShape again, and since we did not release it, we will call the figure finalization method, where we will set the second point of the line, and free our buffer object:
procedure TmsDrawness.FinalizeCurrentShape(const aFinish: TPointF);
begin
  Assert(CurrentAddedShape <> nil);
  CurrentAddedShape.FinalPoint := aFinish;
  FCurrentAddedShape := nil;
end;

And again we redraw all the figures.

For the rest of the figures, in FDrawness.AddPrimitive, after adding the figure to the list, we immediately free our “buffer”.

After a little refactoring (we’ll call our methods more sane, and transfer the processing of mouse clicks to Drawness), we get the following picture:
procedure TmsDiagramm.ProcessClick(const aStart: TPointF);
begin
 if ShapeIsEnded then
 // - мы НЕ ДОБАВЛЯЛИ примитива - надо его ДОБАВИТЬ
  BeginShape(aStart)
 else
  EndShape(aStart);
end;
function TmsDiagramm.ShapeIsEnded: Boolean;
begin
 Result := (CurrentAddedShape = nil);
end;
procedure TmsDiagramm.BeginShape(const aStart: TPointF);
begin
 Assert(CurrentClass <> nil);
 FCurrentAddedShape := CurrentClass.Create(aStart, aStart);
 FShapeList.Add(FCurrentAddedShape);
 if not FCurrentAddedShape.IsNeedsSecondClick then
 // - если не надо SecondClick, то наш примитив - завершён
  FCurrentAddedShape := nil;
 Invalidate;
end;
procedure TmsDiagramm.EndShape(const aFinish: TPointF);
begin
 Assert(CurrentAddedShape <> nil);
 CurrentAddedShape.EndTo(aFinish);
 FCurrentAddedShape := nil;
 Invalidate;
end;
procedure TmsDiagramm.Invalidate;
begin
 Clear;
 DrawTo(FCanvas, FOrigin);
end;

Since TDrawness is already essentially a drawing controller, its responsibility to prepare Canvas for drawing is to use enumerator at the same time:
procedure TmsDrawness.DrawTo(const aCanvas: TCanvas; const aOrigin : TPointF);
var
 l_Shape : TmsShape;
begin
 aCanvas.BeginScene;
 try
  for l_Shape in FShapeList do
   l_Shape.DrawTo(aCanvas, aOrigin);
 finally
  aCanvas.EndScene;
 end;//try..finally
end;

When drawing a line, draw a circle at the first click:
procedure TmsLine.DrawTo(const aCanvas : TCanvas; const aOrigin : TPointF);
var
 l_Proxy : TmsShape;
begin
 if (StartPoint = FinishPoint) then
 begin
  l_Proxy := TmsPointCircle.Create(StartPoint, StartPoint);
  try
   l_Proxy.DrawTo(aCanvas, aOrigin);
  finally
   FreeAndNil(l_Proxy);
  end;//try..finally
 end//StartPoint = FinishPoint
 else
  aCanvas.DrawLine(StartPoint.Add(aOrigin),
                   FinishPoint.Add(aOrigin), 1);
end;

As you can see, we create and draw a small circle, however, we do not add it to the list of primitives in Drawness so when you click the mouse a second time, our canvas will be redrawn and there will be no circle.

Add a new shape - a circle:
type
 TmsCircle = class(TmsShape)
 protected
  procedure DrawShape(const aCanvas : TCanvas; const aOrigin : TPointF); override;
 public
  class function IsNeedsSecondClick : Boolean; override;
 end;
implementation
const
 c_CircleRadius = 50;
{ TmsCircle }
procedure TmsCircle.DrawShape(const aCanvas: TCanvas; const aOrigin : TPointF);
var
 l_StartPoint, l_FinalPoint: TPointF;
begin
 l_StartPoint.X := FStartPoint.X - c_CircleRadius;
 l_StartPoint.Y := FStartPoint.Y - c_CircleRadius;
 l_FinalPoint.X := FStartPoint.X + c_CircleRadius;
 l_FinalPoint.Y := FStartPoint.Y + c_CircleRadius;
 aCanvas.DrawEllipse(TRectF.Create(l_StartPoint.Add(aOrigin),
                                   l_FinalPoint.Add(aOrigin)), 1);
end;
class function TmsCircle.IsNeedsSecondClick: Boolean;
begin
 Result := False;
end;
end.

In the circle class, replace the constant with a virtual method call:
class function TmsCircle.Radius: Integer;
begin
 Result := 50;
end;

As a result, in the class for a small circle, we only need to redefine the Radius method:
type
 TmsPointCircle = class(TmsCircle)
 protected
  class function Radius: Integer; override;
 end;
implementation
{ TmsPointCircle }
class function TmsPointCircle.Radius: Integer;
begin
 Result := 10;
end;
end.

Finish our Dependency Injection. We transfer the registration of classes from the container to each class. And add a new Register method to TmsShape. We also declare it abstract:

The TmsShape class now looks like this:
type
 TmsShape = class abstract (TObject)
 private
  FStartPoint: TPointF;
  FFinishPoint: TPointF;
 protected
  property StartPoint : TPointF read FStartPoint;
  property FinishPoint : TPointF read FFinishPoint;
  class procedure Register;
 public
  constructor Create(const aStartPoint, aFinishPoint: TPointF); virtual;
  procedure DrawTo(const aCanvas : TCanvas; const aOrigin : TPointF); virtual; abstract;
  class function IsNeedsSecondClick : Boolean; virtual;
  procedure EndTo(const aFinishPoint: TPointF);
 end;
implementation
uses
  msRegisteredPrimitives
  ;
class procedure TmsShape.Register;
begin
 TmsRegisteredPrimitives.Instance.AddPrimitive(Self);
end;
constructor TmsShape.Create(const aStartPoint, aFinishPoint: TPointF);
begin
 FStartPoint := aStartPoint;
 FFinishPoint := aFinishPoint;
end;
procedure TmsShape.EndTo(const aFinishPoint: TPointF);
begin
 FFinishPoint := aFinishPoint;
end;
class function TmsShape.IsNeedsSecondClick : Boolean;
begin
 Result := false;
end;
end.

And in each class there was a line about registering the class, for example in the TmsRectangle class:
initialization
 TmsRectangle.Register;

With the following primitive, add a rounded rectangle:
type
 TmsRoundedRectangle = class(TmsRectangle)
 protected
  procedure DrawTo(const aCanvas : TCanvas; const aOrigin : TPointF); override;
 end;//TmsRoundedRectangle
implementation
procedure TmsRoundedRectangle.DrawTo(const aCanvas: TCanvas; const aOrigin : TPointF);
begin
 aCanvas.DrawRect(TRectF.Create(StartPoint.Add(aOrigin),
                                FinishPoint.Add(aOrigin)),
                  10, 10,
                  AllCorners, 1,
                  TCornerType.ctRound);
end;
initialization
 TmsRoundedRectangle.Register;
end.

And that’s it! Thanks to the registration of the figure in the container, this is all the code that we need.
Again.
We need to inherit the class from any shape, and override the drawing method (if necessary).
Since TmsShape is a superclass, the class that registers with the container will be added directly to the Register method.
Further, on FormCreate, all classes from the container are entered into the ComboBox.
And when choosing a specific figure, the mechanisms already written will work out.

The next step, thanks to inheritance and virtual functions, will simplify the drawing of a new figure. In the TmsRectangle class, we introduce the CornerRadius class method, and change the drawing, removing the magic numbers at the same time.
class function TmsRectangle.CornerRadius: Single;
begin
 Result := 0;
end;
procedure TmsRectangle.DrawTo(const aCanvas: TCanvas; const aOrigin : TPointF);
begin
 aCanvas.DrawRect(TRectF.Create(StartPoint.Add(aOrigin),
                                FinishPoint.Add(aOrigin)),
                  CornerRadius,
                  CornerRadius,
                  AllCorners,
                  1,
                  TCornerType.ctRound);
end;

Now in our new class, it’s enough to simply rewrite the CornerRadius method with the required angle for rounding the corners. The class as a whole looks like this:
type
 TmsRoundedRectangle = class(TmsRectangle)
 protected
  class function CornerRadius: Single; override;
 end;//TmsRoundedRectangle
implementation
class function TmsRoundedRectangle.CornerRadius: Single;
begin
 Result := 10;
end;
initialization
 TmsRoundedRectangle.Register;
end.

In a similar way we get rid of constants. And also add the fill color. Let's try to fill in the rectangle:
procedure TmsRectangle.DrawTo(const aCanvas: TCanvas; const aOrigin : TPointF);
begin
 aCanvas.Fill.Color := TAlphaColorRec.White;
 aCanvas.DrawRect(TRectF.Create(StartPoint.Add(aOrigin),
                                FinishPoint.Add(aOrigin)),
                  CornerRadius,
                  CornerRadius,
                  AllCorners,
                  1,
                  TCornerType.ctRound);
 aCanvas.FillRect(TRectF.Create(StartPoint.Add(aOrigin),
                                FinishPoint.Add(aOrigin)),
                  CornerRadius,
                  CornerRadius,
                  AllCorners,
                  1,
                  TCornerType.ctRound);
end;

As you can see, in order to paint the figure, you need to set the color of the canvas. Thus, in order not to duplicate the code, and not add a new parameter to the drawing method, we will use the FillColor virtual method for TmsShape. And also rewrite the drawing method of the super class.

We will first set all the necessary parameters to the canvas, and only then call the virtual method of drawing each figure:
procedure TmsShape.DrawTo(const aCanvas : TCanvas; const aOrigin : TPointF);
begin
 aCanvas.Fill.Color := FillColor;
 DoDrawTo(aCanvas, aOrigin);
end;

To add the following primitive, add virtual functions for the circle:
type
 TmsCircle = class(TmsShape)
 protected
  class function InitialRadiusX: Integer; virtual;
  class function InitialRadiusY: Integer; virtual;
  function FillColor: TAlphaColor; override;
  procedure DoDrawTo(const aCanvas : TCanvas; const aOrigin : TPointF); override;
 public
  constructor Create(const aStartPoint, aFinishPoint: TPointF); override;
 end;

The following primitive will make the yellow oval:
type
 TmsUseCaseLikeEllipse = class(TmsCircle)
 protected
  class function InitialRadiusY: Integer; override;
  function FillColor: TAlphaColor; override;
 end;//TmsUseCaseLikeEllipse
implementation
class function TmsUseCaseLikeEllipse.InitialRadiusY: Integer;
begin
 Result := 35;
end;
function TmsUseCaseLikeEllipse.FillColor: TAlphaColor;
begin
 Result := TAlphaColorRec.Yellow;
end;
initialization
 TmsUseCaseLikeEllipse.Register;
end.

Add a new triangle primitive:
type
 TmsTriangle = class(TmsShape)
 protected
  function FillColor: TAlphaColor; override;
  procedure DoDrawTo(const aCanvas : TCanvas; const aOrigin : TPointF); override;
 end;//TmsTriangle
implementation
uses
 System.Math.Vectors
 ;
function TmsTriangle.FillColor: TAlphaColor;
begin
 Result := TAlphaColorRec.Green;
end;
procedure TmsTriangle.DoDrawTo(const aCanvas: TCanvas; const aOrigin : TPointF);
const
 cHeight = 100;
var
 l_P : TPolygon;
begin
 SetLength(l_P, 4);
 l_P[0] := TPointF.Create(StartPoint.X - cHeight div 2,
                          StartPoint.Y + cHeight div 2);
 l_P[1] := TPointF.Create(StartPoint.X + cHeight div 2,
                          StartPoint.Y + cHeight div 2);
 l_P[2] := TPointF.Create(StartPoint.X,
                          StartPoint.Y - cHeight div 2);
 l_P[3] := l_P[0];
 aCanvas.DrawPolygon(l_P, 1);
 aCanvas.FillPolygon(l_P, 0.5);
end;
initialization
 TmsTriangle.Register;
end.

As you can see, drawing a triangle is somewhat different from other shapes. But it’s still very easy to do. The TPolygon type is a dynamic array from TPointF. We fill it thanks to simple calculations, with all this, the last point of the polygon should be its first point. Drawing is organized by standard methods.

Put the class names in order. Rename the TmsDrawness class to TmsDiagramm. Also, considering that all operations with Canvas are performed by the Diagramm class, we will make Canvas part of Diagramm.

We remove the “extra knowledge” from the form and transfer it to the Diagramm class, thereby highlighting the full controller who is responsible for creating and drawing all the shapes of our application.
type
 TmsDiagramm = class(TObject)
 private
  FShapeList : TmsShapeList;
  FCurrentClass : RmsShape;
  FCurrentAddedShape : TmsShape;
  FCanvas : TCanvas;
  FOrigin : TPointF;
 private
  procedure DrawTo(const aCanvas : TCanvas; const aOrigin : TPointF);
  function CurrentAddedShape: TmsShape;
  procedure BeginShape(const aStart: TPointF);
  procedure EndShape(const aFinish: TPointF);
  function ShapeIsEnded: Boolean;
  class function AllowedShapes: RmsShapeList;
  procedure CanvasChanged(aCanvas: TCanvas);
 public
  constructor Create(anImage: TImage);
  procedure ResizeTo(anImage: TImage);
  destructor Destroy; override;
  procedure ProcessClick(const aStart: TPointF);
  procedure Clear;
  property CurrentClass : RmsShape read FCurrentClass write FCurrentClass;
  procedure Invalidate;
  procedure AllowedShapesToList(aList: TStrings);
  procedure SelectShape(aList: TStrings; anIndex: Integer);
 end;
implementation
uses
 msRegisteredPrimitives
 ;
class function TmsDiagramm.AllowedShapes: RmsShapeList;
begin
 Result := TmsRegisteredPrimitives.Instance.Primitives;
end;
procedure TmsDiagramm.AllowedShapesToList(aList: TStrings);
var
 l_Class : RmsShape;
begin
 for l_Class in AllowedShapes do
  aList.AddObject(l_Class.ClassName, TObject(l_Class));
end;
procedure TmsDiagramm.SelectShape(aList: TStrings; anIndex: Integer);
begin
 CurrentClass := RmsShape(aList.Objects[anIndex]);
end;
procedure TmsDiagramm.ProcessClick(const aStart: TPointF);
begin
 if ShapeIsEnded then
 // - мы НЕ ДОБАВЛЯЛИ примитива - надо его ДОБАВИТЬ
  BeginShape(aStart)
 else
  EndShape(aStart);
end;
procedure TmsDiagramm.BeginShape(const aStart: TPointF);
begin
 Assert(CurrentClass <> nil);
 FCurrentAddedShape := CurrentClass.Create(aStart, aStart);
 FShapeList.Add(FCurrentAddedShape);
 if not FCurrentAddedShape.IsNeedsSecondClick then
 // - если не надо SecondClick, то наш примитив - завершён
  FCurrentAddedShape := nil;
 Invalidate;
end;
procedure TmsDiagramm.Clear;
begin
 FCanvas.BeginScene;
 try
  FCanvas.Clear(TAlphaColorRec.Null);
 finally
  FCanvas.EndScene;
 end;//try..finally
end;
constructor TmsDiagramm.Create(anImage: TImage);
begin
 FShapeList := TmsShapeList.Create;
 FCurrentAddedShape := nil;
 FCanvas := nil;
 FOrigin := TPointF.Create(0, 0);
 ResizeTo(anImage);
 FCurrentClass := AllowedShapes.First;
end;
procedure TmsDiagramm.ResizeTo(anImage: TImage);
begin
 anImage.Bitmap := TBitmap.Create(Round(anImage.Width), Round(anImage.Height));
 CanvasChanged(anImage.Bitmap.Canvas);
end;
procedure TmsDiagramm.CanvasChanged(aCanvas: TCanvas);
begin
 FCanvas := aCanvas;
 Invalidate;
end;
function TmsDiagramm.CurrentAddedShape: TmsShape;
begin
 Result := FCurrentAddedShape;
end;
destructor TmsDiagramm.Destroy;
begin
 FreeAndNil(FShapeList);
 inherited;
end;
procedure TmsDiagramm.DrawTo(const aCanvas: TCanvas; const aOrigin : TPointF);
var
 l_Shape : TmsShape;
begin
 aCanvas.BeginScene;
 try
  for l_Shape in FShapeList do
   l_Shape.DrawTo(aCanvas, aOrigin);
 finally
  aCanvas.EndScene;
 end;//try..finally
end;
procedure TmsDiagramm.EndShape(const aFinish: TPointF);
begin
 Assert(CurrentAddedShape <> nil);
 CurrentAddedShape.EndTo(aFinish);
 FCurrentAddedShape := nil;
 Invalidate;
end;
procedure TmsDiagramm.Invalidate;
begin
 Clear;
 DrawTo(FCanvas, FOrigin);
end;
function TmsDiagramm.ShapeIsEnded: Boolean;
begin
 Result := (CurrentAddedShape = nil);
end;
end.

The form code now looks like this:
var
 fmMain: TfmMain;
implementation
{$R *.fmx}
procedure TfmMain.btnClearImageClick(Sender: TObject);
begin
 FDiagramm.Clear;
end;
procedure TfmMain.btnDrawAllClick(Sender: TObject);
begin
 FDiagramm.Invalidate;
end;
procedure TfmMain.cbbPrimitivesChange(Sender: TObject);
begin
 FDiagramm.SelectShape(cbbPrimitives.Items, cbbPrimitives.ItemIndex);
end;
procedure TfmMain.FormCreate(Sender: TObject);
begin
 FDiagramm := TmsDiagramm.Create(imgMain);
 FDiagramm.AllowedShapesToList(cbbPrimitives.Items);
end;
procedure TfmMain.FormDestroy(Sender: TObject);
begin
 FreeAndNil(FDiagramm);
end;
procedure TfmMain.imgMainMouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Single);
begin
 Caption := 'x = ' + FloatToStr(X) + '; y = ' + FloatToStr(Y);
end;
procedure TfmMain.imgMainResize(Sender: TObject);
begin
 FDiagramm.ResizeTo(imgMain);
end;
procedure TfmMain.imgMainMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Single);
begin
 FDiagramm.ProcessClick(TPointF.Create(X, Y));
end;
procedure TfmMain.miAboutClick(Sender: TObject);
begin
 ShowMessage(self.Caption);
end;
procedure TfmMain.miExitClick(Sender: TObject);
begin
 self.Close;
end;
end.

As you can see, all the code that we first recorded in the event handlers is now completely hidden in the TmsDiagram controller.

The next step is adding a list of diagrams, since we want to be able to independently draw several diagrams at the same time:
type
 TmsDiagrammList = TObjectList;
 TmsDiagramms = class(TObject)
 private
  f_Diagramms : TmsDiagrammList;
  f_CurrentDiagramm : TmsDiagramm;
 public
  constructor Create(anImage: TImage; aList: TStrings);
  destructor Destroy; override;
  procedure ProcessClick(const aStart: TPointF);
  procedure Clear;
  procedure SelectShape(aList: TStrings; anIndex: Integer);
  procedure AllowedShapesToList(aList: TStrings);
  procedure ResizeTo(anImage: TImage);
  procedure AddDiagramm(anImage: TImage; aList: TStrings);
  function CurrentDiagrammIndex: Integer;
  procedure SelectDiagramm(anIndex: Integer);
 end;//TmsDiagramms
implementation
uses
 System.SysUtils
 ;
constructor TmsDiagramms.Create(anImage: TImage; aList: TStrings);
begin
 inherited Create;
 f_Diagramms := TmsDiagrammList.Create;
 AddDiagramm(anImage, aList);
end;
procedure TmsDiagramms.AddDiagramm(anImage: TImage; aList: TStrings);
begin
 f_CurrentDiagramm := TmsDiagramm.Create(anImage, IntToStr(f_Diagramms.Count + 1));
 f_Diagramms.Add(f_CurrentDiagramm);
 aList.AddObject(f_CurrentDiagramm.Name, f_CurrentDiagramm);
 //f_CurrentDiagramm.Invalidate;
end;
function TmsDiagramms.CurrentDiagrammIndex: Integer;
begin
 Result := f_Diagramms.IndexOf(f_CurrentDiagramm);
end;
procedure TmsDiagramms.SelectDiagramm(anIndex: Integer);
begin
 if (anIndex < 0) OR (anIndex >= f_Diagramms.Count) then
  Exit;
 f_CurrentDiagramm := f_Diagramms.Items[anIndex];
 f_CurrentDiagramm.Invalidate;
end;
destructor TmsDiagramms.Destroy;
begin
 FreeAndNil(f_Diagramms);
 inherited;
end;
procedure TmsDiagramms.ProcessClick(const aStart: TPointF);
begin
 f_CurrentDiagramm.ProcessClick(aStart);
end;
procedure TmsDiagramms.Clear;
begin
 f_CurrentDiagramm.Clear;
end;
procedure TmsDiagramms.SelectShape(aList: TStrings; anIndex: Integer);
begin
 f_CurrentDiagramm.SelectShape(aList, anIndex);
end;
procedure TmsDiagramms.AllowedShapesToList(aList: TStrings);
begin
 f_CurrentDiagramm.AllowedShapesToList(aList);
end;
procedure TmsDiagramms.ResizeTo(anImage: TImage);
begin
 f_CurrentDiagramm.ResizeTo(anImage);
end;
end.

As you can see, the chart list class essentially represents a wrapper for each chart, and the details of the implementation of the list.

We take into account that each diagram has its own chosen primitive. Add the IndexOf method to the container:
function TmsRegisteredShapes.IndexOf(const aValue : RmsShape): Integer;
begin
 Result := f_Registered.IndexOf(aValue);
end;

Now add a diagram method:
function TmsDiagramm.CurrentShapeClassIndex: Integer;
begin
 Result := AllowedShapes.IndexOf(FCurrentClass);
end;

And according to the list of diagrams:
function TmsDiagramms.CurrentShapeClassIndex: Integer;
begin
 Result := f_CurrentDiagramm.CurrentShapeClassIndex;
end;

However, we are still accessing the list of diagrams directly from the form, it is time to get rid of this as well. Why we will create a “real chart controller”. This class will be responsible for the operation of the form controls, as well as for event handling:
type
 TmsDiagrammsController = class(TObject)
 private
  imgMain: TImage;
  cbShapes: TComboBox;
  cbDiagramm: TComboBox;
  btAddDiagramm: TButton;
  FDiagramm: TmsDiagramms;
  procedure cbDiagrammChange(Sender: TObject);
  procedure imgMainResize(Sender: TObject);
  procedure cbShapesChange(Sender: TObject);
  procedure btAddDiagrammClick(Sender: TObject);
  procedure imgMainMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Single);
 public
  constructor Create(aImage: TImage; aShapes: TComboBox; aDiagramm: TComboBox; aAddDiagramm: TButton);
  destructor Destroy; override;
  procedure Clear;
  procedure ProcessClick(const aStart: TPointF);
 end;//TmsDiagrammsController
implementation
uses
 System.SysUtils
 ;
constructor TmsDiagrammsController.Create(aImage: TImage; aShapes: TComboBox; aDiagramm: TComboBox; aAddDiagramm: TButton);
begin
 inherited Create;
 imgMain := aImage;
 cbShapes := aShapes;
 cbDiagramm := aDiagramm;
 btAddDiagramm := aAddDiagramm;
 FDiagramm := TmsDiagramms.Create(imgMain, cbDiagramm.Items);
 FDiagramm.AllowedShapesToList(cbShapes.Items);
 cbShapes.ItemIndex := FDiagramm.CurrentShapeClassIndex;
 cbDiagramm.ItemIndex := FDiagramm.CurrentDiagrammIndex;
 cbDiagramm.OnChange := cbDiagrammChange;
 imgMain.OnResize := imgMainResize;
 cbShapes.OnChange := cbShapesChange;
 btAddDiagramm.OnClick := btAddDiagrammClick;
 imgMain.OnMouseDown := imgMainMouseDown;
end;
procedure TmsDiagrammsController.cbDiagrammChange(Sender: TObject);
begin
 FDiagramm.SelectDiagramm(cbDiagramm.ItemIndex);
 cbShapes.ItemIndex := FDiagramm.CurrentShapeClassIndex;
end;
procedure TmsDiagrammsController.imgMainResize(Sender: TObject);
begin
 FDiagramm.ResizeTo(imgMain);
end;
procedure TmsDiagrammsController.cbShapesChange(Sender: TObject);
begin
 FDiagramm.SelectShape(cbShapes.Items, cbShapes.ItemIndex);
end;
procedure TmsDiagrammsController.btAddDiagrammClick(Sender: TObject);
begin
 FDiagramm.AddDiagramm(imgMain, cbDiagramm.Items);
 cbDiagramm.ItemIndex := FDiagramm.CurrentDiagrammIndex;
 cbShapes.ItemIndex := FDiagramm.CurrentShapeClassIndex;
end;
destructor TmsDiagrammsController.Destroy;
begin
 FreeAndNil(FDiagramm);
end;
procedure TmsDiagrammsController.Clear;
begin
 FDiagramm.Clear;
end;
procedure TmsDiagrammsController.ProcessClick(const aStart: TPointF);
begin
 FDiagramm.ProcessClick(aStart);
end;
procedure TmsDiagrammsController.imgMainMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin
 Self.ProcessClick(TPointF.Create(X, Y));
end;
end.

Now all we need to do is create our controller:
procedure TfmMain.FormCreate(Sender: TObject);
begin
 FDiagrammsController := TmsDiagrammsController.Create(imgMain, cbShapes, cbDiagramm, btAddDiagramm);
end;


Application Picture:



UML Class Diagram:



BitBucket repository

So, in the article we showed how to get rid of code duplication sequentially through the use of inheritance and virtual functions. They gave an example of Dependency Injection. Which made life very easy for us. Otherwise, incoherent case of and Object is would constantly occur in the code. They demonstrated, in sequence, how to avoid writing code inside event handlers. By creating a special class, the controller, who assumes all obligations. They also showed how not to arrange “Swiss knives” from the class, dividing each layer as they were responsible. TmsDiagramm is responsible for drawing. TmsDiagramms is responsible for the list of diagrams, however, in addition to this, it also has all the interaction of the operation of each diagram with the main controller. And finally, the TmsDiagrammsController class, which is the link between the user and the charts.

PS Dear Habrausers. I will be happy to hear all your comments and suggestions. The article is intended for a wide range of readers, so some points are painted very meticulously. This is my first article on Habr; therefore, do not judge strictly.

Part 1 .
Part 2 .
Part 3 .
Part 3.1

Also popular now: