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

Adding designable borders to user controls

0.00/5 (No votes)
8 Dec 2003 1  
A tutorial on adding design-time enabled borders to user controls. Covers topics including atributes, interop, and custom overrides

Introduction

This tutorial will walk you through the process of creating a control which has a border that can be changed at design time. The tutorial assumes that you are reasonably familiar with writing controls in C#, and know your way around Visual Studio .NET. This tutorial is a little different in that the border is handled by changing the style properties of your window, meaning that you don't have to paint it yourself.

Setting up the project

Create a new C# windows application project, called "BorderSample".

Now add another project � This one will contain the new control you�re going to create. In the File menu, choose "Add Project -> New Project". Choose a C# Windows Control Library as the project type, and call it "BorderControls".

At this point, you�re in design mode looking at a blank user control. Rename UserControl1.cs to SimpleControl.cs. In the code, change both the class name and constructor to match.

If you look at the control in design mode, you'll notice that it has no border. The default user control doesn't have one, but it's relatively simple to add.

When a control is created, the Forms library uses the CreateParams property of the control to get all the info it needs about the window to create for the control. CreateParams is virtual, so it can be overridden. Check out the System.Windows.Forms.CreateParams type (this is the class returned by the property). It contains a number of things, but the ones we're concerned with are Style and ExStyle.

If you're familiar with Win32 programming, you'll recognize these. Style and ExStyle contain flags which control many behavioural and visual traits of a window. Since UserControl doesn't have a border by default, we can assume that none of the styles relating to border have been set. Right under the "Component Designer Generated Code" region, type this:

override CreateParams

If you hit enter immediately after typing those two words, Visual Studio should fill in the following for you:

protected override CreateParams CreateParams
{
    get{ return base.CreateParams; } 
}

As you can see, it's simply getting the values from the base class and passing them through untouched. This gives us a chance to change the values, but what values do we use? If you look up the windows help topics on CreateWindowEx, you'll find an extended style value called WS_EX_CLIENTEDGE, which gives the window a sunken 3D border. This is what we want. Do a search on your machine for a file called WinUser.h, which is the header containing this constant (and many others). Find the value in the header, or just trust me:

// Constant taken from WinUser.h

private const int WS_EX_CLIENTEDGE = 0x00000200;

Now that we know what the value is, we need to apply it:

// Provide window style constants to enable control border

protected override CreateParams CreateParams
{
    get
    {
        // Take the default params from the base class

        CreateParams p = base.CreateParams;
        
        // Add the extended "3d sunken border" style

        p.ExStyle = p.ExStyle | WS_EX_CLIENTEDGE;
        return p;
    }
}

That's it. Go back to the "BorderSample" project, and open "Form1.cs" in the designer. If you look at the "My User Controls" tab in your toolbox, you should see your "SimpleControl" in there. Drag it onto your form. You now have a bordered control.

Creating a design-enabled border property

You've created a user control with a border, which is cool and all, but what if you want a user to choose what kind of border to give the control? That's a little more involved, and requires using Interop, but it isn't difficult.

The first thing you need is a value to hold the type of border you want. Controls in the Forms library use the System.Drawing.Drawing2D.BorderStyle type, so we'll use it too. Add the following code to SimpleControl:

private System.Windows.Forms.BorderStyle borderStyle = BorderStyle.Fixed3D;

This variable can take one of three values: None, FixedSingle, or Fixed3D. We've set the default to be Fixed3D.

Now you need a way to translate those values into window styles suitable for use by CreateParams. None is pretty easy � it�s no border at all. FixedSingle is a single black line around the control, and it maps to the basic window style WS_BORDER. Fixed3D is a sunken 3D border, and maps to the extended window style WS_EX_CLIENTEDGE.

Looking through "WinUser.h", we find the following values:

// These constants were taken from WinUser.h

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

Now we need a function to map the three possible BorderStyle values to style and extended style values for the window. All it does is take the existing window styles, mask off the WS_BORDER and WS_EX_CLIENTEDGE styles, then apply whichever style is appropriate, based on our internal borderStyle value:

// Convert borderStyle to Style and ExStyle values for Win32

private void BorderStyleToWindowStyle(ref int style, ref int exStyle)
{
    style &= ~WS_BORDER;
    exStyle &= ~WS_EX_CLIENTEDGE;
    switch(borderStyle)
    {
        case BorderStyle.Fixed3D:
            exStyle |= WS_EX_CLIENTEDGE;
            break;

        case BorderStyle.FixedSingle:
            style |= WS_BORDER;
            break;

        case BorderStyle.None:
            // No border style values

            break;
    }
}

Now the CreateParams property can be modified to use this new function. Change the CreateParams property to this:

// Provide window style constants to enable control border

protected override CreateParams CreateParams
{
    get
    {
        // Get the default values from the base class

        CreateParams p = base.CreateParams;

        // Store the Style and ExStyle values

        int style = p.Style;
        int exStyle = p.ExStyle;

        // Modify the values to match the desired border style

        BorderStyleToWindowStyle(ref style, ref exStyle);

        // Store the results back in the CreateParams class

        p.Style = style;
        p.ExStyle = exStyle;
        return p;
    }
}

This will use the borderStyle value to modify both the Style and ExStyle properties of the CreateParams used to create the window. When our control is created, the border will reflect the style chosen by the borderStyle variable.

Now we need a public property to allow users (designers) to change the border style:

/// <summary>

