Painting in the MDI Client Area

Find and respond to messages sent to the MDIClient area in .NET Forms.

MDI Client Paint Demonstration

Although the .NET Framework Form allows you to set a background bitmap or color for the MDI Client area, there's no direct way to draw onto the area to customise it. This sample demonstrates how to create an object which derives from NativeWindow to intercept the messages from the MDI Client area and draw onto it using standard System.Drawing calls.

Finding the MDI Client

An MDI form is actually built using two Windows: the main MDI Window itself and a child window which contains the MDI child forms. This window is termed the "MDIClient" and can be seen if you point a tool like Spy++ at the MDI window:

Spy++ Reveals the MDI Client Window

Spy++ Reveals the MDI Client Window

The class name of the form is given as "WindowsForms10.MDICLIENT.app5f", although I assume that this might change depending on the version of the .NET Framework you're using. By default in the .NET Framework there is no object which provides you access to this Window: the Framework rightly or wrongly assumes that you don't need to do anything with it. However, if you can find the handle to the Window then you create a System.Windows.Forms.NativeWindow object from it, and start responding to its message stream.

Finding The MDI Client

The MDI Client window is a child of the main MDI form, but that means it is at the same level as any other controls (such as Toolbars or Statusbars) which are present on the MDI form's surface. So to find the client we enumerate the child windows of the MDI form and look for one which has a class name containing the word "MDICLIENT" (ignoring case). The code to do this is as follows:

using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;

private delegate int EnumWindowsProc(IntPtr hwnd, int lParam);

[DllImport("user32")]
private extern static int EnumChildWindows (
    IntPtr hWndParent,
    EnumWindowsProc lpEnumFunc, 
    int lParam);

[DllImport("user32", CharSet = CharSet.Auto)]
private extern static int GetClassName (
    IntPtr hWnd, 
    StringBuilder lpClassName, 
    int nMaxCount);

private void GetWindows(
	IntPtr hWndParent)
{
    EnumChildWindows(
        hWndParent,
        new EnumWindowsProc(this.WindowEnum),
        0);
}

/// 
/// The enum Windows callback.
/// 
/// Window Handle
/// Application defined value
/// 1 to continue enumeration, 0 to stop
private int WindowEnum(
    IntPtr hWnd,
    int lParam)
{
    StringBuilder className = new StringBuilder(260, 260);
    GetClassName(hWnd, className, className.Capacity);
    if (className.ToString().ToUpper().IndexOf("MDICLIENT") > 0)
    {
        // stop
        hWndMdiClient = hWnd;
        return 0;
    }
    else
    {
    	// continue
        return 1;
    }
}

A more detailed look at finding windows is provided in the article Enumerating Windows.

Getting at Messages From a Window

Normally, the .NET Framework makes it easy to access the message stream for anything that's based on a Windows window: the base Control class includes a virtual WndProc method that provides access to the message stream. However, you can get at this functionality for any arbitrary Window handle (provided the window has been created within the application's process) using the NativeWindow class provided as part of the System.Windows.Form namespace. Once the NativeWindow class has been assigned to a window handle (using the AssignHandle method) then it also provides a virtual WndProc method.

Using this class allows us to easily provide a class which connects to an MDI client and allows the caller to respond to messages passed to the MDI client.

Wrapping it Up: The MDIClientWindow Code

The MDIClientWindow code provided in the download provides reusablable code you can use to mess around with MDI Client messages. There are two components in the code:

  • The MDIClientWindow class
    This class extends NativeWindow in order to override the WndProc method. Whenever it calls, the class notifies the owner of the class through the IMDIClientNotify interface of the message. The client can then choose to process or ignore the message.
  • The IMDIClientNotify Interface
    This interface contains a single WndProc message interface. Using an interface to call the client allows for better performance than using events, and provides flexibility as to which class responds to the messages.

To hook up the object to an MDI form, first you implement the IMDIClientNotify interface and then instantiate an instance of the MDIClientWindow class:

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Windows.Forms;

using vbAccelerator.Components.Win32;

namespace YourAppNamespace
{

    public class YourMDIForm : 
		System.Windows.Forms.Form, 
		IMDIClientNotify
    {
        /// 
        /// Class to hold the MDI Client
        /// 
        private MDIClientWindow mdiClient = null;

        public YourMDIForm
	{
            //
            // Required for Windows Form Designer support
            //
            InitializeComponent();

            // Add a form Load event handler (if one hasn't
            // been added in the designer already) so we
            // can hook up the MDIClient notifications
            //
            this.Load += new EventHandler(this.frm_Load);
        }

        // InitializeComponent, Dispose omitted for clarity

        private void frm_Load(object sender, EventArgs e)
        {
            mdiClient = new MDIClientWindow(
                 this,        // Object implementing IMDIClientNotify
                 this.Handle  // MDI form handle
                 );
        }

        public void WndProc(ref Message m, ref bool doDefault)
	{
            // process message here.  Set doDefault = false
            // to prevent the base window procedure from being
            // called.
        }
    }

}

