Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Getting a "Handle" on the MDI Client

0.00/5 (No votes)
6 Oct 2004 3  
A component to customize the MDI area of a Form with color, images, border styles, and more.

MdiClient Demo Application

Contents

Introduction

In a recent project, I decided that using a multiple-document interface (MDI) would be the best approach. I was pleasantly surprised by how easy creating an MDI application in Visual Studio and on the .NET platform is. Simply setting the IsMdiContainer property of the System.Windows.Forms.Form allows other forms to be hosted in the application workspace. If you're like me, however, you begin to wonder what that workspace would look like with a different color, custom painting, or maybe a different border style. I quickly found that the Form control exposed no such properties to control this behavior. A search of the web revealed that many others have desired to do the same and had various approaches on how to accomplish this. After using their suggestions successfully in my application and creating a few of my own, I decided to collect all such information into one place and perhaps develop a component that would allow easy setting of these properties.

The MdiClient Control

As it turns out, the MDI area of a Windows Form is just another control. When the IsMdiContainer property is set to true, a control of type System.Windows.Forms.MdiClient is added to the Controls collection of the Form. Iterating through the Form's controls after loading will reveal the MdiClient control and is also probably the best way to get a reference to it. The MdiClient control does have a public constructor and could be added to the Form's Controls collection programmatically, but a better practice is to set the Form's IsMdiContainer property and have it do the work. To set a reference to the MdiClient control, iterate through the controls until the MdiClient control is found:

MdiClient mdiClient = null;

// Get the MdiClient from the parent form.

for(int i = 0; i < parentForm.Controls.Count; i++)
{
    // If the form is an MDI container, it will contain an MdiClient control

    // just as it would any other control.

    mdiClient = parentForm.Controls[i] as MdiClient;
    if(mdiClient != null)
    {
        // The MdiClient control was found.

        // ...

        //


        break;
    }
}

Using the as keyword here is better than a direct cast in a try/catch block or using the is keyword, because if the type is a match, a reference to the control will result or null will be returned. It's like getting two calls for the price of one.

Note: In tests, I discovered that it is possible to add more than one MdiClient control to the Form's Controls collection. In such a case, only one of the MdiClient controls will act as the host and this code may fail by returning a reference to the MdiClient that is not performing the hosting of child forms. One more good reason to use the IsMdiContainer property rather than adding the control to the form manually.

What a Reference Can Do

Changing the Background Color

With a reference to the MdiClient control in hand, many of the common control properties can be set as you would expect. The most often requested of course is changing the background color. The default background color of the application workspace is global to all Windows applications and can be changed in the Control Panel. The .NET framework exposes this color in the System.Drawing.SystemColors.AppWorkspace static property. Changing the background color is done as you would expect, through the BackColor property:

// Set the color of the application workspace.

mdiClient.BackColor = value;

That as well as many properties common to other controls will work as expected with the MdiClient control.

What a Handle Can Do

Changing the Border Styles

What's absent from the MdiClient control, however, is a BorderStyle property. Gone are the typical System.Windows.Forms.BorderStyle enumeration options of Fixed3D, FixedSingle, and None. By default, the application workspace of a MDI form is inset with a 3D border equivalent to what would be Fixed3D. Just because this behavior is not exposed by the control does not mean it is not accessible. From this point forward, you will see that the Handle of the MdiClient becomes much more valuable than just a reference to it.

To change the appearance of the border requires the use of Win32 function calls. (More information on this can be gleaned from Jason Dorie's article: Adding designable borders to user controls.) Each window (i.e. - Control) in Windows has information that can be retrieved by using the GetWindowLong and set by using the SetWindowLong function. Both functions require a flag that specifies what information we would like to get and set. In this case, we are interested in the GWL_STYLE and the GWL_EXSTYLE, which get and set the window style and the extended window style flags, respectively. Because these changes are made to the non-client area of the control, calling the control's Invalidate method will not cause the borders to be repainted. Instead, we call the SetWindowPos function to cause an update of the non-client area. These functions and constants are defined like this:

// Win32 Constants

private const int GWL_STYLE   = -16;
private const int GWL_EXSTYLE = -20;

private const int WS_BORDER        = 0x00800000;
private const int WS_EX_CLIENTEDGE = 0x00000200;

private const uint SWP_NOSIZE           = 0x0001;
private const uint SWP_NOMOVE           = 0x0002;
private const uint SWP_NOZORDER         = 0x0004;
private const uint SWP_NOREDRAW         = 0x0008;
private const uint SWP_NOACTIVATE       = 0x0010;
private const uint SWP_FRAMECHANGED     = 0x0020;
private const uint SWP_SHOWWINDOW       = 0x0040;
private const uint SWP_HIDEWINDOW       = 0x0080;
private const uint SWP_NOCOPYBITS       = 0x0100;
private const uint SWP_NOOWNERZORDER    = 0x0200;
private const uint SWP_NOSENDCHANGING   = 0x0400;


// Win32 Functions

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int GetWindowLong(IntPtr hWnd, int Index);

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int SetWindowLong(IntPtr hWnd, int Index, int Value);

[DllImport("user32.dll", ExactSpelling = true)]
private static extern int SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter,
    int X, int Y, int cx, int cy, uint uFlags);