/// Gets or sets the border style of the tree view control.

/// </summary>

[Category("Appearance")]
[DescriptionAttribute("Border style of the control")]
[DefaultValue(typeof(System.Windows.Forms.BorderStyle), "Fixed3D")]
public BorderStyle BorderStyle
{
    get {return borderStyle;}
    set {borderStyle = value;}
}

The problem with this code is that it won't quite work. The window has already been created, and its style values have been set. The only way to change them is through a Win32 call, specifically SetWindowLong. You also have to tell Windows that you've changed the style values, which means a call to SetWindowPos as well. Luckily, this isn't difficult at all, as C# (and .NET in general) has good support for calling native code in DLL's. Calling Win32 API functions like this is done using a mechanism called Platform Invoke, or P/Invoke for short.

Using P/Invoke to set window styles

If you were to write the code to change the border style of a window using native C++, it�d look like this:

// Get style and exstyle values

int style = GetWindowLong(hWnd, GWL_STYLE);
int exStyle = GetWindowLong(hWnd, GWL_EXSTYLE);

// Modify existing style values

style = style & ~WS_BORDER;
exStyle = exStyle | WS_EX_CLIENTEDGE;

// Set new style values

SetWindowLong(hWnd, GWL_STYLE, style);
SetWindowLong(hWnd, GWL_EXSTYLE, exStyle);

// Tell windows that we changed the window frame style

SetWindowPos(hWnd, NULL, 0, 0, 0, 0,
    SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
    SWP_NOOWNERZORDER | SWP_FRAMECHANGED);

To make this work in C#, we�ll need the following things:

  • The GetWindowLong and SetWindowLong functions
  • The GWL_STYLE and GWL_EXSTYLE constants
  • The SetWindowPos function
  • All the SWP_* constants

All of these things can be found in WinUser.h. First we'll do the constants. They're pretty easy, since all we have to do is change the #define values we find into integer constants, like this:

// Constants from WinUser.h

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

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

Now for the fun bit: calling the functions. Calling functions in external DLL's is pretty easy. You tell C# what the function looks like (the prototype), what the arguments and return values are, and where to find it. C# takes care of the rest. The GetWindowLong function is in User32.dll, and looks like this:

int GetWindowLong(HWND hWnd, DWORD Index);

First, you�ll need to add this line to the top of your file:

using System.Runtime.InteropServices; // Needed for DLLImport

Interop is short for Inter-operation, which is yet another name for Platform Invoke. Added to our class, the P/Invoke specification for GetWindowLong looks like this:

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

The DllImport attribute tells C# where to find the function (User32.dll).

The CharSet=CharSet.Auto attribute is important because SetWindowLong and GetWindowLong are Unicode Aware functions. That is, they have both Ansi and Unicode versions, suffixed with either an A (for Ansi) or W (for Wide) depending on whether you have Unicode enabled in your code. Adding the CharSet=CharSet.Auto attribute tells C# that it can decide which version to use, which will generally be the Unicode one. Look up the "DllImport Attribute" topic in the .NET Framework help files for a more complete description.

The actual function prototype looks like a static member function, except that it has the extern keyword on it, which tells C# that the function is implemented in an external library. IntPtr is the type used to represent window handles by the CLR. For other mappings between CLR types and Win32 types, look at the DllImport documentation.

The other two functions are handled the same way as the first:

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

[DllImport("User32", ExactSpelling=true)]
private static extern int SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, 
    int x, int y, int cx, int cy, uint uFlags);

There's one difference here � SetWindowPos never deals with strings, so it only comes in one flavor, not Unicode and Ansi like the other two. This means that the function name isn't suffixed with A or W, which is why we leave out the CharSet property and replace it with ExactSpelling=true.

Once you've added the constants and the function prototypes to your control class, they're ready for use.

Change your BorderStyle property to this:
/// <summary>

/// Gets or sets the border style of the tree view control.

/// </summary>

[Category("Appearance")]
[Description ("Border style of the control")]
[DefaultValue(typeof(System.Windows.Forms.BorderStyle), "Fixed3D")]
public BorderStyle BorderStyle
{
    get {return borderStyle;}
    set
    {
        borderStyle = value;
        // Get Styles using Win32 calls

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

        // Modify Styles to match the selected border style

        BorderStyleToWindowStyle(ref style, ref exStyle);

        // Set Styles using Win32 calls

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

        // Tell Windows that the frame changed

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

Since we've defined the functions we need using interop, they appear as part of our class, and can be called like any other function. The BorderStyle property is done, compile your code then go back to your BorderSample project again. If you look at the properties of the control, you should see one called BorderStyle. Change it and the control border immediately updates to reflect the change.

A brief explanation about the attributes applied to the BorderStyle property is in order. The Category attribute specifies what heading the attribute will appear under in the designer. If you look at the properties of the control in the Forms designer, you�ll see BorderStyle shows up under Appearance. You�ll also notice that when you select the BorderStyle property, the text that shows up in the help box matches the text in the Description attribute. Finally, specifying the DefaultValue attribute tells the Forms designer that when it writes the code to create our control, if the user has chosen the Fixed3D value for the border style, it doesn't need to write code for it, since that's our default value. Without that line, the attribute would have code written to set it whether it was the default or not, which bloats the code if you have a lot of properties in your control.

Using attributes on your property values is a simple way to give the designer hints about how you want the properties presented to the user, and how your control should be persisted in code.

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