Alvin Lin
Alvin Lin

Reputation: 199

Call Procedure on Separate Unit with Timer

I am trying to write a separate unit for my main form to call, all of my other units are working except for one that uses TTimer.

Basically what the function is supposed to be doing is that the main form uDataReceived calls BlinkRect(Gateway) which is processed in rRectControl unit and the according Rectangle will blink in the main form.

Here are the codes:

unit uRectControl;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes,
  System.Variants, System.IOUtils, FMX.Graphics, FMX.Types, FMX.Objects;

var
  Blinks: array [0 .. 2] of record Rectangle: TRectangle;
  Timer: TTimer;
end;

type
  TMyClass = Class(TObject)
  private
    Timer1: TTimer;
    procedure Timer1Timer(Sender: TObject);
  public
    procedure BlinkRect(Gateway: integer);
  end;

procedure AssignRectangles;

implementation

uses uDataReceived;
// Error shows "Cannot resolve unit name 'uDataReceived'

{ TMyClass }

procedure AssignRectangles;
var
  i: integer;
begin
  Blinks[0].Rectangle := TC_Theft_Detection.rect1;
  // Error shows Undeclared Identifier TC_Theft_Detection (which is the name of the main form)
  Blinks[0].Timer := nil;

  Blinks[1].Rectangle := TC_Theft_Detection.rect2;
  Blinks[1].Timer := nil;

  Blinks[2].Rectangle := TC_Theft_Detection.rect3;
  Blinks[2].Timer := nil;

  for i := 0 to 2 do
    Blinks[i].Rectangle.Fill.Color := TAlphacolors.blue;
end;

procedure TMyClass.BlinkRect(Gateway: integer);
begin
  Blinks[Gateway].Rectangle.Fill.Color := TAlphacolors.Red;
  Blinks[Gateway].Rectangle.Fill.Kind := TBrushKind.Solid;
  Blinks[Gateway].Rectangle.Stroke.Thickness := 0.3;
  Blinks[Gateway].Rectangle.Stroke.Color := TAlphacolors.Black;
  if Blinks[Gateway].Timer = nil then
  begin
    Blinks[Gateway].Timer := TTimer.Create(nil);
    Blinks[Gateway].Timer.OnTimer := Timer1Timer;
    Blinks[Gateway].Timer.Interval := 500;
    Blinks[Gateway].Timer.Tag := Gateway;
    Blinks[Gateway].Timer.Enabled := True;
  end;
end;

procedure TMyClass.Timer1Timer(Sender: TObject);
var
  Timer: TTimer;
begin
  Timer := TTimer(Sender);
  Blinks[Timer.Tag].Rectangle.Visible := not Blinks[Timer.Tag]
    .Rectangle.Visible;
end;

end.

I know there must be something wrong with the unit shown above, and my question is:

How to work with TTimer in a separate unit and how to call the procedure BlinkRect(Gateway) on the main form.

Thanks a lot!!

Upvotes: 0

Views: 826

Answers (1)

Tom Brunberg
Tom Brunberg

Reputation: 21045

Your code in uRectControl works provided AssignRectangles is called before you attempt to call BlinkRect. However there are a number of issues to be addressed.

1) Cross dependency of units

The form (uDataReceived) apparently uses uRectControl and that is fine. The way uRectControl is written it needs to use (uses uDataReceived in the implementation) the form and this is not good. This error is simple to correct, because the AssignRectangles procedure is the only place where the form is referred to. AssignRectangles could just as well be in the form, since the Blinks[] array is global (in the interface of uRectControl) and can therefore be accessed by the form.

2) Global variables

Global variables should be avoided as much as possible. You have defined both the Blinks[] array and the Timer to be global, so you might by mistake access and modify them from anywhere in your program just by adding uRectControl to a uses clause. In future development you might add new forms that have indicators you want to blink and add TRectangles to the Blinks[] array possibly overwriting value that are already there and you end up in a mess. I will address this issue in my suggestion below.

3) Hardcoded entities

