Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Dissecting the MessageBox

0.00/5 (No votes)
26 Apr 2005 1  
A look at what goes into creating a message box, and in the process, create a customizable message box that supports custom buttons, icons, fonts ,"Don't ask me again" functionality, timeouts, localization and lots more.

Sample custom message box

Contents

Introduction

Sometime ago, I was musing about current user interface design trends. I had been doing a lot of development at that time and VSS was one frequently used app, I started thinking about the VSS message box, you know the one which has six buttons like "Replace", "Merge", "Leave" etc. and the "Apply to all items" checkbox. If we need to implement something like that today, we would have to roll our own message box for each such message box, well, that's just a waste of time. So I started thinking about a reusable message box that supported stuff like custom buttons, "Don't ask me again" feature, etc. I did find some articles about custom message boxes but most of them were implemented using hooks. Well, I didn't like that too much and would never use such solutions in a production environment. So I decided to create a message box from scratch and provide the functionality that I needed. I also wanted to expose the functionality in an easy to use manner. This article describes some of the hurdles and interesting things I discovered while implementing this custom message box. The source code accompanying this article implements a custom message box that:

  • Implements all the features of the normal message box.
  • Supports custom buttons with tooltips.
  • Supports custom icons.
  • Allows the user to save his/her response.
  • Supports custom fonts.
  • Supports disabling the alert sound that is played while displaying the message box.
  • Supports timeouts.
  • Supports Localization.

The MessageBoxEx component can be used as is in your applications to show message boxes where the standard message box won't do. Also, it can be used as a starting point for creating your own custom message box.

What does it take to replicate the MessageBox?

Let's take a look at what all is required if you need to implement a message box that duplicates the functionality provided by default.

Size and Position

The message box dynamically resizes itself to best fit its content. The factors that determine the size of the message box are message text, caption text and number of buttons. Also, I discovered that it imposes some limits on its size, both horizontally and vertically. So no matter how long the text of the message box is, the message box will never extend beyond your screen area, in fact, it does not even come close to covering the entire screen area. The message box also displays itself in the center of the screen.

So we need to first determine the maximum size for the message box. This can be done by using the SystemInformation class.

_maxWidth = (int)(SystemInformation.WorkingArea.Width * 0.60);
_maxHeight = (int)(SystemInformation.WorkingArea.Height * 0.90);

So the message box has a max width of 60% of the screen width and max height of 90% of the screen height.

For fitting the size of the message box to its contents, we can make use of the Graphics.MeasureString() method.

/// <summary>

/// Measures a string using the Graphics object for this form with

/// the specified font

/// </summary>

/// <param name="str">The string to measure</param>

/// <param name="maxWidth">The maximum width

///          available to display the string</param>

/// <param name="font">The font with which to measure the string</param>

/// <returns></returns>

private Size MeasureString(string str, int maxWidth, Font font)
{
    Graphics g = this.CreateGraphics();
    SizeF strRectSizeF = g.MeasureString(str, font, maxWidth);
    g.Dispose();

    return new Size((int)Math.Ceiling(strRectSizeF.Width), 
               (int)Math.Ceiling(strRectSizeF.Height));
}

The above code is used to determine the size of the various elements in the message box. Once we have the size required by each of the elements, we determine the optimal size for the form and then layout all the elements in the form. The code for determining the optimal size is in the method MessageBoxExForm.SetOptimumSize(), and the code for laying out the various elements is in MessageBoxExForm.LayoutControls().

One interesting thing is that the font of the caption is determined by the system. Thus we cannot use the Form's Font property to measure the size of the caption string. To get the font of the caption, we can use the Win32 API SystemParametersInfo.

private Font GetCaptionFont()
{
    
    NONCLIENTMETRICS ncm = new NONCLIENTMETRICS();
    ncm.cbSize = Marshal.SizeOf(typeof(NONCLIENTMETRICS));
    try
    {
        bool result = SystemParametersInfo(SPI_GETNONCLIENTMETRICS, 
                                           ncm.cbSize, ref ncm, 0);
    
        if(result)
        {
            return Font.FromLogFont(ncm.lfCaptionFont);
        }
        else
        {
            int lastError = Marshal.GetLastWin32Error();
            return null;
        }
    }
    catch(Exception /*ex*/)
    {
        //System.Console.WriteLine(ex.Message);

    }
    
    return null;
}

