Click Here to Install Silverlight*
United StatesChange|All Microsoft Sites
MSDN
|Developer Centers|Library|Downloads|Code Center|Subscriptions|MSDN Worldwide
Search for


Advanced Search
MSDN Home > MSJ > February 1997
February 1997


Code for this article: Wicked0297.exe (10KB)
Jeff Prosise writes extensively about programming in Windows and is a contributing editor of several computer magazines. He is the author of Programming Windows 95 with MFC (Microsoft Press).

Sometimes the simplest things in Windows® turn out to be the most difficult. I was reminded of this recently when I received email from a reader who wondered how to implement a cancel dialog. He wanted to display a dialog with a Cancel button at the outset of a lengthy message-processing operation so the user could terminate the operation if it took longer than expected.
Sounds simple, right? But as this reader had discovered, there's more to it than meets the eye. Simply displaying a dialog with a Cancel button won't cut it, because as long as the application that created the dialog isn't retrieving and dispatching messages, WM_COMMAND messages reporting clicks of the Cancel button can't get through (nor can any other messages). And if the dialog is modal, control is wrested away from your window procedure and handed to the dialog procedure the moment the dialog is created.
The classic way to make a cancel dialog work is to fake a modal dialog with a modeless dialog that disables its owner, and to pump messages to the dialog by having the application—which receives control again once a modeless dialog is created—periodically pause to retrieve and dispatch messages waiting in the message queue. It's not exactly rocket science, but it's not what I'd call straightforward either. In fact, every time I do it, I find myself thinking that there has to be a better way. The model that I think of is the MFC CWaitCursor class, which makes displaying an hourglass cursor (and later restoring the original cursor) as simple as adding a

 CWaitCursor wait;
statement to your application. It's easy to display an hourglass cursor. Why shouldn't it be equally easy to display a cancel dialog?
It should be. That's why I wrote a handy little CWaitDialog class that you can download from the link at the top of this article. CWaitDialog makes it a snap to display a cancel dialog at the outset of a time-consuming operation and to have your application respond to clicks of the Cancel button. The grunge work is hidden inside the class, so about all you have to do is construct a CWaitDialog object when you want a cancel dialog displayed. The dialog is destroyed automatically when the CWaitDialog object goes out of scope. As a bonus, the dialog includes a progress control that you can step to report the ongoing status of the operation being carried out by the main body of the application. Sound enticing? Here's the lowdown on how CWaitDialog works—and how you can put it to work in your own applications.

Using the CWaitDialog Class

