Original post in Russian -http://habrahabr.ru/post/234801/
Table of contents
Good day to you. In this article I will continue to tell how we develop software for FireMonkey. Two interesting objects will be added. Both of them will remind us of vector algebra and trigonometry. The methods from OOP that we use will also be shown in the post. A number of lines (that differ from each other only by dashes, dot-and-dash, point-to-point, etc.) that we added were made in the same way as the description of previous primitives. Now it is time to move on to more complex figures (including compound ones). The first primitive to be added is an arrowed line (as an arrow an ordinary triangle of smaller size will serve). To begin with, let us introduce a triangle “looking” to the right. It means we will inherit the ordinary triangle and rewrite for it Polygon method that is responsible for the coordinates of the vertices.
Further, we inherit a so-called “small triangle”:
Now the task becomes more interesting. We have to rotate a triangle perpendicular to the line that has drawn it. In order to do this we’ll introduce method GetArrowAngleRotation calculating the rotation angle. Let us imagine that our line is a hypotenuse of a right-angled triangle; then let’s find an angle with a leg of the triangle. By this angle the triangle will rotate around the line:
Next, we’ll add an object responsible for moving of the figures.
We’ll use the following algorithm:
1. We need a method to determine whether a point gets to a specific figure, say ContainsPt, for each figure. Since formulas for calculating the getting are unique for each figure, we’ll use virtual functions.
2. The next method is required to determine which figure the point has got to in case figures cross. Since the figures get on the list as they appear on the form, provided that figures cross, the figure at the beginning of the list is the last to have appeared and is therefore at the top. In fact, there’s a pitfall in this logic, but for now let’s assume it is right and leave the correction for the next post.
3. On the first click in the figure to which we’ve got we have to change its outline or a number of other characteristics.
4. On the second click we have to move the figure to which we’ve got.
The class of the moving will be inherited from a standard figure, but it will store a figure that it moves, and this class is the one to redraw the figure on the second click (I have described the peculiarities of drawing the lines in my previous post).
We implement the methods I’ve described.
1. This method detects whether a point gets to a figure (in our case it is a rectangle):
function TmsRectangle.ContainsPt(const aPoint: TPointF): Boolean;
Let’s ensure that our controller (TmsDiagramm) inherits from TmsInterfacedNonRefcounted and the interfaces. Let us also change one line in BeginShape method. We had the following:
Let’s somewhat make over object registration in the container. Now each class registers itself. Let’s put registration into a separate module.
In this post we’ve tried to show how to make our life easier by using contexts, interfaces and the fabric method. More details on the fabric method you can find here and here. In the next post we will tell how we’ve “tied” DUnit to FireMonkey. We will also develop a number of tests, some of which will cause an error at once.
Source
Table of contents
Good day to you. In this article I will continue to tell how we develop software for FireMonkey. Two interesting objects will be added. Both of them will remind us of vector algebra and trigonometry. The methods from OOP that we use will also be shown in the post. A number of lines (that differ from each other only by dashes, dot-and-dash, point-to-point, etc.) that we added were made in the same way as the description of previous primitives. Now it is time to move on to more complex figures (including compound ones). The first primitive to be added is an arrowed line (as an arrow an ordinary triangle of smaller size will serve). To begin with, let us introduce a triangle “looking” to the right. It means we will inherit the ordinary triangle and rewrite for it Polygon method that is responsible for the coordinates of the vertices.
function TmsTriangleDirectionRight.Polygon: TPolygon; begin SetLength(Result, 4); Result[0] := TPointF.Create(StartPoint.X - InitialHeight / 2, StartPoint.Y - InitialHeight / 2); Result[1] := TPointF.Create(StartPoint.X - InitialHeight / 2, StartPoint.Y + InitialHeight / 2); Result[2] := TPointF.Create(StartPoint.X + InitialHeight / 2, StartPoint.Y); Result[3] := Result[0]; end;That is how our triangles look like:
Further, we inherit a so-called “small triangle”:
type TmsSmallTriangle = class(TmsTriangleDirectionRight) protected function FillColor: TAlphaColor; override; public class function InitialHeight: Single; override; end; // TmsSmallTriangleAs you can see, all we have done is overridden functions that are unique for the new triangle. Next, we’ll add a class of an arrowed line inherited from an ordinary line. Only the procedure of drawing the primitive will be overridden in the class, in other words, the line will be drawn by a base class and the triangle is an heir.
procedure TmsLineWithArrow.DoDrawTo(const aCtx: TmsDrawContext); var l_Proxy : TmsShape; l_OriginalMatrix: TMatrix; l_Matrix: TMatrix; l_Angle : Single; l_CenterPoint : TPointF; l_TextRect : TRectF; begin inherited; if (StartPoint <> FinishPoint) then begin l_OriginalMatrix := aCtx.rCanvas.Matrix; try l_Proxy := TmsSmallTriangle.Create(FinishPoint); try // Just yet in order to experiment we specify rotation by 0 degrees // to ensure the triangle is drawn correctly l_Angle := DegToRad(0); l_CenterPoint := TPointF.Create(FinishPoint.X , FinishPoint.Y); // We’ve stored the initial matrix l_Matrix := l_OriginalMatrix; // We’ve transferred the origin of coordinates in the point where a rotation will be made l_Matrix := l_Matrix * TMatrix.CreateTranslation(-l_CenterPoint.X, -l_CenterPoint.Y); // The process itself l_Matrix := l_Matrix * TMatrix.CreateRotation(l_Angle); // We’ve put back the origin of the coordinates l_Matrix := l_Matrix * TMatrix.CreateTranslation(l_CenterPoint.X, l_CenterPoint.Y); // We are applying our space matrix to the canvas aCanvas.SetMatrix(l_Matrix); // We’re drawing :) l_Proxy.DrawTo(aCanvas, aOrigin); finally FreeAndNil(l_Proxy); end; // try..finally finally // Since we’ve drawn the required figure we put back the initial matrix to the canvas aCanvas.SetMatrix(l_OriginalMatrix); end; end;//(StartPoint <> FinishPoint) end;There is nothing much to explain, everything is already given in the commentaries. Anyway, if you wish to recollect what vector algebra is and how to work with vector graphic (moving, figures rotation and so on) I recommend a remarkable post on Habr on this subject as well as articles here and here. As shown in the picture, our triangle is now drawn only if we draw a line from left to right:
Now the task becomes more interesting. We have to rotate a triangle perpendicular to the line that has drawn it. In order to do this we’ll introduce method GetArrowAngleRotation calculating the rotation angle. Let us imagine that our line is a hypotenuse of a right-angled triangle; then let’s find an angle with a leg of the triangle. By this angle the triangle will rotate around the line:
function TmsLineWithArrow.GetArrowAngleRotation: Single; var l_ALength, l_CLength, l_AlphaAngle, l_X, l_Y, l_RotationAngle: Single; l_PointC: TPointF; l_Invert: SmallInt; begin Result := 0; // The formula for calculating the distance between two points l_X := (FinishPoint.X - StartPoint.X) * (FinishPoint.X - StartPoint.X); l_Y := (FinishPoint.Y - StartPoint.Y) * (FinishPoint.Y - StartPoint.Y); // We find the length of the hypotenuse of the right-angled triangle l_CLength := sqrt(l_X + l_Y); l_PointC := TPointF.Create(FinishPoint.X, StartPoint.Y); // The formula for calculating the distance between two points l_X := (l_PointC.X - StartPoint.X) * (l_PointC.X - StartPoint.X); l_Y := (l_PointC.Y - StartPoint.Y) * (l_PointC.Y - StartPoint.Y); // We find the length of the leg l_ALength := sqrt(l_X + l_Y); // The angle in radians l_AlphaAngle := ArcSin(l_ALength / l_CLength); l_RotationAngle := 0; l_Invert := 1; if FinishPoint.X > StartPoint.X then begin l_RotationAngle := Pi / 2 * 3; if FinishPoint.Y > StartPoint.Y then l_Invert := -1; end else begin l_RotationAngle := Pi / 2; if FinishPoint.Y < StartPoint.Y then l_Invert := -1; end; Result := l_Invert * (l_AlphaAngle + l_RotationAngle); end;Now our line looks like this:
Next, we’ll add an object responsible for moving of the figures.
We’ll use the following algorithm:
1. We need a method to determine whether a point gets to a specific figure, say ContainsPt, for each figure. Since formulas for calculating the getting are unique for each figure, we’ll use virtual functions.
2. The next method is required to determine which figure the point has got to in case figures cross. Since the figures get on the list as they appear on the form, provided that figures cross, the figure at the beginning of the list is the last to have appeared and is therefore at the top. In fact, there’s a pitfall in this logic, but for now let’s assume it is right and leave the correction for the next post.
3. On the first click in the figure to which we’ve got we have to change its outline or a number of other characteristics.
4. On the second click we have to move the figure to which we’ve got.
The class of the moving will be inherited from a standard figure, but it will store a figure that it moves, and this class is the one to redraw the figure on the second click (I have described the peculiarities of drawing the lines in my previous post).
We implement the methods I’ve described.
1. This method detects whether a point gets to a figure (in our case it is a rectangle):
function TmsRectangle.ContainsPt(const aPoint: TPointF): Boolean;
var l_Finish : TPointF; l_Rect: TRectF; begin Result := False; l_Finish := TPointF.Create(StartPoint.X + InitialWidth, StartPoint.Y + InitialHeight); l_Rect := TRectF.Create(StartPoint,l_Finish); Result := l_Rect.Contains(aPoint); end;2. This method is used to detect which figure we’ve got to:
class function TmsShape.ShapeByPt(const aPoint: TPointF; aList: TmsShapeList): TmsShape; var l_Shape: TmsShape; l_Index: Integer; begin Result := nil; for l_Index := aList.Count - 1 downto 0 do begin l_Shape := aList.Items[l_Index]; if l_Shape.ContainsPt(aPoint) then begin Result := l_Shape; Exit; end; // l_Shape.ContainsPt(aPoint) end; // for l_Index end;3. On the first click in the figure that we’ve got to we have to change its outline or a number of other characteristics. To implement this method we’ll perform a slight refactoring. We introduce a so-called “drawing context”:
type TmsDrawContext = record public rCanvas: TCanvas; rOrigin: TPointF; rMoving: Boolean; // We define that the currently drawn primitive is moving constructor Create(const aCanvas: TCanvas; const aOrigin: TPointF); end; // TmsDrawContextIf we specify the figure as “movable” in the drawing context, the drawing will be performed in the different way.
procedure TmsShape.DrawTo(const aCtx: TmsDrawContext); begin aCtx.rCanvas.Fill.Color := FillColor; if aCtx.rMoving then begin aCtx.rCanvas.Stroke.Dash := TStrokeDash.sdDashDot; aCtx.rCanvas.Stroke.Color := TAlphaColors.Darkmagenta; aCtx.rCanvas.Stroke.Thickness := 4; end else begin aCtx.rCanvas.Stroke.Dash := StrokeDash; aCtx.rCanvas.Stroke.Color := StrokeColor; aCtx.rCanvas.Stroke.Thickness := StrokeThickness; end; DoDrawTo(aCtx); end;4. On the second click we have to move the figure to which we’ve got. To begin with, let’s introduce a factory method responsible for figure constructing (the list of the figures is required so that TmsMover could call all the figures drawn within the current diagram).
class function TmsShape.Make(const aStartPoint: TPointF; aListWithOtherShapes: TmsShapeList): TmsShape; begin Result := Create(aStartPoint); end; ... class function TmsMover.Make(const aStartPoint: TPointF; aListWithOtherShapes: TmsShapeList): TmsShape; var l_Moving: TmsShape; begin l_Moving := ShapeByPt(aStartPoint, aListWithOtherShapes); if (l_Moving <> nil) then Result := Create(aStartPoint, aListWithOtherShapes, l_Moving) else Result := nil; end;As a result of using class function we’ve fundamentally separated creating of the moved object and of the other figures. However, this approach has a disadvantage. For instance, we introduced a parameter of creating aListWithOtherShapes which is not required for the other figures at all.
type TmsMover = class(TmsShape) private f_Moving: TmsShape; f_ListWithOtherShapes: TmsShapeList; protected procedure DoDrawTo(const aCtx: TmsDrawContext); override; constructor Create(const aStartPoint: TPointF; aListWithOtherShapes: TmsShapeList; aMoving: TmsShape); public class function Make(const aStartPoint: TPointF; aListWithOtherShapes: TmsShapeList): TmsShape; override; class function IsNeedsSecondClick: Boolean; override; procedure EndTo(const aFinishPoint: TPointF); override; end; // TmsMover implementation uses msRectangle, FMX.Types, System.SysUtils; constructor TmsMover.Create(const aStartPoint: TPointF; aListWithOtherShapes: TmsShapeList; aMoving: TmsShape); begin inherited Create(aStartPoint); f_ListWithOtherShapes := aListWithOtherShapes; f_Moving := aMoving; end; class function TmsMover.Make(const aStartPoint: TPointF; aListWithOtherShapes: TmsShapeList): TmsShape; var l_Moving: TmsShape; begin l_Moving := ShapeByPt(aStartPoint, aListWithOtherShapes); if (l_Moving <> nil) then Result := Create(aStartPoint, aListWithOtherShapes, l_Moving) else Result := nil; end; class function TmsMover.IsNeedsSecondClick: Boolean; begin Result := true; end; procedure TmsMover.EndTo(const aFinishPoint: TPointF); begin if (f_Moving <> nil) then f_Moving.MoveTo(aFinishPoint); f_ListWithOtherShapes.Remove(Self); // Now I must delete MYSELF since we do not need mover in the general list after it has performed its function end; procedure TmsMover.DoDrawTo(const aCtx: TmsDrawContext); var l_Ctx: TmsDrawContext; begin if (f_Moving <> nil) then begin l_Ctx := aCtx; l_Ctx.rMoving := true; f_Moving.DrawTo(l_Ctx); end; // f_Moving <> nil end; initialization TmsMover.Register; end.In the controller we just have to change methods of figures creating:
procedure TmsDiagramm.BeginShape(const aStart: TPointF); begin Assert(CurrentClass <> nil); FCurrentAddedShape := CurrentClass.Make(aStart, FShapeList); if (FCurrentAddedShape <> nil) then begin FShapeList.Add(FCurrentAddedShape); if not FCurrentAddedShape.IsNeedsSecondClick then FCurrentAddedShape := nil; Invalidate; end; // FCurrentAddedShape <> nil end; procedure TmsDiagramm.EndShape(const aFinish: TPointF); begin Assert(CurrentAddedShape <> nil); CurrentAddedShape.EndTo(aFinish); FCurrentAddedShape := nil; Invalidate; end;In case of mover a call CurrentAddedShape.EndTo(aFinish) calls another one - MoveTo, i.e. moves the figure, and the redrawing, as shown above, is initiated by the controller:
procedure TmsMover.EndTo(const aFinishPoint: TPointF); begin if (f_Moving <> nil) then f_Moving.MoveTo(aFinishPoint); f_ListWithOtherShapes.Remove(Self); // Now I must delete MYSELF since we do not need mover in the general list after it has performed its function end; ... procedure TmsShape.MoveTo(const aFinishPoint: TPointF); begin FStartPoint := aFinishPoint; end;Since the controller is responsible for figures behavior, we’ll take the method of checking the “getting to a figure” away to the controller, and while creating the objects we’ll pass a check function:
type TmsShapeByPt = function (const aPoint: TPointF): TmsShape of object; ... class function Make(const aStartPoint: TPointF; aShapeByPt: TmsShapeByPt): TmsShape; override; ... procedure TmsDiagramm.BeginShape(const aStart: TPointF); begin Assert(CurrentClass <> nil); // The call itself FCurrentAddedShape := CurrentClass.Make(aStart, Self.ShapeByPt); if (FCurrentAddedShape <> nil) then begin FShapeList.Add(FCurrentAddedShape); if not FCurrentAddedShape.IsNeedsSecondClick then FCurrentAddedShape := nil; Invalidate; end;//FCurrentAddedShape <> nil end;Since we have to pass two parameters in order to create objects, we define a context of “creating”:
type TmsMakeShapeContext = record public rStartPoint: TPointF; rShapeByPt: TmsShapeByPt; constructor Create(aStartPoint: TPointF; aShapeByPt: TmsShapeByPt); end;//TmsMakeShapeContextLet’s add interfaces implemented by the controller and also a class of interface object. In the future we’ll realise our own reference counting in it.
type TmsInterfacedNonRefcounted = class abstract(TObject) protected function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; end;//TmsInterfacedNonRefcounted TmsShape = class; ImsShapeByPt = interface function ShapeByPt(const aPoint: TPointF): TmsShape; end;//ImsShapeByPt ImsShapesController = interface procedure RemoveShape(aShape: TmsShape); end;//ImsShapeRemoverLet’s slightly change TmsMakeShapeContext:
type TmsMakeShapeContext = record public rStartPoint: TPointF; rShapesController: ImsShapesController; constructor Create(aStartPoint: TPointF; const aShapesController: ImsShapesController); end; // TmsMakeShapeContextI recommend two interesting posts on interfaces and details of working with them in Delphi: 18delphi.blogspot.com/2013/04/iunknown.html and habrahabr.ru/post/181107/
Let’s ensure that our controller (TmsDiagramm) inherits from TmsInterfacedNonRefcounted and the interfaces. Let us also change one line in BeginShape method. We had the following:
FCurrentAddedShape := CurrentClass.Make(aStart, Self.ShapeByPt);And it became like this:
FCurrentAddedShape := CurrentClass.Make(TmsMakeShapeContext.Create(aStart, Self));In case of moving, EndTo method will look as follows:
procedure TmsMover.EndTo(const aCtx: TmsEndShapeContext); begin if (f_Moving <> nil) then f_Moving.MoveTo(aCtx.rStartPoint); f_Moving := nil; aCtx.rShapesController.RemoveShape(Self); // Now I must delete MYSELF end;In the previous post I’ve told about hiding of “unique configurations” (filling color, line width, etc.) in virtual methods that each figure defines independently. For example:
function TmsTriangle.FillColor: TAlphaColor; begin Result := TAlphaColorRec.Green; end;We pack all figures configurations in the context:
type TmsDrawOptionsContext = record public rFillColor: TAlphaColor; rStrokeDash: TStrokeDash; rStrokeColor: TAlphaColor; rStrokeThickness: Single; constructor Create(const aCtx: TmsDrawContext); end;//TmsDrawOptionsContextIn TmsShape class we make a virtual procedure similarly to the previous example. In the future we’ll easily expand the number of unique figure configurations:
procedure TmsTriangle.TransformDrawOptionsContext(var theCtx: TmsDrawOptionsContext); begin inherited; theCtx.rFillColor := TAlphaColorRec.Green; theCtx.rStrokeColor := TAlphaColorRec.Blue; end;Using the context we put the logic (do we really draw a mover?) away from the method of drawing and hide it in the record constructor:
constructor TmsDrawOptionsContext.Create(const aCtx: TmsDrawContext); begin rFillColor := TAlphaColorRec.Null; if aCtx.rMoving then begin rStrokeDash := TStrokeDash.sdDashDot; rStrokeColor := TAlphaColors.Darkmagenta; rStrokeThickness := 4; end // aCtx.rMoving else begin rStrokeDash := TStrokeDash.sdSolid; rStrokeColor := TAlphaColorRec.Black; rStrokeThickness := 1; end; // aCtx.rMoving end;After that our method of drawing becomes like this:
procedure TmsShape.DrawTo(const aCtx: TmsDrawContext); var l_Ctx: TmsDrawOptionsContext; begin l_Ctx := DrawOptionsContext(aCtx); aCtx.rCanvas.Fill.Color := l_Ctx.rFillColor; aCtx.rCanvas.Stroke.Dash := l_Ctx.rStrokeDash; aCtx.rCanvas.Stroke.Color := l_Ctx.rStrokeColor; aCtx.rCanvas.Stroke.Thickness := l_Ctx.rStrokeThickness; DoDrawTo(aCtx); end; function TmsShape.DrawOptionsContext(const aCtx: TmsDrawContext): TmsDrawOptionsContext; begin Result := TmsDrawOptionsContext.Create(aCtx); // We get the configurations for each figure TransformDrawOptionsContext(Result); end;The only thing left to make our figures movable is to code for each figure ContainsPt method responsible for checking if a point gets to a figure. This is a routine trigonometry; all the formulas can be found on the Internet.
Let’s somewhat make over object registration in the container. Now each class registers itself. Let’s put registration into a separate module.
unit msOurShapes; interface uses msLine, msRectangle, msCircle, msRoundedRectangle, msUseCaseLikeEllipse, msTriangle, msDashDotLine, msDashLine, msDotLine, msLineWithArrow, msTriangleDirectionRight, msMover, msRegisteredShapes ; implementation procedure RegisterOurShapes; begin TmsRegisteredShapes.Instance.Register([ TmsLine, TmsRectangle, TmsCircle, TmsRoundedRectangle, TmsUseCaseLikeEllipse, TmsTriangle, TmsDashDotLine, TmsDashLine, TmsDotLine, TmsLineWithArrow, TmsTriangleDirectionRight, TmsMover ]); end; initialization RegisterOurShapes; end.Let’s add registration method in the container:
procedure TmsRegisteredShapes.Register(const aShapes: array of RmsShape); var l_Index: Integer; begin for l_Index := Low(aShapes) to High(aShapes) do Self.Register(aShapes[l_Index]); end; procedure TmsRegisteredShapes.Register(const aValue: RmsShape); begin Assert(f_Registered.IndexOf(aValue) < 0); f_Registered.Add(aValue); end;
In this post we’ve tried to show how to make our life easier by using contexts, interfaces and the fabric method. More details on the fabric method you can find here and here. In the next post we will tell how we’ve “tied” DUnit to FireMonkey. We will also develop a number of tests, some of which will cause an error at once.
Source
Комментариев нет:
Отправить комментарий