
Introduction
In this article, I will explain how to extend the native message box in managed code. There're two functions that I will add to the message box:
- Display a countdown message, and automatically close the message box when the specified timeout expires, and return the default selection of the dialog.
- Add a checkbox control to the message box to give the user an extra option.
Background
There're some articles that have already implemented these functions via Windows Hooks. Although they are very effective, I think the technique is too complex to handle, specially for beginners. So, I decided to try another way. Thammadi's article gave me a better idea. He uses the Windows timer service to close a message box programmatically; I can do the same to achieve my goal.
Declaring Native Objects
Since the message box window is a native window, we can only access it via Windows APIs. The following is a list of APIs we need to complete this mission. Most of them were disassembled from System.Window.Forms.dll by using Lutz Roeder's Reflector. :)
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SendMessage(IntPtr hWnd,
int msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern bool SetWindowText(IntPtr hWnd, string text);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern int GetWindowText(IntPtr hWnd,
StringBuilder text, int maxCount);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public extern static IntPtr FindWindow(string className, string caption);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public extern static IntPtr FindWindowEx(IntPtr hwndParent,
IntPtr hwndChildAfter, string className, string caption);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public extern static int GetWindowLong(IntPtr hWnd, int index);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public extern static IntPtr SetWindowLong(IntPtr hWnd,
int index, IntPtr newLong);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public extern static IntPtr SetParent(IntPtr hWndChild,
IntPtr hWndNewParent);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern bool EnumChildWindows(IntPtr hWndParent,
EnumChildProc callback, IntPtr param);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern int GetClassName(IntPtr hWnd,
StringBuilder className, int maxCount);
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
public static extern bool GetWindowRect(IntPtr hWnd,
[In, Out] ref NativeMethods.RECT rect);
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
public static extern bool GetClientRect(IntPtr hWnd,
[In, Out] ref NativeMethods.RECT rect);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter,
int x, int y, int cx, int cy, int flags);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern bool ScreenToClient(IntPtr hWnd,
[In, Out] ref NativeMethods.POINT point);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern bool DestroyWindow(IntPtr hWnd);
We also need two struct types to process the native window, see the code below:
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int x;
public int y;
}
There're some more constants we need. Please refer to the file NativeMethods.cs that is avaolable in the source code, for details.
Handling the Message Box
The managed MessageBox
class calls a native API MessageBox
to show the message box. If we want to take control of it, and avoid using Windows hooks, we must do things after the message box is shown. As we all know, the message box is a modal dialog, we cannot just write code after the MessageBox.Show()
statement to handle the dialog. We need the code to be executed when the program is blocked by the modal dialog. The Timer
component can help us to do so.
Setting up a Timer component
The Timer component can be found in the Component sections on the toolbox, drag and drop it to your form. This component has four important members: an Interval
property, Start
and Stop
methods, and a Tick
event. What we need to do is create an event handing method for the Tick event. By default, the name of the method would be timer1_Tick
, and all the handling code goes there.
Preparing for showing the message box
Showing the message box can be done by a single statement. But for handling it, we must start the timer first and do some initialization works. Since we will do some fantastic work the first time the timer1_Tick
method is invoked, we should set the Interval
property of timer1
to a very small value, so that the user would not notice the changes that have been made to the message box.
seconds = -1;
timer1.Interval = 10;
timer1.Start();
System.Windows.Forms.MessageBox.Show(this,
"Message body goes here.",
"Dialog Caption",
MessageBoxButtons.YesNoCancel,
MessageBoxIcon.Information,
MessageBoxDefaultButton.Button2);
Implementation of the Timeout Function
The timeout function makes the message box to be closed automatically after the specified number of seconds. So, we need a field variable to keep the elapsed seconds, and name this variable seconds
. As we want to display a countdown message to the user, a Label
control is also required.
Implementing the timer1_Tick method
First, in this method, we need to ensure that the message box dialog has actually been opened. Call the FindWindow
function to get the handle of this dialog. As shown above, this API has two parameters. The first parameter specifies the class name of the window we want to find, we may pass null
to ignore it. The second one is the text displayed in the window's title bar. If the return value is non-zero, it means the message box has been found, and the returned value is the handle to the message box.
After the message box has been found, we should determine whether it is the first time the method has been invoked (seconds
== -1). If so, we need to stop the timer, decorate the message box, change timer1
's interval to 1000, and restart it. The variable seconds
should be increased each time the method is invoked. And then, check to see if the variable exceeds the timeout. When expiration is detected, close the message box immediately. While the timer is ticking, we can also display a countdown message to notify he user. To do this, we should place a Label
control on the message box at the first tick. For details, see the following two sections.
If the message box was not found, we should consider that the message box has been closed. So, stop the timer.
The following code demonstrates a simple implementation of the timer1_Tick
method:
IntPtr hWndMsgBox;
hWndMsgBox = FindWindow(null, "Dialog Caption");
if (hWndMsgbox != IntPtr.Zero)
{
if (seconds == -1)
{
timer1.Stop();
timer1.Interval = 1000;
timer1.Start();
}
label.Text = string.Format("{0} seconds elapsed.", 30 - ++seconds);
if (seconds >= 30)
{
timer1.Stop();
}
}
else
timer1.Stop();
Adding a Label control to the message box
Adding a control to a native window can be done by calling the API function SetParent
. What we should be concerned about is where and how to place this control. In my project, I placed the Label
control at the bottom-left corner of the message box. Of course, it is not good enough that we just place the control, we should also modify the dimensions of the message box to fit the Label
.
Here, we need the following API functions:
GetWindowRect
- retrieves the dimensions of the bounding rectangle of the message box.
SetWindowPos
- changes the size of the message box.
GetClientRect
- retrieves the coordinates of the message box's client area.
The code sample for adding the Label control (see the MessageBox.DecorateMessageBox
method for details):
RECT rect = new RECT();
GetWindowRect(hWndMsgBox, ref rect);
SetWindowPos(hWndMsgBox, IntPtr.Zero, 0, 0, rect.right - rect.left,
rect.bottom - rect.top + lable.Height, SWP_NOZORDER | SWP_NOMOVE);
SetParent(lable.Handle, hWndMsg);
GetClientRect(hWndMsgBox, ref rect);
label.Location = new Point(0, rect.bottom - label.Height);
Closing the message box programmatically
To close the message box programmatically, there's one thing important to be considered. If we simply send a WM_CLOSE
message or call the DestoryWindow
function, the returned value would not be the default button's corresponding value. What I do is send a WM_COMMAND
message to simulate button clicking.
The WM_COMMAND
message needs three parameters, a notification code, an identifier, and the handle of the button which has been clicked. The notification code can be retrieved directly from MSDN, but other parameters should be retrieved programmatically. Considering reusing these parameters, I created two more classed to hold information of all the child controls of the message box, MessageBoxChild
and MessageBoxChildCollection
.