In Proof Of Concept code it is acceptable (or not) to hardcode constants, sizes of arrays etc. but not in production code. Just think about all changes you need to do just to add one more blinking rectangle to the form. Dynamical arrays or better TList and its derivatives etc. comes to rescue here. You have also limited yourself to only TRectangles. What if you would like to have circular indicators in your form?

4) Unsyncronized blinking

It may look cool (not really) when indicators are blinking all over the place, but actually it is just distracting. I guess you tried to change this with the timer in TMyClass, but you still left the individual timers in the Blinks records. I will address this also in my suggestion below.

Here is a suggestion

unit ShapeBlinker;
interface
uses
  System.SysUtils, System.UITypes, System.Classes, System.Generics.Collections,
  FMX.Graphics, FMX.Types, FMX.Objects;

type
  TBlinkState = (bsOff, bsBlinking, bsSteady);

I have a background in Fire Alarm Systems, and it is common to have three states; off, blinking and steady lit. TBlinkState represents these.

Then comes a class that represent indicators in the UI. An indicator can be any TShape derivative like TRectangle, TCircle, TPath etc. Each state can have its own color.

type
  [...]
  TBlinkingShape = class
  private
    FShape: TShape;
    FState: TBlinkState;
    FOffColor: TAlphaColor;
    FBlinkColor: TAlphaColor;
    FSteadyColor: TAlphaColor;
  public
    constructor Create(AShape: TShape);
    procedure SetBlinkState(NewState: TBlinkState);
  end;

The field FShape holds a reference to a TShape derivative. Through this reference we have access to the actual component on the UI form and can change its color. We will see later how the TShape is passed to the constructor.

Then the second class which manages a collection of TBlinkingShape, timing and actual color changes of the indicators on the form.

type
  [...]
  TShapeBlinker = class
  private
    FBlinkingShapes: TObjectList<TBlinkingShape>;
    FBlinkPhase: integer;
    FTimer: TTimer;
  public
    constructor Create;
    destructor Destroy; override;
    procedure RegisterShape(Shape: TShape; OffColor, BlinkColor, SteadyColor: TAlphaColor);
    procedure UnRegisterShape(Shape: TShape);
    procedure BlinkTimer(Sender: TObject);
    procedure SetBlinkState(Shape: TShape; NewState: TBlinkState);
    function GetBlinkState(Shape: TShape): TBlinkState;
  end;

FBlinkingShapes is the object list that holds instances of TBlinkingShapes. FBlinkPhase syncronizes blinking of the indicators so that all blinking indicators change to the BlinkColor simultaneously. FTimer is common for all indicators. Procedure RegisterShape is called by the UI when it wants to add an indicator to the list. UnRegister is called when an indicator is to be removed from the list. SetBlinkState is used to change state and GetBlinkState to retrieve the state of an indicator.

The unit is designed to be usable by any number of forms, synchronizing blinking for all of them. This requires that the TShapeBlinker is a singleton. It is therefore created in the initialization section of the unit, and freed in the finalization. The instance is held by a var in the implementation, thus inaccessible directly from any other unit. Access is provided by a function declared as the last item in the interface of the unit:

  function ShapeBlinker: TShapeBlinker;

This effectively prevents a mistake to accidentally call ShapeBlinker.Create.

Instead of commenting on each method I just copy the implementation here:

implementation

var
  SShapeBlinker: TShapeBlinker;

function ShapeBlinker: TShapeBlinker;
begin
  result := SShapeBlinker;
end;

{ TBlinkingShape }

constructor TBlinkingShape.Create(AShape: TShape);
begin
  FShape := AShape;
  FState := bsOff;
end;

procedure TBlinkingShape.SetBlinkState(NewState: TBlinkState);
begin
  FState := NewState;
  case NewState of
    bsOff: begin
      FShape.Fill.Color := FOffColor;
    end;
    bsBlinking: begin
      FShape.Fill.Color := FBlinkColor;
    end;
    bsSteady: begin
      FShape.Fill.Color := FSteadyColor;
    end;
  end;
