Click here to Skip to main content
15,880,796 members
Articles / Desktop Programming / Windows Forms

How to Make MessageBoxes Center on their Parent Forms

Rate me:
Please Sign up or sign in to vote.
4.87/5 (33 votes)
17 Feb 2010CPOL4 min read 142.9K   3.2K   72   16
Make MessageBoxes center on their parent forms
This article presents a simple mechanism for implementing MessageBoxes that appear centered on their parent.

Introduction

Everyone uses MessageBox - it's been a fixture of Windows since day 1, and its format and invocation have changed very little over the years.

For me, the single biggest drawback of MessageBox has been that it centers on the screen, not on its parent, and there's no way to tell it to center on its parent:

MessageBoxCenterOnParent/regular.PNG

Here's what I want:

MessageBoxCenterOnParent/centered.PNG

You'd think that at least one of MessageBox.Show()'s 21 overloads would have some way of doing this, or perhaps that there'd be a MessageBoxOptions.CenterOnParent flag, but no such luck.

This article presents a simple mechanism for implementing MessageBoxes that appear centered on their parent.

Background

My technique uses a custom class - I call it MsgBox - that wraps the standard MessageBox.Show call with a Windows hook. The hook is set before popping the MessageBox, the hookproc finds and centers the MessageBox before it's initially displayed, then the hook is released.

For simplicity, my example works only on a single thread. If you have multiple threads that pop MessageBoxes in an uncoordinated fashion, you'll need to add some code to handle that situation.

For additional references/articles, CodeProject has many articles on hooking. Search on "SetWindowsHookEx".

Microsoft's docs are available at this link.

Using the Code

As mentioned above, the core mechanism is a Windows hook. For the uninitiated, this is a Windows mechanism that allows your code to get access to some low-level Windows functionality; you essentially inject a bit of your app's code into the inner workings of Windows.

There are may types of hooks; my code uses the WH_CBT hook and acts on the HCBT_ACTIVATE event. (Read the Microsoft page linked above for details on WH_CBT.)

Hooks are not a part of .NET. To use them, you must use PInvoke to access the Win32 hooking APIs SetWindowsHookEx(), CallNextHookEx(), and UnhookWindowsHookEx(). These set local hooks, meaning that they only operate on windows within our process. This is exactly what we want - we do not want to handle message boxes displayed by other applications, only our own.

When you set a WH_CBT hook, your callback will receive notifications of window events such as creation, activation, moving/sizing, and destruction. We're interested in activation: when a message box is first activated (but before it's initially visible), we'll reposition it, and then we're done.

The code snippets below are lifted from the attached sample. In them, you'll see me using a Win32.* syntax - in the sample, I've collected all P/Invoke methods and defs in a separate class named Win32, a common practice that I've adopted for my projects.

Preparation

First, you need to import the hooking APIs:

C#
using System.Runtime.InteropServices;

public class Win32
{
   public const int WH_CBT = 5;
   public const int HCBT_ACTIVATE = 5;

   public delegate int WindowsHookProc(int nCode, IntPtr wParam, 
                                       IntPtr lParam);

   [DllImport("user32.dll", CharSet = CharSet.Auto, 
              CallingConvention = CallingConvention.StdCall)]
   public static extern int SetWindowsHookEx(int idHook, 
          WindowsHookProc lpfn, IntPtr hInstance, int threadId);

   [DllImport("user32.dll", CharSet = CharSet.Auto, 
              CallingConvention = CallingConvention.StdCall)]
   public static extern bool UnhookWindowsHookEx(int idHook);

   [DllImport("user32.dll", CharSet = CharSet.Auto, 
              CallingConvention = CallingConvention.StdCall)]
   public static extern int CallNextHookEx(int idHook, int nCode, 
                            IntPtr wParam, IntPtr lParam);
}

and define a few variables for managing the hook:

C#
private int _hHook = 0;
private Win32.WindowsHookProc _hookProcDelegate;
private static string _title = null;
private static string _msg = null;

Setting, Processing, and Releasing the Hook

Create a callback delegate then calls SetWindowsHookEx() to set the hook. It returns a hook ID that you'll use in your callback and when you release the hook.

C#
// Remember the title & message that we'll look for.
// The hook sees *all* windows, so we need
// to make sure we operate on the right one.
_msg = msg;
_title = title;

Win32.WindowsHookProc hookProcDelegate = 
                      new Win32.WindowsHookProc(HookCallback);
_hHook = Win32.SetWindowsHookEx(Win32.WH_CBT, hookProcDelegate, 
         IntPtr.Zero, AppDomain.GetCurrentThreadId()); 

