Introduction
"Do not ask again" Message Boxes now appear in a lot of applications. Doing individual dialogs for each message box turns out to be a full nightmare that I did not even try myself! This article proposes a very straight way to implement these: with a Windows hook. The idea is to catch the creation of message boxes (see below how this is done) and then add the necessary controls (a separator and a checkbox) to the message box after having resized it. As Shog9 suggested I am going to do this in two different ways.
First way: Hooking
Hooking Message Boxes
Windows hooking is a nice feature WIN32 offers us: it allows us to intercept a lot of system calls especially regarding window management. In particular, we are able to catch the WM_INITDIALOG
message for every dialog our application opens: our own dialog classes but more important Common Control dialogs or whatever. When we catch the WM_INITDIALOG
for a new dialog, we can decide to subclass it or not. When we subclass it, a function of our own will be called each time the dialog gets a message (WM_INITDIALOG
or WM_COMMAND
for instance).
Detecting a new message box is not an exact science: once it is created it can't be differentiated from another dialog. The method used here is simply to inspect all the children of the dialogs. The criteria for a dialog to be a message box is: 1 static icon, 1 static text, between 1 and 3 buttons and no control of any other type. We use the EnumChildWindows
function to accomplish this. Moreover we check that the static text is one we have recorded before opening the message box.
Once a message box has been detected we subclass it. Our subclassing performs the following on WM_INITDIALOG
:
- Resize the window to hold the new controls (separator line and checkbox)
- If the width of the window was changed, then move the buttons so that they still appear centered
- Create the new controls
It also catches WM_COMMAND
to handle mouse click on the checkbox. The cleanup (some delete
) is done on WM_NCDESTROY
which is the very last message we get for the dialog.
Usage
Before being able to use the hook you must install it. Best place to do this is in the InitInstance
of your application. In the same way, the hook should be uninstalled in the ExitInstance
of your application. Two utility methods are provided to do this: InstallMessageBoxHook()
and UninstallMessageBoxHook()
. The install function takes two optional parameters that are the label of the checkbox: this is for easy localization.
The usage is very straightforward using the new function NabMessageBox()
which replaces AfxMessageBox()
, but that keeps the same interface. Whenever you want a dialog box to have a "Do Not Ask Again" checkbox, simply use NabMessageBox()
instead of AfxMessageBox()
. The return code will be exactly the same except that it will be opposite (negative I mean) if the "Do not..." box was checked (eg. -MB_OK
instead of MB_OK
).
Second way: MFC subclassing
Subclassing Message Boxes
The way suggested by Shog9 is to use the MFC subclassing and have a class encapsulate the whole subclassing. This leads to a much much cleaner code. This relies on MFC hooking through the AfxHookWindowCreate
function. With this, we redirect the message box messages to us and therefore are able to catch (once more) WM_INITDIALOG
. Here we resize the window, add the control and so on...
The big advantage is that our class is in a way the message box itself. Therefore we do not have to detect if we are a message box by inspecting our children, our any of the fancy tricks used in the 1st way. Also the click on the button is simply handled with a normal ON_COMMAND
entry in the message map.
Usage
There is no need to install or uninstall a hook with this method. Simply instantiate a CnbMessageBox
passing the parent, then call MessageBox()
passing the same arguments as to AfxMessageBox()
. To know if the "Do not ... again" box was checked simply call GetChecked()
on the dialog.
Implementing it
In both cases it is your responsibility to uniquely identify each prompt of your application and serialize (in the registry or in a file) the "do not ask again" answers, that were made to make sure to not prompt the same message twice!
You will probably need a unique persistent object (a singleton) that will load the answer when your app starts and save them on exit. Each time you want to display a message box, then something like this should be called (this is for the hooking way):
int CMessagePrompter::DoMessageBox(CString strMessage,
int nType, int nUniqueId)
{
if (HasStoredReply(nUniqueId))
{
return GetStoredReply(nUniqueId);
}
else
{
int rc = NabMessageBox(strMessage, nType);
if (rc<0)
StoreReply(nUniqueId, -rc);
return abs(rc);
}
}
Comparison
Hooking exhibits a big limitation: you cannot display two message boxes with the exact same text at the same time. You can display two message boxes (from different threads) without any problem though. The subclassing routine uses this text to know if it is supposed to modify the dialog or not. If two message boxes with the exact same text are displayed at the same time, then we will run into problems. Other than that, everything seems to work fine!
The MFC subclassing does not show any problem so it is probably better :-)
Conclusion
We have seen two simple ways to implement such message boxes in an application. The MFC subclassing method has the big advantage of having a much more simple and cleaner code. So it is up to you to decide!