Delphi TTimer providing unusual results in Win 10 - delphi

I have an app that allows my users to turn on and off a timer to track their time spent on a certain task. The timer runs a clock used to show the elapsed time to the user, much like a stopwatch.
The code below has worked as I thought it should for a few years now. However, when the app is run on Win 10, sometimes the "time" rate speeds up by 2 or 3 times during a session. If the user restarts the app, it may run at normal speed.
Win 10 Delphi 10.3
procedure TfmTimeCard.btnTimerClick(Sender: TObject);
begin
if btnTimer.Caption = 'Start &Timer' then
begin
btnTimer.Down := True;
btnTimer.Caption := 'Stop &Timer';
pnlTimer.Color := clPurple;
btnResume.Enabled := True;
btnAssign.Enabled := False;
Timer1.Enabled := true;
UpdateTimer.Enabled := True;
ElapsedTime := ElapsedTime;
//btnPostRecord.Enabled := False;
btnCancel.Enabled := False;
btnDeleteTimeCard.Enabled := False;
end
else
begin
btnTimer.Down := False;
btnTimer.Caption := 'Start &Timer';
pnlTimer.ParentColor := True;
btnResume.Enabled := False;
btnAssign.Enabled := True;
pnlTimer.Color := clMoneyGreen;
end;
end;
procedure TfmTimeCard.Timer1Timer(Sender: TObject);
begin
if btnTimer.Caption = 'Stop &Timer' then
begin
ElapsedTime := ElapsedTime + 0.0000115740;
cxClock1.time := ElapsedTime;
cxTimeEditTimer.Time := ElapsedTime;
end;
end;

This is a terrible way to keep track of elapsed time with a TTimer. TTimer is not a real-time timer, or even an accurate timer. It is based on the WM_TIMER window message, which is
a low-priority message. The GetMessage and PeekMessage functions post this message only when no other higher-priority messages are in the thread's message queue.
Don't calculate your ElapsedTime based on how often the TTimer fires its OnTimer event. Keep track of the current time when starting the TTimer, and then subtract that value from the next current time whenever the OnTimer event is eventually generated. That will give you a more real elapsed time.
Try something more like this:
uses
..., System.DateUtils;
private
StartTime: TDateTime;
ElapsedSecs: Int64;
procedure TfmTimeCard.btnTimerClick(Sender: TObject);
begin
if btnTimer.Tag = 0 then
begin
btnTimer.Tag := 1;
...
ElapsedSecs := 0;
StartTime := Now;
Timer1.Enabled := true;
...
end
else
begin
btnTimer.Tag := 0;
...
ElapsedSecs := SecondsBetween(Now, StartTime);
Timer1.Enabled := false;
...
end;
end;
procedure TfmTimeCard.Timer1Timer(Sender: TObject);
begin
if btnTimer.Tag = 1 then
begin
ElapsedSecs := SecondsBetween(Now, StartTime);
// use ElapsedSecs as needed ...
end;
end;
Or:
uses
..., Winapi.Windows;
private
StartTime: DWORD;
ElapsedSecs: Integer;
procedure TfmTimeCard.btnTimerClick(Sender: TObject);
begin
if btnTimer.Tag = 0 then
begin
btnTimer.Tag := 1;
...
ElapsedSecs := 0;
StartTime := GetTickCount;
Timer1.Enabled := true;
...
end
else
begin
btnTimer.Tag := 0;
...
ElapsedSecs := (GetTickCount - StartTime) div 1000;
Timer1.Enabled := false;
...
end;
end;
procedure TfmTimeCard.Timer1Timer(Sender: TObject);
begin
if btnTimer.Tag = 1 then
begin
ElapsedSecs := (GetTickCount - StartTime) div 1000;
// use ElapsedSecs as needed ...
end;
end;
Or:
uses
..., System.Diagnostics;
private
SW: TStopwatch;
ElapsedSecs: Integer;
procedure TfmTimeCard.btnTimerClick(Sender: TObject);
begin
if not SW.IsRunning then
begin
...
ElapsedSecs := 0;
SW := TStopWatch.Start;
Timer1.Enabled := true;
...
end
else
begin
...
SW.Stop;
ElapsedSecs := Trunc(SW.Elapsed.TotalSeconds);
Timer1.Enabled := false;
...
end;
end;
procedure TfmTimeCard.Timer1Timer(Sender: TObject);
begin
if SW.IsRunning then
begin
ElapsedSecs := Trunc(SW.Elapsed.TotalSeconds);
// use ElapsedSecs as needed ...
end;
end;

