delphiiprogressdialog

Cancel IProgressDialog in Delphi


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;

Solution

  • 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.