【转载自codeproject】重写MessageBox
http://www.codeproject.com/KB/dialog/MessageBoxEx.aspx
Dissecting the MessageBox
By Sijin | 26 Apr 2005
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.
Download demo project - 32.6 Kb
Download source - 21.3 Kb
Contents
Introduction
What does it take to replicate the MessageBox?
Size and Position
Disabling the Close button
Alerts
Design of the component
Using the code
Timeouts
Localization
Things to remember
To do
Known Issues
History
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.
Collapse
_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.
Collapse
/// <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.
Collapse
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 )
{
//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.
Collapse
[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 )
{
//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:
Collapse
[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 );
}
}
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:
Collapse
/// <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:
Collapse
/// <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().
Collapse
/// <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.
Collapse
/// <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.
Collapse
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:
Collapse
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:
Collapse
/// <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