To get information of all the child controls, we should call the EnumChildWindows
function. This function enumerates all the child controls of the message box, and calls an application-defined callback function. In the managed code, the callback function can be alternated with the delegate. The following is the declaration of the delegate:
public delegate bool EnumChildProc(IntPtr hWnd, IntPtr param);
In the callback function, we retrieve the handle, the identifier, the class name, style, etc., of each child. These information are stored in MessageBoxChild
classes, and will be added to an instance of the MessageBoxChildCollection
class one by one. While adding to the collection object, it determines what the control is. Thus, we can conveniently use them later.
A code sample for enumerating the child controls of the message box is shown here:
collection.Clear();
EnumChildWindows(hWndMsgBox, new EnumChildProc(EnumChildren), IntPtr.Zero);
A code sample for the EnumChildren
method is shown here (see the MessageBox.EnumChildren
method):
private bool EnumChildren(IntPtr hWnd, IntPtr lParam)
{
StringBuilder name = new StringBuilder(1024);
StringBuilder caption = new StringBuilder(1024);
RECT rect = new RECT();
int style, id;
GetClassName(hWnd, name, 1024);
GetWindowText(hWnd, caption, 1024);
GetWindowRect(hWnd, ref rect);
style = GetWindowLong(hWnd, GWL_STYLE);
id = GetWindowLong(hWnd, GWL_ID);
collection.Add(new MessageBoxChild(hWnd, name.ToString(),
caption.ToString(), rect, style, id));
return true;
}
A code sample for determining what the control is, is shown here (see the MessageBoxChildCollection.Add
method):
if (child.ClassName == "Button")
this.buttons.Add(child);
else if (child.ClassName == "Static")
{
if ((child.Style & SS_ICON) != 0)
this.icon = child;
else
this.label = child;
}
Once all the information we need is prepared, we can pick up the default button's information from the collection object. Then, we call the SendMessage
function to send a WM_COMMAND
message to simulate the clicking of the default button.
MessageBoxChild bn = collection.GetButton(MessageBoxDefaultButton.Button2);
SendMessage(hWndMsgBox, WM_COMMAND,
(BN_CLICKED << 16) | bn.Id,
bn.Handle);
Implementation of a CheckBox Function
By adding a ChecBox
control to the message box, we can give the user an extra option to select. Usually, this option would be "Don't show this again". Implementation of this function is similar to adding a Label
control. The difference is the location. I place the CheckBox
control above the buttons, and align its left edge with the message of the dialog. Thus, we should not only increase the height of the message box, but also move the buttons lower.
About how to retrieve information of the child controls, I have already explained it in the previous section. But, the coordinates we retrieved previously are relative to the upper-left corner of the screen. So, we must convert them to the coordinates which are relative to the upper-left corner of the client area of the message box window.
The code sample for adding the CheckBox control is shown here (see the MessageBox.DecorateMessageBox
method for details):
POINT point = new POINT();
RECT rect = new RECT();
int top;
foreach (MessageBoxChild bn in collection.Buttons)
{
point.x = bn.Rectangle.left;
point.y = bn.Rectangle.top;
ScreenToClient(hWndMsgBox, ref point);
SetWindowPos(bn.Handle, IntPtr.Zero,
point.x, point.y
+ 10
+ checkBox.Height,
0, 0,
SWP_NOZORDER | SWP_NOSIZE);
}
top = point.y;
point.x = collection.Label.Rectangle.left;
point.y = collection.Label.Rectangle.top;
ScreenToClient(hWndMsgBox, point);
SetParent(checkBox.Handle, hWndMsgBox);
checkBox.Location = new Point(point.x, top);
GetWindowRect(hWndMsgBox, ref rect);
SetWindowPos(hWndMsgBox, IntPtr.Zero, 0, 0,
rect.right - rect.left, rect.bottom - rect.top + checkBox.Height + 10,
SWP_NOZORDER | SWP_NOMOVE);
Creating Our Own Component
I've introduced all the key knowledge of this article. Next, we should combine them and encapsulate into a single component. In this component, we need to establish serial properties and methods, and provide design-time support to give the user a good experience of using this component. See the following picture for the members I have established:


