When the default pushbutton is invoked, the invoke goes to the top-level dialog


One quirk of nested dialogs lies in what happens when the user presses Enter to invoke the default pushbutton: The resulting WM_COMMAND message goes to the top-level dialog, even if the default pushbutton belongs to a sub-dialog.

Why doesn't it send the WM_COMMAND to the parent of the default pushbutton? I mean, the dialog manager knows the handle of the button, so it can send the message to the button's parent, right?

Well, the dialog manager knows the handle of a button. But not necessarily the button. Recall that if focus is not on a pushbutton, then the dialog manager sets the default pushbutton based on the control ID returned by the DM_GET­DEF­ID message, and it does this by just searching the dialog for a control with that ID. If you have two controls with the same ID, it picks one of them arbitrarily. So far so bad.

It's like having two John Smiths living in your house, one in the second bedroom and one living in the guest room. The post office is very strict and won't let you write "John Smith, Second Bedroom, 1 Main Street" and "John Smith, Guest Room, 1 Main Street." All you're allowed to write is a name and an address. Therefore, all the mail addressed to "John Smith, 1 Main Street" ends up in a single mailbox labeled "1 Main Street" and now you have to figure out who gets each piece of mail.

Okay, so we saw that when converting an ID to a window, and there are multiple windows with the same ID, the dialog manager will just pick one arbitrarily. And if it picks the wrong one, it would have sent the WM_COMMAND to the wrong dialog procedure entirely! At least by sending it to the top-level dialog, it says, "Dude, I think it's this window but I'm not sure, so if you have some really clever way of telling which is which, you can try to sort it out." And now that the WM_COMMAND sometimes goes to the top-level dialog, you're pretty much stuck having it always go to the top-level dialog for consistency. It's better to be consistently wrong in a predictable manner (so people can work around it reliably) than to be mostly-right and occasionally-completely-wrong.

Third rationale: Because you're asking for code to be written to handle a case that people shouldn't have gotten into in the first place. (Namely, duplicate control IDs.)

Whatever the reason, it's something you need to be on the lookout for. If you did everything right and avoided control ID duplication, then the workaround in your WM_COMMAND handler is straightforward:

void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
    if (hwndCtl != nullptr)
    {
        HWND hwndCtlParent = GetParent(hwndCtl);
        if (hwndCtlParent != nullptr &&
            hwndCtlParent != hwnd &&
            IsChild(hwnd, hwndCtlParent))
        {
           FORWARD_WM_COMMAND(hwndCtlParent, id,
                              hwndCtl, codeNotify, SendMessage);
           return;
        }
    }
    ... the message was for me after all, so let's handle it...
    switch (id)
    {
    ...
    }
}

When we get the WM_COMMAND message, we first check that it really came from one of our direct children. If not, then we forward the message on to the control's actual parent. (The window that should have gotten the message in the first place in an ideal world.)

Exercise: Under what circumstances can the above workaround fail? (Not counting the scenario we've spent the past three days discussing.)

Anyway, back to the question from last time: How does the property sheet manager deal with multiple property sheets pages having conflicting control IDs? In addition to what we previously discussed, another mitigating factor is that the property sheet manager keeps only one child dialog visible at a time. This takes the hidden child dialogs out of the running for most dialog-related activities, such as dialog navigation, since invisible controls cannot be targets of dialog navigation. Furthermore, hidden child dialogs are skipped when searching for keyboard accelerators, thereby avoiding the problem of hidden accelerators. So as long as the property sheet manager makes sure that focus doesn't stay on a hidden control after a page change, there shouldn't be any notifications coming from a hidden child dialog. The only conflicts it needs to worry about are conflicts between the page and the frame.

Comments (7)
  1. alegr1 says:

    By the way, Raymond, I don't think it's appropriate to compare HWND to nullptr. It's not a real pointer. Compare it to NULL, because all APIs have C conventions, not C++.

    [nullptr and NULL (in pointer context) are both null pointer constants, so it doesn't matter which one you pick. -Raymond]
  2. nullptr is valid C++11, but I don't see any mention of it in C11 Wikipedia page…  I guess it's not in C yet (if it ever will be).

  3. Joshua says:

    @JamesJohnston: It won't be. No need in C.

    The workaround code doesn't work if the two controls have the same parent.

    The workaround code doesn't work if the OK button is inside a group box (or its moral equivalent).

    [If the two controls have the same parent, then that common parent receives the notification, just like you expect. And if it's inside another window, then the notification is delivered to the control's immediate parent, just like it does for all other notifications. Try again. -Raymond]
  4. Adam Rosenfield says:

    nullptr is not in C, but that doesn't entirely matter since Raymond's scratch program on which this snippet is build is written in C++, not C (blogs.msdn.com/…/410773.aspx).  If someone wants to convert it to C, they should be smart enough to handle replacing nullptr with NULL.

    It looks like Raymond's been using nullptr since around April or so, according to a search for "nullptr oldnewthing site:blogs.msdn.com".  But this is the first time I actually noticed it.

  5. Joshua says:

    [And if it's inside another window, then the notification is delivered to the control's immediate parent]

    Infinite recursion. The group box will forward to its parent. If that is _you_ you'll forward to group box again.

    [Right, if the control's parent forwards to the grandparent, you'll get stuck in a loop. The parent window needs to know that "Oh, this one's mine, thanks." -Raymond]
  6. skSdnW says:

    Is the IsChild check really required? It will protect you from a stray message where the hwndCtl is not part of the tree at all but the normal switch does not have this check. Does it serve any other purpose that I'm missing? Some kind of infinite loop perhaps?

  7. Joshua says:

    @WndSks: You know you can get WM_COMMAND message from outside controls on specific request right?

Comments are closed.