Date: | April 29, 2005 / year-entry #109 |
Tags: | code |
Orig Link: | https://blogs.msdn.microsoft.com/oldnewthing/20050429-00/?p=35743 |
Comments: | 21 |
Summary: | 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... |
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 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
Our message box will need a dialog procedure
which ends the dialog when the 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 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:
Therefore, to get the original pixel value back,
we need to solve this formula for The template itself follows the format we discussed earlier, no surprises.
One subtlety is that the control identifier for our OK button
is 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)
Comments are closed. |
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]
Don’t you need a "using namespace std"?
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.
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.
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
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.
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.
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.
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!
> 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?’…
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.
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…
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?
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.
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.
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.
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?
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
You can’t. It’s gone.
You can’t think of everything.
PingBack from http://www.keyongtech.com/2359717-create-multiple-dialogs-at-runtime