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;
Related
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;
I have a Delphi MDI application that has a customer search child form which can only be opened once (checking isAssigned), however the view / edit form can be opened multiple times so that the end user can open multiple customers at once (Tabbed), what I'd like to do is be able to stop them from opening the same customer record more than once, on the open of the customer form I set the caption to the customers account reference and if that form exists I would like to .BringToFront, if not I'll create it.
What would be the best way to achieve this please, as I'm scratching my head!
Thanks in advance.
procedure TfrmCustomerSearch.ViewCustomerExecute(Sender: TObject);
begin
screen.cursor := crappstart;
if not IsMDIChildOpen(frmMainMenu, 'frmCustomerView', pfrmCaption) then
frmCustomerView := TfrmCustomerView.createform(nil,dmCustomerSearchfrm.FDQCustSearchreference.Value,cxGrid1DBTableView1.DataController.FocusedRecordIndex)
else
frmCustomerView.BringToFront;
screen.cursor := crdefault;
end;
function TfrmCustomerSearch.IsMDIChildOpen(const AFormName: TForm; const AMDIChildName, AMDICaption : string): Boolean;
var
i: Integer;
begin
Result := False;
for i := Pred(AFormName.MDIChildCount) DownTo 0 do
if (AFormName.MDIChildren[i].name = AMDIChildName) then
begin
if (AFormName.MDIChildren[i].caption = AMDICaption) then
begin
Result := True;
Break;
end
end;
end;
Try something more like this instead:
procedure TfrmCustomerSearch.ViewCustomerExecute(Sender: TObject);
begin
Screen.Cursor := crAppStart;
try
frmCustomerView := TfrmCustomerView(FindMDIChildOpen(frmMainMenu, TfrmCustomerView, pfrmCaption));
if frmCustomerView = nil then
frmCustomerView := TfrmCustomerView.CreateForm(nil, dmCustomerSearchfrm.FDQCustSearchreference.Value, cxGrid1DBTableView1.DataController.FocusedRecordIndex);
frmCustomerView.BringToFront;
finally
Screen.Cursor := crDefault;
end;
end;
function TfrmCustomerSearch.FindMDIChildOpen(const AParentForm: TForm; const AMDIChildClass: TFormClass; const AMDICaption : string): TForm;
var
i: Integer;
Child: TForm;
begin
Result := nil;
for i := Pred(AParentForm.MDIChildCount) DownTo 0 do
begin
Child := AParentForm.MDIChildren[i];
if Child.InheritsFrom(AMDIChildClass) and
(Child.Caption = AMDICaption) then
begin
Result := Child;
Exit;
end;
end;
end;
I defined a Tactionlist which contains all actions to show/hide my forms. This could be modal (showmodal) or non modal (visible:=true). I found some code to catch the screen shots by this:
procedure GetScreenShot(shotType: TScreenShotType; var img: TJpegImage);
var
w,h: integer;
DC: HDC;
hWin: Cardinal;
r: TRect;
tmpBmp: TBitmap;
begin
hWin := 0;
case shotType of
sstActiveWindow:
begin //This is what I use
//only the active window
hWin := GetForegroundWindow;
dc := GetWindowDC(hWin);
GetWindowRect(hWin,r);
w := r.Right - r.Left;
h := r.Bottom - r.Top;
end; //sstActiveWindow
sstActiveClientArea:
begin
end; //sstActiveClientArea
sstPrimaryMonitor:
begin
end; //sstPrimaryMonitor
sstDesktop:
begin
end; //sstDesktop
else begin
Exit;
end; //case else
end; //case
//convert to jpg
tmpBmp := TBitmap.Create;
try
tmpBmp.Width := w;
tmpBmp.Height := h;
BitBlt(tmpBmp.Canvas.Handle,0,0,tmpBmp.Width,
tmpBmp.Height,DC,0,0,SRCCOPY);
img.Assign(tmpBmp);
finally
ReleaseDC(hWin,DC);
FreeAndNil(tmpBmp);
end; //try-finally
end;
My "scan" routine is as follows:
for ACnt := 0 to GenActions.ActionCount - 1 do
begin
try
LogBook.ML(Format('%d. Aktion %s gestartet',[ACnt,quotedstr(GenActions.Actions[ACnt].Name)]));
if GenActions.Actions[ACnt].Tag > 0 then
begin // Action is ready for test
TAction(GenActions.Actions[ACnt]).checked:=true;
if GenActions.Actions[ACnt].Execute then
begin
LogBook.ML(Format('%d. Aktion %s erfolgreich ausgeführt',[ACnt,quotedstr(GenActions.Actions[ACnt].Name)]));
if SaveScreens then // var boolean
begin
img:=TJPEGImage.Create;
try
GetScreenShot(sstActiveWindow,img);
img.SaveToFile(IncludeTrailingBackslash(Optionen.PictPfad.Text)+inttostr(ACnt)+'.jpg');
finally
img.Free;
end;
end;
repeat
sleep(100);
Application.ProcessMessages;
until not DM_Gen.TestTimer.Enabled ; //for modal windows a timer sends modalresult:=mrcancel
end;
end
else
begin
LogBook.ML(Format('%d Aktion %s nicht getestet',[ACnt,quotedstr(GenActions.Actions[ACnt].Name)]));
end;
except
on E: Exception do
LogBook.ML(Format('%d. Aktion hat Fehler %s gemeldet',[ACnt,E.Message]));
end;
end;
finally
LogBook.ML('Testlauf beendet');
end;
When I run this code I get for about the first 150 actions the mainform, then some other forms like the logbook or the browser or ... Nearly never the form I want.
I found some posts which recommended the use of "findwindow". Here is my problem that I don't know the exact caption of the window, because in all windows the caption is modified in the onshow event in order to show actual information.
Any ideas how can catch my actual opened window?
So a problem is to understand how my actions work. Here two typical examples:
procedure TDM_Gen.VALstVisActExecute(Sender: TObject);
begin
if Sender is TAction then
begin // set some properties
end;
ListeVeranst_2.Visible:=VALstVisAct.Checked;
end;
procedure TDM_Gen.NewVAActExecute(Sender: TObject);
var
NewVA : TNewVeranstaltung;
begin
if Sender <> nil then
begin
if Sender is TButton then
begin //do something depending on who fired
end;
end;
try
NewVA:=TNewVeranstaltung.Create(nil);
case NewVA.ShowModal of
mrOk:
begin // e.g. refresh some lists
end;
mrCancel:
begin // clean up
end;
end;
finally
NewVA.Free;
end;
end;
The caption of the window is set during the onshow event by:
caption:=Format('This is window %s %s',[Param1, Param2]);
Problem you are facing is due to ShowModal method that is blocking call. That means that all subsequent code after that call will start executing after the form is closed.
Code flow in following simplified example:
MyAction.Execute;
CaptureScreen;
procedure TSomeForm.MyActionExecute(Sender: TObject);
var frm: TForm;
begin
frm := TForm.Create(nil);
try
frm.ShowModal; // this call blocks execution of subsequent code in this method until form is closed
finally
frm.Free;
end;
end;
will be MyAction.Execute -> frm.ShowModal -> frm.Close -> frm.Free -> CaptureScreen
You will have to initiate screen capturing from within your modal form in order to capture its screen.
Using Delphi 2010
Hi, I am looking for a way to break out of a loop using a key press (example 'x')
procedure TfrmMain.btnSpinClick(Sender: TObject);
function IsControlKeyPressed: Boolean;
begin
Result := GetKeyState(Ord('x')) < 0;
end;
var
ProductList: TStringList;
I, Integer;
begin
Screen.Cursor:= crHourGlass;
Spinning:= True;
UpdateAll;
Application.ProcessMessages;
//create a product list
ProductList:= TStringList.Create;
ProductList.LoadFromFile(edtProductsFile.Text);
Progressbar1.Min:= 1;
Progressbar1.Max:= ProductList.Count - 1;
//interate through the product list
//skip first line (its the field names) and start at the second line
for I:= 1 to ProductList.Count - 1 do
begin
//***************
//other code here
//***************
Progressbar1.Position:= Progressbar1.Position + 1;
***if IsControlKeyPressed then Break;
Application.ProcessMessages;***
end; //for I:= 1 to ProductList.Count - 1 do
ProductList.Clear;
ProductList.Free;
Thesaurus.Clear;
Thesaurus.Free;
Screen.Cursor:= crDefault;
Spinning:= False;
UpdateAll;
Application.ProcessMessages;
end;
Move your long-running code into a separate thread. In it, occasionally check whether a certain flag is set. When it's set, stop.
Then, write an OnKeyPress event handler for your form. When that event handler detects that the magic key combination has been pressed, set the flag. That will cause the thread to stop doing its work.
It could work something like this:
type
TProcessProductListThread = class(TThread)
private
FFileName: string;
FProgressBar: TProgressBar;
FMax: Integer;
procedure SetProgressBarRange;
procedure IncrementProgressBar;
procedure ProcessProduct(const AProduct: string);
protected
procedure Execute; override;
public
constructor Create(const AFileName: string; AProgressBar: TProgressBar;
OnThreadTerminate: TNotifyEvent);
end;
The constructor receives all the information it will need to do its work, but doesn't actually start doing any of it. That's reserved for the Execute method. We set FreeOnTerminate := False because the main thread will need to continue to have access to the thread object after it's begun running.
constructor TProcessProductListThread.Create(const AFileName: string;
AProgressBar: TProgressBar; OnThreadTerminate: TNotifyEvent);
begin
inherited Create(False);
FFileName := AFileName;
FProgressBar := AProgressBar;
OnTerminate := OnThreadTerminate;
FreeOnTerminate := False;
end;
Your code interacts with the GUI in a couple of places. That needs to happen from the GUI thread, so we'll extract that code into separate methods that can be passed to Synchronize:
procedure TProcessProductList.SetProgressBarRange);
begin
FProgressBar.Min := 1;
FProgressBar.Position := FProgressBar.Min;
FProgressBar.Max := FMax;
end;
procedure TProcessProduceList.IncrementProgressBar;
begin
FProgressBar.Position := FProgressBar.Position + 1;
end;
You'll notice that the Execute method looks similar to your original code. Notice how it uses the values previously saved from the constructor.
procedure TProcessProductList.Execute;
var
ProductList: TStringList;
I: Integer;
begin
ProductList := TStringList.Create;
try
ProductList.LoadFromFile(FFileName);
FMax := ProductList.Count - 1;
Synchronize(SetProgressBarRange);
// skip first line (it's the field names) and start at the second line
for I := 1 to ProductList.Count - 1 do begin
ProcessProduct(ProductList[I]);
Synchronize(IncrementProgressBar);
if Terminated then
exit;
end;
finally
ProductList.Free;
end;
end;
To start the thread, create it like this:
ProcessThread := TProcessProductList.Create(edtProductsFile.Text, Progressbar1,
OnProcessProductListTerminate);
Handle the termination with an event handler like below. It's mostly the stuff from the epilogue of your original code, but it also clears ProcessThread; that way, its value can indicate whether the thread is still running.
procedure TForm1.OnProcessProductListTerminate(Sender: TObject);
begin
Thesaurus.Clear;
Thesaurus.Free;
UpdateAll;
ProcessThread := nil;
end;
Remember that I said you should set a flag when the key is pressed? In the code above, the flag it checks is simply the thread's own Terminated property. To set it, call the thread's Terminate method.
procedure TForm1.FormKeyPress(Sender: TObject; var Key: Char);
begin
if Char = 'X' then begin
ProcessThread.Terminate;
ProcessThread.Free;
Char := #0;
end;
end;
I have a project with one FadeIn Timer and One FadeOut Timer. My form is created by FadeIn Timer and is closed by FadeOut Timer. Initially FadeIn Timer is enabled and FadeOut Timer is disabled. FadeIn Timer Code :
if MainForm.AlphaBlendValue >= 235 then
Timer01.Enabled := false
else
MainForm.AlphaBlendValue := MainForm.AlphaBlendValue + 5;
FadeOut Timer Code :
if MainForm.AlphaBlendValue <= 0 then
Timer02.Enabled := false
else
MainForm.AlphaBlendValue := MainForm.AlphaBlendValue - 5;
close;
My requirement is that the FadeOut Timer will be active if I click on the "X" Button of the Caption Bar. So I defined
if Msg.Result=htClose then
FadeOutTimer.Enabled:=true;
But it not working. Please help me.
If the form just closes immediately, then you need a global form variable like FAllowClose that you set to False when the form is created. Then you need to write code for the Form.CloseQuery event. Something simple like this should work:
procedure Form.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
if FAllowClose then
Exit;
CanClose := False;
FadeOut.Enabled := True;
end;
procedure Form.FadeOutOnTimer(Sender: TObject);
begin
// do fade out
FAllowClose := True;
Self.Close
end;
<<< 2012/07/17 Edit >>>
When the user clicks the "X" button on the form, the only one way to stop the form from closing is to cancel it in the OnCloseQuery event. Then when you are done fading out the form, you close the form. You'll need a global variable like FAllowClose to signal the OnCloseQuery event that you are close the form instead of the user. This code is a little more illustrative, and should handle the situation where a user clicks the "X" again while it is fading out.
interface
type TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
procedure FadeOutTimer(Sender: TObject);
private
FAllowClose: Boolean;
public
end;
implementation
procedure TForm1.FormCreate(Sender: TObject);
begin
FAllowClose := False;
end;
procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
if FAllowClose then // <- is 'False' when user clicks "X"
Exit;
CanClose := False; // <- cancels close when user clicks "X"
FadeOut.Enabled := True;
end;
procedure TForm1.FadeOutTimer(Sender: TObject);
begin
if Form1.AlphaBlendValue > 0 then
Form1.AlphaBlendValue := Form1.AlphaBlendValue - 5
else
begin
FadeOut.Enabled := False;
FAllowClose := True;
Self.Close;
end;
end;
I think this is the proper fade out code for James L's answer:
procedure Form.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
if FAllowClose then
begin
CanClose := True;
Exit;
end;
CanClose := False;
FadeOut.Enabled := True;
end;
procedure Form.FadeOutOnTimer(Sender: TObject);
begin
if MainForm.AlphaBlendValue <= 0 then
begin
FadeOut.Enabled := false
FAllowClose := True;
Self.Close
end
else
MainForm.AlphaBlendValue := MainForm.AlphaBlendValue - 5;
end;