Building a dialog template at run-time


We've spent quite a bit of time over the past year learning about dialog templates and the dialog manager. Now we're going to put the pieces together to do something interesting: Building a dialog template on the fly.

What we're going to write is an extremely lame version of the MessageBox function. Why bother writing a bad version of something that Windows already does? Because you can use it as a starting point for further enhancements. For example, once you learn how to generate a template dynamically, you can dynamically add buttons beyond the boring "OK" button, or you can add additional controls like a "Repeat this answer for all future occurrences of this dialog" checkbox or maybe insert an animation control.

I'm going to start with a highly inefficient dialog template class. This is not production-quality, but it's good enough for didactic purposes.

#include <vector>
class DialogTemplate {
public:
 LPCDLGTEMPLATE Template() { return (LPCDLGTEMPLATE)&v[0]; }
 void AlignToDword()
  { if (v.size() % 4) Write(NULL, 4 - (v.size() % 4)); }
 void Write(LPCVOID pvWrite, DWORD cbWrite) {
  v.insert(v.end(), cbWrite, 0);
  if (pvWrite) CopyMemory(&v[v.size() - cbWrite], pvWrite, cbWrite);
 }
 template<typename T> void Write(T t) { Write(&t, sizeof(T)); }
 void WriteString(LPCWSTR psz)
  { Write(psz, (lstrlenW(psz) + 1) * sizeof(WCHAR)); }

private:
 vector<BYTE> v;
};

I didn't spend much time making this class look pretty because it's not the focus of this article. The DialogTemplate class babysits a vector of bytes to which you can Write data. There is also a little AlignToDword method that pads the buffer to the next DWORD boundary. This'll come in handy, too.

Our message box will need a dialog procedure which ends the dialog when the IDCANCEL button is pressed. If we had made any enhancements to the dialog template, we would handle them here as well.

INT_PTR CALLBACK DlgProc(HWND hwnd, UINT wm, WPARAM wParam, LPARAM lParam)
{
 switch (wm) {
 case WM_INITDIALOG: return TRUE;
 case WM_COMMAND:
  if (GET_WM_COMMAND_ID(wParam, lParam) == IDCANCEL) EndDialog(hwnd, 0);
  break;
 }
 return FALSE;
}

Finally, we build the template. This is not hard, just tedious. Out of sheer laziness, we make the message box a fixed size. If this were for a real program, we would have measured the text (using ncm.lfCaptionFont and ncm.lfMessageFont) to determine the best size for the message box.

BOOL FakeMessageBox(HWND hwnd, LPCWSTR pszMessage, LPCWSTR pszTitle)
{
 BOOL fSuccess = FALSE;
 HDC hdc = GetDC(NULL);
 if (hdc) {
  NONCLIENTMETRICSW ncm = { sizeof(ncm) };
  if (SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, 0, &ncm, 0)) {
   DialogTemplate tmp;

   // Write out the extended dialog template header
   tmp.Write<WORD>(1); // dialog version
   tmp.Write<WORD>(0xFFFF); // extended dialog template
   tmp.Write<DWORD>(0); // help ID
   tmp.Write<DWORD>(0); // extended style
   tmp.Write<DWORD>(WS_CAPTION | WS_SYSMENU | DS_SETFONT | DS_MODALFRAME);
   tmp.Write<WORD>(2); // number of controls
   tmp.Write<WORD>(32); // X
   tmp.Write<WORD>(32); // Y
   tmp.Write<WORD>(200); // width
   tmp.Write<WORD>(80); // height
   tmp.WriteString(L""); // no menu
   tmp.WriteString(L""); // default dialog class
   tmp.WriteString(pszTitle); // title

   // Next comes the font description.
   // See text for discussion of fancy formula.
   if (ncm.lfMessageFont.lfHeight < 0) {
     ncm.lfMessageFont.lfHeight = -MulDiv(ncm.lfMessageFont.lfHeight,
              72, GetDeviceCaps(hdc, LOGPIXELSY));
   }
   tmp.Write<WORD>((WORD)ncm.lfMessageFont.lfHeight); // point
   tmp.Write<WORD>((WORD)ncm.lfMessageFont.lfWeight); // weight
   tmp.Write<BYTE>(ncm.lfMessageFont.lfItalic); // Italic
   tmp.Write<BYTE>(ncm.lfMessageFont.lfCharSet); // CharSet
   tmp.WriteString(ncm.lfMessageFont.lfFaceName);

   // Then come the two controls.  First is the static text.
   tmp.AlignToDword();
   tmp.Write<DWORD>(0); // help id
   tmp.Write<DWORD>(0); // window extended style
   tmp.Write<DWORD>(WS_CHILD | WS_VISIBLE); // style
   tmp.Write<WORD>(7); // x
   tmp.Write<WORD>(7); // y
   tmp.Write<WORD>(200-14); // width
   tmp.Write<WORD>(80-7-14-7); // height
   tmp.Write<DWORD>(-1); // control ID
   tmp.Write<DWORD>(0x0082FFFF); // static
   tmp.WriteString(pszMessage); // text
   tmp.Write<WORD>(0); // no extra data

   // Second control is the OK button.
   tmp.AlignToDword();
   tmp.Write<DWORD>(0); // help id
   tmp.Write<DWORD>(0); // window extended style
   tmp.Write<DWORD>(WS_CHILD | WS_VISIBLE |
                    WS_GROUP | WS_TABSTOP | BS_DEFPUSHBUTTON); // style
   tmp.Write<WORD>(75); // x
   tmp.Write<WORD>(80-7-14); // y
   tmp.Write<WORD>(50); // width
   tmp.Write<WORD>(14); // height
   tmp.Write<DWORD>(IDCANCEL); // control ID
   tmp.Write<DWORD>(0x0080FFFF); // static
   tmp.WriteString(L"OK"); // text
   tmp.Write<WORD>(0); // no extra data

   // Template is ready - go display it.
   fSuccess = DialogBoxIndirect(g_hinst, tmp.Template(),
                                hwnd, DlgProc) >= 0;
  }
  ReleaseDC(NULL, hdc); // fixed 11 May
 }
 return fSuccess;
}

