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:
private const int WS_EX_CLIENTEDGE = 0x00000200;
Now that we know what the value is, we need to apply it:
protected override CreateParams CreateParams
{
get
{
CreateParams p = base.CreateParams;
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:
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:
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:
break;
}
}
Now the CreateParams
property can be modified to use this new function. Change the CreateParams
property to this:
protected override CreateParams CreateParams
{
get
{
CreateParams p = base.CreateParams;
int style = p.Style;
int exStyle = p.ExStyle;
BorderStyleToWindowStyle(ref style, ref exStyle);
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:
[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:
int style = GetWindowLong(hWnd, GWL_STYLE);
int exStyle = GetWindowLong(hWnd, GWL_EXSTYLE);
style = style & ~WS_BORDER;
exStyle = exStyle | WS_EX_CLIENTEDGE;
SetWindowLong(hWnd, GWL_STYLE, style);
SetWindowLong(hWnd, GWL_EXSTYLE, exStyle);
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:
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;
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:
[Category("Appearance")]
[Description ("Border style of the control")]
[DefaultValue(typeof(System.Windows.Forms.BorderStyle), "Fixed3D")]
public BorderStyle BorderStyle
{
get {return borderStyle;}
set
{
borderStyle = value;
int style = GetWindowLong(Handle, GWL_STYLE);
int exStyle = GetWindowLong(Handle, GWL_EXSTYLE);
BorderStyleToWindowStyle(ref style, ref exStyle);
SetWindowLong(Handle, GWL_STYLE, style);
SetWindowLong(Handle, GWL_EXSTYLE, exStyle);
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.