Related

Open more ClientDataSets at same time with TTHread

I'm a beginner with threads in delphi. I wrote this code and it works very well.
It starts with opening ClientDataSet1 until it is completed and then opening ClientDataSet2 and so on.
My question is: how to start opening them at the same time.
TTHread.CreateAnonymousThread(
procedure
begin
TTHread.Synchronize(nil,
procedure
begin
with ClientDataSet1 do
try
ProgressBar1.Max := 2000; // number of records of ClientDataSet1
PacketRecords := 50;
Open;
DisableControls;
while not Eof do
begin
ProgressBar1.Position := ProgressBar1.Position + 1;
Label1.Caption := ClientDataSet1.RecordCount.ToString;
Next;
Application.ProcessMessages;
end;
EnableControls;
except
// ShowMessage(Msg);
end;
end);
TTHread.Synchronize(nil,
procedure
begin
with ClientDataSet2 do
try
ProgressBar2.Max := 2330; // number of records of ClientDataSet2
PacketRecords := 80;
Open;
DisableControls;
while not Eof do
begin
ProgressBar2.Position := ProgressBar2.Position + 1;
Label2.Caption := ClientDataSet2.RecordCount.ToString;
Next;
Application.ProcessMessages;
end;
EnableControls;
except
// ShowMessage(Msg);
end;
end);
end).Start;
Please help me.

Wait for a global variable to change its value

I have created following abstract code where the user has 2 buttons:
One button is starting some kind of process. The global variable PleaseStop will tell the running process that it should stop its work.
The other button sets the global variable PleaseStop which will tell the procedure to stop.
-
var
PleaseStop: boolean;
IsRunning: boolean;
procedure TForm1.RunActionClick(Sender: TObject);
var
rnd: integer;
tic: Cardinal;
begin
try
IsRunning := true;
rnd := Random(100);
while not PleaseStop do
begin
tic := GetTickCount;
while (GetTickCount-tic < 1000) and not PleaseStop do
begin
Application.ProcessMessages;
Sleep(10);
end;
Memo1.Lines.Add(IntToStr(rnd));
end;
finally
IsRunning := false;
PleaseStop := false;
end;
end;
procedure TForm1.StopBtnClick(Sender: TObject);
begin
PleaseStop := true;
end;
Everything works as expected.
Now there will be problems if the user doesn't click the Stop button, but instead clicks the Run button again (which should be allowed).
I have now modified my code like this:
var
PleaseStop: boolean;
IsRunning: boolean;
procedure TForm1.Button1Click(Sender: TObject);
var
rnd: integer;
tic: Cardinal;
begin
// ---- BEGIN NEW ----
if IsRunning then
begin
PleaseStop := true; // End the previous actions
while PleaseStop do // Wait until the previous actions are done
begin
// TODO: this loop goes forever. PleaseStop will never become false
Application.ProcessMessages;
Sleep(10);
end;
// Now we can continue
end;
// ---- END NEW ----
try
IsRunning := true;
rnd := Random(100);
while not PleaseStop do
begin
tic := GetTickCount;
while (GetTickCount-tic < 1000) and not PleaseStop do
begin
Application.ProcessMessages;
Sleep(10);
end;
Memo1.Lines.Add(IntToStr(rnd));
end;
finally
IsRunning := false;
PleaseStop := false;
end;
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
PleaseStop := true;
end;
Clicking the Start button again will cause a deadlock.
I assume the compiler thinks that while PleaseStop do is equal to while true do since I just set PleaseStop to true earlier. But in fact, this variable should be monitored...
I also tried putting [volatile] in front of the variables, and make them member of TForm1, but that doesn't work either.
Why didn't I use threads?
The code is heavily VCL dependent.
The run button will start a dia show. Every time, the run button is clicked, a random picture folder will be chosen.
So, when the user doesn't like the pictures, he will click "Run" again to switch to a new folder and start the new dia show automatically. The previous run should be stoppped therefore.
Your diagnosis is not exactly accurate, ProcessMessages, simply, cannot cause a previously retrieved message's processing to continue. You have to stop processing and let the execution continue from where re-entrancy occurred. Re-entrancy is the primary avoidance reason of Application.ProcessMessages, and you're doing it on purpose. Hard to work it out...
If you don't want to use synchronization and threading, you can use a timer instead. The code will be much simpler too.
procedure TForm1.FormCreate(Sender: TObject);
begin
Timer1.Enabled := False;
Randomize;
end;
var
rnd: Integer;
procedure TForm1.StartClick(Sender: TObject);
begin
rnd := Random(100);
Timer1.Enabled := True;
end;
procedure TForm1.StopClick(Sender: TObject);
begin
Timer1.Enabled := False;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
Memo1.Lines.Add(IntToStr(rnd));
end;
Try to rewrite your event handler like this:
var Needtorestart:Boolean = false;
procedure TForm1.Button1Click(Sender: TObject);
var
rnd: integer;
tic: Cardinal;
begin
// ---- BEGIN NEW ----
if IsRunning then
begin
PleaseStop := true; // End the previous action
Needtorestart := true;
Exit;
// Now we can continue
end;
// ---- END NEW ----
try
repeat
IsRunning := true;
rnd := Random(100);
while not PleaseStop do
begin
Tic := GetTickCount;
while (GetTickCount-tic < 1000) and not PleaseStop do
begin
Application.ProcessMessages;
Sleep(10);
end;
Memo1.Lines.Add(IntToStr(rnd));
end;
PleaseStop := false;
until not Needtorestart;
finally
IsRunning := false;
PleaseStop := false;
Needtorestart := false;
end;
end;