end;

{ TShapeBlinker }

constructor TShapeBlinker.Create;
begin
  FBlinkingShapes := TObjectList<TBlinkingShape>.Create;
  FTimer := TTimer.Create(nil);
  FTimer.OnTimer := BlinkTimer;
  FTimer.Interval := 500;
  FTimer.Enabled := False;
end;

destructor TShapeBlinker.Destroy;
begin
  FTimer.Enabled := False;
  FTimer.Free;
  FBlinkingShapes.Free;
  inherited;
end;

function TShapeBlinker.GetBlinkState(Shape: TShape): TBlinkState;
var
  RegShape: TBlinkingShape;
begin
  result := bsOff;
  for RegShape in FBlinkingShapes do
    if Shape = RegShape.FShape then result := RegShape.FState;
end;

procedure TShapeBlinker.SetBlinkState(Shape: TShape; NewState: TBlinkState);
var
  RegShape: TBlinkingShape;
begin
  for RegShape in FBlinkingShapes do
    if Shape = RegShape.FShape then RegShape.SetBlinkState(NewState);
  self.FTimer.Enabled := True;
end;

procedure TShapeBlinker.BlinkTimer(Sender: TObject);
var
  i: integer;
begin
  FTimer.Enabled := False;
  FBlinkPhase := (FBlinkPhase + 1) mod 2;
  for i := 0 to FBlinkingShapes.Count-1 do
  with FBlinkingShapes[i] do
  begin
    case FState of
      bsOff: begin
        FShape.Fill.Color := FOffColor;
      end;
      bsBlinking: begin
        if FBlinkPhase = 1 then
          FShape.Fill.Color := FOffColor // alt. FSteadyColor
        else
          FShape.Fill.Color := FBlinkColor;
        FTimer.Enabled := True;
      end;
      bsSteady: begin
        FShape.Fill.Color := FSteadyColor;
      end;
    end;
  end;
end;

procedure TShapeBlinker.RegisterShape(Shape: TShape; OffColor, BlinkColor, SteadyColor: TAlphaColor);
begin
  with FBlinkingShapes[FBlinkingShapes.Add(TBlinkingShape.Create(Shape))] do
  begin
    FOffColor := OffColor; //TAlphaColors.Silver;
    FBlinkColor := BlinkColor; //TAlphaColors.Red;
    FSteadyColor := SteadyColor; //TAlphaColors.Yellow;
  end;
end;

procedure TShapeBlinker.UnRegisterShape(Shape: TShape);
var
  i: integer;
begin
  for i := FBlinkingShapes.Count-1 downto 0 do
    if FBlinkingShapes[i].FShape = Shape then
      FBlinkingShapes.Delete(i);
end;

initialization

  SShapeBlinker := TShapeBlinker.Create;

finalization

  SShapeBlinker.Free;

end.

Finally a few words about usage. Consider a form, say TAlarmView, with 2 TRectangle and 1 TCircle. In FormCreate you might register these for blinking as follows

procedure TAlarmView.FormCreate(Sender: TObject);
begin
  ShapeBlinker.RegisterShape(Rect1, TAlphaColors.Silver, TAlphaColors.Red, TAlphaColors.Yellow);
  ShapeBlinker.RegisterShape(Circle1, TAlphaColors.Silver, TAlphaColors.Red, TAlphaColors.Yellow);
  ShapeBlinker.RegisterShape(Rect3, TAlphaColors.Silver, TAlphaColors.Red, TAlphaColors.Yellow);
end;

and then test them with button clicks like

procedure TAlarmView.Button1Click(Sender: TObject);
begin
  case ShapeBlinker.GetBlinkState(Rect1) of
    bsOff:      ShapeBlinker.SetBlinkState(Rect1, bsBlinking);
    bsBlinking: ShapeBlinker.SetBlinkState(Rect1, bsSteady);
    else        ShapeBlinker.SetBlinkState(Rect1, bsOff);
  end;
end;

As you see I just go through the different states for each click.

Upvotes: 5

Related Questions