Introduction
How important is it to have a smooth, sexy, cool but subtle looking user interface? The look & feel of an application can be quickly improved by using visually attractive designs, e.g., by simply using gradient background fills or (semi) transparent controls. The GradientPanel
control will provide this kind functionality for you, yet it will be as easy to use just like the regular Panel
control.
Because of the lack of a decent transparent Label
control in .NET 1.1, I included my AlphaLabel
control in the sources (it is derived from Label
). The AlphaLabel
control uses alpha blending to create a semi or fully tansparent background effect. Note that .NET 2.0 users have a Label
control that is transparant by default. However, it doesn't support alpha blending :-p.
This article is meant for beginner programmers who are interested in designing and writing their own custom controls. I know there are a number of beginner articles out there that explain how to build custom controls. This article just describes the process from my experience and my perspective, and I hope this can be a valuable contribution to the community.
As a 'case', I decided to use the GradientPanel
control as an example on how to do this. I will not explain the AlphaLabel
control in this article, however I have fully documented the source code and it should not be difficult to understand.
The AlphaLabel
supports the following features:
- Control the transparency level by setting the
Alpha
property (0=100% transparent, 255=100% opaque).
- Regular
Label
behaviour.
The GradientPanel
will support the following features:
- Standard
Panel
functionality (including turning off gradient fills).
- Allow containing other controls (just like
Panel
, this will be a container control).
- Linear gradient fill between two colors maximum.
- Gradient fill under any angle.
- Automatically adjusting the gradient after resizing the control.
Additionally, I will give some hints and tips on how to make the control look professional.
Background
The C# code used in this article is based on Visual Studio 2003 for .NET 1.1. However, it is not difficult to implement this also in a .NET 2.0 environment. The GradientPanel
control is a good example of how to implement your own custom control, as it requires and shows some basic features used for almost every custom control:
- How and where are some commonly used drawing operations used?
A little bit of GDI+ (Graphics Device Interface) programming will be involved here. If you plan on creating more visually attractive controls, it sure pays off to get to know the GDI+ functions well. This article, however, will only use some basic GDI functions. Also, this article will show how to reserve GDI+ resources.
- How and which event to override?
This is not always very trivial, especially when you implement a control that is derived from another control, as in this case. I will discuss the most commonly used events to override in this article.
- How to make the control configurable in your application at design time?
A lot of controls presented here on CodeProject sadly often skip this part. They only implement the core functionality of the control, but forget that there is also a thing called a 'design time' development environment. Controls are meant to be used by fellow programmers and designers. Therefore, publicly distributed controls should support 'design time' development. This article will give an introduction on how this can be achieved.
Using the code
I will explain building the control on a step-by-step basis. First off, all I want to point out my personal view on custom controls to any potential control builder; try to stick to the following rules of thumb:
- Make the custom control as lightweight as possible.
Every programmer loves lightweight controls, just drag, drop, and go! Nobody wants to bother with installers, missing sources, or missing references or missing resources (e.g., fonts or graphics). Programmers (yes, those are your end-users) are human too, if it does not work at once, they will often just give up.
- Limit the use of external (non-system) libraries.
Personally, I would advise never to use external or non-system libraries, unless absolutely necessary. What good is a component if you first need to install other components to use it?
- Choose a suitable parent control to derive from.
Unless you are planning to fully build your control from scratch (which is an advanced topic), I would suggest to choose a suitable base control that handles most of the controls' functions for you. This way, the amount of coding needed is limited. In this example, we will be using the Panel
control as a base control.
- Design the custom control for only a limited set of functions.
Basically, it means to decompose potential complex controls into smaller parts. For instance, it may be tempting to set a text property for the panel control to allow the user to write text in the control. Instead, choosing to create a gradient panel, which will only add gradient, nothing more nothing less makes it simple and very clear for the user (programmer) what the control can do.
- Try to keep the number of properties as limited as possible.
Properties define the states of the control. The number of states of the control increases exponentially with each new property introduced to the control, therefore the complexity of your control increases exponentially. So choose your properties wisely, and try to stick to the commonly chosen properties as used by other controls, where possible.
Hopefully, this piece of advice can help as a guideline for the design of your future control. So let's start some implementation.
Step 1: Setting up the project
- Create a new C# Windows Application project in Visual Studio (2003) called "GradientPanelApp".
For now, I will presume that the control will be implemented directly into the application. This has two advantages: the sources stay small, and the control will be easier to debug (!). Alternatively, you can implement the control in a separate library. For now, this is out of the scope for this article.
- Add a new class called 'GradientPanel.cs'.
- Press "ctrl-shift-B": Compile the project (!).
It is good practice to compile whenever you change something. This will prevent having to troubleshoot errors in an early stage.
Step 2: Choose a base class
- Open the GradientPanel.cs.
- In order to work with controls, forms, and GDI+, we will need to add some namespaces to the file. Add/modify the following namespaces at the top of the file:
using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
- Derive the class from
Panel
. Change the class as follows:
class GradientPanel : Panel
- Compile the project.
Step 3: Test the control
- It is good practice to test the control from an early stage on. Therefore, we will now add the control to the control toolbox.
- Go to the Tools menu, and select "Add/Remove Toolbox Items...".
- Click the Browse button, and browse to the location of the GradientPanelApp project. Then, select the subfolder bin\debug. This folder should contain the GradientPanelApp.exe after you compiled succesfully.
- The trick is to select your own executable for the component. Press OK, the
GradientPanel
control should now show up in the components list. Press OK, the control will now be available in the toolbox (My user controls tab).
- Open Form1.cs in designer mode.
- Drag the
GradientPanel
control from the toolbox onto the Form
. The control should render like a regular Panel
control, except that it is now called a GradientPanel
control.
- Compile the project.
Note: if you are trying this in VS 2005, the control gets added to the toolbox automatically after compilation.
Step 3: Customize the control
So now that the basic project structure is setup and running, it is now time to actually start some customization. Basically, we want to keep the same functionality as the regular Panel
except that it should render a gradient background. Because the gradient fill requires two colors, we will be re-using the ForeColor
and BackColor
properties for this (this means less programming!).
Additionally, we would like to specify the angle under which the gradient is to be drawn. Because we want this parameter to be configurable at design time, the best solution is to implement a public
property for this called GradientAngle
.
- Open the GradientPanel.cs source.
- In order to create the new property, add the following code to the class:
public float GradientAngle
{
get {
return gradientAngle;
}
set {
gradientAngle = value;
this.Invalidate(true);
}
}
- Notice that the property 'setter' include the
this.Invalidate()
call. This call will make the control ask the operating system to refresh itself. This means that the refreshing is not done in the Invalidate()
call itself, but at some other time when the operating system has time to process it. This is done through the OnPaint
and OnPaintBackground
events.
- Because we want to draw the gradient fill on the background of the control, we will be overriding the
OnPaintBackground
event. By simply typing the word 'override' and then press space, a list will popup showing all base control methods that can be overridden! Very handy indeed. Select the OnPaintBackground
and press the tab button. Automatically, the body of the method is generated for you (less typing!).
Now, simply add some code to gradient fill the background, and your code should look something like this:
protected override void OnPaintBackground(PaintEventArgs pevent)
{
base.OnPaintBackground (pevent);
Brush brush = new LinearGradientBrush(this.ClientRectangle,
ForeColor, BackColor, gradientAngle, true);
pevent.Graphics.FillRectangle(brush, this.ClientRectangle);
done painting
brush.Dispose();
}
- Once again, compile your code.
- Switch back to Form1 in design mode, and notice already a gradient fill from Black (forecolor) to the control color (backcolor) from left to right (angle of 0 degrees).
There you have it, a GradientPanel
with a gradient background. Feel free to play with the control's properties. Change the angle, and the fore- and backcolor at design time.
The basic functionality is now implemented and the control is ready for use. However, one last step is sometimes required. In this case, you can decide for yourself if you need it.
Step 4: Optimize your code
Typically, when building controls, it is very important to be efficient when using system resources. Especially because a form may contain multiple instances of your control. If your control is inefficient with resources, the form may become very slow and irresponsive. In the worst case, it will eat your system resources like memory or CPU cycles and ultimately freeze your application.
Even in this simple control, we are using system resources. Notice in the OnPainBackground
method the use of a Brush
and of the Graphics
object. These objects come from the System.Drawing
namespace and are .NET wrapper classes around the GDI+ objects used by the operating system. Notice that in this particular example, the Brush
object is created, used, and disposed every time the background needs repainting. Disposing the Brush
here is required because we need to release the system resources as soon as we are done with them. But this is really a waste of CPU cycles: why create and dispose the Brush
object on every Paint
event? Most of the time, the Brush
object will not change. The Brush
will only change if the forecolor, backcolor, or angle changes.
If you plan to optimize your code, you can take the following steps:
- Declare the
Brush
object to be used by this control as a private
member. To make sure it has some valid value, initialize it to the control color.
private Brush brush = new SolidBrush(SystemColors.Control);
- Create a new
private
method CreateBrush()
in which the linear brush is created. Insert the code to dispose of the Brush
if it exists and create a new one.
private void CreateBrush()
{
if (brush != null)
brush.Dispose();
brush = new LinearGradientBrush(this.ClientRectangle,
ForeColor, BackColor, gradientAngle, true);
this.Invalidate(true);
}
- Override the
ForeColor
and BackColor
properties. In the setter of the GradientAngle
, ForeColor
, and BackColor
properties, add the call to CreateBrush()
.
public float GradientAngle
{
get { return gradientAngle; }
set {
gradientAngle = value;
CreateBrush();
}
}
public override Color ForeColor
{
get { return base.ForeColor; }
set {
base.ForeColor = value;
CreateBrush();
}
}
public override Color BackColor
{
get { return base.BackColor; }
set {
base.BackColor = value;
CreateBrush();
}
}
- Remove the inefficient code from the
OnPaintBackground
method.
protected override void OnPaintBackground(PaintEventArgs pevent)
{
base.OnPaintBackground (pevent);
pevent.Graphics.FillRectangle(brush, this.ClientRectangle);
}
- Initialize the
Brush
when the control is created. This can be done by overriding the OnCreateControl
event.
protected override void OnCreateControl()
{
base.OnCreateControl ();
CreateBrush();
}
- Compile your control and try it out in the
Form
at design time.
You may have noticed that when you resize the control at design time, or when you change the form's backcolor, the gradient is not displayed correctly. Before optimizing our code, the Brush
was created on every OnPaintBackground
event. When you resize the control, this event is fired a number of times. But because we are not creating the Brush
in the OnPaintBackground
event anymore, we will have to take care of this manually.
- To make sure the
Brush
is created when resizing the control or when its parent control or form's background color is changed, override the OnResize()
and OnBackColorChanged
methods, and call the CreateBrush()
.
protected override void OnResize(EventArgs e)
{
base.OnResize (e);
CreateBrush();
}
protected override void OnBackColorChanged(EventArgs e)
{
base.OnBackColorChanged (e);
CreateBrush();
}
- Compile the control again, and test if it is working correctly now in the
Form
at design- and runtime!
This concludes the introduction to building your own controls. If you still encounter problems, find the sources available for download on this page.
Points of Interest
As shown in this introduction, even creating a simple control such as this already requires a number of overrides and additional coding in order to get the desired functionality both at design- and runtime.
Before rounding up this article, I want to give some general hints, tips, and reminders for beginner programmers:
- This may sound trivial, but try to comment your newly defined public properties and methods using the
///<summary>
notation. These comments will show up in the programmer's IDE. Specially, when the control gets more complex and has many more properties, it is important to have these well documented in order to avoid confusion.
- Use the
#region
and #endregion
to group similar methods or functionality together. This will greatly improve the readability of your code (of course, this is a very general statement and applies to all source code).
- Use so called 'Attributes' to set 'properties for your properties'. I did not discuss this topic here as it is more advanced. However, in the provided source code, I set attributes for my custom created properties. These attributes are used by the .NET environment at compile time, and adds additional behaviour to your code with very little effort. For instance, by setting the
[Category("Appearance")]
attribute for the GradientAngle
property, means that the property will show up in the Appearance section in the categorized propery view in your IDE.
- Test, test, and test. Compile often. Try to test every path of execution. E.g., for controls, a typcial situation is when you drag a new instance of the control on your form at design time. Often, the control is not rendered correctly, and some additional initialization code is needed.
- You may experience that you want to debug the behaviour of your control at design time. Unfortunately, this is not supported. In order to get some kind of feedback of what is happening, you can put 'trace' statements in your code that display relevant information (e.g., the name of the event fired). You can do this simply by inserting the following code:
Console.WriteLine("important debug information");
These trace statements will display in your IDE's Output window, also at design time!
- As stated earlier, keep your controls as simple as possible. Decompose complicated controls to smaller simpler ones. Try to keep a small 'footprint'. If your control uses a lot of external references (of non-standard libraries), all of those libraries will need to be deployed with your control! Why is this bad? Well, controls are meant to be reusable. Everytime you would want to reuse the control, you would get 'infected' with all sorts of external libraries. Therefore, try to keep references at a minimum. Instead, include the sources if possible rather than using an external reference.
- Remember your target audience. Who are your end users? Most of the time, your end users will be programmers rather than application users. Therefore, controls should be designed to be used and refined by programmers. Make your controls configurable through properties, and extensible by firing events (not discussed in this article), and allowing overrides and subclassing of your control (not discussed here either).
- If you decide to create more complex properties, e.g., a collection of controls, serialization of these properties is not automatically handled by your IDE. What does this mean? It means you will have to implement your own code to store the settings of your properties that were set by the programmer at design time! Luckily, if you stick to simple property types, VS2003 handles this for you and stores it in the 'Windows Form Designer generated code' region.
There are obviously many more hints and tips out there. The best way of learning is simply by trying it in practice. I hope this article has given you more insight in the not so trivial world of building custom controls, and I hope you can use it as a starting point for your next custom control project.
History
Version 1.0 of the GradientPanel
and AlphaLabel
controls were written on September 2006. This article was published in September 2006.
Currently Herre Kuijpers is employed at Rubicon. During his career he developed skills with all kinds of technologies, methodologies and programming languages such as c#, ASP.Net, .Net Core, VC++, Javascript, SQL, Agile, Scrum, DevOps, ALM. Currently he fulfills the role of software architect in various projects.
Herre Kuijpers is a very experienced software architect with deep knowledge of software design and development on the Microsoft .Net platform. He has a broad knowledge of Microsoft products and knows how these, in combination with custom software, can be optimally implemented in the often complex environment of the customer.