Generating PDF files cross-platform with ease

Thursday, December 22, 2016

With the new 1.7 release of the TMS FNC UI Pack we've added a completely new PDF library built from the ground up, that is capable of generating PDF files on all supported frameworks (FMX, VCL, LCL). Via all supported frameworks, you can easily target minimum 5 different operating systems: Windows, macOS, iOS, Android, Linux (&Raspbian),.. To introduce this new PDF library I've written a small tutorial to start generating your own PDF files.

Getting Started

To get started with the PDF library, add the FMX.TMSFNCPDFLib, VCL.TMSFNCPDFLib or LCLTMSFNCPDFLib depending on the chosen framework. The PDF library class is called TTMSFNCPDFLib, and the code is shareable between the three supported frameworks.

Starting a new document

Starting a new document can be file-based, or TMemoryStream-based. To start a new document, call the BeginDocument function. The BeginDocument always needs to be paired with EndDocument, which is responsible for writing the contents to a file or TMemoryStream. When the AFileName parameter in the BeginDocument call is empty, the contents will be written to a TMemoryStream, returned by the EndDocument call. The EndDocument call also has an additional parameter to allow opening the generated PDF file in the default PDF reader application.

procedure TForm1.GeneratePDF(AFileName: string);
var
  p: TTMSFNCPDFLib;
begin
  p := TTMSFNCPDFLib.Create;
  try
    p.BeginDocument(AFileName);
    p.EndDocument;
  finally
    p.Free;
  end;
end;

Adding pages

Adding pages can be done by calling the NewPage function. The NewPage is responsible for starting a new page content stream on which the graphics / text can be written. Each NewPage call will clear the content buffer and allow you to start with new text and graphics. Please note though that all appearance settings such as fill, stroke and font are stored for the entire document, so starting a new page will allow you to continue in the same appearance settings as the previous page.

procedure TForm1.GeneratePDF(AFileName: string);
var
  p: TTMSFNCPDFLib;
begin
  p := TTMSFNCPDFLib.Create;
  try
    p.BeginDocument(AFileName);
    p.NewPage;
    p.EndDocument;
  finally
    p.Free;
  end;
end;

Drawing content on a page

After adding a new page, the page can be filled with content such as HTML formatted text, plain text as well as some basic drawing primitives such as rectangles, circles and lines. To starting drawing content, the pdf library has a Graphics property that provides access to the drawing functionality. The following sample uses the Fill and Stroke properties to generate a PDF with a gradient rectangle and a dotted border.

procedure TForm1.GeneratePDF(AFileName: string);
var
  p: TTMSFNCPDFLib;
begin
  p := TTMSFNCPDFLib.Create;
  try
    p.BeginDocument(AFileName);
    p.NewPage;
    p.Graphics.Stroke.Color := gcRed;
    p.Graphics.Stroke.Width := 3;
    p.Graphics.Stroke.Kind := gskDashDotDot;
    p.Graphics.Fill.Kind := gfkGradient;
    p.Graphics.Fill.Color := gcBlue;
    p.Graphics.Fill.ColorTo := gcOrange;
    p.Graphics.DrawRectangle(RectF(10, 50, 100, 150));
    p.EndDocument(True);
  finally
    p.Free;
  end;
end;

Drawing Text

Drawing text on a PDF page is done via the DrawText function. The DrawText has a set of parameters to allow drawing wordwrapped text in a rectangle, or simply as-is at a specific position. The DrawText function has a set of overloads that also supports column drawing. Each call to DrawText returns a value that can either contain the calculated text rectangle or the amount of left-over characters after an overflow is detected when drawing text in a column. The font that is used when drawing text can be controlled separately via the Font property. With this property, the font name, size, color and style can be set.

procedure TForm1.GeneratePDF(AFileName: string);
var
    p: TTMSFNCPDFLib;
begin
    p := TTMSFNCPDFLib.Create;
  try
    p.BeginDocument(AFileName);
    p.NewPage;
    p.Graphics.Font.Name := 'Segoe UI';
    p.Graphics.Font.Size := 16;
    p.Graphics.Font.Color := gcRed;
    p.Graphics.Font.Style := [TFontStyle.fsBold];
    p.Graphics.DrawText('Hello World !', PointF(10, 50));
    p.EndDocument(True);
  finally
    p.Free;
  end;
end;

HTML formatted text

HTML formatted text is also supported by using the DrawHTMLText call. When passing a TMSFNCBitmapContainer reference it can even draw images referenced by the <IMG> tag. HTML support is based on the miniHTML reference.

procedure TForm1.GeneratePDF(AFileName: string);
var
  p: TTMSFNCPDFLib;
  s: string;
  r: TRectF;
begin
  p := TTMSFNCPDFLib.Create;
  try
    s := 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum     has been the industry''s standard dummy'+
'text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. '+
'It has survived not only five centuries, but also the leap into electronic typeset'+
'ting, 
remaining essentially unchanged. It'+ ' was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with des'+ 'ktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.'; p.BitmapContainer := TMSFNCBitmapContainer1; p.BeginDocument(AFileName); p.NewPage; p.Graphics.Font.Name := 'Arial'; p.Graphics.Font.Size := 10; p.Graphics.Fill.Color := gcNull; r := RectF(10, 50, 300, 400); p.Graphics.DrawHTMLText(s, r); p.EndDocument(True); finally p.Free; end; end;

The PDF library supports more than simple text and graphics. More information on the PDF library and the features can be found at the following page http://www.tmssoftware.com/site/tmsfncuipack.asp?s=fncpdf#features.

The PDF library is also available in the TMS FMX UI Pack and is coming to the TMS Component Pack at the beginning of 2017!

Pieter Scheldeman


Bookmarks: 

This blog post has not received any comments yet. Add a comment.



TMS X-Cloud Todolist with FNC

Wednesday, June 22, 2016

It's almost three months since we released the first version of the TMS FNC UI Pack, a set of Framework Neutral Components (FNC), and have meanwhile released 2 major updates which include the TTMSFNCTabSet/TTMSFNCPageControl (v1.1), the recently introduced TTMSFNCListBox/TTMSFNCCheckedListBox and significant improvements / new features to the TTMSFNCTreeView such as filtering, sorting, keyboard lookup, clipboard support, ... (v1.2).

