I'm running a long operation and I figured a good way to present it to the user was to use a system progress dialog using the IProgressDialog
object.
I only found a couple of usage examples, and this is my implementation. The problems I have are that the application is still irresponsive (I understand I may need to use a thread) but also the Cancel button simply doesn't work (which may be consecuence of the first issue.)
I'm using Delphi XE under Windows 8.1.
Edit: I've added an Application.ProcessMessages
call just before evaluating HasUserCancelled
but it doesn't appear to help much (dialog still doesn't process clicking on the Cancel button.)
var
i, procesados: Integer;
IDs: TList<Integer>;
pd: IProgressDialog;
tmpPtr: Pointer;
begin
procesados := 0;
try
tmpPtr := nil;
CoCreateInstance(CLSID_ProgressDialog, nil, CLSCTX_INPROC_SERVER,
IProgressDialog, pd);
// also seen as pd := CreateComObject(CLSID_ProgressDialog) as IProgressDialog;
pd.SetTitle('Please wait');
pd.SetLine(1, PWideChar(WideString('Performing a long running operation')),
false, tmpPtr);
pd.SetAnimation(HInstance, 1001); // IDA_OPERATION_ANIMATION ?
pd.Timer(PDTIMER_RESET, tmpPtr);
pd.SetCancelMsg(PWideChar('Cancelled...'), tmpPtr);
pd.StartProgressDialog(Handle, nil, PROGDLG_MODAL or
PROGDLG_NOMINIMIZE, tmpPtr);
pd.SetProgress(0, 100);
IDs := GetIDs; // not relevant, returns List<Integer>
try
for i in IDs do
begin
try
Application.ProcessMessages;
if pd.HasUserCancelled then
Break; // this never happens
Inc(procesados);
pd.SetProgress(procesados, IDs.Count);
LongRunningOp(id);
except
// ?
end;
end;
finally
IDs.Free;
end;
finally
pd.StopProgressDialog;
// pd.Release; doesn't exist
end;
end;
end;
You will need to use a thread of you want the application to be responsive. The reason why your cancel button is not working is that messages aren't being processed in your loop. Putting something like Application.ProcessMessages in the loop will let it respond to the click on the cancel button but a thread is still the better option.
You should put your loop with LongRunninOp(id) in a thread and then feed back to the UI with Synchronize. Something like this:
procedure TMyThread.Execute;
var
i: Integer;
begin
for i in IDs do
begin
try
// If pd.HasUserCancelled is thread safe then this will work
// if Terminated or pd.HasUserCancelled then
// Break;
// If pd.HasUserCancelled is not thread safe then you will need to do something like this
Synchronize(
procedure
begin
if pd.HasUserCancelled then
Terminate():
end);
if Terminated then
Break;
Synchronize(
procedure
begin
MainForm.pd.SetProgress(I, IDs.Count);
end);
LongRunningOp(id);
except
// ?
end;
end;
end;
With threads you will need to make sure that you are not accessing something like IDs from the main thread and the background thread. I am also not a fan of the MainForm.pd.SetProgress type of call but I put it there to show you what is happening. It's much better to have a method on the main form that you call.
In the code above, the check for Terminated will return true when a call from the main thread to the MyThread.Terminate()
is made. This is what you should put in the event handler for your cancel button. This is an indication to the thread that it should shut down. Ideally, this check should be made inside the LongRunningOp call too to prevent a delayed response when Terminate
is called.
As Remy indicated, you can use the threads OnTerminate
event to tell the main form when the thread has finished, either when it is terminated or when the thread ends on upon completion.
I just tested with the following code and it is working as expected:
var
iiProgressDialog: IProgressDialog;
pNil: Pointer;
begin
pNil := nil;
iiProgressDialog := CreateComObject(CLASS_ProgressDialog) as IProgressDialog;
iiProgressDialog.SetTitle('test');
iiProgressDialog.StartProgressDialog( Handle, nil, PROGDLG_NOMINIMIZE, pNil);
repeat
Application.ProcessMessages;
until iiProgressDialog.HasUserCancelled > 0;
iiProgressDialog.StopProgressDialog;
end;
Pressing cancel will terminate the loop. The reason why you are not seeing the same effect is that your LongRunningOp is taking too long. Inside of that routine there is nothing to process messages. If you want to run in a single thread then you will need to call Application.ProcessMessages periodically in that routine too.
The following component may also help you:
http://www.bayden.com/delphi/iprogressdialog.htm
It wraps up IProgressDialog into a Delphi component. It creates a thread that monitors the cancel click and will trigger an event if the button is clicked.