Using MDIClientWindow to Paint In the MDI Client

In order to paint in the MDI Client area, you need to handle at two or three Windows messages. The basic messages involved in painting are WM_PAINT and WM_ERASEBKGND. However, note that the default painting setup for the MDI Client area is only to paint newly exposed areas. If your background consists of a tile, or a solid colour, then that is fine, and you can do all the processing in these messages. However, if existing areas need to be redrawn when the size changes then you also need to respond to WM_SIZE in order to refresh the background.

  • Responding to WM_ERASEBKGND

    The WM_ERASEBKGND message is sent whenever an area of the MDI Client needs to be erased. The WParam member of the message contains the device context to draw to, and you should return 1 if you process the message and erase the background.

    In this sample, the background is filled with a gradient at a 45 degree angle. Therefore this is applied during this method:

    public void WndProc(ref Message m, ref bool doDefault)
    {
    
        ...
    
        if (m.Msg == UnManagedMethods.WM_ERASEBKGND)
        {
            //
            // Fill the background:
            //
    
            RECT rc = new RECT();
            UnManagedMethods.GetClientRect(m.HWnd, ref rc);
    
            // Convert to managed code world
            Graphics gfx = Graphics.FromHdc(m.WParam);
            Rectangle rcClient = new Rectangle(
    	    rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top);
    
            int angle = 45;
            LinearGradientBrush linGrBrush = new LinearGradientBrush(
                rcClient,
                Color.FromArgb(255, 230, 242, 255),// pale blue
                Color.FromArgb(255, 0, 72, 160),   // deep blue
                angle);  
    
            gfx.FillRectangle(linGrBrush, rcClient);
    
            linGrBrush.Dispose();
            gfx.Dispose();
    
            // Tell Windows we've filled the background:
            m.Result = (IntPtr)1;
    
            // Don't call the default procedure:
            doDefault = false;
        }
    }
    
  • Responding to WM_PAINT

    The WM_PAINT message is called after WM_ERASEBKGND and is used to paint anything over the newly erased background. Note that you can do all of the painting in this message if you prefer.

    In the sample, the text "vbAccelerator.com" is drawn vertically at the bottom-edge of the client area:

    public void WndProc(ref Message m, ref bool doDefault)
    {
    
        ...
    
        if (m.Msg == UnManagedMethods.WM_PAINT)
        {
            //
            // Here we draw a logo on the "right" hand side 
            // of the form (depends on RTL)
            //
            PAINTSTRUCT ps = new PAINTSTRUCT();
    
            // Get the paint parameters from Windows:
            UnManagedMethods.BeginPaint(m.HWnd, ref ps);
            RECT rc = new RECT();
            UnManagedMethods.GetClientRect(m.HWnd, ref rc);
    
            // Convert to managed code world				
            Graphics gfx = Graphics.FromHdc(ps.hdc);
            RectangleF rcClient = new RectangleF(
                rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top);
    	Rectangle rcPaint = new Rectangle(
                ps.rcPaint.left, 
                ps.rcPaint.top,
                ps.rcPaint.right - ps.rcPaint.left,
                ps.rcPaint.bottom - ps.rcPaint.top);
    
            // Draw the logo bottom right:
            SolidBrush brText = new SolidBrush(Color.White);
            
            StringFormat strFormat = new StringFormat();
            strFormat.Alignment = StringAlignment.Far;
            strFormat.FormatFlags = StringFormatFlags.DirectionVertical |
                StringFormatFlags.NoWrap;
            strFormat.LineAlignment = StringAlignment.Far;					
    			
            Font logoFont = new Font(this.Font.FontFamily, 20, FontStyle.Bold);
    
            gfx.DrawString("vbAccelerator.com", logoFont, brText, rcClient, strFormat);
            
            logoFont.Dispose();
            strFormat.Dispose();
            brText.Dispose();
    
            gfx.Dispose();
    
            // Tell Windows the painting is completed:
            UnManagedMethods.EndPaint(m.HWnd, ref ps);
        }
    }
    
  • Redrawing the Entire Area on WM_SIZE

    The WM_SIZE message provides the new size of the Window in the LParam of the message: the Loword is the width and the HiWord is the height. Then Windows' InvalidateRect API call can be used to redraw the window (which is the equivalent of the Invalidate.. calls in the .NET Framework):

    public void WndProc(ref Message m, ref bool doDefault)
    {
    
        ...
    
        if (m.Msg == UnManagedMethods.WM_SIZE)
        {
            // If your background is a tiled image then
            // you don't need to do this.  This is only required
            // when the entire background needs to be updated 
            // in response to the size of the object changing.
            RECT rect = new RECT();
            rect.left = 0;
            rect.top = 0;
            rect.right = ((int)m.LParam) & 0xFFFF;
            rect.bottom = (int)(((uint)(m.LParam) & 0xFFFF0000) >> 16);
            UnManagedMethods.InvalidateRect(m.HWnd, ref rect, 1);
         }
    }