As already explained in previous blog posts (http://www.tmssoftware.com/site/blog.asp?post=335 and http://tmssoftware.com/site/blog.asp?post=346), the TMS FNC UI Pack is a set of 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. So, with a single set of controls, you have the freedom of choice to use Delphi, C++Builder or the free Lazarus to create applications for a myriad of operating systems, you have a single learning curve to these components and as demonstrated here, you can use a single source to create apps for multiple targets.

This blog post will cover the TTMSFNCCheckedListBox, which is one of the new components that are added in the latest release (v1.2) of the TMS FNC UI Pack, show how to use myCloudData.net to store data and demonstrate how easy it is to switch between FMX, VCL and LCL with one shared source code. myCloudData is an easy to use & flexible service to make use of structured storage in the cloud from Windows, web, mobile or IoT apps and is offered by tmssoftware.com. myCloudData is OAUTH/JSON REST based and our TMS Cloud Pack includes a component to access the service and thus your data seamlessly.

Click image for more screenshots.

A single shared source

As with our TV-guide sample we have created a single shared source file that is used in a FMX, VCL and LCL project. The unit starts by defining the business logic class that will be instantiated in our application main form unit.
  TTODOListLogic = class
  private
    FTable: TMyCloudDataTable;
    FListBox: TTMSFNCCheckedListBox;
    FMyCloudDataAccess: TMyCloudDataAccess;
    FOnConnected: TNotifyEvent;
    FBitmapContainer: TTMSFNCBitmapContainer;
  protected
    procedure DoConnected(Sender: TObject);
  public
    destructor Destroy; override;
    procedure InitListBox(AListBox: TTMSFNCCheckedListBox);
    procedure InitMyCloudData;
    procedure Refresh;
    procedure InitializeTable;
    procedure AddNewItem(AText: string; ADate: TDateTime; APriority: TPriority);
    procedure DeleteItem;
    procedure Connect;
    procedure DoBeforeDrawItem(Sender: TObject; AGraphics: TTMSFNCGraphics; ARect: TRectF; AItem: TTMSFNCListBoxItem; var AAllow: Boolean; var ADefaultDraw: Boolean);
    procedure DoItemCheckChanged(Sender: TObject; AItem: TTMSFNCCheckedListBoxItem);
    procedure DoItemCompare(Sender: TObject; Item1, Item2: TTMSFNCListBoxItem; var ACompareResult: Integer);
    property OnConnected: TNotifyEvent read FOnConnected write FOnConnected;
    property BitmapContainer: TTMSFNCBitmapContainer read FBitmapContainer write FBitmapContainer;
  end;
Each framework has its own set of units in order to compile succesfully. We use the conditional defines added to our project to make the difference between each framework.
uses
  Classes, SysUtils, DB
  {$IFDEF VCL}
  ,VCL.TMSFNCListBox, VCL.TMSFNCCheckedListBox, CloudMyCloudData, CloudBase, VCL.TMSFNCUtils,
  CloudCustomMyCloudData, VCL.TMSFNCGraphics, VCL.Dialogs, VCL.TMSFNCTypes, Types, VCL.TMSFNCBitmapContainer;
  {$ENDIF}

  {$IFDEF FMX}
  ,FMX.TMSFNCListBox, FMX.TMSFNCCheckedListBox, FMX.TMSCloudMyCloudData, FMX.TMSCloudBase,
  FMX.TMSFNCUtils, FMX.TMSCloudCustomMyCloudData, FMX.TMSFNCGraphics, FMX.TMSFNCTypes, FMX.Dialogs, Types, FMX.TMSFNCBitmapContainer;
  {$ENDIF}

  {$IFDEF LCL}
  ,LCLTMSFNCListBox, LCLTMSFNCCheckedListBox, LCLTMSCloudMyCloudData, LCLTMSCloudBase,
  LCLTMSFNCUtils, LCLTMSCloudCustomMyCloudData, LCLTMSFNCGraphics, Dialogs, LCLTMSFNCTypes, LCLTMSFNCBitmapContainer;
  {$ENDIF}

myCloudData

As our todolist is storing its todo items in the cloud we take advantage of our own service, that can virtually store anything we want. The initialization is done programmatically.
procedure TTODOListLogic.InitMyCloudData;
begin
  FMyCloudDataAccess := TMyCloudDataAccess.Create(nil);
  FMyCloudDataAccess.PersistTokens.Location := plIniFile;
  {$IFDEF FMX}
  FMyCloudDataAccess.PersistTokens.Key := TTMSFNCUtils.GetDocumentsPath + PthDel + 'myclouddatafmx.ini';
  FMyCloudDataAccess.OnConnected := DoConnected;
  {$ENDIF}
  {$IFDEF VCL}
  FMyCloudDataAccess.PersistTokens.Key := TTMSFNCUtils.GetDocumentsPath + PthDel + 'myclouddatavcl.ini';
  FMyCloudDataAccess.OnConnected := DoConnected;
  {$ENDIF}
  {$IFDEF LCL}
  FMyCloudDataAccess.PersistTokens.Key := TTMSFNCUtils.GetDocumentsPath + PthDel + 'myclouddatalcl.ini';
  FMyCloudDataAccess.OnConnected := @DoConnected;
  {$ENDIF}
  FMyCloudDataAccess.PersistTokens.Section := 'tokens';
  FMyCloudDataAccess.App.Key := MYCLOUDDATAKEY;
  FMyCloudDataAccess.App.Secret := MYCLOUDDATASECRET;
  FMyCloudDataAccess.App.CallBackPort := 8888;
  FMyCloudDataAccess.App.CallBackURL := 'http://127.0.0.1:8888';
end;
You might notice 3 things here. First is the TMyCloudDataAccess class which is common between FMX, VCL and LCL. This is defined earlier in our business logic as the unit names are different for FMX, VCL and LCL.
type
  {$IFDEF VCL}
  TMyCloudDataAccess = class(TAdvMyCloudData);
  {$ENDIF}

  {$IFDEF FMX}
  TMyCloudDataAccess = class(TTMSFMXCloudMyCloudData);
  {$ENDIF}

  {$IFDEF LCL}
  TMyCloudDataAccess = class(TTMSLCLCloudMyCloudData);
  {$ENDIF}
Second, is the event handler assignment, that we also need to wrap with conditional defines because LCL works with an additional @. Third is the ini file that is also created with a framework suffix, as the token and token encryption are unique per application and not shareable. After defining our business logic, it's time to setup our GUI. The form unit is shared between FMX, VCL and LCL and there you will also notice the uses list and the form file is separated with defines. After designing our form (using the TTMSFNCCheckedListBox, some tool bar buttons (TTMSFNCToolBarButton) we are ready to connect to our business logic and create a working todo list that stores its items in the cloud.
procedure TTODOListForm.DoConnected(Sender: TObject);
begin
  Panel1.Enabled := True;
  Panel2.Enabled := True;
  TMSFNCToolBarButton2.Enabled := False;
  TMSFNCCheckedListBox1.Enabled := True;
  TMSFNCToolBarButton4.Enabled := True;
  TMSFNCToolBarButton5.Enabled := True;
  TMSFNCToolBarButton6.Enabled := True;
  TMSFNCToolBarItemPicker1.Enabled := True;
  FTODOListLogic.InitializeTable;
  FTODOListLogic.Refresh;
end;

procedure TTODOListForm.FormCreate(Sender: TObject);
begin
  FTODOListLogic := TTODOListLogic.Create;
  FTODOListLogic.InitListBox(TMSFNCCheckedListBox1);
  FTODOListLogic.InitMyCloudData;
  {$IFDEF LCL}
  FTODOListLogic.OnConnected := @DoConnected;
  {$ELSE}
  FTODOListLogic.OnConnected := DoConnected;
  {$ENDIF}
  TMSFNCCheckedListBox1.BitmapContainer := TMSFNCBitmapContainer1;
  TMSFNCToolBarItemPicker1.BitmapContainer := TMSFNCBitmapContainer1;
  TMSFNCToolBarItemPicker1.Bitmaps.Clear;
  TMSFNCToolBarItemPicker1.Bitmaps.AddBitmapName('low');
  TMSFNCToolBarItemPicker1.DisabledBitmaps.Assign(TMSFNCToolBarItemPicker1.Bitmaps);
  TMSFNCToolBarButton2.DisabledBitmaps.Assign(TMSFNCToolBarButton2.Bitmaps);
  TMSFNCToolBarButton4.DisabledBitmaps.Assign(TMSFNCToolBarButton4.Bitmaps);
  TMSFNCToolBarButton5.DisabledBitmaps.Assign(TMSFNCToolBarButton5.Bitmaps);
  TMSFNCToolBarButton6.DisabledBitmaps.Assign(TMSFNCToolBarButton6.Bitmaps);

  dt := TMyDateTimePicker.Create(Self);
  {$IFDEF FMX}
  dt.Position.X := 5;
  dt.Position.Y := 40;
  {$ELSE}
  dt.Left := 5;
  dt.Top := 40;
  {$ENDIF}
  dt.Date := Now;
  dt.Parent := Panel1;
  dt.Width := 105;
end;

procedure TTODOListForm.FormDestroy(Sender: TObject);
begin
  FTODOListLogic.Free;
end;

procedure TTODOListForm.TMSFNCCheckedListBox1ItemSelected(Sender: TObject;
  AItem: TTMSFNCListBoxItem);
begin
  TMSFNCToolBarButton6.Enabled := True;
end;

procedure TTODOListForm.TMSFNCToolBarButton1Click(Sender: TObject);
begin
  FTODOListLogic.Refresh;
end;

procedure TTODOListForm.TMSFNCToolBarButton2Click(Sender: TObject);
begin
  FTODOListLogic.Connect;
end;

procedure TTODOListForm.TMSFNCToolBarButton3Click(Sender: TObject);
begin
  FTODOListLogic.DeleteItem;
end;

procedure TTODOListForm.TMSFNCToolBarButton4Click(Sender: TObject);
begin
  FTODOListLogic.AddNewItem(Edit1.Text, dt.Date, TPriority(TMSFNCToolBarItemPicker1.SelectedItemIndex));
end;

procedure TTODOListForm.TMSFNCToolBarItemPicker1ItemSelected(Sender: TObject;
  AItemIndex: Integer);
begin
  TMSFNCToolBarItemPicker1.Bitmaps.Clear;
  TMSFNCToolBarItemPicker1.Bitmaps.AddBitmapName(TMSFNCBitmapContainer1.Items[AItemIndex].Name);
  TMSFNCToolBarItemPicker1.DisabledBitmaps.Assign(TMSFNCToolBarItemPicker1.Bitmaps);
end;
When starting the application, and clicking the connect button, our business logic unit will do the work. It will create a table in myCloudData, send a notification to our GUI, which will then allow to add items to our listbox, refresh or delete existing items, and this is done with one source code, available on multiple frameworks, multiple platforms.

The full source code is available for download

Click image for more screenshots.

Pieter Scheldeman


Bookmarks: 

This blog post has not received any comments yet. Add a comment.



Developing 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
We'll also use 3 packages: a package for VCL, a package for FMX and a package for LCL. We can install the VCL & FMX package simultanously in the Delphi IDE and the LCL package in the Lazarus IDE. The package for the custom control will have a dependency to the framework plus to the TMS FNC UI Pack for the framework. The structure of the packages in the Delphi 10.1 Berlin IDE is:


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;
for LCL
uses
  Classes, Types, LCLTMSFNCCustomControl, LCLTMSFNCGraphics, LCLTMSFNCTypes;
The FNC control we'll write here is very rudimentary for the sake of focusing on the abstractions. It is basically a gauge control that can be clicked with the mouse to set the gauge value, has a gauge value property to set it and can use the key up & down to change the gauge value. So, we need painting of the gauge, we need some gauge line color & font settings and we need handling of the mouse click and keyboard.


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;
In the constructor, initialization of the line color property value is done as well as the border color and the font is created. This is of the type TTMSFNCGraphicsFont and the main purpose is to have a font with font color in all frameworks. There is one more thing particular in the constructor and that is the assignment of the Font.OnChange event handler. In Delphi Pascal, we can simply assign the object method but for the FPC compiler, this needs to be prefixed with the @ symbol. Fortunately, the Lazarus environment has the LCL define we can use to handle this.

{ 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;
Painting

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;
The implementation is straightforward. One thing that is noteworthy, is that the unit xxx.TTMSFNCTypes has a list of keys in a framework and operating system neutral way, i.e. we can check for the arrow up and arrow down keys with the constants KEY_DOWN and KEY_UP.

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;
In the mouse down handler, we set the focus to the control when possible and then calculate the value of the gauge that matches with the mouse click position. Note that the X,Y values are of the type single to accomodate FMX and will contain non fractional values in VCL and LCL.

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;
Creating the units for FMX and LCL

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"
With these units created, we can compile the packages and install our FNC custom control for use from VCL, FMX and LCL applications in Lazarus.


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


Bookmarks: 

This blog post has received 7 comments. Add a comment.



Introducing TMS FNC UI Pack

Tuesday, April 26, 2016

TMS FNC UI Controls, a set of framework neutral controls

Introduction

As your customers use an ever increasing number of devices & operating systems, it is a daily challenge for us, software developers to make our software available for the myriad of target platforms in use. Fortunately, as Pascal developers, we already have a huge benefit that Delphi meanwhile targets 4 platforms: Windows, Mac OSX, iOS and Android. Add Lazarus to that, and Pascal developers can also target Linux and its derivatives such as Raspbian and many more. The 3 main frameworks we have available to create our software for these platforms are: VCL, FMX and LCL. The framework to use will be determined by the target and the IDE used. That implies that the controls that can be used are also typically determined by this choice and might limit your abilities when some controls or control features are not available for one of these frameworks. Addressing that limitation is exactly one of the primary goals of the TMS FNC UI Controls. It offers you a set of powerful & feature-rich UI controls that you can use in Delphi's VCL framework, FMX framework and Lazarus LCL framework. It allows to create Win32, Win64, Mac OS-X, iOS, Android, Linux, Raspbian, ... applications with a single codebase and a single learning curve.

Concept

FNC controls enable you to write and use 100% identical Pascal code, be it in a VCL app, FMX app or LCL app. The FNC components methods, properties and events are therefore 100% identical regardless of the framework being used. As an example, the following code creates a new event in our FNC Planner control:

var
  plIt: TTMSFNCPlannerItem;
begin
  plIt := TMSFNCPlanner1.Items.Add;
  plIt.StartTime := Date + EncodeTime(8,30,0,0);
  plIt.EndTime := Date + EncodeTime(11,0,0,0);
  plIt.Title := 'New event';
  plIt.Text := 'Content';
end;
and from this code, it is impossible to tell whether it will be from a VCL, FMX or LCL app. In the application UI, it will also look exactly the same regardless of framework or operating system:


This means that if you properly separate your logic or adopt an MVC approach, you can easily share .PAS files between VCL and FMX projects, between VCL and LCL projects etc... There are in this respect actually only two limitations. First limitation is the design-time form fileformat that is different between VCL, FMX and LCL. VCL uses the .dfm file, FMX uses the .fmx file and LCL uses the .lfm file. For applications for different devices with different form factors, it typically already requires to design the form separately for separate frameworks, so this isn't too much of a limitation. For other applications, a solution is to create the controls at runtime. A second limitation is the namespaces (unit names). To be able to register identical classnames for different framework controls in Delphi, it is required that these live in different namespaces. As such, the FNC VCL controls unit names have the prefix VCL., the FNC FMX controls unit names have the prefix FMX. and the FNC LCL controls use prefix LCL (without dot, to be able to support FPC versions older than v3.0)

In practice, this means that for the example above with the TMSFNCPlanner, the unit clauses for the different frameworks would be as below. To keep using a single source file, a solution is to set a define at project level depending on the framework and write:

uses 
{$IFDEF VCL}
  VCL.TMSFNCPlannerBase, VCL.TMSFNCPlannerData, VCL.TMSFNCPlanner, VCL.TMSFNCCustomControl;
{$ENDIF}

{$IFDEF FMX}
  FMX.TMSFNCPlannerBase, FMX.TMSFNCPlannerData, FMX.TMSFNCPlanner, FMX.TMSFNCCustomControl;
{$ENDIF}

{$IFDEF LCL}
 LCLTMSFNCPlannerBase,  LCLTMSFNCPlannerData, LCLTMSFNCPlanner, LCLTMSFNCCustomControl;    
{$ENDIF}
In the same way, when used, we could include the resource of our form file that is different in each framework via a conditional define:

{$IFDEF VCL}
{$R *.dfm}
{$ENDIF}

{$IFDEF FMX}
{$R *.fmx}
{$ENDIF}

{$IFDEF LCL}
{$R *.lfm}
{$ENDIF}
These are of course the things you need to take in account when you want to create a single codebase to build projects with multiple frameworks. In other cases, you do not need to take care of this and you can enjoy the exact same feature set of this UI component library irrespective of the IDE and platform you target.

Another important core concept is the introduction of the TMS FNC Graphics library that is included. This enables you to write graphics code that is framework independent. This includes framework neutral colors, fill, stroke, alignment, font, path types and the TTMSFNCGraphics class using this to draw everything you need. This is a sample code snippet of framework neutral drawing:

var
  gr: TTMSFNCGraphics;
begin
  gr := TTMSFNCGraphics.Create(PaintBox1.Canvas);

  gr.Fill.Color := gcYellow;
  gr.Stroke.Color := gcGray;

  gr.DrawRectangle(0,0,100,20);

  gr.Font.Color := gcRed;
  gr.DrawText(2,0,100,20,'Hello world',false)
  gr.Free;
end;
The result is:


and is exactly the same on every framework, target, device, ...

Controls

In TMS FNC UI Pack v1.0, there are already 29 controls included. On the tool palette this looks like:

VCL, FMX



LCL



This includes a grid, planner, richeditor, treeview, various color, font, fontsize, bitmap ... picker , toolbar, ... and more.

Introducing the TMS FNC Grid

The TMS FNC Grid is a high-performance, not data-bound grid capable of dealing with hundreds of thousands of rows, has a wide range of cell types and inplace editors, offers built-in sorting, filtering and grouping and can import and export data in several file formats. To illustrate some of the capabilities of the TMS FNC Grid, here is some framework code initializing the grid to show several cell capability features:

begin
  TMSFNCGrid1.FixedRows := 2;
  TMSFNCGrid1.ColumnCount := 7;
  TMSFNCGrid1.MergeCells(1,0,2,1);
  TMSFNCGrid1.MergeCells(3,0,2,1);
  TMSFNCGrid1.MergeCells(5,0,2,1);

  TMSFNCGrid1.Cells[1,0] := 'Monday';
  TMSFNCGrid1.HorzAlignments[1,0] := gtaCenter;
  TMSFNCGrid1.Cells[1,1] := 'AM';
  TMSFNCGrid1.Cells[2,1] := 'PM';

  TMSFNCGrid1.Cells[3,0] := 'Tuesday';
  TMSFNCGrid1.HorzAlignments[3,0] := gtaCenter;
  TMSFNCGrid1.Cells[3,1] := 'AM';
  TMSFNCGrid1.Cells[4,1] := 'PM';

  TMSFNCGrid1.Cells[5,0] := 'Wednesday';
  TMSFNCGrid1.HorzAlignments[5,0] := gtaCenter;
  TMSFNCGrid1.Cells[5,1] := 'AM';
  TMSFNCGrid1.Cells[6,1] := 'PM';
  TMSFNCGrid1.AutoNumberCol(0);

  TMSFNCGrid1.AddCheckBox(1,2,false);
  TMSFNCGrid1.AddRadioButton(1,3,1);

  TMSFNCGrid1.AddProgressBar(3,2,50);
  TMSFNCGrid1.Cells[3,3] := 'Hello world';

  TMSFNCGrid1.AddBitmapFile(5,2,'e:	mscalendar.png');
  TMSFNCGrid1.AddBitmapFile(5,3,'e:	msmail.png');

  TMSFNCGrid1.Cells[1,4] := 'Red';
  TMSFNCGrid1.Colors[1,4] := gcRed;
  TMSFNCGrid1.Cells[3,4] := 'Yellow';
  TMSFNCGrid1.Colors[3,4] := gcYellow;
  TMSFNCGrid1.Cells[5,4] := 'Lime';
  TMSFNCGrid1.Colors[5,4] := gcLime;

  TMSFNCGrid1.FontNames[1,4] := 'Courier New';
  TMSFNCGrid1.FontStyles[3,4] := [fsBold];
  TMSFNCGrid1.FontSizes[5,4] := 12;

  TMSFNCGrid1.AddNode(2,2);
end;


As another quick introduction to the grid, this 2 line snippet demonstrates how data from a CSV file can be loaded and automatic filtering via a drop down in the column header is enabled:

  TMSFNCGrid1.LoadFromCSV('e:	mscars.csv');
  TMSFNCGrid1.Options.Filtering.DropDown := true;

Of course, this is just a very brief introduction to the TMS FNC Grid. Just the FNC grid alone could deserve multiple articles to cover it in detail. You can familiarize yourself with the TMS FNC Grid by reading the full PDF developers guide you can find at http://www.tmssoftware.biz/download/manuals/TMSFNCGridDevGuide.pdf or use the trial or full version of the component that comes with several samples.

Introducing the TMS FNC Planner


Our TMS FNC Planner is a scheduling component with various built-in time-axis options, i.e. a day, week, month, period, half-day period, timeline as well as custom time-axis mode where you can fully control the duration of each timeslot in the Planner. The Planner supports single and multi resource views and can have the time-axis horizontal or vertical. When targetting the Planner to a mobile device, it will automatically use a touch-friendly approach to select, insert, delete, pan in the Planner. In a previous blog, we have presented a fully framework neutral sample TVGuide application that uses our TMS FNC Planner in this blog article: http://www.tmssoftware.com/site/blog.asp?post=335

Introducing the TMS FNC RichEditor

With the TMS FNC Rich Editor you can assemble a WordPad-style editor or Outlook style mail application in a matter of minutes. TMS FNC Rich Editor comes with capabilities to do WYSIWYG editing of rich text with images, URLs, bullet lists, custom graphics, mail merging etc... To make development even faster, there is a pre-built toolbar for rich editor editing and formatting and non-visual components to facilitate the import and export from HTML & RTF files and that of course in all frameworks, operating systems and target devices supported.

In this introduction sample, drop the TTMSFNCRichEditor on the form as well as the TTMSFNCRichEditorFormatToolbar and assign the TTMSFNCRichEditor to TTMSFNCRichEditorFormatToolbar.RichEditor. Also add a TTMSFNCRichEditorHTMLIO and TTMSFNCRichEditorRTFIO non-visual component on the form and also assign the TTMSFNCRichEditor to TTMSFNCRichEditorHTMLIO.RichEditor and TTMSFNCRichEditorRTFIO.RichEditor.

The rich editor content can be initialized with following code to perform a mail-merge that uses here two merge fields : NAME and EMAIL.

begin
  TMSFNCRichEditor1.AddText('Dear Mr. NAME');
  TMSFNCRichEditor1.AddLineBreak;
  TMSFNCRichEditor1.AddText('CC: EMAIL');
  TMSFNCRichEditor1.SelStart := 9;
  TMSFNCRichEditor1.SelLength := 4;
  TMSFNCRichEditor1.SetSelectionMergeField('NAME');

  TMSFNCRichEditor1.SelStart := 21;
  TMSFNCRichEditor1.SelLength := 5;
  TMSFNCRichEditor1.SetSelectionMergeField('EMAIL');
end;
When the app is started, the text can be further decorated by editing & formatting via the toolbar. When it is ready, following code performs the merge with the NAME and EMAIL field and is exported to RTF via TTMSFNCRichEditorRTFIO and after this, the merge is undone:


var
  sl: TStringList;
begin
  sl := TStringList.Create;

  try
    sl.Add('NAME=Elon Musk');
    sl.Add('EMAIL=elon@tesla.com');
    TMSFNCRichEditor1.Merge(sl);
  finally
    sl.Free;
  end;

  TMSFNCRichEditorRTFIO1.Save('e:	msmerge.rtf');

  TMSFNCRichEditor1.UnMerge;
end;

Introducing the TMS FNC TreeView

Finally, another large feature-packed control from the TMS FNC UI Controls set we want to introduce is the TMS FNC TreeView, TTMSFNCTreeView. This is a multi-column treeview control with regular mode and virtual mode and designed for and capable of using millions of nodes. In addition, the nodes support rich information, text atttributes can be customized per node cell, HTML formatted text in node cells is possible, images, checkboxes can be added and optional inplace editing is available.

In this introduction we'd like to demonstrate the difference in regular (node collection) based mode and virtual mode when using the TTMSFNCTreeView. The first code snippet demonstrates an initialization of a 2 column treeview:

var
  tn,cn: TTMSFNCTreeViewNode;
begin
  TMSFNCTreeView1.BeginUpdate;

  TMSFNCTreeView1.Columns.Clear;
  TMSFNCTreeView1.Nodes.Clear;

  TMSFNCTreeView1.Columns.Add.Text := 'Country';
  TMSFNCTreeView1.Columns.Add.Text := 'Capital';

  tn := TMSFNCTreeView1.AddNode(nil);
  tn.Text[0] := 'Europe';

  cn := TMSFNCTreeView1.AddNode(tn);
  cn.Text[0] := 'Germany';
  cn.Text[1] := 'Berlin';

  cn := TMSFNCTreeView1.AddNode(tn);
  cn.Text[0] := 'France';
  cn.Text[1] := 'Paris';

  cn := TMSFNCTreeView1.AddNode(tn);
  cn.Text[0] := 'United Kingdom';
  cn.Text[1] := 'London';

  tn := TMSFNCTreeView1.AddNode(nil);
  tn.Text[0] := 'Asia';

  cn := TMSFNCTreeView1.AddNode(tn);
  cn.Text[0] := 'Japan';
  cn.Text[1] := 'Tokyo';

  cn := TMSFNCTreeView1.AddNode(tn);
  cn.Text[0] := 'China';
  cn.Text[1] := 'Peking';

  TMSFNCTreeView1.EndUpdate;
end;



Important to note here is that the text in the multiple columns of the treeview can be simply accessed with an array indexed Node.Text[]: string property.

In a next step, we'll use the TTMSFNCTreeView in virtual mode and insert 1 million nodes! Columns are not virtual, so this must be initialized and to keep it simple, this will be initialized to one column:

begin
  TMSFNCTreeView1.Columns.Clear;
  TMSFNCTreeView1.Columns.Add;
  TMSFNCTreeView1.Columns[0].Text := 'Large treeview';
end;

To use the TTMSFNCTreeView in virtual mode, two events are crucial: the OnGetNumberOfNodes() event and the OnGetNodeText() event. The first is triggered to know how many nodes at root level or child level should be added. The latter is used to retrieve the column text of the node. Let's start with the OnGetNumberOfNodes event. This event has parameters ANode and a var parameter ANumberOfNodes. ANode is either a node with ANode.Level set to -1 indicating the number of root level nodes is requested or it contains the node for which the number of child nodes is requested. With the ANode.Level property, you can know how many hierarchical levels deep the node is. In this example, we'll insert 1 million (100x100x100) nodes by inserting 100 root level nodes that have each 100 childs and each child has again 100 subchilds.

This event takes care of this:

procedure TForm1.TMSFNCTreeView1GetNumberOfNodes(Sender: TObject;
  ANode: TTMSFNCTreeViewVirtualNode; var ANumberOfNodes: Integer);
begin
    if ANode.Level = -1 then
    ANumberOfNodes := 100
  else  
  if ANode.Level = 0 then
    ANumberOfNodes := 100
  else  
  if ANode.Level = 1 then
    ANumberOfNodes := 100;
end;

Then, the other event for virtual node handling, OnGetNodeText is used to return the text for node columns. Note that this event will be triggered for each node and for each column for this node. The column for which the event is triggered is indicated with the AColumn parameter. As we have only 1 column in this example, this is ignored and the node text is directly returned:

procedure TForm1.TMSFNCTreeView1GetNodeText(Sender: TObject;
  ANode: TTMSFNCTreeViewVirtualNode; AColumn: Integer;
  AMode: TTMSFNCTreeViewNodeTextMode; var AText: string);
begin
  if ANode.Level = 0 then
    AText := 'Root node '+inttostr(ANode.Index)
  else
  if ANode.Level = 1 then
    AText := 'Child node '+inttostr(ANode.Index)
  else
  if ANode.Level = 2 then
    AText := 'Subchild node '+inttostr(ANode.Index);
end;




We hope this brief introduction of the major controls in the TMS FNC UI Pack whetted your appetite to start exploring the components, discovering the benefits and efficiency of having one UI component set to cover all the target operating systems you want to target and perhaps cook up your first Linux GUI apps with LCL. We're eager to learn how your experience is going and to hear your feedback, comments and further wishes and needs in this direction.

Bruno Fierens


Bookmarks: 

This blog post has received 4 comments. Add a comment.



A single-source TV-guide for 6 operating systems

Wednesday, April 06, 2016

Introduction

With the first release of our brand new TMS FNC UI Pack we are venturing into a new way of designing and creating components. A way that allows developers to easily switch between 3 frameworks (FMX, VCL and LCL). As the TMS FNC UI Pack targets these three frameworks it automatically comes with support for a multitude of operating systems. As we wanted to take "easily switching between 3 frameworks" to the test we have created a TV-guide application that uses the planner component, parses JSON retrieved with our TMS Cloud Components and made it running on 6! major operating systems: Windows 10, Mac OS X Yosemite, iOS 9.0, Android Lollipop, Ubuntu and Raspbian.

Click image for more screenshots.

Cross-platform, cross-framework code

Creating our business logic

After installing the TMS FNC UI Pack the TTMSFNCPlanner component is available on FMX, VCL and LCL and we are ready to start developing applications. Now, to start using it, it would be wise to think a few moments on how to write an application that is running on multiple frameworks, multiple operating systems. If we want to start with VCL and want to move to FMX in a couple of months, it would not be very resource and time friendly to write an application that does not use the power of FNC. Therefore we want to create a single source universal business logic unit that will be used in three different projects, one for every framework. To create a single source unit and use it in different projects, which is compatible with FMX, VCL and LCL we need to add a conditional define to our project to identify each framework, if only because unit names for FNC components must be different (requirement in the Delphi IDE hosting both FMX & VCL).





To initialize the planner and retrieving data from our service, we start by adding a TTVGuideLogic class that is instantiated in each separate project main form unit and contains the business logic for the app.
  TTVGuideLogic = class
  private
    FPlanner: TTMSFNCPlanner;
    FChannels: TTVChannels;
    FAccess: TCloudAccess;
  public
    destructor Destroy; override;
    function GetJSONArray(URL: string; AID: String = ''): TJSONArray;
    function FindChannelByName(AName: String): TTVChannel;
    procedure InitPlanner(APlanner: TTMSFNCPlanner);
    procedure InitChannels;
    procedure UpdateResources(AChannel: TTVChannel; AResource: Integer);
  end;
Each framework has its own set of units in order to compile succesfully. We use the conditional defines added to our project to make the difference between each framework.
uses
  {$IFDEF VCL}
  Classes, SysUtils, VCL.TMSFNCPlanner, VCL.TMSFNCCustomControl, VCL.TMSFNCPlannerBase, VCL.TMSFNCPlannerData, CloudBase,
  Generics.Collections, JSON, VCL.TMSFNCGraphics, VCL.TMSFNCUtils, DateUtils;
  {$ENDIF}

  {$IFDEF FMX}
  Classes, SysUtils, FMX.TMSFNCPlanner, FMX.TMSFNCCustomControl, FMX.TMSFNCPlannerBase, FMX.TMSFNCPlannerData, FMX.TMSCloudBase,
  Generics.Collections, JSON, FMX.TMSFNCGraphics, FMX.TMSFNCUtils, DateUtils;
  {$ENDIF}

  {$IFDEF LCL}
  Classes, SysUtils, LCLTMSCloudBase, LCLTMSFNCPlanner, LCLTMSFNCPlannerBase, LCLTMSFNCPlannerData, LCLTMSFNCGraphics, LCLTMSFNCUTils, DateUtils,
  fgl, fpjson, jsonparser;
  {$ENDIF}

Initializing the planner

The initialization code for the planner look & feel is added to the InitPlanner method, which is called after creating an instance of the TTVGuideLogic class in your project. When comparing this to our unit section, you will notice it doesn't require any conditional defines in order to succesfully compile. With the TMS FNC UI Pack we have added a few helper units to set the font size, set the color and have also created our own fill and stroke classes that are used in every FNC component.
procedure TTVGuideLogic.InitPlanner(APlanner: TTMSFNCPlanner);
var
  I: Integer;
begin
  FPlanner := APlanner;
  FPlanner.BeginUpdate;
  FPlanner.Items.Clear;
  FPlanner.Positions.Count := 6;
  FPlanner.OrientationMode := pomHorizontal;

  FPlanner.DefaultItem.TitleColor := gcSlategray;
  FPlanner.DefaultItem.TitleFontColor := gcWhite;
  FPlanner.DefaultItem.Color := gcWhitesmoke;
  FPlanner.DefaultItem.ActiveColor := gcSlateGray;

  FPlanner.Interaction.ReadOnly := True;

  FPlanner.Resources.Clear;
  for I := 0 to FPlanner.Positions.Count - 1 do
    FPlanner.Resources.Add;

  FPlanner.ItemsAppearance.Stroke.Color := gcWhite;
  FPlanner.ItemsAppearance.Stroke.Kind := gskSolid;
  FPlanner.ItemsAppearance.Stroke.Width := 2;
  FPlanner.ItemsAppearance.TitleStroke.Assign(FPlanner.ItemsAppearance.Stroke);

  FPlanner.GridCellAppearance.InActiveFill.Assign(FPlanner.GridCellAppearance.Fill);
  FPlanner.PositionsAppearance.Layouts := [pplTop, pplBottom];

  FPlanner.ModeSettings.StartTime := Now;
  FPlanner.ModeSettings.EndTime := Now;
  FPlanner.Mode := pmDay;

  FPlanner.TimeLineAppearance.Layouts := [ptlLeft, ptlRight];
  FPlanner.TimeLineAppearance.RightVerticalTextAlign := gtaTrailing;
  FPlanner.TimeLineAppearance.RightSubVerticalTextAlign := gtaLeading;
  FPlanner.TimeLine.CurrentTimePosition := pctpOverItems;
  FPlanner.TimeLine.DisplayUnitType := pduMinute;
  FPlanner.TimeLine.DisplayUnit := 5;
  FPlanner.TimeLine.DisplayStart := 0;
  FPlanner.TimeLine.DisplayEnd := (MinsPerDay div FPlanner.TimeLine.DisplayUnit) - 1;

  TTMSFNCUtils.SetFontSize(FPlanner.ItemsAppearance.TitleFont, 14);
  TTMSFNCUtils.SetFontSize(FPlanner.PositionsAppearance.BottomFont, 14);
  TTMSFNCUtils.SetFontSize(FPlanner.PositionsAppearance.TopFont, 14);

  FPlanner.EndUpdate;
  FPlanner.TimeLine.ViewStart := IncHour(Now, -2);

  InitChannels;

  UpdateResources(FindChannelByName('MTV'), 0);
  UpdateResources(FindChannelByName('Eurosport 1'), 1);
  UpdateResources(FindChannelByName('BBC 1'), 2);
  UpdateResources(FindChannelByName('TLC'), 3);
  UpdateResources(FindChannelByName('Disney XD'), 4);
  UpdateResources(FindChannelByName('CNN'), 5);
end;

Using the cloud to access information

A TV-guide application would only be a TV-guide application if it would show some TV-channels and the TV-shows that are playing at a specific time range. In the previous code snippet we have initialized the planner to show a time range of 24 hours, and the service that is used to retrieve the TV-shows of a specific TV-channel is parameterized to always return the TV-shows of today. To keep a reference to TV-channels and TV-shows we additionally add the classes needed to retrieve and persist information. In this code snippet, we have just conditional defines because of a small difference in handling generic lists between the Delphi compiler and the FPC compiler and the TMS Cloud access classes that have a different class name for VCL, FMX and LCL.
  TTVChannel = class;
  TTVShow = class;

  {$IFDEF VCL}
  TCloudAccess = class(TCloudBase);
  TTVShows = TObjectList<TTVShow>;
  TTVChannels = TObjectList<TTVChannel>;
  {$ENDIF}

  {$IFDEF FMX}
  TCloudAccess = class(TTMSFMXCloudBase);
  TTVShows = TObjectList<TTVShow>;
  TTVChannels = TObjectList<TTVChannel>;
  {$ENDIF}

  {$IFDEF LCL}
  TCloudAccess = class(TTMSLCLCloudBase);
  TTVShows = specialize TFPGObjectList<TTVShow>;
  TTVChannels = specialize TFPGObjectList<TTVChannel>;
  {$ENDIF}

  TTVShow = class
  private
    FGenre: string;
    FStartTime: TDateTime;
    FTitle: string;
    FID: string;
    FEndTime: TDateTime;
    FKind: string;
  public
    property ID: string read FID write FID;
    property Title: string read FTitle write FTitle;
    property Genre: string read FGenre write FGenre;
    property Kind: string read FKind write FKind;
    property StartTime: TDateTime read FStartTime write FStartTime;
    property EndTime: TDateTime read FEndTime write FEndTime;
  end;

  TTVChannel = class
  private
    FName: string;
    FID: string;
    FShows: TTVShows;
  public
    constructor Create;
    destructor Destroy; override;
    property ID: string read FID write FID;
    property Name: string read FName write FName;
    property Shows: TTVShows read FShows;
  end;
After defining the necessary classes, we create an instance of our cloud base component for FMX (TTMSFMXCloudBase) and we use that instance to retrieve our TV-channels in JSON.
procedure TTVGuideLogic.InitChannels;
var
  i: integer;
  arr: TJSONArray;
  o: TJSONObject;
  c: TTVChannel;
begin
  FChannels := TTVChannels.Create;

  FAccess := TCloudAccess.Create(nil);
  arr := GetJSONArray('http://www.tvgids.nl/json/lists/channels.php');
  if Assigned(arr) then
  begin
    for i := 0 to GetArraySize(arr) - 1 do
    begin
      o := GetArrayItem(arr, i) as TJSONObject;
      c := TTVChannel.Create;
      c.ID := FAccess.GetJSONProp(o,'id');
      c.Name := StringReplace(FAccess.GetJSONProp(o,'name'), '&eacute;', 'é', [rfReplaceAll]);
      FChannels.Add(c);
    end;
  end;
end;
And we call InitChannels in our planner initialization code
  InitChannels;
After retrieving the TV-channels, the TV-shows are also retrieved, parsed and added as items to our TTMSFNCPlanner.
procedure TTVGuideForm.UpdateResources(AChannel: TTVChannel; AResource: Integer);
var
  c: TTVChannel;
  s: TTVShow;
  arr: TJSONArray;
  i: Integer;
  o: TJSONObject;
  it: TTMSFNCPlannerItem;
  dt: TDateTime;
begin
  dt := Now;
  TMSFNCPlanner1.BeginUpdate;
  for I := TMSFNCPlanner1.Items.Count - 1 downto 0 do
  begin
    if TMSFNCPlanner1.Items[I].Resource = AResource then
      TMSFNCPlanner1.Items[I].Free;
  end;

  TMSFNCPlanner1.Resources[AResource].Text := 'No Channel Selected';
  if Assigned(AChannel) then
  begin
    c := AChannel;
    c.Shows.Clear;
    TMSFNCPlanner1.Resources[AResource].Text := c.Name;
    arr := GetJSONArray('http://www.tvgids.nl/json/lists/programs.php?channels='+c.ID+'&day=0', c.ID);
    if Assigned(arr) then
    begin
      for i := 0 to GetArraySize(arr) - 1 do
      begin
        o := GetArrayItem(arr, i) as TJSONObject;
        s := TTVShow.Create;
        s.ID := FAccess.GetJSONProp(o,'db_id');
        s.Title := FAccess.GetJSONProp(o,'titel');
        s.Genre := FAccess.GetJSONProp(o,'genre');
        s.Kind  := FAccess.GetJSONProp(o,'soort');
        s.StartTime := FAccess.IsoToDateTime(FAccess.GetJSONProp(o,'datum_start'));
        s.EndTime := FAccess.IsoToDateTime(FAccess.GetJSONProp(o,'datum_end'));
        c.Shows.Add(s);
        it := TMSFNCPlanner1.AddOrUpdateItem(s.StartTime, s.EndTime, s.Title, s.Kind);
        it.Resource := AResource;
        it.Hint := it.Title + ' [' + TimeToStr(Frac(s.StartTime)) + ' - ' + TimeToStr(Frac(s.EndTime)) + ']';
        if (dt >= it.StartTime) and (dt <= it.EndTime) then
        begin
          it.Color := gcYellowgreen;
          it.FontColor := gcWhite;
        end;
      end;
    end;
  end;

  TMSFNCPlanner1.EndUpdate;
end;
The UpdateResources call is also added to our planner initialization code and retrieves the channel of choice and adds an item for each show to the planner.
  UpdateResources(FindChannelByName('MTV'), 0);
  UpdateResources(FindChannelByName('Eurosport 1'), 1);
  UpdateResources(FindChannelByName('BBC 1'), 2);
  UpdateResources(FindChannelByName('TLC'), 3);
  UpdateResources(FindChannelByName('Disney XD'), 4);
  UpdateResources(FindChannelByName('CNN'), 5);
To starting using the code from this separate unit, we add the unit to our project and use the following code, after dropping an instance of the TTMSFNCPlanner on the form.
procedure TTVGuideForm.FormCreate(Sender: TObject);
begin
  FTVGuideLogic := TTVGuideLogic.Create;
  FTVGuideLogic.InitPlanner(TMSFNCPlanner1);
end;

procedure TTVGuideForm.FormDestroy(Sender: TObject);
begin
  FTVGuideLogic.Free;
end;
After succesfully retrieving our TV-channels and TV-shows, the application runs on Windows 10, but it could very well be iOS 9, or MAC OS X Yosemite, or Raspbian, or ...

The full source code is available for download

Click image for more screenshots.

Pieter Scheldeman


Bookmarks: 

This blog post has not received any comments yet. Add a comment.




Previous  |  Next  |  Index