April, 2005

  • The Old New Thing

    The dialog manager, part 9: Custom accelerators in dialog boxes

    • 2 Comments
    Along lines similar to last time, you can also add custom accelerators to your dialog box. (In a sense, this is a generalization of custom navigation, since you can make your navigation keys be accelerators.)

    So let's use accelerators to navigate instead of picking off the keys manually. Our accelerator table might look like this:

    IDA_PROPSHEET ACCELERATORS
    BEGIN
        VK_TAB      ,IDC_NEXTPAGE       ,VIRTKEY,CONTROL
        VK_TAB      ,IDC_PREVPAGE       ,VIRTKEY,CONTROL,SHIFT
    END
    

    Here you can see my comma placement convention for tables. I like to put commas at the far end of the field rather than jamming it up against the last word in the column. Doing this makes cut/paste a lot easier, since you can cut a column and paste it somewhere else without having to go back and twiddle all the commas.

    Assuming you've loaded this accelerator table into the variable "hacc", you can use that table in your custom dialog loop:

    while (<dialog still active> &&
           GetMessage(&msg, NULL, 0, 0, 0)) {
     if (!TranslateAccelerator(hdlg, hacc, &msg) &&
         !IsDialogMessage(hdlg, &msg)) {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
     }
    }
    

    The TranslateAccelerator function checks if the message matches any entries in the accelerator table. If so, then it posts a WM_COMMAND message to the window passed as its first parameter. In our case, we pass the dialog box handle. Not shown above is the WM_COMMAND handler in the dialog box that responds to IDC_NEXTPAGE and IDC_PREVPAGE by performing a navigation.

    The same as last time, if you think there might be modeless dialogs owned by this message loop, you will have to do filtering so that you don't pick off somebody else's navigation keys.

    while (<dialog still active> &&
           GetMessage(&msg, NULL, 0, 0, 0)) {
     if (!((hdlg == msg.hwnd || IsChild(hdlg, msg.hwnd)) &&
           !TranslateAccelerator(hdlg, hacc, &msg)) &&
         !IsDialogMessage(hdlg, &msg)) {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
     }
    }
    

    Okay, I think that's enough of dialog boxes for now.

  • The Old New Thing

    The dialog manager, part 8: Custom navigation in dialog boxes

    • 1 Comments

    Some dialog boxes contain custom navigation that goes beyond what the IsDialogMessage function provides. For example, property sheets use Ctrl+Tab and Ctrl+Shift+Tab to change pages within the property sheet. Remember the core of the dialog loop:

    while (<dialog still active> &&
           GetMessage(&msg, NULL, 0, 0, 0)) {
     if (!IsDialogMessage(hdlg, &msg)) {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
     }
    }
    

    (Or the modified version we created in part 7.)

    To add custom navigation, just stick it in before calling IsDialogMessage.

    while (<dialog still active> &&
           GetMessage(&msg, NULL, 0, 0, 0)) {
     if (msg.message == WM_KEYDOWN &&
         msg.wParam == VK_TAB &&
         GetKeyState(VK_CONTROL) < 0) {
      ... do custom navigation ...
     } else if (!IsDialogMessage(hdlg, &msg)) {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
     }
    }
    

    After retrieving a message, we check whether it was Ctrl+Tab before dispatching it or indeed even before letting IsDialogMessage see it. If so, then treat it as a navigation key.

    Note that if you intend to have modeless dialogs controlled by this message loop, then your test needs to be a little more focused, because you don't want to pick off keyboard navigation keys destined for the modeless dialog.

    while (<dialog still active> &&
           GetMessage(&msg, NULL, 0, 0, 0)) {
     if ((hdlg == msg.hwnd || IsChild(hdlg, msg.hwnd)) &&
         msg.message == WM_KEYDOWN &&
         msg.wParam == VK_TAB &&
         GetKeyState(VK_CONTROL) < 0) {
      ... do custom navigation ...
     } else if (!IsDialogMessage(hdlg, &msg)) {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
     }
    }
    

    Next time, we'll see another way of accomplishing this same task.

  • The Old New Thing

    The dialog manager, part 7: More subtleties in message loops

    • 10 Comments

    Last time, we solved the problem with the EndManualModalDialog function by posting a harmless message. Today, we're going to solve the problem in an entirely different way.

    The idea here is to make sure the modal message loop regains control, even if all that happened were incoming sent messages, so that it can detect that the fEnded flag is set and break out of the modal loop.

    Instead of changing the EndManualModalDialog function, we will change the modal 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) {
      if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
       if (msg.message == WM_QUIT) { /*  fixed 8am */
        break;
       } else if (!IsDialogMessage(hdlg, &msg)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
       } /* fixed 10am */
      } else if (!ds.fEnded) {
       WaitMessage();
      }
     }
     if (msg.message == WM_QUIT) {
      PostQuitMessage((int)msg.wParam);
     }
     EnableWindow(hwnd, TRUE);
     DestroyWindow(hdlg);
     return ds.iResult;
    }
    

    We changed the call to GetMessage into a call to the PeekMessage function, asking to remove the peeked message if any. Like GetMessage, this delivers any incoming sent messages, then checks if there are any posted messages in the queue. The difference is that whereas GetMessage keeps waiting if there are no posted message, PeekMessage returns and tells you that there were no posted messages.

    That's the control we want. If PeekMessage says that it couldn't find a posted message, we check our fEnded flag once again, in case an incoming sent message set the fEnded flag. If not, then we call the WaitMessage function to wait until there is something to do (either an incoming sent message or a posted message).

    Exercise: If the whole point was to regain control after sent messages are delivered, why isn't there a test of the fEnded flag immediately after DispatchMessage returns?

  • The Old New Thing

    The dialog manager, part 6: Subtleties in message loops

    • 3 Comments

    Last time, I left you with a homework exercise: Find the subtle bug in the interaction between EndManualModalDialog and the modal message loop.

    The subtlety is that EndManualModalDialog sets some flags but does nothing to force the message loop to notice that the flag was actually set. Recall that the GetMessage function does not return until a posted message arrives in the queue. If incoming sent messages arrive, they are delivered to the corresponding window procedure, but the GetMessage function doesn't return. It just keeps delivering incoming sent messages until a posted message finally arrives.

    The bug, therefore, is that when you call EndManualModalDialog, it sets the flag that tells the modal message loop to stop running, but doesn't do anything to ensure that the modal message loop will wake up to notice. Nothing happens until a posted message arrives, which causes GetMessage to return. The posted message is dispatched and the while loop restarted, at which point the code finally notices that the fEnded flag is set and breaks out of the modal message loop.

    There are a few ways of fixing this problem. The quick solution is to post a meaningless message.

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

    This will force the GetMessage to return, since we made sure there is at least one posted message in the queue waiting to be processed. We chose the WM_NULL message because it doesn't do anything. We aren't interested in what the message does, just the fact that there is a message at all.

    Next time, a different solution to the same problem.

  • The Old New Thing

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

    • 9 Comments

    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.

  • The Old New Thing

    The dialog manager, part 4: The dialog loop

    • 24 Comments

    The dialog loop is actually quite simple. At its core, it's just

    while (<dialog still active> &&
           GetMessage(&msg, NULL, 0, 0, 0)) {
     if (!IsDialogMessage(hdlg, &msg)) {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
     }
    }
    

    If you want something fancier in your dialog loop, you can take the loop above and tinker with it.

    But let's start from the beginning. The work happens in DialogBoxIndirectParam. (You should already know by now how to convert all the other DialogBoxXxx functions into DialogBoxIndirectParam.)

    INT_PTR WINAPI DialogBoxIndirectParam(
        HINSTANCE hinst,
        LPCDLGTEMPLATE lpTemplate, HWND hwndParent,
        DLGPROC lpDlgProc, LPARAM lParam)
    {
     /*
      * App hack!  Some people pass GetDesktopWindow()
      * as the owner instead of NULL.  Fix them so the
      * desktop doesn't get disabled!
      */
     if (hwndParent == GetDesktopWindow())
      hwndParent = NULL;
    

    That's right, we start with an app hack. The problem of passing GetDesktopWindow() instead of NULL was discussed in an earlier entry. So many people make this mistake that we had to put this app hack into the core OS. It would be pointless to make a shim for it since that would mean that thousands of apps would need to be shimmed.

    Since only top-level windows can be owners, we have to take the putative hwndParent (which might be a child window) and walk up the window hierarchy until we find a top-level window.

     if (hwndParent)
      hwndParent = GetAncestor(hwndParent, GA_ROOT);
    

    With that second app hack out of the way, we create the dialog.

     HWND hdlg = CreateDialogIndirectParam(hinst,
                   lpTemplate, hwndParent, lpDlgProc,
                   lParam);
    

    Note: As before, I am going to ignore error checking and various dialog box esoterica because it would just be distracting from the main point of this entry.

    Modal windows disable their parent, so do it here.

     BOOL fWasEnabled = EnableWindow(hwndParent, FALSE);
    

    We then fall into the dialog modal loop:

     MSG msg;
     while (<dialog still active> &&
            GetMessage(&msg, NULL, 0, 0)) {
      if (!IsDialogMessage(hdlg, &msg)) {
       TranslateMessage(&msg);
       DispatchMessage(&msg);
      }
     }
    

    Per the convention on quit messages, we re-post any quit message we may have received so the next outer modal loop can see it.

     if (msg.message == WM_QUIT) {
      PostQuitMessage((int)msg.wParam);
     }
    

    (Astute readers may have noticed an uninitialized variable bug: If EndDialog was called during WM_INITDIALOG handling, then msg.message is never set. I decided to ignore this fringe case for expository purposes.)

    Now that the dialog is complete, we clean up. Remember to enable the owner before destroying the owned dialog.

    if (fWasEnabled)
     EnableWindow(hwndParent, TRUE);
    DestroyWindow(hdlg);
    

    And that's all. Return the result.

     return <value passed to EndDialog>;
    }
    

    Congratulations, you are now an expert on dialog boxes. Tomorrow we'll look at how you can put this new expertise to good use.

    Exercise: Find a way to sneak through the two layers of hwndParent parameter "repair" and end up with a dialog box whose owner is the desktop window. Explain the dire consequences of this scenario.

Page 3 of 3 (26 items) 123