Использование VCL TTimer в консольном приложении Delphi
Как говорится в вопросе. У меня есть консольное приложение в Delphi, которое содержит переменную TTimer
. То, что я хочу сделать, это назначить обработчик события TTimer.OnTimer
. Я совершенно не знаком с Delphi, я использовал С# и добавление обработчиков событий к событиям совершенно иная. Я выяснил, что не просто назначить процедуру событию как обработчика, вам нужно создать фиктивный класс с помощью метода, который будет обработчиком, а затем назначить этот метод событию. Вот код, который у меня есть:
program TimerTest;
{$APPTYPE CONSOLE}
uses
SysUtils,
extctrls;
type
TEventHandlers = class
procedure OnTimerTick(Sender : TObject);
end;
var
Timer : TTimer;
EventHandlers : TEventHandlers;
procedure TEventHandlers.OnTimerTick(Sender : TObject);
begin
writeln('Hello from TimerTick event');
end;
var
dummy:string;
begin
EventHandlers := TEventHandlers.Create();
Timer := TTimer.Create(nil);
Timer.Enabled := false;
Timer.Interval := 1000;
Timer.OnTimer := EventHandlers.OnTimerTick;
Timer.Enabled := true;
readln(dummy);
end.
Мне кажется правильным, но по какой-то причине это не работает.
ИЗМЕНИТЬ
Похоже, что компонент TTimer
не будет работать, потому что консольные приложения не имеют цикла сообщений. Есть ли способ создать таймер в моем приложении?
Ответы
Ответ 1
Ваш код не работает, потому что компонент TTimer
внутренне использует обработку сообщений WM_TIMER
, а консольное приложение не имеет цикла сообщений. Чтобы заставить ваш код работать, вы должны сами создать цикл пересылки сообщений:
program TimerTest;
{$APPTYPE CONSOLE}
uses
SysUtils, Windows,
extctrls;
type
TEventHandlers = class
procedure OnTimerTick(Sender : TObject);
end;
var
Timer : TTimer;
EventHandlers : TEventHandlers;
procedure TEventHandlers.OnTimerTick(Sender : TObject);
begin
writeln('Hello from TimerTick event');
end;
procedure MsgPump;
var
Unicode: Boolean;
Msg: TMsg;
begin
while GetMessage(Msg, 0, 0, 0) do begin
Unicode := (Msg.hwnd = 0) or IsWindowUnicode(Msg.hwnd);
TranslateMessage(Msg);
if Unicode then
DispatchMessageW(Msg)
else
DispatchMessageA(Msg);
end;
end;
begin
EventHandlers := TEventHandlers.Create();
Timer := TTimer.Create(nil);
Timer.Enabled := false;
Timer.Interval := 1000;
Timer.OnTimer := EventHandlers.OnTimerTick;
Timer.Enabled := true;
MsgPump;
end.
Ответ 2
Как уже упоминалось, в консольных приложениях нет насоса сообщений.
Вот класс потоков TConsoleTimer
, который имитирует класс TTimer
. Основное отличие состоит в том, что код в событии выполняется в потоке TConsoleTimer
.
Обновление
В конце этого сообщения есть способ вызвать это событие в основном потоке.
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.
И тест:
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.
Как уже упоминалось выше, как сделать событие выполненным в основном потоке?
Чтение Delphi 7: обработка событий в консольном приложении (TidIRC) дает ответ.
Добавьте метод в TConsoleTimer
:
procedure TConsoleTimer.SwapToMainThread;
begin
FTimerProc(Self);
end;
и измените вызов в методе Execute
на:
Synchronize(SwapToMainThread);
Для накачки синхронизированных вызовов используйте функцию CheckSynchronize()
в модуле Classes:
while not KeyPressed do CheckSynchronize(); // Pump the synchronize queue
Примечание: консольную функцию KeyPressed
можно найти здесь: Как я могу реализовать функцию IsKeyPressed в консольном приложении delphi?.
Ответ 3
Консольные приложения не имеют сообщений, но имеют потоки. Если вы создаете поток, который выполняет работу и ждет следующую секунду, когда работа будет выполнена, вы должны получить нужный результат. Прочитайте документацию о TThread, как создать выделенный поток. Однако получение данных в поток и из него является менее простым. Вот почему существует ряд альтернатив "необработанному" TThread, который помогает в этом, например, OmniThreadLibrary.