Your hook callback looks something like this. Once you're done processing the notification, you must pass the event along to the next hook via CallNextHookEx(). (Note the use here of your hook ID, _hHook.)

C#
private static int HookCallback(int code, IntPtr wParam, IntPtr lParam)
{
   if (code == Win32.HCBT_ACTIVATE)
   {
      // wParam is the handle to the Window being activated.
      if(TestForMessageBox(wParam))
      {
         CenterWindowOnParent(wParam);
         Unhook();   // Release hook - we've done what we needed
      }
   }
   return Win32.CallNextHookEx(_hHook, code, wParam, lParam); 
}

Then, finally, when you're done looking for the message box, you release the hook:

C#
private static void Unhook()
{
   Win32.UnhookWindowsHookEx(_hHook);
   _hHook = 0;
   _hookProcDelegate = null;
   _title = null;
   _msg = null;
}

Finding Your Message Box

Simply watch for a dialog box which has the correct title and message:

C#
private static bool TestForMessageBox(IntPtr hWnd)
{
   string cls = Win32.GetClassName(hWnd);
   if (cls == "#32770") // MessageBoxes are Dialog boxes
   {
      string title = Win32.GetWindowText(hWnd);
      string msg = Win32.GetDlgItemText(hWnd, 0xFFFF); // -1 aka IDC_STATIC
      {
         if ((title == _title) && (msg == _msg))
         {
            return true;
         }
      }
   }
   return false;
}

Centering the Message Box on Its Parent Form

Centering one window on another - nothing special here:

C#
private static void CenterWindowOnParent(IntPtr hChildWnd)
{
   // Get child (MessageBox) size
   Win32.RECT rcChild = new Win32.RECT();
   Win32.GetWindowRect(hChildWnd, ref rcChild);
   int cxChild = rcChild.right - rcChild.left;
   int cyChild = rcChild.bottom - rcChild.top;

   // Get parent (Form) size & location 
   IntPtr hParent = Win32.GetParent(hChildWnd);
   Win32.RECT rcParent = new Win32.RECT();
   Win32.GetWindowRect(hParent, ref rcParent);
   int cxParent = rcParent.right - rcParent.left;
   int cyParent = rcParent.bottom - rcParent.top;

   // Center the MessageBox on the Form 
   int x = rcParent.left + (cxParent - cxChild) / 2;
   int y = rcParent.top + (cyParent - cyChild) / 2;
   uint uFlags = 0x15; // SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE;

   Win32.SetWindowPos(hChildWnd, IntPtr.Zero, x, y, 0, 0, uFlags);
}

and that's about it!

In your calling code, just call MsgBox.Show() instead of MessageBox.Show(), and you'll get parent-centered pop-ups.

Points of Interest

I've frequently wondered why message boxes appear centered on the screen rather than on their parent. I assumed that there must be some method to the madness - Microsoft wouldn't let a bug like this slip for 20 years! (Well, maybe they would... :) )

While developing this code, I found a plausible explanation: say, for example, that your window is positioned outside the display boundaries and an error occurs that pops a message box. In this case, if the message box were centered on its parent, you'd never see it. I think that this is a valid concern, and you should consider it when using centered message boxes, maybe choosing to not parent-center some error messages, just in case.

History

  • 17th February, 2010: Initial version

License

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


Written By
Software Developer
United States United States
I can type "while" and "for" very quickly

Comments and Discussions

 
PraiseExcellent job Pin
Tabarak Hussain12-Apr-16 22:48
Tabarak Hussain12-Apr-16 22:48 
SuggestionThis is not a good approach Pin
ahmd023-Jun-14 13:35
ahmd023-Jun-14 13:35 
Questionerror Pin
Asniper7-Sep-12 4:49
Asniper7-Sep-12 4:49 
AnswerRe: error Pin
DLChambers12-Sep-12 7:45
DLChambers12-Sep-12 7:45 
QuestionVB.Net Pin
hroenick26-Apr-12 16:09
hroenick26-Apr-12 16:09 
All, i am a newbie, but can see a great need for this in VB.Net. Message boxes not centered on their parent is really ugly on the screen.

Has anyone converted this to VB? I tried, but got a number of errors I do not know how to interpret.

For example, below is the class as best I can interpret. The VB compiler does not like the Pragma warnings. I am not sure what to do with them.

Thank you!

HAR


Imports System.Windows.Forms

Namespace CenteredMessageboxDemo
Public Class MsgBox
Private Shared _hookProcDelegate As Win32.WindowsHookProc = Nothing
Private Shared _hHook As Integer = 0
Private Shared _title As String = Nothing
Private Shared _msg As String = Nothing

