39. How do I allow the user to cancel a time-consuming operation?
Ooooh, this question comes up ALL the time...
It frequently happens that you wish to carry out
a time-consuming operation, but allow the user to abort that operation at any
time. Also you may wish to display the progress of the operation using a
common-control progress bar. The problem arises when you implement this, and
carry out your operation in a tight loop. While that loop is running, your
machine is not processing ("pumping") any messages, so your window
doesn't receive WM_PAINT, preventing your progress bar from updating, and your
user will be unable to push the Cancel/Abort button because you're not
processing any WM_COMMAND messages.
There are a number of options open to you. Lets start with the ridiculous and
work our way up to the sublime:
|
Add a timer and do a chunk of
processing on each timer tick. The upside is it's simple to implement. The
downsides are:
- You may well waste a lot of time after
the processing is done, waiting for the next tick.
- You have to organise it so if the next
tick occurs before you've finished the last block of processing, your
algorithm doesn't screw up.
- The application won't scale well to a
faster processor.
- Windows timers are notoriously
inaccurate.
I think you'll agree that this approach
basically sucks.
|
|
For MFC apps, perform a chunk
of processing on each OnIdle call. MFC calls OnIdle when there aren't any
more messages to fetch from the queue. Can be tricky to get right, and
again it requires you to break the processing into small chunks, which may
not suit your algorithm.
|
|
Add a PeekMessage loop within
your processing loop, calling it after each small chunk of processing.
Here is a example of an MFC-friendly implementation of this technique :
while (<some criterion>)
{
<do a small bit of processing>
MFC_Yield ();
}
.....
}
void CMyClass::MFC_Yield ()
{
MSG msg ;
while (PeekMessage (&msg, 0, 0, 0, PM_NOREMOVE))
{
if (!(AfxGetApp()->PumpMessage()))
{
// regenerate WM_QUIT for main message loop.
::PostQuitMessage(0);
break;
}
}
// let MFC do its idle processing
LONG lIdle = 0;
while (AfxGetApp()->OnIdle(lIdle++));
}
This works well, but again it requires you
to break the processing algorithm into chunks. It also gives rise to re-entrancy
problems : you are in the middle of a function, then begin processing
messages, any of which could cause your own WndProc to be called again,
invoking some more of your code. Handling this re-entrancy can turn into a
bit of a nightmare, as I found out years ago when I wrote some comms code
under Win16 which peeked left right and centre.
|
|
Use a worker thread. Split the
algorithm into a worker thread, and your main GUI thread can continue
running as if the processing wasn't going on. The main thread can set a
"please abort" event which the worker thread checks for. Again it's advisable to break
the algorithm down so the thread can check its abort event every so
often, but these breaks don't have to be as frequent as they do with the
above solution because we don't have to worry about processing every last
WM message that's flying around - just the latency between when the user
presses Cancel and when the process actually stops. The worker can also
post progress messages to the main thread window (or call its functions -
they're in the same address space, remember).
If you MUST have an uninterrupted run, and
you don't care about brutality, and the worker thread doesn't allocate any
resource which might leak, then you could leave the algorithm as a
straight loop and simply have the main thread call TerminateThread on the
worker if the user cancels the operation. Beware though : doing this means
any DLLs called by the worker won't receive a THREAD_DETACH, so you might
get unexpected side-effects.
For details on implementing worker threads,
see tip #31 |