|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Contents
IntroductionSometime 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:
The 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 PositionThe 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 _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 /// <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 One interesting thing is that the font of the caption is determined by the system. Thus we cannot use the 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 Disabling the Close buttonAnother 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 [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. IconsInitially, 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 Another interesting thing that I noticed was that out of the eight enumeration values in AlertsWhen 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 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 componentThe 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 The approach I have used is to have a This means that a message box once created can be reused. If it is not required anymore, then it can be disposed using the Below is the public interface for the /// <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 Below is the public interface for /// <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 /// <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 codeUsing 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:
TimeoutsWhile 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
}
LocalizationAfter 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 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
To do
Known Issues
History
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||