Using VCL TTimer in Delphi console application

后端 未结 3 2011
走了就别回头了
走了就别回头了 2021-02-04 09:14

As the question subject says. I have a console application in Delphi, which contains a TTimer variable. The thing I want to do is assign an event handler to T

3条回答
  •  旧时难觅i
    2021-02-04 10:04

    As others have mentioned, console applications don't have a message pump.

    Here is a TConsoleTimer thread class which mimics a TTimer class. The main difference is that the code in the event is executed in the TConsoleTimer thread.

    Update

    At the end of this post is a way to have this event called in the main thread.

    unit ConsoleTimer;
    
    interface
    
    uses
      Windows, Classes, SyncObjs, Diagnostics;
    
    type
      TConsoleTimer = Class(TThread)
      private
        FCancelFlag: TSimpleEvent;
        FTimerEnabledFlag: TSimpleEvent;
        FTimerProc: TNotifyEvent; // method to call
        FInterval: integer;
        procedure SetEnabled(doEnable: boolean);
        function GetEnabled: boolean;
        procedure SetInterval(interval: integer);
      protected
        procedure Execute; override;
      public
        Constructor Create;
        Destructor Destroy; override;
        property Enabled : boolean read GetEnabled write SetEnabled;
        property Interval: integer read FInterval write SetInterval;
        // Note: OnTimerEvent is executed in TConsoleTimer thread
        property OnTimerEvent: TNotifyEvent read FTimerProc write FTimerProc;
      end;
    
    implementation
    
    constructor TConsoleTimer.Create;
    begin
      inherited Create(false);
      FTimerEnabledFlag := TSimpleEvent.Create;
      FCancelFlag := TSimpleEvent.Create;
      FTimerProc := nil;
      FInterval := 1000;
      Self.FreeOnTerminate := false; // Main thread controls for thread destruction
    end;
    
    destructor TConsoleTimer.Destroy; // Call TConsoleTimer.Free to cancel the thread
    begin
      Terminate; 
      FTimerEnabledFlag.ResetEvent; // Stop timer event
      FCancelFlag.SetEvent; // Set cancel flag
      Waitfor; // Synchronize
      FCancelFlag.Free;
      FTimerEnabledFlag.Free;
      inherited;
    end;
    
    procedure TConsoleTimer.SetEnabled(doEnable: boolean);
    begin
      if doEnable then
        FTimerEnabledFlag.SetEvent
      else
        FTimerEnabledFlag.ResetEvent;
    end;
    
    procedure TConsoleTimer.SetInterval(interval: integer);
    begin
      FInterval := interval;
    end;
    
    procedure TConsoleTimer.Execute;
    var
      waitList: array [0 .. 1] of THandle;
      waitInterval,lastProcTime: Int64;
      sw: TStopWatch;
    begin
      sw.Create;
      waitList[0] := FTimerEnabledFlag.Handle;
      waitList[1] := FCancelFlag.Handle;
      lastProcTime := 0;
      while not Terminated do
      begin
        if (WaitForMultipleObjects(2, @waitList[0], false, INFINITE) <>
          WAIT_OBJECT_0) then
          break; // Terminate thread when FCancelFlag is signaled
        if Assigned(FTimerProc) then
        begin
          waitInterval := FInterval - lastProcTime;
          if (waitInterval < 0) then
            waitInterval := 0;
          if WaitForSingleObject(FCancelFlag.Handle,waitInterval) <> WAIT_TIMEOUT then
            break;
    
          if WaitForSingleObject(FTimerEnabledFlag.Handle, 0) = WAIT_OBJECT_0 then
          begin
            sw.Start;
            FTimerProc(Self);
            sw.Stop;
            // Interval adjusted for FTimerProc execution time
            lastProcTime := sw.ElapsedMilliSeconds;
          end;
        end;
      end;
    end;
    
    function TConsoleTimer.GetEnabled: boolean;
    begin
      Result := (FTimerEnabledFlag.Waitfor(0) = wrSignaled);
    end;
    
    end.
    

    And a test:

    program TestConsoleTimer;
    
    {$APPTYPE CONSOLE}
    
    {$R *.res}
    
    uses
      System.SysUtils,ConsoleTimer;
    
    type
      TMyTest = class
        procedure MyTimerProc(Sender: TObject);
      end;
    
    procedure TMyTest.MyTimerProc(Sender: TObject);
    begin
      // Code executed in TConsoleTimer thread !
      WriteLn('Timer event');
    end;
    
    var
      MyTest: TMyTest;
      MyTimer: TConsoleTimer;
    begin
      MyTest := TMyTest.Create;
      try
        MyTimer := TConsoleTimer.Create;
        MyTimer.Interval := 1000;
        MyTimer.OnTimerEvent := MyTest.MyTimerProc;
        WriteLn('Press [Enter] key to end.');
        MyTimer.Enabled := true;
        ReadLn;
        MyTimer.Free;
      finally
        MyTest.Free;
        WriteLn('End.');
      end;
    end.
    

    As mentioned above, how do I make the event execute in the main thread?

    Reading Delphi 7: Handling events in console application (TidIRC) gives the answer.

    Add a method in TConsoleTimer:

    procedure TConsoleTimer.SwapToMainThread;
    begin
      FTimerProc(Self);
    end;
    

    and change the call in the Execute method to:

    Synchronize(SwapToMainThread);
    

    To pump the synchronized calls, use CheckSynchronize() function in Classes unit:

    while not KeyPressed do CheckSynchronize(); // Pump the synchronize queue
    

    Note: the console KeyPressed function can be found here:How i can implement a IsKeyPressed function in a delphi console application?.

提交回复
热议问题