Using CWaitDialog is simplicity itself. To illustrate, suppose you're writing the world's next great spreadsheet program. When a lengthy recalc operation is begun, you'd like to display a cancel dialog so the user can interrupt the recalc if it takes longer than expected. Here's what you do.
Just before *(e recalc operation is begun, construct a CWaitDialog object on the stack. Pass the constructor the address of a BOOL variable that's initialized to TRUE. If the user clicks the Cancel button, the dialog will use this pointer to set the variable equal to FALSE.
Begin the recalc operation. Periodically call the dialog's SetPercentComplete function to update the progress bar, and call its Pump function to pump messages through the message queue. After calling Pump, check to see if the BOOL variable initialized previously has changed from TRUE to FALSE. If it has, terminate the recalc and allow the message handler to return; if it hasn't, then proceed as normal. When the recalc is complete, close the dialog by calling its Close function or by allowing the CWaitDialog object to go out of scope.
Here's how it might look in code:


 BOOL bContinue = TRUE;
 CWaitDialog dlg (&bContinue);
 
 for (int i=0; i<nNumRows && bContinue; i++) {
 for (j=0; j<nNumCols && bContinue; j++) {
         RecalcCell (i, j);
         int nPercent = (((i * j) + j) * 100) /
             (nNumRows * nNumCols);
         dlg.SetPercentComplete (nPercent);
         dlg.Pump ();
     }
 }
The key here is that calling Pump enacts a secondary message loop that allows the dialog to set bContinue to FALSE when the Cancel button is clicked. The more often Pump is called (and the more often bContinue is checked), the more responsive the application will be to clicks of the Cancel button. If you fail to call Pump or check the bContinue flag, the Cancel button will be ignored.
Figure 1 Default caption
Figure 1 Default caption
Figure 1 shows what CWaitDialog's cancel dialog looks like. The progress control is updated each time CWaitDialog:: SetPercentComplete is called. As the recalc operation proceeds, the bar steps from left to right. When the recalc is finished, the dialog is destroyed when CWaitDialog goes out of scope and its destructor is called. If there's a delay between the time the recalc is finished and CWaitDialog goes out of scope, you can destroy the dialog manually by calling its Close function. In CWaitCursor terms, this is equivalent to calling CWaitCursor::Restore to restore the original cursor image before a CWaitCursor object goes out of scope.
By default, a CWaitDialog's caption reads "Working," and the text in the body of the dialog says "Click Cancel to cancel the operation," as shown in Figure 1. You can specify alternative text strings when you construct a CWaitDialog object. The statement

 CWaitDialog dlg (&bContinue, "Recalculating",
     "Click Cancel to stop recalculating");
creates a CWaitDialog whose caption reads "Recalculating" and whose body contains the text "Click Cancel to stop recalculating." If you'd like, you can update the text displayed in the body of the dialog box on the fly with CWaitDialog::SetMessageText. The statements

 if (nPercent >= 90)
     dlg.SetMessageText ("Almost done!");
change the message displayed in the dialog to "Almost done!" when the percent complete figure reaches 90 percent. For reference, Figure 2 summarizes all public member functions of CWaitDialog, including Pump, Close, SetMessageText, SetPercentComplete, and, of course, the class constructor.
Figure 3 contains the complete source code for an application that uses CWaitDialog, as well as the source code for CWaitDialog itself. When the left mouse button is clicked in the window's client area, an OnLButtonDown handler draws several thousand randomly sized and colored ellipses. Depending on the size of the window and the speed of the host PC, the drawing loop could require anywhere from a few seconds to a couple of minutes to run its course. Fortunately, drawing can be interrupted at any time by clicking the Cancel button. And because the app calls CWaitDialog::Pump and checks the bContinue flag between every ellipse, Cancel terminates the loop almost immediately.

CWaitDialog Internals

If you're curious to know more about how CWaitDialog works, check out the source code files CWaitDlg.h and CWaitDlg.cpp. One thing you'll discover is that the dialog is modeless, not modal. That's important because it allows Windows to return control to the main body of your application as soon as the dialog is created—that is, as soon as the call to Create in the dialog's constructor returns. If DoModal was called instead of Create, the statement that constructs a CWaitDialog object wouldn't return until after the dialog was dismissed.
A CWaitDialog may be modeless, but it disables input to its owner like a modal dialog. The secret is the following statement in the class constructor:


 AfxGetMainWnd ()->EnableWindow (FALSE);
Because the application's main window isn't reenabled until just before the dialog is destroyed, the user can't switch away from the dialog and back to the application as long as the dialog is displayed on the screen. The MFC CView::OnFilePrint function uses similar logic to get modal-like behavior from a modeless print-status dialog.
For a CWaitDialog to function properly, WM_COMMAND messages stemming from clicks of the Cancel button must be retrieved from the message queue and dispatched to the dialog so OnCancel can be activated. Before closing the dialog, OnCancel sets the BOOL flag created in the main body of the application to FALSE so the application will know that Cancel has been clicked. CWaitDialog::Pump maintains the message flow by retrieving and dispatching messages separate and apart from the application's primary message loop.
I experimented with a multithreaded version of CWaitDialog that used a separate user interface (UI) thread to process messages to the dialog, but gave up on it after learning firsthand that 32-bit Windows doesn't like it when a dialog runs in one thread and its owner runs in another. UI threads are fine when each thread creates a top-level window, but using multiple UI threads for child windows and owned windows can lead to some strange behavior. For one thing, a dialog running in thread B can't disable an owner window running in thread A because Win32
® stores activation states and other information (such as who has the input focus) on a per-thread basis.
Bottom line: using a separate input queue for a CWaitDialog would do away with the need for CWaitDialog::Pump, but it would invariably complicate your application's code in other ways. The single-threaded version of CWaitDialog works fine, and it's not that big a deal to call Pump every now and then since you have to pause to check the continuation flag anyway.

Your Needs, Your Ideas

Are there tough Win32 programming questions you'd like to see answered in this column? If so, email them to me at the address listed below. I regret that time doesn't permit me to respond individually to all questions, but rest assured that I'll read every one and consider each for inclusion in a future installment of Wicked Code.

Have a tricky issue dealing with Windows? Send your questions via email to Jeff Prosise: 72241.44@compuserve.com

From the February 1997 issue of Microsoft Systems Journal. Get it at your local newsstand, or better yet, subscribe.

© 1997 Microsoft Corporation. All rights reserved. Legal Notices.

© 2014 Microsoft Corporation. All rights reserved. Contact Us |Terms of Use |Trademarks |Privacy & Cookies
Microsoft