1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
|
{
This file is part of the Free Component Library (FCL)
Copyright (c) 1999-2000 by Michael Van Canneyt.
See the file COPYING.FPC, included in this distribution,
for details about the copyright.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
**********************************************************************}
{
A generic timer component. Can be used in GUI and non-GUI apps.
Based heavily on an idea by Graeme Geldenhuys, extended so
the tick mechanism is pluggable.
Note that the system implementation will only work for timers
in the main thread, as it uses synchronize to do the job.
You need to enable threads in your application for the system
implementation to work.
A nice improvement would be an implementation that works
in all threads, such as the threadedtimer of IBX for linux.
Replaced SLEEP with TEvent for those platforms supporting threading:
Windows, Linux, BSD.
On the other platforms, use sleep. This unfortunately has a high overhead
resulting in drift. A five minute timer could be up to 40 seconds late
do to entering and returning (linux x64). MOdified to check the absolute
time every minute, has reduced that lag to about 0.100 second. This is
still greater than TEvent, where the delay is only a few milliseconds (0-3).
}
unit fptimer;
{$mode objfpc}{$H+}
{
Windows, or any platform that uses Cthreads has TEvent with a timed wait
which can include android and embedded.
You can force the use of the Sleep() based timer by defining USESLEEP
}
{$IFNDEF USESLEEP}
{$if Defined(MSWINDOWS) or (Defined(UNIX) and not Defined(BEOS))}
{$define Has_EventWait}
{$endif}
{$ENDIF}
interface
uses
Classes;
type
TFPTimerDriver = Class;
{ TFPCustomTimer }
TFPCustomTimer = class(TComponent)
private
FDriver : TFPTimerDriver;
FOnStartTimer : TNotifyEvent;
FOnStopTimer : TNotifyEvent;
FOnTimer : TNotifyEvent;
FInterval : Cardinal;
FActive : Boolean;
FEnabled : Boolean;
FUseTimerThread : Boolean;
procedure SetEnabled(const AValue: Boolean );
procedure SetInterval(const AValue: Cardinal);
protected
property Active: Boolean read FActive write FActive;
Function CreateTimerDriver : TFPTimerDriver;
procedure Timer; virtual;
public
Constructor Create(AOwner: TComponent); override;
Destructor Destroy; override;
procedure StartTimer; virtual;
procedure StopTimer; virtual;
protected
property Enabled: Boolean read FEnabled write SetEnabled;
property Interval: Cardinal read FInterval write SetInterval;
property UseTimerThread: Boolean read FUseTimerThread write FUseTimerThread;
property OnTimer: TNotifyEvent read FOnTimer write FOnTimer;
property OnStartTimer: TNotifyEvent read FOnStartTimer write FOnStartTimer;
property OnStopTimer: TNotifyEvent read FOnStopTimer write FOnStopTimer;
end;
TFPTimer = Class(TFPCustomTimer)
Published
Property Enabled;
Property Interval;
Property UseTimerThread;
Property OnTimer;
Property OnStartTimer;
Property OnStopTimer;
end;
{ TFPTimerDriver }
TFPTimerDriver = Class(TObject)
Protected
FTimer : TFPCustomTimer;
FTimerStarted : Boolean;
procedure SetInterval(const AValue: Cardinal); virtual;
Public
Constructor Create(ATimer : TFPCustomTimer); virtual;
Procedure StartTimer; virtual; abstract;
Procedure StopTimer; virtual; abstract;
Property Timer : TFPCustomTimer Read FTimer;
property TimerStarted: Boolean read FTimerStarted;
end;
TFPTimerDriverClass = Class of TFPTimerDriver;
Var
DefaultTimerDriverClass : TFPTimerDriverClass = Nil;
implementation
uses
SysUtils;
{ ---------------------------------------------------------------------
TFPTimer
---------------------------------------------------------------------}
constructor TFPCustomTimer.Create(AOwner: TComponent);
begin
inherited;
FDriver:=CreateTimerDriver;
end;
destructor TFPCustomTimer.Destroy;
begin
StopTimer;
FDriver.FTimer:=Nil;
FreeAndNil(FDriver);
Inherited;
end;
Function TFPCustomTimer.CreateTimerDriver : TFPTimerDriver;
begin
Result:=DefaultTimerDriverClass.Create(Self);
end;
procedure TFPCustomTimer.SetEnabled(const AValue: Boolean);
begin
if AValue <> FEnabled then
begin
FEnabled := AValue;
if FEnabled then
StartTimer
else
StopTimer;
end;
end;
procedure TFPCustomTimer.SetInterval(const AValue: Cardinal);
begin
if FInterval <> AValue then
begin
fInterval := AValue;
if FActive and (fInterval > 0) then
FDriver.SetInterval(AValue) // Allow driver to update Interval
else
StopTimer; // Timer not required
end;
end;
procedure TFPCustomTimer.StartTimer;
var
IsActive: Boolean;
begin
IsActive:=FEnabled and (fInterval > 0) and Assigned(FOnTimer);
If IsActive and not fActive and Not (csDesigning in ComponentState) then
begin
FDriver.StartTimer;
if FDriver.TimerStarted then
begin
FActive := True;
if Assigned(OnStartTimer) then
OnStartTimer(Self);
end;
end;
end;
procedure TFPCustomTimer.StopTimer;
begin
if FActive then
begin
FDriver.StopTimer;
if not FDriver.TimerStarted then
begin
FActive:=False;
if Assigned(OnStopTimer) then
OnStopTimer(Self);
end;
end;
end;
procedure TFPCustomTimer.Timer;
begin
{ We check on FEnabled: If by any chance a tick comes in after it was
set to false, the user won't notice, since no event is triggered.}
If FActive and Assigned(FOnTimer) then
FOnTimer(Self);
end;
{ ---------------------------------------------------------------------
TFPTimerDriver
---------------------------------------------------------------------}
Constructor TFPTimerDriver.Create(ATimer : TFPCustomTimer);
begin
FTimer:=ATimer;
end;
procedure TFPTimerDriver.SetInterval(const AValue: Cardinal);
begin
// Default implementation is to restart the timer on Interval change
if TimerStarted then
begin
StopTimer;
FTimerStarted := (AValue > 0);
if FTimerStarted then
StartTimer;
end;
end;
{ ---------------------------------------------------------------------
Default implementation. Threaded timer, one thread per timer.
---------------------------------------------------------------------}
const
cMilliSecs: Extended = 60.0 * 60.0 * 24.0 * 1000.0;
Type
{ TFPTimerThread }
TFPTimerThread = class(TThread)
private
FTimerDriver: TFPTimerDriver;
FStartTime : TDateTime;
{$ifdef Has_EventWait}
FWaitEvent: PEventState;
{$else}
fSignaled: Boolean;
{$endif}
fInterval: Cardinal;
Function Timer : TFPCustomTimer;
Function GetWakeTime(var AInterval,Counter : Int64; Out WakeInterval : Integer; Out WakeTime : TDateTime) : Boolean;
public
procedure Execute; override;
constructor CreateTimerThread(ATimerDriver: TFPTimerDriver);
procedure Terminate;
procedure SetInterval(const AValue: Cardinal);
end;
{ TFPThreadedTimerDriver }
TFPThreadedTimerDriver = Class(TFPTimerDriver)
Private
FThread : TFPTimerThread;
protected
Procedure SetInterval(const AValue: cardinal); override;
Public
Procedure StartTimer; override;
Procedure StopTimer; override;
end;
{ ---------------------------------------------------------------------
TFPTimerThread
---------------------------------------------------------------------}
constructor TFPTimerThread.CreateTimerThread(ATimerDriver: TFPTimerDriver);
begin
inherited Create(True);
FTimerDriver:=ATimerDriver;
{$ifdef Has_EventWait}
FWaitEvent := BasicEventCreate(nil,false,false,'');
{$else}
fSignaled := False;
{$endif}
fInterval := ATimerDriver.Timer.Interval;
FreeOnTerminate := True;
end;
procedure TFPTimerThread.Terminate;
begin
inherited Terminate;
{$ifdef Has_EventWait}
BasicEventSetEvent(fWaitEvent);
{$else}
fSignaled := True;
{$endif}
end;
procedure TFPTimerThread.SetInterval(const AValue: Cardinal);
begin
if fInterval <> AValue then
begin
fInterval := AValue;
{$ifdef Has_EventWait}
BasicEventSetEvent(fWaitEvent); // Wake thread
{$else}
fSignaled := True;
{$endif}
end;
end;
Function TFPTimerThread.Timer : TFPCustomTimer;
begin
If Assigned(FTimerDriver) Then
Result:=FTimerDriver.FTimer
else
Result:=Nil;
end;
Function TFPTimerThread.GetWakeTime(var AInterval,Counter : Int64; Out WakeInterval : Longint; Out WakeTime : TDateTime) : Boolean;
Var
Diff: Extended;
begin
Result:=False;
{ Use Counter*fInterval to avoid numerical errors resulting from adding
small values (AInterval/cMilliSecs) to a large real number (TDateTime),
even when using Extended precision }
WakeTime := FStartTime + (Counter*AInterval / cMilliSecs);
Diff := (WakeTime - Now);
if Diff > 0 then
begin
WakeInterval := Trunc(Diff * cMilliSecs);
if WakeInterval < 10 then
WakeInterval := 10; // Provide a minimum wait time
end
else
begin
WakeInterval:=MaxInt;
// Time has already expired, execute Timer and restart wait loop
try
if not Timer.UseTimerThread then
Synchronize(@Timer.Timer) // Call user event
else
Timer.Timer;
except
// Trap errors to prevent this thread from terminating
end;
Inc(Counter);
Result:=True;
end;
end;
{$ifdef Has_EventWait}
procedure TFPTimerThread.Execute;
var
WakeTime, StartTime: TDateTime;
WakeInterval: Integer;
Counter: int64; { use Int64 to avoid overflow with Counter*fInterval (~49 days)}
AInterval: int64;
Diff: Extended;
Const
wrSignaled = 0;
wrTimeout = 1;
wrAbandoned= 2;
wrError = 3;
begin
WakeInterval := MaxInt;
Counter := 1;
AInterval := fInterval;
FStartTime := Now;
while not Terminated do
begin
if GetWakeTime(AInterval,Counter,WakeInterval,WakeTime) then
Continue;
if not Terminated then
case BasicEventWaitFor(WakeInterval,fWaitEvent) of
wrTimeout:
begin
if Terminated then
Break
else
begin
try
if not Timer.UseTimerThread then
// If terminate is called while here, then the Synchronize will be
// queued while the stoptimer is being processed.
// StopTimer cannot wait until thread completion as this would deadlock
Synchronize(@Timer.Timer) // Call user event
else
Timer.Timer;
except
// Trap errors to prevent this thread from terminating
end;
Inc(Counter); // Next interval
end;
end;
wrSignaled:
begin
if Terminated then
Break
else
begin // Interval has changed
Counter := 1; // Restart timer without creating new thread
AInterval := fInterval;
FStartTime := Now;
end;
end;
else
Break;
end
end;
BasicEventDestroy(fWaitEvent);
end;
{$ELSE Has_EventWait}
procedure TFPTimerThread.Execute;
var
WakeTime, StartTime: TDateTime;
WakeInterval: Integer;
Counter: int64; { use Int64 to avoid overflow with Counter*fInterval (~49 days)}
AInterval: int64;
Diff: Extended;
S,Last: Cardinal;
RecheckTimeCounter: integer;
const
cSleepTime = 500; // 0.5 second, better than every 5 milliseconds
cRecheckTimeCount = 120; // Recheck clock every minute, as the sleep loop can loose time
begin
WakeInterval := MaxInt;
Counter := 1;
AInterval := fInterval;
FStartTime := Now;
while not Terminated do
begin
if GetWakeTime(AInterval,Counter,WakeInterval,WakeTime) then
Continue;
if not Terminated then
begin
RecheckTimeCounter := cRecheckTimeCount;
s := cSleepTime;
repeat
if s > WakeInterval then
s := WakeInterval;
sleep(s);
if fSignaled then // Terminated or interval has changed
begin
if not Terminated then
begin
fSignaled := False;
Counter := 1; // Restart timer
AInterval := fInterval;
StartTime := Now;
end;
break; // Need to break out of sleep loop
end;
dec(WakeInterval,s); // Update total wait time
dec(RecheckTimeCounter); // Do we need to recheck current time
if (RecheckTimeCounter < 0) and (WakeInterval > 0) then
begin
Diff := (WakeTime - Now);
WakeInterval := Trunc(Diff * cMilliSecs);
RecheckTimeCounter := cRecheckTimeCount;
s := cSleepTime;
end;
until (WakeInterval<=0) or Terminated;
if WakeInterval <= 0 then
try
inc(Counter);
if not Timer.UseTimerThread then
// If terminate is called while here, then the Synchronize will be
// queued while the stoptimer is being processed.
// StopTimer cannot wait until thread completion as this would deadlock
Synchronize(@Timer.Timer) // Call user event
else
Timer.Timer;
except
// Trap errors to prevent this thread from terminating
end;
end
end;
end;
{$ENDIF Has_EventWait}
{ ---------------------------------------------------------------------
TFPThreadedTimerDriver
---------------------------------------------------------------------}
procedure TFPThreadedTimerDriver.SetInterval(const AValue: cardinal);
begin
if FThread <> nil then
begin
if AValue > 0 then
FThread.SetInterval(AValue)
else
StopTimer;
end;
end;
Procedure TFPThreadedTimerDriver.StartTimer;
begin
if FThread = nil then
begin
FThread:=TFPTimerThread.CreateTimerThread(Self);
FThread.Start;
FTimerStarted := True;
end;
end;
Procedure TFPThreadedTimerDriver.StopTimer;
begin
if FThread <> nil then
begin
try
// Cannot wait on thread in case
// 1. this is called in a Synchonize method and the FThread is
// about to run a synchronize method. In these cases we would have a deadlock
// 2. In a DLL and this is called as part of DLLMain, which never
// returns endthread (hence WaitFor) until DLLMain is exited
FThread.Terminate; // Will call FThread.Wake;
finally
FThread := nil;
end;
FTimerStarted := False;
end;
end;
Initialization
DefaultTimerDriverClass:=TFPThreadedTimerDriver;
end.
|