private const int SPI_GETNONCLIENTMETRICS = 41;
private const int LF_FACESIZE = 32;

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]
private struct LOGFONT
{ 
    public int lfHeight; 
    public int lfWidth; 
    public int lfEscapement; 
    public int lfOrientation; 
    public int lfWeight; 
    public byte lfItalic; 
    public byte lfUnderline; 
    public byte lfStrikeOut; 
    public byte lfCharSet; 
    public byte lfOutPrecision; 
    public byte lfClipPrecision; 
    public byte lfQuality; 
    public byte lfPitchAndFamily; 
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public string lfFaceSize;
}

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]
private struct NONCLIENTMETRICS
{
    public int cbSize;
    public int iBorderWidth;
    public int iScrollWidth;
    public int iScrollHeight;
    public int iCaptionWidth;
    public int iCaptionHeight;
    public LOGFONT lfCaptionFont;
    public int iSmCaptionWidth;
    public int iSmCaptionHeight;
    public LOGFONT lfSmCaptionFont;
    public int iMenuWidth;
    public int iMenuHeight;
    public LOGFONT lfMenuFont;
    public LOGFONT lfStatusFont;
    public LOGFONT lfMessageFont;
} 

[DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)]
private static extern bool SystemParametersInfo(int uiAction, 
   int uiParam, ref NONCLIENTMETRICS ncMetrics, int fWinIni);

One interesting thing that happened while I was working on getting the caption font was with the definition of the LOGFONT structure. You see in MSDN documentation, the first five fields of the LOGFONT structure were declared as type long. I blindly copied the definition from the MSDN documentation and my call to SystemParametersInfo always returned false. After banging my head for four hours trying to figure out what the problem was, I came across a code snippet that used SystemParametersInfo. I downloaded that snippet and it worked perfectly on my machine. On further inspection, I noticed that the size of the structure I was passing to SystemParametersInfo was different from what the code snippet had. And then the lights came on, long should have been mapped to int...aaaargh.

Disabling the Close button

Another interesting thing that I had never really noticed about the message box was that if you don't have a Cancel button in your message box, the Close button on the top right is disabled. You can check this by showing a message box with "Yes", "No" buttons only. Not only is the Close button disabled but the system menu also does not show a Close option. So, that called for some more P/Invoke magic to disable the Close button if more than one button was present and there were no Cancel buttons. Of course, since the buttons themselves are custom, each button has a IsCancelButton property that you can set if you want to make the button a Cancel button.

[DllImport("user32.dll", CharSet=CharSet.Auto)]
private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);

[DllImport("user32.dll", CharSet=CharSet.Auto)]
private static extern bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem,
uint uEnable);

private const int SC_CLOSE  = 0xF060;
private const int MF_BYCOMMAND  = 0x0;
private const int MF_GRAYED  = 0x1;
private const int MF_ENABLED  = 0x0;

private void DisableCloseButton(Form form)
{
    try
    {
        EnableMenuItem(GetSystemMenu(form.Handle, false), 
                   SC_CLOSE, MF_BYCOMMAND | MF_GRAYED);
    }
    catch(Exception /*ex*/)
    {
//System.Console.WriteLine(ex.Message);

    }
}

The above code disables the Close button, and also disables the Alt+F4 and Close option from the system menu.

Icons

Initially, I had obtained all the standard message box icons from various system files, using ResHacker. After I had posted the article, Carl pointed out the SystemIcons enumeration which provides all the standard system icons. So now, instead of embedding the standard icons into the message box, I am using the SystemIcons enumeration to draw the standard icons. Which means that on Windows XP, instead of , the real system icon is shown. An interesting problem that came up was that initially I was using a PictureBox control to display the icon. Now since it can only take Image objects, I tried converting the icon returned by SystemIcons to a Bitmap via the Icon.ToBitmap() method. This worked alright for Win2K icons, but on XP where the icons had an alpha channel, the icons came out looking terrible. Next, I tried manually creating a bitmap and then painting the icon onto the bitmap using Graphics.DrawIcon(), that too gave the same results. So finally, I had to draw the icon directly on the surface of the message box and drop the picture box.

Another interesting thing that I noticed was that out of the eight enumeration values in MessageBoxIcon, only four had unique values. Thus, Asterisk = Information, Error = Hand, Exclamation = Warning and Hand = Stop. The difference I believe is in the support for Compact Framework.

Alerts

When I was almost finished with my implementation, I realized that my message box made no sound when it displayed. I knew that the sounds were configurable via the Control Panel so I could not embed the sounds in the library. Fortunately, there is an API called MessageBeep which is exactly what I required. It takes only one parameter which is an integer representing the icon that is being displayed for the message box.

Below is the code that plays the alerts whenever a message box is popped:

[DllImport("user32.dll", CharSet=CharSet.Auto)]
private static extern bool MessageBeep(uint type);

