In part one, I showed how to extend Form to access the non-client area of the window. Now we can start painting the borders we want. Having access to the Graphics object we can use any features of GDI+: draw lines, rectangles or other shapes, fill them using brushes, draw text and paint images. How you do this depends of your needs. For purpose of this article I will show you how to do this using set of ready-made bitmaps.
Constructing Custom Borders from Bitmap Parts
First lets figure out how we construct the border. We could use a single image only if the window had a constant size. If its going to be resizable we should split it into parts to allow it. Figure 2 shows all parts used to draw the border.
Figure 2. Bitmap parts composing a window border.
To make all this reusable the actual appearance of the form is defined in the CustomBorderAppearance class. Each edge of window consists of three parts: two corner images (properties BorderTopLeft, BorderTopRight, BorderBottomLeft and BorderBottomRight) and the image that goes between them (BorderTop, BorderLeft, BorderBottom, BorderRight). Of course the corner images are reused for adjacent edges. To account for resizing you can specify how the edge image should be drawn (using the BorderTopSizing, BorderLeftSizing, BorderBottomSizing, BorderRightSizing properties). It can either be a portion of very wide image (SizingType.FixedSize), resize it to fit required space (SizingType.Stretch) or repeat it as much as needed (SizingType.Tile). Having all this specified painting is quite straightforward:
protected virtual void OnNonClientAreaPaintBackground(NonClientPaintEventArgs e)
{
if (BorderAppearance == null)
return;
// left border
Rectangle leftBorderRect = new Rectangle(0,
BorderAppearance.BorderTopLeft.Height, BorderAppearance.BorderLeft.Width,
e.Bounds.Height - BorderAppearance.BorderTopLeft.Height
- BorderAppearance.BorderBottomLeft.Height);
DrawUtil.DrawImage(e.Graphics, BorderAppearance.BorderLeftSizing,
BorderAppearance.BorderLeft, leftBorderRect);
// right border
Rectangle borderRightRect = new Rectangle(
e.Bounds.Width - BorderAppearance.BorderRight.Width,
BorderAppearance.BorderTopRight.Height, BorderAppearance.BorderRight.Width,
e.Bounds.Height - BorderAppearance.BorderTopRight.Height
- BorderAppearance.BorderBottomRight.Height);
DrawUtil.DrawImage(e.Graphics, BorderAppearance.BorderRightSizing,
BorderAppearance.BorderRight, borderRightRect);
// top border
Rectangle topBorderRect = new Rectangle(
BorderAppearance.BorderTopLeft.Width, 0,
e.Bounds.Width - BorderAppearance.BorderTopLeft.Width
- BorderAppearance.BorderTopRight.Width,
BorderAppearance.BorderTop.Height);
DrawUtil.DrawImage(e.Graphics, BorderAppearance.BorderTopSizing,
BorderAppearance.BorderTop, topBorderRect);
// bottom border
Rectangle bottomBorderRect = new Rectangle(
BorderAppearance.BorderBottomLeft.Width,
e.Bounds.Height - BorderAppearance.BorderBottom.Height,
e.Bounds.Width - BorderAppearance.BorderBottomLeft.Width
- BorderAppearance.BorderBottomRight.Width,
BorderAppearance.BorderBottom.Height);
DrawUtil.DrawImage(e.Graphics, BorderAppearance.BorderBottomSizing,
BorderAppearance.BorderBottom, bottomBorderRect);
// top-left corner
DrawUtil.DrawImageUnscaled(e.Graphics, BorderAppearance.BorderTopLeft, 0, 0);
// top-right corner
DrawUtil.DrawImageUnscaled(e.Graphics, BorderAppearance.BorderTopRight,
e.Bounds.Width - BorderAppearance.BorderTopRight.Width, 0);
// bottom-left corner
DrawUtil.DrawImageUnscaled(e.Graphics, BorderAppearance.BorderBottomLeft,
0, e.Bounds.Height - BorderAppearance.BorderBottomLeft.Height);
// bottom-right corner
DrawUtil.DrawImageUnscaled(e.Graphics, BorderAppearance.BorderBottomRight,
e.Bounds.Width - BorderAppearance.BorderBottomRight.Width,
e.Bounds.Height - BorderAppearance.BorderBottomRight.Height);
}
After calculating the rectangle of each edge, I use helper method that draws image according to selected mode. See DrawUtil class for details.
This reresents border's background. On top of it we will paint the foreground consisting of window title bar that includes the window icon, title and control buttons. Icon and title text are already defined in the Form class, so the CustomBorderAppearance class only specifies font and color of title text (TitleFont and TitleColor properties). There are also properties for window buttons but we will deal with them later.
protected virtual void OnNonClientAreaPaintForeground(NonClientPaintEventArgs e)
{
if (BorderAppearance == null)
return;
// draw window icon
if (this.Icon != null)
e.Graphics.DrawIcon(this.Icon, new Rectangle(BorderAppearance.BorderSize.Left+4,
BorderAppearance.BorderSize.Top+2, 16, 16));
string text = this.Text;
if (!String.IsNullOrEmpty(text) && BorderAppearance.TitleColor.IsEmpty == false)
{
// disable text wrapping and request elipsis characters on overflow
StringFormat sf = new StringFormat();
sf.Trimming = StringTrimming.EllipsisCharacter;
sf.FormatFlags = StringFormatFlags.NoWrap;
// find position of the first button from left
int firstButton = e.Bounds.Width;
foreach (CaptionButton button in this.CaptionButtons)
if (button.Visible)
firstButton = Math.Min(firstButton, button.Bounds.X);
// draw text
using (Brush b = new SolidBrush(BorderAppearance.TitleColor))
{
int textOffset = BorderAppearance.BorderSize.Left + 16 + 8;
Rectangle textRect = new Rectangle(textOffset, 5,
firstButton - textOffset, BorderAppearance.TitleBarSize);
Font f = this.Font;
if (BorderAppearance.TitleFont != null)
f = BorderAppearance.TitleFont;
e.Graphics.DrawString(text, f, b, textRect, sf);
}
}
// paint buttons
foreach (CaptionButton button in this.CaptionButtons)
button.DrawButton(e.Graphics);
}
As you can see I exclude the areas occupied both by the icon and the buttons when calculating the rectangle for the text. The StringFormat flags are set to disable wrapping and to draw ellipsis characters when there is not enough space for the whole text to emulate standard behavior.
The visual size of border is specified by the BorderSize structure and you can make each edge to have different width or height. These values are used to calculate the margin between the window and its client area:
protected override void OnNonClientAreaCalcSize(ref Rectangle bounds)
{
if (this.BorderAppearance == null)
return;
bounds = new Rectangle(
bounds.X + BorderAppearance.BorderSize.Left,
bounds.Y + BorderAppearance.BorderSize.Top + this.BorderAppearance.TitleBarSize,
bounds.Width - BorderAppearance.BorderSize.Right - BorderAppearance.BorderSize.Left,
bounds.Height - BorderAppearance.BorderSize.Bottom - BorderAppearance.BorderSize.Top
- this.BorderAppearance.TitleBarSize);
UpdateCaptionButtonBounds(bounds);
}
There are also another sizes that apply to border; they define the offset from the edge that the mouse will see as sizable. This is specified by the SizingBorderWidth property. Similarly, the SizingCornerOffset property indicates the offset from the window corner where sizing can be done in two directions. As you probably guessed these two properties are used by the OnNonClientAreaHitTest method to calculate the hit-test code for given point.
Handling mouse actions
TBD
Version History
July 20, 2005
Although the main supported platform is .NET 2.0, the source code also includes a solution file for VisualStudio.NET 2003. All files specific to .NET version 1.1 end with digit 1. So the solution and project files are CustomBorderForm1.sln and CustomBorderForm1.csproj respectively. I also had to provide different sets of Resource and DemoForm files. Appart from this all remaining code works on both platforms. I used Resource File Creator tool by Jason Haley to quickly create resource file compatible with VS2003.
February 4, 2006
In this release fixes several of the reported issues:
- Form size changed each time application was statrted from Visual Studio having Form open in designer. This was due fact that designer saves Form size by assigning it's ClientSize. This in turn uses standard system metrics to add border padding and calculate window Size. Fortunatelly, in .NET 2.0 Form and Control classes use virtual SetClientSizeCore method do this which I could override it to provide my own metrics.
- Form's Icon was sometimes drawn using scaled-down version although small version existed. I used the Graphics.DrawIcon method for this. Now I first create and cache the small icon and use Graphics.DrawIconUnstretched method instead.
- Added round corners in LonghornForm sample. Actually, making Form nonractengual turns out pretty trivial and you only need to provide own Region. I do this in OnResize method and create Region using GraphicsPath with rounded corners.
- The code was also slightly refactored and run through FxCop but this has no impact on overall design.