The fancy formula for determining the font point size is not that fancy after all. The dialog manager converts the font height from point to pixels via the standard formula:

fontHeight = -MulDiv(pointSize, GetDeviceCaps(hdc, LOGPIXELSY), 72);

Therefore, to get the original pixel value back, we need to solve this formula for pointSize so that when it is sent through the formula again, we get the original value back.

The template itself follows the format we discussed earlier, no surprises.

One subtlety is that the control identifier for our OK button is IDCANCEL instead of the IDOK you might have expected. That's because this message box has only one button, so we want to let the user hit the ESC key to dismiss it.

Now all that's left to do is take this function for a little spin.

void OnChar(HWND hwnd, TCHAR ch, int cRepeat)
{
 if (ch == TEXT(' ')) {
  FakeMessageBox(hwnd,
   L"This is the text of a dynamically-generated dialog template. "
   L"If Raymond had more time, this dialog would have looked prettier.",
   L"Title of message box");
 }
}

    // add to window procedure
    HANDLE_MSG(hwnd, WM_CHAR, OnChar);

Fire it up, hit the space bar, and observe the faux message box.

Okay, so it's not very exciting visually, but that wasn't the point. The point is that you now know how to build a dialog template at run-time.

Comments (21)
  1. steven says:

    I ran into this a while ago when I used this technique to build a custom error dialog for my app: there does not seem to be a way to retrieve the localised strings for "OK", "Cancel", "Apply" etc. from the system. Is there?

    That said, people do get confused when they see something like:

    "You have unsaved changes. Are you sure you want to quit?" [Ja] [Nee] [Annuleren]

  2. Boris Zakharin says:

    Don’t you need a "using namespace std"?

  3. CornedBee says:

    Either that or (far better) fully qualify std::vector.

    The problem with importing the complete std namespace is that it’s huge, and contains things like "min" and "copy", which you wouldn’t want in the global namespace.

  4. Universalis says:

    I hope this series will include, somewhere, suggestions for how to size the message box and position its buttons in order to make it look like a real Windows message box.

    This information is essential if you actually want the result to look just like any other message box, only with an extra "Don’t show me again" button (or whatever other modification you’re making) and as far as I know it is documented nowhere.

  5. J. Edward Sanchez says:

    Universalis: For the specific modification you mentioned, you could just use the SHMessageBoxCheck() function:

    http://msdn.microsoft.com/library/en-us/shellcc/platform/shell/reference/shlwapi/others/SHMessageBoxCheck.asp

  6. Tom says:

    Quote

    Universalis: For the specific modification you mentioned, you could just use the SHMessageBoxCheck() function:

    http://msdn.microsoft.com/library/en-us/shellcc/platform/shell/reference/shlwapi/others/SHMessageBoxCheck.asp

    From the article

    Security Alert: Do not take any dangerous actions if the function returns either -1 or iDefault. If an error occurs when attempting to display the message box SHMessageBoxCheck returns -1 or, in some cases, iDefault. Such errors can be caused, for example, by insufficient memory or resources. If you get one of these return values, you should be aware that the user did not necessarily see the dialog box and consequently did not positively agree to any action.

    Note Do not confuse "Do not show this dialog box" with "Remember this answer". SHMessageBoxCheck does not provide "Remember this answer" functionality. If the user opts to suppress the message box again, the function does not preserve which button they clicked. Instead, subsequent invocations of SHMessageBoxCheck simply return the value specified by iDefault. Consider the following example.

    Yikes! Those two together pretty much convince me to write my own.

  7. Alex says:

    I’m not exactly a chatty guy, but I’ve been following this blog for some time now, enjoying most of what I read.

    Related to what steven said, I’d really like to know if there’s any way to get hold of default system dialog templates, like the one that MessageBox() uses. With knowledge gained in this series of articles it should be pretty straightforward to perform small modifications (translating buttons for one) and passing the template along its merry way to DialogBoxIndirect().

    Of course, if MessageBox() itself builds the template on the fly, then this is out of the question.

  8. Jim Kane says:

    One of the possible uses for this is modifying the Open file standard dialog. If I create a template in memory in a hglobal and pass it in the openfilename structure in the hInstance member using the OFN_ENABLETEMPLATEHANDLE flag, who is responsible for freeing the global memory? I dont see a comment in msdn either way.

  9. oldnewthing says:

    Jim Kane: Remember, the GetOpenFileName function is not psychic. Since the memory block passed as the template is just a block of memory, clearly it cannot free it since it doesn’t know how it was allocated – if it was even allocated at all. It might have (and indeed probably did) come from a resource, which is just a static block of memory.

    Alex: MessageBox builds the template on the fly. Notice that the size of the dialog changes depending on how much text you pass?

    Dean Harding: But what if the user accepts the default? For example, suppose it’s a Yes/No question with a default of Yes, and the user selected No last time. The user selects Yes this time, which happens to be the default, and your algorithm will change it to a No!

  10. Dean Harding says:

    > Those two together pretty much convince me to write my own.

    The second problem just means that you need to store the ‘last’ value that the user specified so that when you get the default back, you assume the last value.

    The first problem is more tricky, but in general I don’t think you should allow the user to suppress *any* security-related dialog. It should only be used for more mundane things like ‘This is not the default xyz application, do you want to set it now?’…

  11. Dean Harding says:

    Oh, I meant if the iDefault value is returned. The SHMessageBoxCheck function returns the value specified in the iDefault parameter if the user has, in the past, checked the "supress this dialog" option. Supposedly you’d set this to some value that’s different to what would be returned if they just hit enter to select the default *button*.

    I think the documentation for SHMessageBoxCheck is a bit confusing in this respect: there’s two defaults, the default button and the default-value-that’s-returned-if-the-dialog-wasn’t-shown.

  12. Dean Harding says:

    Hmm. OK, I admit I’ve never actually used SHMessageBoxCheck myself, so my comments before were just based on my reading the documentation. But I just read it again, and it’s got this note at the bottom:

    "Note: The default button displayed by the message box should agree with your iDefault value. The lack of support for the MB_DEFBUTTON2 flag means that iDefault should be set to IDOK if you have specified the MB_OK or MB_OKCANCEL flag. The iDefault value should be set to IDYES if you have set the MB_YESNO flag."

    Which tells me that SHMessageBoxCheck is expecting iDefault to be the ID of one of the buttons displayed. I had just assumed that it could be any old integer that I could used to work out if I need to used the saved value or not.

    So if it *is* the case that iDefault has to be the ID of one of the buttons, then it seems making your own message box to add that functionaly makes sense. I don’t know why it *would* work like that, though…

  13. J. Edward Sanchez says:

    I too have never actually used SHMessageBoxCheck() myself. I was curious enough about its behavior that I tried to write a quick test app last night so I could see exactly how iDefault is used in various situations.

    Thing is, I could find neither a version of of shlwapi.h that declared the function, nor a version of shlwapi.lib or (shlwapi.dll) that exported it. I looked in several places, including the Microsoft Windows Server 2003 SP1 Platform SDK – April 2005 Edition.

    Is the MSDN documentation in error as to where this function is defined? If so, where can I find it?

  14. Dean Harding says:

    OK, here’s what I can gather about SHMessageBoxCheckEx. It’s signature looks like this:

    int WINAPI SHMessageBoxCheckExW(HWND, HINSTANCE, LPCWSTR, DLGPROC, LPARAM, int, LPCWSTR)

    It basically just lets you specify your own dialog resource to use as the message box, and your own dialog procedure. SHMessageBoxCheck, therefore, is just a wrapper around SHMessageBoxCheckEx which passes in a dialog resource from shlwapi.dll (#4608) and a default dlgproc.

    So while it looks like SHMessageBoxCheck is probably pretty neat, there’s still a good reason to write your own MessageBoxWithCheck function to get the proper window size, better handling of the case when the checkbox is checked, and all that.

  15. J. Edward Sanchez says:

    To answer my own question: SHMessageBoxCheck() is exported by shlwapi.dll, but only by ordinal (185 for ASCII, 191 for Unicode).

    I played around with it, and have come to the conclusion that it’s best to let iDefault be some value that doesn’t coincide with any button command ID, or with the -1 error code. For example, -2 seems to work well. This is contrary to the second "Note" in the documentation, which appears to have been written by someone confused about the purpose of iDefault.

    With that in mind, there are two ways to approach using this function sensibly:

    The first way is as Dean Harding suggested: always cache the return code of the call whenever it corresponds to a valid command ID. When the function starts returning -2 (my suggested iDefault), it means that the user requested a "don’t show again" on the *previous* call; when this happens, use the cached value of the return code instead. Because the function does not inform you of the user’s "don’t show again" request until the next call, the return code must be cached every time. Furthermore, this cached value must be made to persist as well, in case the user exits the application before the function has a chance to be called again (and return -2).

    The second way is to call the function, and then immediately check each time to see if the appropriate "DontShowMeThisDialogAgain" registry value has been created. (This may be ugly, but it should be a reliable thing to check, since it’s documented.) If it has, cache the *current* return code of the function, and be sure to make it persist for next time. When the function starts returning -2, use that cached return code instead. Actually, once you’ve seen that the registry value has been created, you can always just skip over the SHMessageBoxCheck() call entirely, and go straight to the cached return code.

    As for the cosmetic appearance of the dialog, it doesn’t really look much like MessageBox(). Here are the differences I’ve noticed:

    – The margins are smaller.

    – The dialog is narrow and appears to be fixed-width; only the height changes to accommodate long text.

    – The buttons are right-aligned instead of centered.

    – The dialog makes no sound at all, regardless of the icon being displayed.

    There is also a fancier SHMessageBoxCheckEx() function (291 ASCII, 292 Unicode), but it isn’t officially documented as far as I can tell, so I haven’t bothered playing with it.

  16. J. Edward Sanchez says:

    I’ve just remembered another MessageBox() feature that, sure enough, SHMessageBoxCheck() also fails to implement:

    – The dialog caption and text are not copied to the clipboard when the user presses Ctrl+Insert or Ctrl+C.

    (I realize that this whole SHMessageBoxCheck() sidetrack is a somewhat off-topic, but I figure that this might serve as a useful feature checklist for those looking to reimplement MessageBox() with extra functionality.)

    While we’re on the subject of SHMessageBoxCheck(), here’s another thought for those still interested in using it despite all its shortcomings:

    It has occurred to me that the first approach to using the function (i.e., Dean Harding’s) can be streamlined by simply passing the previous return code value as iDefault. Of course, this still means that the return code must be cached and made persistent, but it greatly simplifies the logic surrounding the function call. No more need to use an arbitrary iDefault like -2; you can just use the return code as-is — unless of course it’s -1, in which case you should fall back to MessageBox().

    Nice and clean. In fact, it makes so much sense (finally) that I suspect that this is actually how SHMessageBoxCheck() was designed to be used — the MSDN documentation notwithstanding.

    How does one suggest a documentation correction to MSDN? If this actually is the way SHMessageBoxCheck() and iDefault were designed to be used, it would be nice to see it documented correctly. It would also be nice to see the true location of this and other shlwapi.dll functions documented (i.e., exported only by ordinal), and the bogus references to shlwapi.h deleted.

  17. binaryc says:

    Shouldn’t you pass NULL as the hwnd to ReleaseDC since you have the DC for the entire screen rather than for a specific window?

  18. Jim Kane says:

    Dear OldNewThing Guy:

    When I asked about disposal of the template I had not yet gotten my code running. You are correct that in retrospect my question was silly. To save someone else the trouble I went thru the docs of OFN structure for hinstance says:

    If the OFN_ENABLETEMPLATEHANDLE flag is set in the Flags member, hInstance is a handle to a memory object containing a dialog box template.

    Which is incorrect – you pass the address of the memory block not a handle! I was trying to pass an hglobal for the longest time but it is the address that worked.

    Jim Kane

  19. You can’t. It’s gone.

  20. You can’t think of everything.

Comments are closed.