Public Shared Function Show(ByVal msg As String, ByVal title As String, ByVal btns As MessageBoxButtons, ByVal icon As MessageBoxIcon) As DialogResult
' Create a callback delegate
_hookProcDelegate = New Win32.WindowsHookProc(AddressOf HookCallback)

' Remember the title & message that we'll look for.
' The hook sees *all* windows, so we need to make sure we operate on the right one.
_msg = msg
_title = title

' Set the hook.
' Suppress "GetCurrentThreadId() is deprecated" warning.
' It's documented that Thread.ManagedThreadId doesn't work with SetWindowsHookEx()
#Pragma warning disable 0618
_hHook = Win32.SetWindowsHookEx(Win32.WH_CBT, _hookProcDelegate, IntPtr.Zero, AppDomain.GetCurrentThreadId())
#Pragma warning restore 0618

' Pop a standard MessageBox. The hook will center it.
Dim rslt As DialogResult = MessageBox.Show(msg, title, btns, icon)

' Release hook, clean up (may have already occurred)
Unhook()

Return rslt
End Function

Private Shared Sub Unhook()
Win32.UnhookWindowsHookEx(_hHook)
_hHook = 0
_hookProcDelegate = Nothing
_msg = Nothing
_title = Nothing
End Sub

Private Shared Function HookCallback(ByVal code As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Integer
Dim hHook As Integer = _hHook
' Local copy for CallNextHookEx() JIC we release _hHook
' Look for HCBT_ACTIVATE, *not* HCBT_CREATEWND:
' child controls haven't yet been created upon HCBT_CREATEWND.
If code = Win32.HCBT_ACTIVATE Then
Dim cls As String = Win32.GetClassName(wParam)
If cls = "#32770" Then
' MessageBoxes are Dialog boxes
Dim title As String = Win32.GetWindowText(wParam)
Dim msg As String = Win32.GetDlgItemText(wParam, &HFFFF)
' -1 aka IDC_STATIC
If (title = _title) AndAlso (msg = _msg) Then
CenterWindowOnParent(wParam)
' Release hook - we've done what we needed
Unhook()
End If
End If
End If
Return Win32.CallNextHookEx(hHook, code, wParam, lParam)
End Function

' Boilerplate window-centering code.
' Split out of HookCallback() for clarity.
Private Shared Sub CenterWindowOnParent(ByVal hChildWnd As IntPtr)
' Get child (MessageBox) size
Dim rcChild As New Win32.RECT()
Win32.GetWindowRect(hChildWnd, rcChild)
Dim cxChild As Integer = rcChild.right - rcChild.left
Dim cyChild As Integer = rcChild.bottom - rcChild.top

' Get parent (Form) size & location
Dim hParent As IntPtr = Win32.GetParent(hChildWnd)
Dim rcParent As New Win32.RECT()
Win32.GetWindowRect(hParent, rcParent)
Dim cxParent As Integer = rcParent.right - rcParent.left
Dim cyParent As Integer = rcParent.bottom - rcParent.top

' Center the MessageBox on the Form
Dim x As Integer = rcParent.left + (cxParent - cxChild) \ 2
Dim y As Integer = rcParent.top + (cyParent - cyChild) \ 2
Dim uFlags As UInteger = &H15
' SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE;
Win32.SetWindowPos(hChildWnd, IntPtr.Zero, x, y, 0, 0, _
uFlags)
End Sub


End Class
End Namespace
QuestionMy vote of 5 Pin
shankar_r198629-Feb-12 18:01
shankar_r198629-Feb-12 18:01 
GeneralMy vote of 4 Pin
Anas.irm3-Oct-11 2:35
Anas.irm3-Oct-11 2:35 
GeneralMy vote of 1 Pin
lecedre11-Aug-10 23:28
lecedre11-Aug-10 23:28 
GeneralRe: My vote of 1 Pin
megalomania99931-Jan-11 2:14
megalomania99931-Jan-11 2:14 
GeneralRe: My vote of 1 PinPopular
DLChambers31-Jan-11 2:43
DLChambers31-Jan-11 2:43 
GeneralGreat Article and Another way Pin
daylightdj25-Feb-10 3:55
daylightdj25-Feb-10 3:55 
GeneralRe: Great Article and Another way Pin
DLChambers25-Feb-10 4:10
DLChambers25-Feb-10 4:10 
GeneralRe: Great Article and Another way Pin
EFEaglehouse11-Mar-11 6:39
EFEaglehouse11-Mar-11 6:39 
GeneralRe: you do not need all of that Pin
johannesnestler18-Feb-10 2:42
johannesnestler18-Feb-10 2:42 
GeneralInteresting article, thanks Pin
BigJim6117-Feb-10 16:01
BigJim6117-Feb-10 16:01 

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.