if(_playAlert)
{
    if(_standardIcon != MessageBoxIcon.None)
    {
        MessageBeep((uint)_standardIcon);
    }
    else
    {
        MessageBeep(0 /*MB_OK*/);
    }
}

Design of the component

The interesting part in the design was how to implement the "Don't ask me again" a.k.a. SaveUserResponse feature. I didn't want that the client code be littered with if statements checking if a saved response was available. So I decided that the client code should always call MessageBoxEx.Show() and if the user had saved a response then that response should be returned by the call rather than the dialog actually popping up. The next problem to handle was message box identity, how do I identify that the same message box is being invoked so that I can lookup if a user has saved a response to that message box? One solution would have been to create a hash of the message text, caption text, buttons etc. to identify the message box. The problem here was that in cases where we didn't want to use the response saved by the user, this approach would fail. Another big disadvantage was that there was no way to undo the saved response; once a user made a choice, he had to stick to it for the entire process lifetime.

The approach I have used is to have a MessageBoxExManager that manages all message boxes. Basically, all message boxes are created with a name. The name can be used to retrieve the message box at a later stage and invoke it. The name can also be used to reset the saved response of the user. One other functionality which I have exposed via the MessageBoxExManager is the ability to persist saved responses. Although there is no implementation for this right now, it can be implemented very easily, only the hashtable containing the saved responses need to be serialized.

This means that a message box once created can be reused. If it is not required anymore, then it can be disposed using the MessageBoxExManager.DeleteMessageBox() method; or if you want to create and show a one time message box, then you can pass null in the call to MessageBoxExManager.CreateMessageBox(). If a message box is created with a null name, then it is automatically disposed after the first call to MessageBoxEx.Show().

Below is the public interface for the MessageBoxExManager, along with explanations:

/// <summary>

/// Manages a collection of MessageBoxes. Basically manages the

/// saved response handling for messageBoxes.

/// </summary>

public class MessageBoxExManager
{
    /// <summary>

    /// Creates a new message box with the specified name. If null is specified

    /// in the message name then the message

    /// box is not managed by the Manager and

    /// will be disposed automatically after a call to Show()

    /// </summary>

    /// <param name="name">The name of the message box</param>

    /// <returns>A new message box</returns>

    public static MessageBoxEx CreateMessageBox(string name);

    
    /// <summary>

    /// Gets the message box with the specified name

    /// </summary>

    /// <param name="name">The name of the message box to retrieve</param>

    /// <returns>The message box

    /// with the specified name or null if a message box

    /// with that name does not exist</returns>

    public static MessageBoxEx GetMessageBox(string name);
    
    
    /// <summary>

    /// Deletes the message box with the specified name

    /// </summary>

    /// <param name="name">The name of the message box to delete</param>

    public static void DeleteMessageBox(string name);
    
    
    /// <summary>

    /// Persists the saved user responses to the stream

    /// </summary>

    public static void WriteSavedResponses(Stream stream);
    

    /// <summary>

    /// Reads the saved user responses from the stream

    /// </summary>

    public static void ReadSavedResponses(Stream stream)
    

    /// <summary>

    /// Reset the saved response for the message box with the specified name.

    /// </summary>

    /// <param name="messageBoxName">The name of the message box

    ///         whose response is to be reset.</param>

    public static void ResetSavedResponse(string messageBoxName);
    

    /// <summary>

    /// Resets the saved responses for all message boxes

    /// that are managed by the manager.

    /// </summary>

    public static void ResetAllSavedResponses();

}

Another design decision was regarding how to expose the MessageBoxEx class itself. Although the MessageBoxEx is a Form, I did not want to expose it as a Form for two reasons: one was to abstract away the implementation details and the second was to reduce intellisense clutter while working with the class. Thus, MessageBoxEx is a proxy to the real Form which is implemented in MessageBoxExForm.

Below is the public interface for MessageBoxEx:

/// <summary>

/// An extended MessageBox with lot of customizing capabilities.

/// </summary>

public class MessageBoxEx
{
    /// <summary>

    /// Sets the caption of the message box

    /// </summary>

    public string Caption


    /// <summary>

    /// Sets the text of the message box

    /// </summary>

    public string Text


    /// <summary>

    /// Sets the icon to show in the message box

    /// </summary>

    public Icon CustomIcon


    /// <summary>

    /// Sets the icon to show in the message box

    /// </summary>

    public MessageBoxExIcon Icon


    /// <summary>

    /// Sets the font for the text of the message box

    /// </summary>

    public Font Font

    /// <summary>

    /// Sets or Gets the ability of the  user to save his/her response

    /// </summary>

    public bool AllowSaveResponse


    /// <summary>

