Click here to Skip to main content
15,860,972 members
Articles / Programming Languages / C#
Article

How to extend the native MessageBox dialog in .NET

Rate me:
Please Sign up or sign in to vote.
4.75/5 (13 votes)
4 Aug 2007CPOL8 min read 286.9K   1.2K   74   21
This article introduces a way to extend the native MessageBox dialog without using Windows Hooks and message processing.

Preview

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:

  1. Display a countdown message, and automatically close the message box when the specified timeout expires, and return the default selection of the dialog.
  2. 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. :)

C#
[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:

C#
[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.

C#
// initializations

seconds = -1;
timer1.Interval = 10;
// starts the timer

timer1.Start();
// shows the message box with Yes, No, Cancel bttons,
// and the default button is No.

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:

C#
IntPtr hWndMsgBox;

// looks for our message box

hWndMsgBox = FindWindow(null, "Dialog Caption");
if (hWndMsgbox != IntPtr.Zero)
{
    if (seconds == -1)
    {
        timer1.Stop();
        
        // todo: adds Label control to the message box at the first tick
        // ...

        timer1.Interval = 1000;
        timer1.Start();
    }
    
    // updates the countdown message.
    // suppose the timeout value is 30 seconds.

    label.Text = string.Format("{0} seconds elapsed.", 30 - ++seconds);
    
    if (seconds >= 30)
    {
        // timeout expired
        // todo: closes the message box
        // ...
        // stops the timer

        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):

C#
RECT rect = new RECT();

// retrives current dimensions of the message box

GetWindowRect(hWndMsgBox, ref rect);
// increase the message box's height to fit label

SetWindowPos(hWndMsgBox, IntPtr.Zero, 0, 0, rect.right - rect.left, 
             rect.bottom - rect.top + lable.Height, SWP_NOZORDER | SWP_NOMOVE);

// adds label to the message box

SetParent(lable.Handle, hWndMsg);
// retrieves the size of the message box's client area

GetClientRect(hWndMsgBox, ref rect);
// sets labels location

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.

Class diagram of MessageBoxChildClass diagram of 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:

C#
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:

C#
// clears the collection object

collection.Clear();
// enumerates all child controls of the message box

EnumChildWindows(hWndMsgBox, new EnumChildProc(EnumChildren), IntPtr.Zero);

A code sample for the EnumChildren method is shown here (see the MessageBox.EnumChildren method):

C#
private bool EnumChildren(IntPtr hWnd, IntPtr lParam)
{
    // local variable declearation

    StringBuilder name = new StringBuilder(1024);
    StringBuilder caption = new StringBuilder(1024);
    RECT rect = new RECT();
    int style, id;

    // retrieves control's class name.

    GetClassName(hWnd, name, 1024);
    // retrieves control's caption.

    GetWindowText(hWnd, caption, 1024);
    // retrieves control's bounds.

    GetWindowRect(hWnd, ref rect);

    // retrieves control's style.

    style = GetWindowLong(hWnd, GWL_STYLE);
    // retrieves control's identifier.

    id = GetWindowLong(hWnd, GWL_ID);

    // creates a new instance of MessageBoxChild
    // and add it to the collection

    collection.Add(new MessageBoxChild(hWnd, name.ToString(), 
                   caption.ToString(), rect, style, id));

    // returns true for continue enumerating

    return true;
}

A code sample for determining what the control is, is shown here (see the MessageBoxChildCollection.Add method):

C#
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.

C#
// retrives information of the default button control

MessageBoxChild bn = collection.GetButton(MessageBoxDefaultButton.Button2);
// send message to the message box the simulate button clicking

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):

C#
POINT point = new POINT();
RECT rect = new RECT();
int top;
// move the buttons lower

foreach (MessageBoxChild bn in collection.Buttons)
{
    point.x = bn.Rectangle.left;
    point.y = bn.Rectangle.top;
    
    // converts screen relative coordinates to client relative corrdinates
    ScreenToClient(hWndMsgBox, ref point);
    
    // moves button
    SetWindowPos(bn.Handle, IntPtr.Zero,
        point.x, point.y
        + 10 /* vertical space between the checkbox and the buttons */
        + checkBox.Height,
        0, 0,
        SWP_NOZORDER | SWP_NOSIZE);
}

// saves Y-coordinate for the CheckBox control
top = point.y;

// retrieves and converts the coordinates of the native label control
point.x = collection.Label.Rectangle.left;
point.y = collection.Label.Rectangle.top;
ScreenToClient(hWndMsgBox, point);

// adds the CheckBox control

SetParent(checkBox.Handle, hWndMsgBox);
// sets the CheckBox control's location

checkBox.Location = new Point(point.x, top);

// retrieves current dimensions of the message box

GetWindowRect(hWndMsgBox, ref rect);
// resizes the message box

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:

Class diagram of MessageBox componentThe look of property grid of the component

For design-time support, we just need to add some attributes to our properties, such as DescriptionAttribute, CategoryAttribute, DefaultValueAttribute, etc.

C#
[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:

C#
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:

C#
// creates a new instance of this component

MessageBox msgBox = new MessageBox();

// sets the text to display

msgBox.Message = "Hello, world!";
// sets the icon of the message box

msgBox.Icon = MessageBoxIcon.Information;
// sets the timeout to 10 seconds

msgBox.Timeout = 10;

// shows the message box

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
China China
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionVB.Net version Pin
SumitSaha11-Oct-15 15:24
SumitSaha11-Oct-15 15:24 
AnswerRe: VB.Net version Pin
Garth J Lancaster11-Oct-15 17:18
professionalGarth J Lancaster11-Oct-15 17:18 
GeneralRe: VB.Net version Pin
SumitSaha12-Oct-15 5:49
SumitSaha12-Oct-15 5:49 
GeneralA display bug Pin
b10101015-Dec-10 18:57
b10101015-Dec-10 18:57 
GeneralRe: A display bug Pin
Yifeng Ding19-Dec-10 13:11
Yifeng Ding19-Dec-10 13:11 
Generalget text from other application Pin
hamid_m3-Apr-08 21:49
hamid_m3-Apr-08 21:49 
GeneralRe: get text from other application Pin
Yifeng Ding4-Apr-08 2:22
Yifeng Ding4-Apr-08 2:22 
General好文章 Pin
lincats3-Jan-08 2:27
lincats3-Jan-08 2:27 
Generalit's good! Pin
justin_wang21-Oct-07 19:17
justin_wang21-Oct-07 19:17 
QuestionBug Pin
Kevin2717593957-Aug-07 15:56
Kevin2717593957-Aug-07 15:56 
AnswerRe: Bug Pin
Yifeng Ding11-Aug-07 22:21
Yifeng Ding11-Aug-07 22:21 
GeneralA small improvement and a small bug Pin
Laurent Muller6-Aug-07 21:27
professionalLaurent Muller6-Aug-07 21:27 
GeneralRe: A small improvement and a small bug Pin
Yifeng Ding11-Aug-07 22:02
Yifeng Ding11-Aug-07 22:02 
GeneralGood One Pin
Murali Babu6-Aug-07 19:28
professionalMurali Babu6-Aug-07 19:28 
GeneralRe: Good One Pin
Yifeng Ding11-Aug-07 22:04
Yifeng Ding11-Aug-07 22:04 
GeneralSeems good, however Pin
aamironline4-Aug-07 20:51
aamironline4-Aug-07 20:51 
GeneralRe: Seems good, however Pin
Yifeng Ding5-Aug-07 3:00
Yifeng Ding5-Aug-07 3:00 
GeneralRe: Seems good, however Pin
Mark Keogh5-Aug-07 12:52
Mark Keogh5-Aug-07 12:52 
GeneralRe: Seems good, however Pin
Yifeng Ding5-Aug-07 17:21
Yifeng Ding5-Aug-07 17:21 
GeneralRe: Seems good, however Pin
aamironline5-Aug-07 20:53
aamironline5-Aug-07 20:53 
GeneralRe: Seems good, however Pin
Michael Sync5-Aug-07 15:58
Michael Sync5-Aug-07 15:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.