Delphi: How do you control multiple alike objects?

Say that I have five TRectangle objects, and a function is going to pass a parameter in to make one of them blink.
I know how to control one object like the following code:
procedure TForm1.TimerTimer(Sender: TObject);
begin
if rect1.Visible then
rect1.Visible := false
else
rect1.Visible := true;
end;
procedure TForm1.Blink_Square;
begin
Timer := TTimer.Create(nil);
Timer.OnTimer := TimerTimer;
rect1.Fill.Color := TAlphacolors.Red;
rect1.fill.Kind := TBrushKind.bkSolid;
rect1.Stroke.Thickness := 1;
rect1.Stroke.Color := Talphacolors.Darkgray;
Timer.Interval := 500;
Timer.Enabled := True;
end;
But I really wonder if there is a way that I can use the blink square repeatedly like having a procedure as procedure TForm1.Blink_Square(rec_number: integer); And we can call Blink_Square(5); to make rect5 blink.
Thanks in Advance
You can store your objects in an array or list, then use your procedure parameter to index into it.
var
Blinks: array[1..5] of record
Rectangle: TRectangle;
Timer: TTimer;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Blinks[1].Rectangle := Rect1;
Blinks[1].Timer := nil;
Blinks[2].Rectangle := Rect2;
Blinks[2].Timer := nil;
Blinks[3].Rectangle := Rect3;
Blinks[3].Timer := nil;
Blinks[4].Rectangle := Rect4;
Blinks[4].Timer := nil;
Blinks[5].Rectangle := Rect5;
Blinks[5].Timer := nil;
end;
procedure TForm1.TimerTimer(Sender: TObject);
var
Timer: TTimer;
begin
Timer := TTimer(Sender);
Blinks[Timer.Tag].Visible := not Blinks[Timer.Tag].Visible;
end;
procedure TForm1.Blink_Square(Number: Integer);
begin
Blinks[Number].Rectangle.Fill.Color := TAlphacolors.Red;
Blinks[Number].Rectangle.fill.Kind := TBrushKind.bkSolid;
Blinks[Number].Rectangle.Stroke.Thickness := 1;
Blinks[Number].Rectangle.Stroke.Color := Talphacolors.Darkgray;
if Blinks[Number].Timer = nil then
begin
Blinks[Number].Timer := TTimer.Create(Self);
Blinks[Number].Timer.OnTimer := TimerTimer;
Blinks[Number].Timer.Interval := 500;
Blinks[Number].Timer.Tag := Number;
Blinks[Number].Timer.Enabled := True;
end;
end;
Alternatively:
var
Rects: array[1..5] of TRectangle;
procedure TForm1.FormCreate(Sender: TObject);
begin
Rects[1] := Rect1;
Rects[2] := Rect2;
Rects[3] := Rect3;
Rects[4] := Rect4;
Rects[5] := Rect5;
end;
procedure TForm1.TimerTimer(Sender: TObject);
begin
TRectangle(Sender).Visible := not TRectangle(Sender).Visible;
end;
procedure TForm1.Blink_Square(Number: Integer);
var
Rec: TRectangle;
Timer: TTimer;
M: TNotifyEvent;
begin
Rec := Rects[Number];
Rec.Fill.Color := TAlphacolors.Red;
Rec.fill.Kind := TBrushKind.bkSolid;
Rec.Stroke.Thickness := 1;
Rec.Stroke.Color := Talphacolors.Darkgray;
if Rec.Tag = 0 then
begin
M := TimerTimer;
TMethod(M).Data := Rec;
Timer := TTimer.Create(Rec);
Timer.OnTimer := M;
Timer.Interval := 500;
Timer.Enabled := True;
Rec.Tag := NativeInt(Timer);
end;
end;

