Blog

All Blog Posts  |  Next Post  |  Previous Post

A single-source TV-guide for 6 operating systems

Wednesday, April 6, 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.

TMS Software Delphi  Components

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).

TMS Software Delphi  Components

TMS Software Delphi  Components

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

TMS Software Delphi  Components

Click image for more screenshots.



Pieter Scheldeman




This blog post has not received any comments yet.



Add a new comment

You will receive a confirmation mail with a link to validate your comment, please use a valid email address.
All fields are required.



All Blog Posts  |  Next Post  |  Previous Post