Szymon Kobalczyk's Blog

A Developer's Notebook

  Home  |   Contact  |   Syndication    |   Login
  106 Posts | 6 Stories | 578 Comments | 365 Trackbacks

News

View Szymon Kobalczyk's profile on LinkedIn

Twitter












Tag Cloud


Article Categories

Archives

Post Categories

Blogs I Read

Tools I Use

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.

Window Anatomy 

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.
posted on Wednesday, July 20, 2005 4:20 AM

Feedback

# re: Drawing Custom Borders in Windows Forms, Part Two 10/18/2005 4:19 PM PissedAtMicrosoft
Have you tried adding a Main Menu to your Form?

# re: Drawing Custom Borders in Windows Forms, Part Two 10/18/2005 4:58 PM PissedAtMicrosoft
There seems to be a strange Form autosizing problem. If I open the Demo Form designer and set the size to something like (300,300), when I build the solution the Form size changes slightly. Any idea what causes this?

# re: Drawing Custom Borders in Windows Forms, Part Two 12/8/2005 5:50 PM Demez Christophe
Hi,

With the last version of your code, the background is not transparent.

In fact, there are some white pixels on the corners.

Have you a solution please ?

Thanks

# Is this project dead? 1/21/2006 10:56 PM Martin
It seems that you do not read the comments... there are some issues with your project, can you answer them?

# re: Drawing Custom Borders in Windows Forms, Part Two 1/22/2006 4:33 AM Szymon
Martin,
Currently I'm aware of two issues: Visual Studio doesn't adjust ClientSize correctly when designing the form, and the form doesn't support transparency yet. For me the frist one is not so serious as I create all the forms programmatically and put UserControls in them. For the second one there are many articles on adding transparency to forms. Right now I'm working on another project and wan't be able to continue working on this one. But I will post any updates here as soon as I can deal with it.

# re: Drawing Custom Borders in Windows Forms, Part Two 3/7/2006 1:49 PM Demez Christophe
Hi

You have not understand :-(

I do not talk about transparency !!!! :-)

I will try to give a better explaination...

If your window has round corners, then some pixels (on the corners) have to
been transparent (and not white)... it sounds that sometimes the pixels are
transparents... but when moving the window or resizing it, there are some
problems...

I can send you a screen shot if you want...

Chris (cdemez2 [at] hotmail.com)

# MDI Bugs - re: Drawing Custom Borders in Windows Forms, Part Two 6/23/2006 1:57 PM Robert Grosz
Hi,

there are two bugs according to MDI childs. First one is that PointToWindow returns wrong values use in case of a MDIChild following code:
Padding pad =CalcNonClientAreaPadding();
Point pt = PointToClient(screenPoint);
pt.Offset(pad.Left, pad.Top);
return pt;

For the second bug i have no code but some surgestions. Now i tell you your second bug. If you maximize the MDIChild and have no mainmenu you cannot change the MDIChild WindowState, some buttons are missing. I think when calculation the CalcNonClientAreaPadding() simple add the height of the menu (GetMenuItemRect(hwnd, handleToMenu, int, RECT)). Maybe you will get some addtional space so Windows can draw this buttons.

Robert

# Help button problem? 6/30/2006 11:55 AM rakesh
Hi
When i put the help button on the dialog.
And click on it ,then in background of my help bitmap there it create a button at click position.
And that question mark is not coming with the mouse pointer every
time.


So please somebody help me to slove this.

Thanks


# re: Drawing Custom Borders in Windows Forms, Part Two 8/11/2006 12:40 AM Chris Reickenbacker
If somebody can reverse this - to arc at the bottom, then the issues with the edges not being transparent will be fixed. I already fixed the top by changing the diam to 18 from 10 but it needs the same at the bottom of the form to hide those clipped edges.
Thanks,
Chris Reickenbacker
http://reickenbacker.com/

protected override void OnResize(EventArgs e)
{
base.OnResize(e);
int diam = 18;
GraphicsPath path = new GraphicsPath();
path.AddArc(0, 0, diam, diam, -90, -90);
path.AddLines(new Point[] {new Point(0,diam), new Point(0, Height),
new Point(Width, Height), new Point(Width, diam)});
path.AddArc(Width - diam, 0, diam, diam, 0, -90);
path.CloseFigure();
this.Region = new Region(path);
}


# re: Drawing Custom Borders in Windows Forms, Part Two 5/16/2007 6:55 AM whidbey
Thanx a lot for providing this piece of code. Its really wonderful to have internet frnds like u which helps us seemlessly.

I run this code in .net framework 2.0 it will generate an error which is given below:-

'_Paint22.Form1.OnNonClientAreaCalcSize(ref System.Drawing.Rectangle)': no suitable method found to override

_Paint22 is the name of solution/project itself.

Please help to remove this error so that i can enjoy error free program.


Regards
Nitin Wadhwa
yindusoft technologies
nitin11@writeme.com | wadhwaster@gmail.com

# re: Drawing Custom Borders in Windows Forms, Part Two 4/26/2008 8:30 AM Omar
Hi;

Nice job, but ther is any chance to have the code in Visual Basic.net

let me know via e-mail if it is possible
thanks in advance

Regards
Omar

# re: Drawing Custom Borders in Windows Forms, Part Two 1/11/2012 9:55 PM Chris
I got this to work, but everytime I load the program, the border does not render right. Once I resize the window or change any of the settings, the border renders properly. Is there a way to fix that?

# re: Drawing Custom Borders in Windows Forms, Part Two 3/1/2013 3:00 AM Wonder Workds
source code is missing

Post A Comment
Title:
Name:
Email:
Comment:
Verification: