The dialog manager, part 5: Converting a non-modal dialog box to modal


Let's apply what we learned from last time and convert a modeless dialog box into a modal one. As always, start with the scratch program and make the following additions:

INT_PTR CALLBACK DlgProc(
    HWND hdlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
 switch (uMsg) {
 case WM_INITDIALOG:
  SetWindowLongPtr(hdlg, DWLP_USER, lParam);
  return TRUE;
 case WM_COMMAND:
  switch (GET_WM_COMMAND_ID(wParam, lParam)) {
  case IDOK:
   EndDialog(hdlg, 2005);
   break;
  case IDCANCEL:
   EndDialog(hdlg, 1776);
   break;
  }
 }
 return FALSE;
}

int DoModal(HWND hwnd)
{
 return DialogBox(g_hinst, MAKEINTRESOURCE(1), hwnd, DlgProc);
}

void OnChar(HWND hwnd, TCHAR ch, int cRepeat)
{
 switch (ch) {
 case ' ': DoModal(hwnd); break;
 }
}

// Add to WndProc
   HANDLE_MSG(hwnd, WM_CHAR, OnChar);

// Resource file
1 DIALOGEX DISCARDABLE  32, 32, 200, 40
STYLE DS_MODALFRAME | DS_SHELLFONT | WS_POPUP |
      WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Sample"
FONT 8, "MS Shell Dlg"
BEGIN
 DEFPUSHBUTTON "OK",IDOK,20,20,50,14
 PUSHBUTTON "Cancel",IDCANCEL,74,20,50,14
END

Not a very exciting program, I grant you that. It just displays a dialog box and returns a value that depends on which button you pressed. The DoModal function uses the DialogBox function to do the real work.

Now let's convert the DoModal function so it implements the modal loop directly. Why? Just to see how it's done. In real life, of course, there would normally be no reason to undertake this exercise; the dialog box manager does a fine job.

First, we need to figure out where we're going to keep track of the flag we called <dialog still active> last time. We'll keep it in a structure that we hang off the dialog box's DWLP_USER window bytes. (I sort of planned ahead for this by having the DlgProc function stash the lParam into the DWLP_USER extra bytes when the dialog is initialized.)

// fEnded tells us if the dialog has been ended.
// When ended, iResult contains the result code.

typedef struct DIALOGSTATE {
 BOOL fEnded;
 int iResult;
} DIALOGSTATE;

void EndManualModalDialog(HWND hdlg, int iResult)
{
 DIALOGSTATE *pds = reinterpret_cast<DIALOGSTATE*>
     (GetWindowLongPtr(hdlg, DWLP_USER));
 if (pds) {
  pds->iResult = iResult;
  pds->fEnded = TRUE;
 }
}

The EndManualModalDialog takes the place of the EndDialog function: Instead of updating the dialog manager's internal "is the dialog finished?" flag, we update ours.

All we have to do to convert our DlgProc from one using the dialog manager's modal loop to our custom modal loop, then, is to change the calls to EndDialog to call our function instead.

INT_PTR CALLBACK DlgProc(
    HWND hdlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
 switch (uMsg) {
 case WM_INITDIALOG:
  SetWindowLongPtr(hdlg, DWLP_USER, lParam);
  return TRUE;
 case WM_COMMAND:
  switch (GET_WM_COMMAND_ID(wParam, lParam)) {
  case IDOK:
   EndManualModeDialog(hdlg, 2005);
   break;
  case IDCANCEL:
   EndManualModeDialog(hdlg, 1776);
   break;
  }
 }
 return FALSE;
}

All that's left is to write the custom dialog message loop.

int DoModal(HWND hwnd)
{
 DIALOGSTATE ds = { 0 };
 HWND hdlg = CreateDialogParam(g_hinst, MAKEINTRESOURCE(1),
             hwnd, DlgProc, reinterpret_cast<LPARAM>(&ds));
 if (!hdlg) {
  return -1;
 }

 EnableWindow(hwnd, FALSE);
 MSG msg;
 msg.message = WM_NULL; // anything that isn't WM_QUIT
 while (!ds.fEnded && GetMessage(&msg, NULL, 0, 0)) {
  if (!IsDialogMessage(hdlg, &msg)) {
   TranslateMessage(&msg);
   DispatchMessage(&msg);
  }
 }
 if (msg.message == WM_QUIT) {
  PostQuitMessage((int)msg.wParam);
 }
 EnableWindow(hwnd, TRUE);
 DestroyWindow(hdlg);
 return ds.iResult;
}

Most of this should make sense given what we've learned over the past few days.

We start by creating the dialog modelessly, passing a pointer to our dialog state as the creation parameter, which as we noted earlier, our dialog procedure squirrels away in the DWLP_USER window bytes for EndManualModalDialog to use.

Next we disable the owner window; this is done after creating the modeless dialog, observing the rules for enabling and disabling windows. We then fall into our message loop, which looks exactly like what we said it should look. All we did was substitute !ds.fEnded for the pseudocode <dialog still active>. After the modal loop is done, we continue with the standard bookkeeping: Re-posting any quit message, re-enabling the owner before destroying the dialog, then returning the result.

As you can see, the basics of modal dialogs are really not that exciting. But now that you have this basic framework, you can start tinkering with it.

First, however, your homework is to find a bug in the above code. It's rather subtle. Hint: Look closely at the interaction between EndManualModalDialog and the modal message loop.

Comments (9)
  1. CornedBee says:

    Does the bug have anything to do with ds.iResult being garbage if the modal loop was terminated by WM_QUIT?

    On a side note, can you suggest to the page engineers to add login/register buttons to all pages, not just the global front page? It gets quite confusing if you want to add a comment, but there is not a single link on the whole page where you can do so!

  2. Anders says:

    ds.iResult is not garbage, it’s inited to 0

    As for the new design, I’m not a big fan of the navigation stuff on the right…

  3. carlso says:

    The code will not work if EndManualModeDialog() is called due to a SendMessage() done on another thread.

    Remember that SendMessage() can also dispatch messages. The scenario here is that the DoModal loop is waiting in the call to GetMessage() for a message to appear — the queue is currently empty. But another thread calls SendMessage(hdlg, WM_COMMAND, IDCANCEL, NULL) to end the dialog.

    The EndManualModeDialog() function will be called from the SendMessage() call, but DoModal would still be stuck in GetMessage() waiting for a message to appear in the queue.

    The fix is to add a PostMessage(hdlg, WM_NULL, 0, 0) as the last line of the EndManualModeDialog() function so that the GetMessage() call in the modal loop will return.

  4. carlso says:

    Just to clarify my last comment (after re-reading, I should have stated things a bit more clearly)…

    When a SendMessage(hdlg,…) is done from another thread, a context switch will occur so that the call to the WndProc/DlgProc/EndManualModalDialog() is made on the same thread that called CreateDialogParam(). The call to EndManualModalDialog() will be done on top of the stack frame that currently has GetMessage on top. SendMessage never posts a message into the queue — the message is executed immediately, and in this case, the message is handled while the thread is waiting in the GetMessage() call.

  5. boxmonkey says:

    Suddenly your blog is hideous in FireFox. Until I tell it to lie to the server and say that it’s IE 6.0…then everything is peachy. Well, mostly. Posting comments requires you to be logged on now?

  6. ndiamond says:

    while (!ds.fEnded && GetMessage(&msg, NULL, 0, 0)) {

    I think that every time you post that code, you do need to post a reminder that you’ve omitted error checking in order to present an example of something else. If anyone finds this page via Google and does a copy-and-paste from it, they get a bug.

    Your page looked bad in IE6 this morning too, but it’s getting better now.

    I agree with the other person’s request for a login button. If it weren’t for that comment, I’d have thought comments were no longer allowed at all, but how did some others sneak in. Then I found the main page that someone talked about.

    Your "contact" page is still broken.

  7. NickFitz says:

    Hi Raymond,

    The whole login before commenting/comment form on separate page stuff about which people are voicing concerns might be related to the issues discussed at

    http://blogs.msdn.com/cyrusn/archive/2005/04/02/404932.aspx

    particularly the first comment by ScottW.

    HTH

  8. Fixing some bugs from last time.

  9. Mike Dunn says:

    In real life, of course, there would normally be no reason to undertake this exercise; the dialog box manager does a fine job.

    MFC does this so it can handle keyboard messages and call TranslateAccelerator on AX controls in the dialog, so you can tab through the UIs of AX controls (eg the WebBrowser control).

Comments are closed.