MindStream. How we write software under FireMonkey
- From the sandbox
- Tutorial
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 for drawing a rectangle:
For the line it’s still easier:
The next step is to select the TMyShape shape class from which we will inherit our TLine and TRectangle shapes:
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:
The DrawTo procedure runs through the entire list and calls the corresponding method for each object:
That is, now, every figure that we want to remember must be added to Drawness. For example, the rectangle creation code becomes:
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:
The next step is to learn how to add lines only by the second click of the mouse, while doing it in the forehead:
Let's do a little refactoring and add the AddPrimitive method to the TDrawness class:
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.
An injection will be the registration of each class inherited from TMsShape.
We enter (on FormCreate) a list of our primitives in ComboBox so that it would be more convenient to call them:
Now, through the simplest operation, we can create the primitive that is selected in the ComboBox:
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:
In this connection, we add to ComboBox.OnChange:
Rewrite adding a shape to Drawness:
Since Drawness is responsible for drawing all the shapes, add the Canvas cleaning method to it:
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.
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.
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:
Next, we will process the current class whether this class needs a second mouse click to draw.
At the same time, change the addition of primitives on the form:
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:
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:
Since TDrawness is already essentially a drawing controller, its responsibility to prepare Canvas for drawing is to use enumerator at the same time:
When drawing a line, draw a circle at the first click:
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:
In the circle class, replace the constant with a virtual method call:
As a result, in the class for a small circle, we only need to redefine the Radius method:
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:
And in each class there was a line about registering the class, for example in the TmsRectangle class:
With the following primitive, add a rounded rectangle:
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.
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:
In a similar way we get rid of constants. And also add the fill color. Let's try to fill in the rectangle:
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:
To add the following primitive, add virtual functions for the circle:
The following primitive will make the yellow oval:
Add a new triangle primitive:
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.
The form code now looks like this:
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:
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:
Now add a diagram method:
And according to the list of diagrams:
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:
Now all we need to do is create our controller:
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
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