Blog
All Blog Posts | Next Post | Previous PostDeveloping your first FNC custom control
Friday, May 13, 2016
Some weeks ago, we released the TMS FNC UI Pack, a set of Framework Neutral Components (FNC), i.e. UI controls that can be used from VCL Windows applications, FireMonkey (FMX) Windows, Mac OS-X, iOS, Android applications and LCL framework based Lazarus applications for Windows, Linux, Mac OS-X,..The TMS FNC UI Pack contains highly complex & feature-rich components such as grid, planner, rich editor, treeview, toolbars. To create such complex components that work under 3 frameworks and a myriad of operating systems is not a trivial excercise and requires intricate knowledge about the VCL, FMX and LCL frameworks as well as the operating systems the controls need to work under.
To help ourselves and the users of the TMS FNC UI Pack, we have introduced several abstractions that facilitate creating framework neutral components and this is what we want to cover in this brief introduction to developing FNC custom controls.
FNC custom control basics
The structure of the FNC custom control we want to present is this of a classic UI control. The control is responsible for painting itself and interacts with keyboard and/or mouse. The control has several properties to control its appearance and behavior. If we look at this concept from the perspective of implementing this for 3 different frameworks, the biggest challenges faced are:
1) abstractions in the code for dealing with graphics: especially VCL and FMX are quite different in this respect, so abstraction is welcome.
2) abstractions in keyboard and mouse handling: also here there are differences, although subtle, between VCL, FMX and LCL.
3) abstractions in types: types such as font, color, rectangles, points are different in FMX and VCL.
So, we'll cover what is included in FNC to facilitate dealing with this.
Unit organisation
Perhaps the biggest stumbling block is unit organisation. As it is desirable to create components that can be installed in the IDE, (which means for Delphi for both the VCL and FireMonkey framework simultanously) we'll need units for VCL and units for FireMonkey. Although we can use the same class name, the class hierarchy for the VCL control and the FMX control will be different. The VCL FNC control will descend from the VCL TCustomControl and the FMX FNC control will descend from the FMX TControl. In Lazarus, the FNC control will descend from the LCL TCustomControl. In a nutshell, to solve this, we create 3 units for a component that will be nearly identical and we provide a conversion step to allow you to write the code in one unit and automatically generate the other units for the other frameworks. For this example, in total, we'll have 6 units: 3 units with the code for the control for the 3 supported frameworks and 3 units for the component registration in the IDE:
// Units for the VCL variant of the FNC control VCL.TMSFNCCust.pas VCL.TMSFNCCustReg.pas // Units for the FMX variant of the FNC control FMX.TMSFNCCust.pas FMX.TMSFNCCustReg.pas // Units for the LCL variant of the FNC control LCLTMSFNCCust.pas LCLTMSFNCCustReg.pas
and in Lazarus, this is:
Getting to grips
Ok, now that the unit structure is setup, we can focus on writing the code. To write this code, we'll use 3 TMS FNC UI Pack units with abstractions: xxx.TMSFNCCustomControl, xxx.TMSFNCGraphics and xxx.TMSFNCTypes (with xxx = the framework). In this example, we'll write the code in VCL and automatically generate the FMX & LCL equivalents from it, so the uses list becomes:
for FMX
uses Classes, Types, FMX.TMSFNCCustomControl, FMX.TMSFNCGraphics, FMX.TMSFNCTypes;
uses Classes, Types, LCLTMSFNCCustomControl, LCLTMSFNCGraphics, LCLTMSFNCTypes;
Control initialization
The control descends from the abstract FNC class TTMSFNCCustomControl and exposes one extra color property for the gauge line color (note that this are of the abstract type TTMSFNCGraphicsColor), a font (of the abstract type TTMSFNCGraphicsFont, that has also a font color in all frameworks) and a gauge value property of the type TControlValue. Note that the Stroke & Fill properties are published. This contains the control border & control background color and even enables things such as setting a background gradient, a border with a specific pen style etc...
TControlValue = 0..100; TTMSFNCCustomControlSample = class(TTMSFNCCustomControl) private FLineColor: TTMSFNCGraphicsColor; FFont: TTMSFNCGraphicsFont; FValue: TControlValue; protected procedure Draw({%H-}AGraphics: TTMSFNCGraphics; {%H-}ARect: TRectF); override; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; published property Stroke; property Fill; property Font: TTMSFNCGraphicsFont read FFont write SetFont; property LineColor: TTMSFNCGraphicsColor read FLineColor write SetLineColor default gcRed; property Value: TControlValue read FValue write SetValue default 0; end;
{ TTMSFNCCustomControlSample } constructor TTMSFNCCustomControlSample.Create(AOwner: TComponent); begin inherited; Stroke.Color := gcBlack; FLineColor := gcRed; FFont := TTMSFNCGraphicsFont.Create; FFont.OnChanged := {$IFDEF LCL}@{$ENDIF}FontChanged; Width := 100; Height := 100; end; destructor TTMSFNCCustomControlSample.Destroy; begin FFont.Free; inherited; end;
The control descends from the abstract FNC class TTMSFNCCustomControl and exposes 3 color properties, the border, background and gauge line color (note that these are of the type TTMSFNCGraphicsColor), a font (of the type TTMSFNCGraphicsFont, that has also a font color in all frameworks) and a gauge value property of the type TControlValue.
Painting is done in the override of the Draw() method that has 2 parameters: AGraphics: TTMSFNCGraphics, a framework neutral graphics library and the rectangle of the control via ARect of the type TRectF. In VCL and LCL only the not fractional part of the floating point numbers is used but of course in the FireMonkey framework, this can use the fractional parts as well.
The painting code itself is:
procedure TTMSFNCCustomControlSample.Draw(AGraphics: TTMSFNCGraphics; ARect: TRectF); var angle: double; lf,tf: TPointF; w: single; begin inherited; angle := Value/High(Value)*2*PI; w := Min(ARect.Right - ARect.Left, ARect.Bottom - ARect.Top) / 2; lf.X := (ARect.Right - ARect.Left)/2; lf.Y := (ARect.Bottom - ARect.Top)/2; tf.X := lf.X + Cos(angle) * w; tf.Y := lf.Y - Sin(angle) * w; AGraphics.Stroke.Color := LineColor; AGraphics.DrawLine(lf,tf); AGraphics.Font.Assign(Font); AGraphics.DrawText(ARect, InttoStr(Value),false, gtaCenter, gtaCenter); end;
Mouse and keyboard handling
The mouse and keyboard handling is via the HandleKeyDown and HandleMouseDown virtual method overrides:
TTMSFNCCustomControlSample = class(TTMSFNCCustomControl) protected procedure HandleKeyDown(var {%H-}Key: Word; {%H-}Shift: TShiftState); override; procedure HandleMouseDown({%H-}Button: TMouseButton; {%H-}Shift: TShiftState; {%H-}X, {%H-}Y: Single); override; end;
procedure TTMSFNCCustomControlSample.HandleKeyDown(var Key: Word; Shift: TShiftState); begin inherited; if Key = KEY_DOWN then begin if Value > Low(Value) then Value := Value - 1; end; if Key = KEY_UP then begin if Value < High(Value) then Value := Value + 1; end; end;
procedure TTMSFNCCustomControlSample.HandleMouseDown(Button: TTMSFNCMouseButton; Shift: TShiftState; X, Y: Single); var angle: single; dx,dy: single; begin inherited; if AllowFocus then SetFocus; dx := x - (Width/2); dy := - y + (Height/2); if dx = 0 then angle := sign(dy) * PI / 2 else angle := ArcTan(dy/dx); if dx < 0 then angle := PI + angle; if angle < 0 then angle := angle + 2 * PI; Value := Round((angle / 2 / PI) * High(Value)); end;
Now that we have the VCL framework FNC component ready that contains 100% framework neutral code, let's create automatically the FMX and LCL units from this. 3 steps are needed:
1) Rename the unit VCL.TMSFNCCust.pas to FMX.TMSFNCCust.pas and LCLTMSFNCCust.pas
2) Change in the unit .PAS file the unit name, i.e. replace VCL.TMSFNCCust by FMX.TMSFNCCust and LCLTMSFNCCust respectively
3) Change the unit references in the uses list from
VCL.TMSFNCCustomControl, VCL.TMSFNCGraphics, VCL.TMSFNCTypes;
to
FMX.TMSFNCCustomControl, FMX.TMSFNCGraphics, FMX.TMSFNCTypes;
or
LCLTMSFNCCustomControl, LCLTMSFNCGraphics, LCLTMSFNCTypes;
To accomplish this, we call a simple powershell script that performs text replacement from VCL.TMS to FMX.TMS or LCLTMS respectively:
powershell -command "(gc VCL.TMSFNCCust.pas) -replace 'VCL.TMS','LCLTMS' |Out-file LCLTMSFNCCust.pas -Encoding utf8" powershell -command "(gc VCL.TMSFNCCust.pas) -replace 'VCL.TMS','FMX.TMS' |Out-file FMX.TMSFNCCust.pas -Encoding utf8"
The full source of this sample FNC custom control can be downloaded here. This sample can be used with the latest version of the TMS FNC UI Pack.
We hope this already whets your appetite for exploring FNC and the power of writing code for UI controls once for use in 3 frameworks. See also this blog article for a more general coverage of what is available in the TMS FNC UI Pack. In a next article, we'll go deeper in compound control creation and also the TTMSFNCGraphics library that offers a vast range of functions, going from drawing text, polygons, polylines, images in various formats, controls like checkboxes, radiobuttons, buttons, ... and much more.
Bruno Fierens
This blog post has received 7 comments.
2. Friday, May 13, 2016 at 2:35:37 PM
The reason is explained in the article and related to a Delphi control hierarchy being different for different frameworks & package limitations.
Bruno Fierens
3. Friday, May 13, 2016 at 4:28:33 PM
The reason invoked does not make sense for private use.
Only the "uses" clauses need to be customized.
A single unit with the following is enough:
uses
Classes, Types
{$ifdef USEFMX}
, FMX.TMSFNCCustomControl, FMX.TMSFNCGraphics, FMX.TMSFNCTypes;
{$endif}
{$ifdef USELCL}
, LCLTMSFNCCustomControl, LCLTMSFNCGraphics, LCLTMSFNCTypes;
{$endif}
{$ifdef USEVCL}
, VCL.TMSFNCCustomControl, VCL.TMSFNCGraphics, VCL.TMSFNCTypes;
{$endif}
(of course, USELCL/USEVCL/USEFMX are to be replaced by the proper conditionals)
No need to create 3 diverse units in end-user code base. This single unit could then be included in every project, with no problem.
Only the "uses" clauses need to be customized.
A single unit with the following is enough:
uses
Classes, Types
{$ifdef USEFMX}
, FMX.TMSFNCCustomControl, FMX.TMSFNCGraphics, FMX.TMSFNCTypes;
{$endif}
{$ifdef USELCL}
, LCLTMSFNCCustomControl, LCLTMSFNCGraphics, LCLTMSFNCTypes;
{$endif}
{$ifdef USEVCL}
, VCL.TMSFNCCustomControl, VCL.TMSFNCGraphics, VCL.TMSFNCTypes;
{$endif}
(of course, USELCL/USEVCL/USEFMX are to be replaced by the proper conditionals)
No need to create 3 diverse units in end-user code base. This single unit could then be included in every project, with no problem.
A. Bouchez
4. Friday, May 13, 2016 at 4:40:34 PM
What is up with the {%H-} ?
C Johnson
5. Friday, May 13, 2016 at 7:47:17 PM
Mr. Bouchez,
The problem is installing the components in the IDE!
With a single unit solution, this DOES NOT WORK to install in the IDE for BOTH VCL and FMX.
End of discussion.
The problem is installing the components in the IDE!
With a single unit solution, this DOES NOT WORK to install in the IDE for BOTH VCL and FMX.
End of discussion.
Bruno Fierens
6. Friday, May 13, 2016 at 7:47:56 PM
Mr. Johsnon,
This is something to suppress warnings for Lazarus/FPC.
This is something to suppress warnings for Lazarus/FPC.
Bruno Fierens
7. Tuesday, May 24, 2016 at 8:58:44 AM
Nice!
Kosinski Tomasz
All Blog Posts | Next Post | Previous Post
But I wonder, why use 3 separated units in private projects?
It does make sense for new components internal to the TNC suite (each platform has its own folder), but for custom components, it may not be worth it, and error-prone, if you forget to generate the LCL/FMX units after a modification to the VCL unit.
Why not use a single unit, but with $ifdef for the uses clause?
Sounds like outside the "uses", the code is 100% platform-independent, e.g. all classes inheriting from TTMSFNCCustomControl.
A. Bouchez