Delphi Form Minimize and Restore using Timer

I am a delphi learner. I am having one Delphi Progect with "MainForm", "MinimizeTimer" and "RestoreTimer". I have defined the following codes.
Minimize Timer :
if MainForm.AlphaBlendValue >= 225 then
begin
MinimizeTimer.Enabled := true;
MainForm.AlphaBlendValue := MainForm.AlphaBlendValue - 5;
end;
Restore Timer :
if MainForm.AlphaBlendValue >= 0 then
begin
RestoreTimer.Enabled := true;
MainForm.AlphaBlendValue := MainForm.AlphaBlendValue + 5;
end;
My requirement is that the MainForm will be first fadeout using "MinimizeTimer" and then will be minimized when "_" Button on Caption Bar is pressed. And also be fadein using "RestoreTimer" and then will be restored after clicking on taskbar. So I defined again the following codes:
private
{ Private declarations }
procedure WMSysCommand(var Msg: TWMSysCommand) ; message WM_SYSCOMMAND;
..
..
..
..
..
procedure TMainForm.WMSysCommand(var Msg: TWMSysCommand) ;
begin
if Msg.CmdType = SC_MINIMIZE then MinimizeTimer.Enabled := true;
DefaultHandler(Msg);
if Msg.CmdType = SC_RESTORE then RestoreTimer.Enabled := true;
DefaultHandler(Msg);
end;
But I am not getting the expected result. The MainForm is Minimized and Restored as in regular way. Please remember in my project I have one "FormCloseQuery" event also.
Please help me.
You are using the wrong logic for your requirements. Try this instead:
procedure TMainForm.MinimizeTimerTimer(Sender: TObject);
begin
if AlphaBlendValue > 0 then
begin
AlphaBlendValue := AlphaBlendValue - 5;
end
else
begin
MinimizeTimer.Enabled := False;
Perform(WM_SYSCOMMAND, SC_MINIMIZE, 0);
end;
end;
procedure TMainForm.RestoreTimerTimer(Sender: TObject);
begin
if AlphaBlendValue < 255 then
begin
AlphaBlendValue := AlphaBlendValue + 5;
end else begin
RestoreTimer.Enabled := False;
end;
end;
procedure TMainForm.WMSysCommand(var Msg: TWMSysCommand);
begin
case Msg.CmdType of
SC_MINIMIZE: begin
if AlphaBlendValue > 0 then
begin
MinimizeTimer.Enabled := True;
Exit;
end;
end;
SC_RESTORE: begin
if AlphaBlendValue < 255 then begin
RestoreTimer.Enabled := True;
end;
end;
end;
inherited;
end;

Delphi Progress Bar

I'm trying to make a progress bar that starts at 0%, and takes 5 seconds to get to 100%. The progress bar will begin to go up as soon as Button1 is clicked. Any advice? I looked on Google, but that gave me nothing good on this sort of thing.
Also, at 0%, there should be a label that says Waiting..., when the progress bar starts, it should go to Working..., and when it's done, it should say Done!.
You can use a timer with interval 50 and firstly set enabled to false.
procedure TForm1.Button1Click(Sender: TObject);
begin
timer1.Enabled := True;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
const
cnt: integer = 1;
begin
ProgressBar1.Position := cnt;
if cnt = 1 then Label1.Caption := 'Waiting...'
else if cnt = 100 then begin
Label1.Caption := 'Done!';
Timer1.Enabled := False;
end else
Label1.Caption := 'Working...';
Inc(cnt);
end;
Using GetTickCount() and initializing variables:
uses Windows;
var mseconds, starttime: integer;
procedore TForm1.FormCreate()
begin
starttime := GetTickCount();
mseconds := 0;
Timer1.Enabled := false;
Label1.Caption := '';
ProgressBar1.Position := 0;
Label1.Caption := 'Waiting...';
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
ProgressBar1.Min := 0;
ProgressBar.Max := 100;
ProgressBar1.Position := 0;
timer1.Enabled := True;
Label1.Caption := 'Working...';
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
mseconds := GetTickCount() - starttime;
if mseconds < 5000 then
ProgressBar1.Position := Trunc(mseconds / 50)
else begin
ProgressBar1.Position := 100;
Label1.Caption := 'Done!';
Timer1.Enabled := false;
end;
end;

Resources