Note: The values of these constants are defined in the Winuser.h header file, which is usually installed by the Platform SDK or Visual Studio .NET.

We can adjust the border according to the BorderStyle enumeration by: including a WS_EX_CLIENTEDGE flag in the extended window styles (Fixed3D), a WS_BORDER flag in the standard window styles (FixedSingle), or removing both of these flags for no border (None). Then call the SetWindowPos function to cause an update. The SetWindowPos function has many options, but we want nothing more than to repaint the non-client area and will pass in the flags necessary to do this:

// Get styles using Win32 calls

int style = GetWindowLong(mdiClient.Handle, GWL_STYLE);
int exStyle = GetWindowLong(mdiClient.Handle, GWL_EXSTYLE);

// Add or remove style flags as necessary.

switch(value)
{
    case BorderStyle.Fixed3D:
        exStyle |= WS_EX_CLIENTEDGE;
        style &= ~WS_BORDER;
        break;

    case BorderStyle.FixedSingle:
        exStyle &= ~WS_EX_CLIENTEDGE;
        style |= WS_BORDER;
        break;

    case BorderStyle.None:
        style &= ~WS_BORDER;
        exStyle &= ~WS_EX_CLIENTEDGE;
        break;
}

// Set the styles using Win32 calls

SetWindowLong(mdiClient.Handle, GWL_STYLE, style);
SetWindowLong(mdiClient.Handle, GWL_EXSTYLE, exStyle);

// Update the non-client area.

SetWindowPos(mdiClient.Handle, IntPtr.Zero, 0, 0, 0, 0,
    SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
    SWP_NOOWNERZORDER | SWP_FRAMECHANGED);

What a Window Message Can Do

The NativeWindow Class

To move into the realm of customization beyond changing simple properties or making Win32 calls, we need to intercept and process window messages. Unfortunately, the MdiClient class is sealed, and therefore, can't be subclassed nor can its WndProc method be overridden. Thankfully, the System.Windows.Forms.NativeWindow class comes to the rescue. The intent of the NativeWindow class is to provide "a low-level encapsulation of a window handle and a window procedure". In other words, it allows us to tap into the window messages a control receives. To make use of NativeWindow, inherit from the class and override its WndProc method. Once a control's handle is assigned to the NativeWindow via the AssignHandle method, the WndProc method behaves just as if it was the control's WndProc method. With the ability to listen to the MdiClient control's window messages, a whole new range of customization is possible.

Hiding the Scrollbars

While making controls outside the application workspace accessible by scrollbars is a great feature, I personally can't remember an MDI application I've used that does the same. Turning off or hiding the scrollbars in the MdiClient is a feature that may be more often requested than changing its color.

The scrollbars of the MdiClient control are part of its non-client area (area outside the ClientRectangle) and are not themselves controls parented to the MdiClient. That rules out the possibility of changing the visibility of the scrollbars and leaves us with window messages and Win32 functions that affect the size of the non-client area. When the non-client area of a control needs to be calculated, the control is sent a WM_NCCALCSIZE message. In order to hide the scrollbars, we could tell Windows that the non-client area is a little bit smaller than it actually is and cover up the scrollbars. My first approach to this was a failed attempt at trying to determine what the size of the non-client area should be. A much better approach would be to hide the scrollbars when the non-client area is calculated using the ShowScrollBar Win32 function. The ShowScrollBar function requires the window handle, the scrollbars to be hidden, and a bool indicating its visibility:

// Win32 Constants

private const int SB_HORZ = 0;
private const int SB_VERT = 1;
private const int SB_CTL  = 2;
private const int SB_BOTH = 3;

// Win32 Functions

[DllImport("user32.dll")]
private static extern int ShowScrollBar(IntPtr hWnd, int wBar, int bShow);

protected override void WndProc(ref Message m)
{
    switch(m.Msg)
    {
        //

        // ...

        //


        case WM_NCCALCSIZE:
            ShowScrollBar(m.HWnd, SB_BOTH, 0 /*false*/);
            break;
    }

    base.WndProc(ref m);
}

After hiding the scrollbar, the WM_NCCALCSIZE message is processed as usual and calculates the non-client area less the recently hidden scrollbars. In case you're wondering, hiding the scrollbar via the ShowScrollBar function does not keep the scrollbar hidden and is immediately reset to visible. That is why it must be hidden every time the non-client area is calculated.

Advanced Painting

In .NET forums around the web, another common request I see is, "How do I put an image in the application workspace of an MDI form?" The easiest way is to listen to the Paint event once you have a reference to the MdiClient. For some situations this may work fine, but I noticed a very bad flicker every time the MdiClient is resized. This is a result of the painting not being double-buffered and painting calls being made in both the WM_PAINT and WM_ERASEBKGND messages. If we had been able to inherit from the MdiClient control, this could be easily remedied by using the control's protected method SetStyle with the flags System.Windows.Forms.ControlStyles.AllPaintingInWmPaint, ControlStyles.DoubleBuffer, and ControlStyles.UserPaint. But as noted earlier, the MdiClient class is sealed and that is not an option. What is an option is listening to the WM_PAINT and WM_ERASEBKGND window messages and implementing our own custom painting. (More information on this is available in Steve McMahon's article: Painting in the MDI Client Area.)

The Win32 items we'll need are the functions BeginPaint and EndPaint, structs called the PAINTSTRUCT and RECT, and a few more constants:

// Win32 Constants

private const int WM_PAINT       = 0x000F;
private const int WM_ERASEBKGND  = 0x0014;
private const int WM_PRINTCLIENT = 0x0318;


// Win32 Structures

[StructLayout(LayoutKind.Sequential, Pack = 4)]
private struct PAINTSTRUCT
{
    public IntPtr hdc;
    public int fErase;
    public RECT rcPaint;
    public int fRestore;
    public int fIncUpdate;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst=32)] 
    public byte[] rgbReserved;
}

[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
    public int left;
    public int top;
    public int right;
    public int bottom;
}

// Win32 Functions

[DllImport("user32.dll")]
private static extern IntPtr BeginPaint(IntPtr hWnd, 
                                ref PAINTSTRUCT paintStruct);

[DllImport("user32.dll")]
private static extern bool EndPaint(IntPtr hWnd, ref PAINTSTRUCT paintStruct);

The typical method of double-buffering is to do all painting to an Image, or rather, get the Graphics object from an Image instead of painting directly to the screen. When painting to the Image is complete then the Image itself is drawn to the screen. That way, all the control's painting is displayed at once instead of intermittent painting that may be in progress. With the MdiClient control's graphics being so simple, we could easily do all the painting ourselves, but a better practice is to not eliminate the base graphics from being drawn, but to incorporate them into our custom painting. That way, if the MdiClient were somehow changed in a way we did not expect, the painting should still be correctly displayed. This is achieved by creating our own window message (WM_PRINTCLIENT) and sending it to the base control using the DefWndProc (i.e. - Default WndProc) method. What we get back in the graphics buffer is a painting of the original control painted by the base control. (More on this can be learned from J Young's article: Generating missing Paint event for TreeView and ListView controls.) From there, any custom painting over top of it can be processed:

protected override void WndProc(ref Message m)
{
    switch(m.Msg)
    {
        //Do all painting in WM_PAINT to reduce flicker.

        case WM_ERASEBKGND:
            return;

        case WM_PAINT:

            // Use Win32 to get a Graphics object.

            PAINTSTRUCT paintStruct = new PAINTSTRUCT();
            IntPtr screenHdc = BeginPaint(m.HWnd, ref paintStruct);

            using(Graphics screenGraphics = Graphics.FromHdc(screenHdc)) 
            {
                // Double-buffer by painting everything to an image and

                // then drawing the image.

                int width = (mdiClient.ClientRectangle.Width > 0 ? 
                                   mdiClient.ClientRectangle.Width : 0);
                int height = (mdiClient.ClientRectangle.Height > 0 ? 
                                  mdiClient.ClientRectangle.Height : 0);
                using(Image i = new Bitmap(width, height))
                {
                    using(Graphics g = Graphics.FromImage(i))
                    {
                        // Draw base graphics and raise the base Paint event.

                        IntPtr hdc = g.GetHdc();
                        Message printClientMessage =
                            Message.Create(m.HWnd, WM_PRINTCLIENT, 
                                                 hdc, IntPtr.Zero);  
                        DefWndProc(ref printClientMessage);
                        g.ReleaseHdc(hdc);

                        //

                        // Custom painting here...

                        //

                    }

                    // Now draw all the graphics at once.

                    screenGraphics.DrawImage(i, mdiClient.ClientRectangle);
                }
            }

            EndPaint(m.HWnd, ref paintStruct);
            return;
    }

    base.WndProc(ref m);
}

Note: More information about the BeginPaint, EndPaint, PAINTSTRUCT, RECT, and WM_PRINTCLIENT can be found in the Platform SDK or MSDN library.

Notice that in this case, we do not let the WM_PAINT message fall through to be processed by the base WndProc because that would cause it to do its default painting right over what we had just done ourselves. The WM_ERASEBKGND message is ignored because we want to do all the painting at one time in the WM_PAINT message. Now, the MdiClient control's Paint event will no longer flicker and custom painting code can be placed in the processing of the WM_PAINT message above.

The MdiClientController Component

Rather than having to put this code into every project that is using a multiple-document interface, we could wrap this all up into a System.ComponentModel.Component that could be copied from project-to-project and dropped onto the design surface. Included in the source files is a component I call the MdiClientController and is found in the Slusser.Components namespace. The component inherits from NativeWindow and implements the System.ComponentModel.IComponent interface to give it its Component behavior. It incorporates all the functionality discussed previously with the addition of some properties that make it easy to place an Image in the application workspace.

To use the component with a MDI form, only the parent Form must be passed in to the constructor or set through the ParentForm property. To set the MdiClientController component's ParentForm property in the designer, we have to customize the Site property to determine if the component is dropped onto a Form. It helps here to have a knowledge of Designers. If indeed the component is dropped onto Form, we set the ParentForm property and it is properly serialized in the designer code:

public ISite Site
{
    get { return site; }
    set
    {
        site = value;

        if(site == null)
            return;

        // If the component is dropped onto a form during design-time,

        // set the ParentForm property.

        IDesignerHost host = 
          (value.GetService(typeof(IDesignerHost)) as IDesignerHost);
        if(host != null)
        {
            Form parent = host.RootComponent as Form;
            if(parent != null)
                ParentForm = parent;
        }
    }
}

One of the challenges in creating this component is knowing when the component will be initialized. Components dropped onto the designer are initialized in the InitializeComponent method of the Form's constructor. If you inspect the InitializeComponent method created by the designer, you'll note that the Form's properties are the last thing to be set. If the MdiClientController were to scan for the MdiClient control in the Form's Controls collection before the Form's IsMdiContainer property is set, no MdiClient control would be found. The solution is to know when the parent Form's Handle is created. This will surely indicate that all child controls and variables have been initialized and when we can start to look for the MdiClient. If the parent form does not have a Handle when the ParentForm property is set, the component will listen to the Form's HandleCreated event and get the MdiClient then:

public Form ParentForm
{
    get { return parentForm; }
    set
    {
        // If the ParentForm has previously been set,

        // unwire events connected to the old parent.

        if(parentForm != null)
            parentForm.HandleCreated -= 
              new EventHandler(ParentFormHandleCreated);

        parentForm = value;

        if(parentForm == null)
            return;

        // If the parent form has not been created yet,

        // wait to initialize the MDI client until it is.

        if(parentForm.IsHandleCreated)
        {
            InitializeMdiClient();
            RefreshProperties();
        }
        else
            parentForm.HandleCreated += 
              new EventHandler(ParentFormHandleCreated);
    }
}


private void ParentFormHandleCreated(object sender, EventArgs e)
{
    // The form has been created, unwire the event,

    // and initialize the MdiClient.

    parentForm.HandleCreated -= 
         new EventHandler(ParentFormHandleCreated);
    InitializeMdiClient();
    RefreshProperties();
}

Using the Component

Once the MdiClientController has been added to the toolbox, simply drag it onto the Form in the designer, or double-click it and it will be displayed in the component tray of the designer. The MdiClientController will not change the Form's IsMdiContainer property, so you must set it. All of the component's properties follow the .NET naming conventions. The border style functionality is wrapped up in the BorderStyle property. The hiding of the scrollbars, I thought, was best put in an AutoScroll property. The BackColor and Paint events are now accessible from the designer for your convenience. In addition, there are three properties that control the displaying of an Image in the client area. The Image property sets the Image to display, the ImageAlign property will place it in different locations of the client area, and the StretchImage property will stretch it to fill the entire client area. In addition, I've added a HandleAssigned event to indicate when the MdiClient has been found and its Handle assigned to the NativeWindow. Of course, all this can be done programmatically.

Conclusion

As with many projects that become articles, I had what I originally needed in about 30 minutes, but spent several days preparing something that I could share with my fellow programmers. The resulting component should suffice for the majority of requests regarding the appearance of MDI forms. It works nicely, it plays nicely, and it makes applications look nice[ly]. There is still a great deal more that could be added to the component if the need arises, which I'm sure for some programmers, it will. There is one feature, or hurdle rather, that I humbly admit I was not able to overcome: design-time preview. Using Reflector, I discovered a great number of roadblocks that prevent the design-time preview of the MDI area. I would welcome any suggestions on how to overcome this. Enjoy.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here