delphidelphi-7

Create and run TTimer in runtime


I'm trying to achieve in Delphi a behavior similar to Javascript's setTimeout() procedure : run things after a delay of some seconds. To do so, I'm creating a TTimer at runtime, running it, and then free it.

Here is my code:

procedure createAndRunTimer();
  procedure goTimer(Sender: TObject);
  begin
    (sender as ttimer).enabled := false;
    // do stuff here
    sender.free;
  end;
var
  t : TTimer;
begin
  t := TTimer.Create(frmprinc);
  t.Interval := 5000;
  t.OnTimer := goTimer(t);
end;

But my code won't compile, the compiler returns the error below:

[DCC Error] unit1.pas(2153): E2010 Incompatible types: 'TNotifyEvent' and 'procedure, untyped pointer or untyped parameter'"

Any hints?


Solution

  • TNotifyEvent is declared as:

    TNotifyEvent = procedure(Sender: TObject) of object;
    

    The of object makes it a closure, which is a special type of method pointer that carries 2 pointers - an pointer to an object, and a pointer to a non-static class method which gets called on the object. As such, you cannot assign a standalone function, and certainly not a nested function, directly to a TNotifyEvent. That is what the compiler is complaining about.

    So, you need to declare a class to wrap your OnTimer event handler, eg:

    type
      TTimerEvents = class
      public
        procedure goTimer(Sender: TObject);
      end;
    
    procedure TTimerEvents.goTimer(Sender: TObject);
    begin
      (Sender as TTimer).Enabled := false;
    
      // do stuff here
    
      // NOTE: you cannot destroy the Sender object from here, you must delay
      // the destruction until after this handler exits!  You can post a
      // custom window message via PostMessage() and have the message handler
      // call Sender.Free().  Or, you can use a worker thread to call
      // Sender.Free() via TThread.Synchronize() (or TThread.Queue() in Delphi
      // 8 and later).  Or, in Delphi 10.2 Tokyo and later, you can call
      // Sender.Free() via TThread.ForceQueue().  Or, use whatever other
      // mechanism you want to use to call Sender.Free(), as long as it works
      // asynchronously and calls Sender.Free() in the same thread that
      // constructed the TTimer object ...
    end;
    
    var
      events: TTimerEvents;
    
    procedure createAndRunTimer();
    var
      t : TTimer;
    begin
      t := TTimer.Create(frmprinc);
      t.Interval := 5000;
      t.OnTimer := events.goTimer;
      t.Enabled := True;
    end;
    
    initialization
      events := TTimerEvents.Create;
    finalization
      events.Free;
    

    Alternatively, you can use a class method so you don't need an actual instance of the wrapper class:

    type
      TTimerEvents = class
      public
        class procedure goTimer(Sender: TObject);
      end;
    
    class procedure TTimerEvents.goTimer(Sender: TObject);
    begin
      (Sender as TTimer).Enabled := false;
    
      // do stuff here
    
      // delay-destroy the Sender as needed ...
    end;
    
    procedure createAndRunTimer();
    var
      t : TTimer;
    begin
      t := TTimer.Create(frmprinc);
      t.Interval := 5000;
      t.OnTimer := TTimerEvents.goTimer;
      t.Enabled := True;
    end;
    

    Or, in Delphi 2006 and later, you can use a class helper:

    type
      TTimerHelper = class helper for TTimer
      public
        procedure goTimer(Sender: TObject);
      end;
    
    procedure TTimerHelper.goTimer(Sender: TObject);
    begin
      (Sender as TTimer).Enabled := false;
    
      // do stuff here
    
      // delay-destroy the Sender as needed ...
    end;
    
    procedure createAndRunTimer();
    var
      t : TTimer;
    begin
      t := TTimer.Create(frmprinc);
      t.Interval := 5000;
      t.OnTimer := t.goTimer;
      t.Enabled := True;
    end;
    

    That being said, there IS a way to use a standalone function without using any class wrapper at all:

    procedure goTimer(Self: Pointer; Sender: TObject);
    begin
      (Sender as TTimer).Enabled := false;
    
      // do stuff here
    
      // delay-destroy the Sender as needed ...
    end;
    
    procedure createAndRunTimer();
    var
      t : TTimer;
      event : TNotifyEvent;
    begin
      t := TTimer.Create(frmprinc);
      t.Interval := 5000;
    
      TMethod(event).Data := nil; // or whatever you want to pass to the Self parameter...
      TMethod(event).Code := @goTimer;
      t.OnTimer := event;
    
      t.Enabled := True;
    end;