简体   繁体   中英

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;

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.

Windows works by passing messages between windows. Unlike some operating systems, these messages are not injected, but rather they have to be polled from the message queue. This is what the Application.ProcessMessages call does.

It is kind of like co-operative multitasking. As a result, any windowed controls created and operating in your thread like this dialog never get to handle events while your processing loop is running.

There are 2 ways. The first is to create a thread for your processing and wait for it to return. This can be a little complicated to arrange and can have cross thread issues to check for.

The second method is to call Application.ProcessMessages in your processing loops - this forces messages to get processed and you can get the click events etc.

HOWEVER the objects you are using can be destroyed inside of your loop because an event can happen INSIDE the loop. For example, if your processing loop is running because of a button click on a form, and the user closes that form and it is destroyed (such as a auto freeing form) - then the form object and all of its controls are no longer valid objects - referencing them will cause memory access violations and exceptions. Easiest solution is to use a flag and never let the form close during the processing loop.

Either way, you are responsible for managing the life time of everything and each approach has issues that have to be handled by you.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM