Contents
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.
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;
for(int i = 0; i < parentForm.Controls.Count; i++)
{
mdiClient = parentForm.Controls[i] as MdiClient;
if(mdiClient != null)
{
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.
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:
mdiClient.BackColor = value;
That as well as many properties common to other controls will work as expected with the MdiClient
control.
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:
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;
[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:
int style = GetWindowLong(mdiClient.Handle, GWL_STYLE);
int exStyle = GetWindowLong(mdiClient.Handle, GWL_EXSTYLE);
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;
}
SetWindowLong(mdiClient.Handle, GWL_STYLE, style);
SetWindowLong(mdiClient.Handle, GWL_EXSTYLE, exStyle);
SetWindowPos(mdiClient.Handle, IntPtr.Zero, 0, 0, 0, 0,
SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
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.
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:
private const int SB_HORZ = 0;
private const int SB_VERT = 1;
private const int SB_CTL = 2;
private const int SB_BOTH = 3;
[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 );
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.
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
, struct
s called the PAINTSTRUCT
and RECT
, and a few more constants:
private const int WM_PAINT = 0x000F;
private const int WM_ERASEBKGND = 0x0014;
private const int WM_PRINTCLIENT = 0x0318;
[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;
}
[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)
{
case WM_ERASEBKGND:
return;
case WM_PAINT:
PAINTSTRUCT paintStruct = new PAINTSTRUCT();
IntPtr screenHdc = BeginPaint(m.HWnd, ref paintStruct);
using(Graphics screenGraphics = Graphics.FromHdc(screenHdc))
{
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))
{
IntPtr hdc = g.GetHdc();
Message printClientMessage =
Message.Create(m.HWnd, WM_PRINTCLIENT,
hdc, IntPtr.Zero);
DefWndProc(ref printClientMessage);
g.ReleaseHdc(hdc);
}
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.
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;
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(parentForm != null)
parentForm.HandleCreated -=
new EventHandler(ParentFormHandleCreated);
parentForm = value;
if(parentForm == null)
return;
if(parentForm.IsHandleCreated)
{
InitializeMdiClient();
RefreshProperties();
}
else
parentForm.HandleCreated +=
new EventHandler(ParentFormHandleCreated);
}
}
private void ParentFormHandleCreated(object sender, EventArgs e)
{
parentForm.HandleCreated -=
new EventHandler(ParentFormHandleCreated);
InitializeMdiClient();
RefreshProperties();
}
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.
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.