    /// Sets the text to show to the user when saving his/her response

    /// </summary>

    public string SaveResponseText


    /// <summary>

    /// Sets or Gets wether the saved response if available should be used

    /// </summary>

    public bool UseSavedResponse


    /// <summary>

    /// Sets or Gets wether an alert sound

    /// is played while showing the message box

    /// The sound played depends on the the Icon selected for the message box

    /// </summary>

    public bool PlayAlsertSound

    /// <summary>

    /// Sets or Gets the time in milliseconds

    /// for which the message box is displayed

    /// </summary>

    public int Timeout
    
    /// <summary>

    /// Controls the result that will be returned when the message box times out

    /// </summary>

    public TimeoutResult TimeoutResult
   
    /// <summary>

    /// Shows the message box

    /// </summary>

    /// <returns></returns>

    public string Show()

    /// <summary>

    /// Shows the messsage box with the specified owner

    /// </summary>

    /// <param name="owner"></param>

    /// <returns></returns>

    public string Show(IWin32Window owner)


    /// <summary>

    /// Add a custom button to the message box

    /// </summary>

    /// <param name="button">The button to add</param>

    public void AddButton(MessageBoxExButton button)


    /// <summary>

    /// Add a custom button to the message box

    /// </summary>

    /// <param name="text">The text of the button</param>

    /// <param name="val">The return value

    ///         in case this button is clicked</param>

    public void AddButton(string text, string val)


    /// <summary>

    /// Add a standard button to the message box

    /// </summary>

    /// <param name="buttons">The standard button to add</param>

    public void AddButton(MessageBoxExButtons button)


    /// <summary>

    /// Add standard buttons to the message box.

    /// </summary>

    /// <param name="buttons">The standard buttons to add</param>

    public void AddButtons(MessageBoxButtons buttons)
}

Also for convenience, the standard message box buttons are available as an enumeration which can be used in AddButton().

/// <summary>

/// Standard MessageBoxEx buttons

/// </summary>

public enum MessageBoxExButtons
{
    Ok = 0,
    Cancel = 1,
    Yes = 2,
    No = 4,
    Abort = 8,
    Retry = 16,
    Ignore = 32,
}

Also, the results of these standard buttons are available as constants.

/// <summary>

/// Standard MessageBoxEx results

/// </summary>

public struct MessageBoxExResult
{
    public const string Ok = "Ok";
    public const string Cancel = "Cancel";
    public const string Yes = "Yes";
    public const string No = "No";
    public const string Abort = "Abort";
    public const string Retry = "Retry";
    public const string Ignore = "Ignore";
    public const string Timeout = "Timeout";
}

Using the code

Using the code is pretty straightforward. Just add the MessageBoxExLib project to your application, and you're ready to go. Below is some code that shows how to create and display a standard message box with the option to save the user's response.

MessageBoxEx msgBox = MessageBoxExManager.CreateMessageBox("Test");
msgBox.Caption = "Question";
msgBox.Text = "Do you want to save the data?";

msgBox.AddButtons(MessageBoxButtons.YesNo);
msgBox.Icon = MessageBoxIcon.Question;

msgBox.SaveResponseText = "Don't ask me again";
msgBox.Font = new Font("Tahoma",11);

string result = msgBox.Show();

Here is the resulting message box:

Here is some code that demonstrates how you can use your own custom buttons with tooltips in your message box:

MessageBoxEx msgBox = MessageBoxExManager.CreateMessageBox("Test2");
msgBox.Caption = "Question";
msgBox.Text = "Do you want to save the data?";

MessageBoxExButton btnYes = new MessageBoxExButton();
btnYes.Text = "Yes";
btnYes.Value = "Yes";
btnYes.HelpText = "Save the data";

MessageBoxExButton btnNo = new MessageBoxExButton();
btnNo.Text = "No";
btnNo.Value = "No";
btnNo.HelpText = "Do not save the data";

msgBox.AddButton(btnYes);
msgBox.AddButton(btnNo);

msgBox.Icon = MessageBoxExIcon.Question;

msgBox.SaveResponseText = "Don't ask me again";
msgBox.AllowSaveResponse = true;

msgBox.Font = new Font("Tahoma",8);

string result = msgBox.Show();

Here is the resulting message box:

Timeouts

While showing the message box, a timeout value can be specified; if the user does not select a response within the specified time frame, then the message box will be automatically dismissed. The result that is returned when the message box times out can be specified using the enumeration shown below:

/// <summary>

/// Enumerates the kind of results that can be returned when a

/// message box times out

/// </summary>

public enum TimeoutResult
{
    /// <summary>

    /// On timeout the value associated with