For design-time support, we just need to add some attributes to our properties, such as DescriptionAttribute
, CategoryAttribute
, DefaultValueAttribute
, etc.
[DefaultValue(0), Category("Timeout")]
[Description("Specifies the amount of seconds that the " +
"message box appears. Set it to 0 to disable the function.")]
public int Timeout
{
get { return timeout; }
set
{
if (value < 0)
throw new ArgumentOutOfRangeException("Timeout",
"Timeout cannot be less than 0.");
timeout = value;
}
}
[Category("Behavior"),
Description("The text to display in the message box.")]
[Editor("System.ComponentModel.Design.MultilineStringEditor,
System.Design, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a",
typeof(System.Drawing.Design.UITypeEditor))]
public string Message
{
get { return message; }
set { message = value; }
}
Please notice the Message
property in the sample code above. I added an EditorAttribute
to it. This allows user multi-line text input in the property grid at design time.
For showing the message box, a ShowDialog
method is required. I have already explained showing the message box in the "Preparing for showing the message box" section before. But, for a flexible appearance of the message box, we must use variables instead of constant values. The code would look like this:
System.Windows.Forms.MessageBox.Show(owner, this.Message, this.Caption,
this.Buttons, this.Icon, this.DefaultButton);
Using the Component
After successfully building the project which contains the component, a new icon is displayed in the toolbox. To use it, either drag and drop it to your form, or create new instances at runtime. The following code demonstrates the second option:
MessageBox msgBox = new MessageBox();
msgBox.Message = "Hello, world!";
msgBox.Icon = MessageBoxIcon.Information;
msgBox.Timeout = 10;
msgBox.ShowDialog(this);
Points of Interest
I've never wrote such a big article before, this is my first time, ha ha. Due to my poor English skills, it took me a long time to finish it. While writing this article, I referred to some kind of translation software for new words and spellings. So I think it was really a hard work, ha ha, just kidding. Finally, I hope you can catch my point.
History
None.