    /// the default button is set as the result.

    /// This is the default action on timeout.

    /// </summary>

    Default,

    /// <summary>

    /// On timeout the value associated with

    /// the cancel button is set as the result. 

    /// If the messagebox does not have a cancel button

    /// then the value associated with 

    /// the default button is set as the result.

    /// </summary>

    Cancel,

    /// <summary>

    /// On timeout MessageBoxExResult.Timeout is set as the result.

    /// </summary>

    Timeout
}

Here is a code snippet that shows how you can use the timeout feature:

MessageBoxEx msgBox = MessageBoxExManager.CreateMessageBox(null);
msgBox.Caption = "Question";
msgBox.Text = "Do you want to save the data?";

msgBox.AddButtons(MessageBoxButtons.YesNo);
msgBox.Icon = MessageBoxExIcon.Question;

//Wait for 30 seconds for the user to respond

msgBox.Timeout = 30000;
msgBox.TimeoutResult = TimeoutResult.Timeout;

string result = msgBox.Show();
if(result == MessageBoxExResult.Timeout)
{
    //Take action to handle the timeout

}

Localization

After my initial posting of this article, Carl and Frank pointed out that the message box could also be useful in localized applications. Now, initially I had thought that I would be able to access the localized strings for standard buttons like "OK", "Cancel" etc. from the OS itself, it seems that there is no such documented way, I even talked to Michael Kaplan and he confirmed that there is no way to get those strings. Now instead of thinking of some hack to get the strings from the OS, I decided to use a simple solution, I moved the strings into a .resx file and used that to show the text for standard buttons based on the CurrentUICulture property of the current thread. I've included resources for French and German using BabelFish. Here is an example of a message box that was created after setting the CurrentUICulture to "fr".

MessageBoxEx msgBox = MessageBoxExManager.CreateMessageBox(null);
msgBox.Caption = "Question";
msgBox.Text = "Voulez-vous sauver les donn�es ?";
msgBox.AddButtons(MessageBoxButtons.YesNoCancel);
msgBox.Icon = MessageBoxExIcon.Question;

msgBox.Show();

The resulting message box is shown below:

Things to remember

  • If a message box is created without a name, i.e., null is passed to CreateMessageBox(), then you cannot use the SaveResponse feature, the message box will be automatically disposed after the first call to Show().
  • If you want to provide a dialog where a user can reset his saved definitions, then you can use MessageBoxExManager.ResetSavedResponse() method.
  • You can always use this code as a starting point for your own message box. You might want to take a look at some of the points I have mentioned in the To Do section to get some ideas as to where you can take this.

To do

  1. Add support for RTF or HTML text in message box text.
  2. Add support for gradient backgrounds and background images.
  3. Add support for showing detailed help.
  4. Add support to choose default button, i.e., the button that is selected by default. Currently, this is the first button that is added.
  5. Add support for persisting saved user responses.
  6. Make a Buttons custom collection that allows insertion of buttons at specified indexes.
  7. Workaround for tooltip bug. See below.

Known Issues

  1. The tooltips don't display the second time a message box is shown. This happens because of a known issue in the .NET framework. See this thread for more info.

History

  • 23-April-2005
    • Fixed a bug where in some cases the scrollbars for the text box were coming up unnecessarily. I adjusted the width and height of the scrollbars as well while calculating the optimum size for the textbox. The size of the scrollbars can be determined using SystemInformation.VerticalScrollbarWidth and SystemInformation.HorizontalScrollbarHeight properties.
  • 26-Mar-2005
    • Fixed a bug where if the text of the message box was too long, the buttons were not visible. I've replaced the label control that I was using to show the message text with a textbox, thus now, even if the text is really long, instead of the text getting truncated, scroll bars come up, allowing the user to see the entire message. This might be useful in case some really long stack trace is being viewed.
    • Replaced embedded icons with icons from the SystemIcons enumeration.
    • Added basic support for localization. Texts of standard message box buttons like "OK", "Cancel" etc. are now read from resource files based on the current culture.
  • 07-Mar-2005
    • Added timeout support.
    • Set the FlatStyle of buttons and checkbox to "System" so that the controls draw themselves based on the active theme. If the application that uses the message box has enabled visual styles or is using the manifest to use Common Control v6.0, then the buttons and checkbox will draw themselves in a themed manner.
    • Fixed a bug where buttons were getting created twice. This was happening because Load event gets fired every time ShowDialog() is called.
    • Fixed a bug where the size of the message box was not correct when fixed-width fonts were used. This was happening because the AutoScale property of the message box form was set to true.
  • 21-Feb-2005
    • Initial release.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here