If you like this article, please vote for it. If you don't like this article, please vote, and please tell me why you don't like it.
Introduction
I started this project to understand how one develops a managed custom control that is similar in quality to the standard .NET Button
control. Like most .NET beginners, I started out inheriting from the UserControl
class, but quickly learned there are significant limitations to some of the concepts I wanted to implement.
After a considerable number of searches of Google and MSDN documents, I settled on developing a button control that inherits from ButtonBase
and IButtonControl
. One of the problems with inheriting from ButtonBase
is that the class is abstract. This means the button control cannot be obtained directly in the designer. However, it can be added to the toolbox, and it works perfectly well in the form designer.
The control is built for flexibility. It allows for either rounded corners or flat square controls. In addition, you can specify which corners are rounded and which are square. You may also change the radius of the corners. I made many of the colors configurable. I also included the ability to draw color gradients for both hover and normal colors. I implemented most of the events that a standard .NET button control can respond to. By implementing the IButtonControl
interface, several standard button behaviors are also exposed.
Background
There are a lot of .NET custom controls uploaded to CodeProject. Why do we need another one? The answer is partly because I am a C++ programmer. I wanted to see what it would take to transfer my WTL and ATL skills to .NET. I wanted to understand the difference between architecting a control for use with WTL and a control for use in VB.NET or C#. I make extensive use of attributes for describing properties and assigning default values. I also added XML documentation tags so that Intellisense would be meaningful when using the control in your form.
I modeled this control based on the work of WiB and Alan Zhao. Both of their articles provided a great deal of inspiration and education.
Compiling and using the source
Download and unzip the demonstration project. Please make sure to preserve the directory structure of the source files.
When you first load the solution, you may see a number of messages regarding a missing reference. This is because the assembly has not been built on your machine. (I didn't include any binary files in the source distribution, hence the missing references.) Simply select release or debug mode, and and recompile. The assembly will be built, and all references will be validated. You can add the control to your toolbox at this point, by dragging the assembly to the toolbox surface, or selecting "Add Item..." from the context menu and navigating to <root dir>\mehControls\release and selecting mehButton.dll.
Using the component
After you've added the control to your toolbox, you can drag it to any form. I've implemented a designer component so all properties show up correctly in the "Properties" view. You'll notice that I've removed some of the most difficult to implement properties in the PostFilterProperties
method of the designer. Other properties, such as the "Flat" properties, are redundant. Programming the mehControl
is fairly standard. The following code illustrates how the form designer wizard initializes the control in a C# project:
this.mehButton1.BackColor = System.Drawing.SystemColors.Control;
this.mehButton1.ButtonStyle = mehControls.mehButton.ButtonStyles.Rectangle;
this.mehButton1.CurveMode = mehControls.mehButton.CornerCurveStyle.None;
this.mehButton1.DialogResult = System.Windows.Forms.DialogResult.None;
this.mehButton1.GradientStyle = mehControls.mehButton.GradientStyles.None;
this.mehButton1.HoverBorderColor = System.Drawing.SystemColors.ControlDark;
this.mehButton1.HoverColorA = System.Drawing.SystemColors.ButtonFace;
this.mehButton1.HoverColorB = System.Drawing.SystemColors.ButtonHighlight;
this.mehButton1.Image = null;
this.mehButton1.Location = new System.Drawing.Point(12, 122);
this.mehButton1.Name = "mehButton1";
this.mehButton1.NormalBorderColor = System.Drawing.SystemColors.WindowFrame;
this.mehButton1.NormalColorA = System.Drawing.SystemColors.ButtonHighlight;
this.mehButton1.NormalColorB = System.Drawing.SystemColors.ButtonFace;
this.mehButton1.Radius = 0;
this.mehButton1.Size = new System.Drawing.Size(127, 38);
this.mehButton1.SmoothingQuality = mehControls.mehButton.SmoothingQualities.AntiAlias;
this.mehButton1.TabIndex = 4;
this.mehButton1.Text = "m&ehButton Flat";
this.mehButton1.UseVisualStyleBackColor = false;
this.mehButton1.MouseLeave += new System.EventHandler(this.mehButton1_MouseLeave);
...
private void mehButton1_MouseLeave(object sender, EventArgs e)
{
this.lblEventText.Text = "Mouse has left mehButton1";
}
Implementing a designer
When you develop a custom visual control for .NET, you will want to implement a designer. The designer may be simple (as the one I've implemented), or as complex as you want. The following code illustrates the definition of a minimal designer:
namespace mehControls
{
ref class mehButtonDesigner :
public System::Windows::Forms::Design::ControlDesigner
{
public:
mehButtonDesigner(void)
{
};
~mehButtonDesigner(void)
{
};
protected:
virtual void PostFilterProperties(IDictionary ^) override;
virtual void OnPaintAdornments(PaintEventArgs ^e) override;
};
}
Attributing properties
You'll want to properly attribute your properties. This will allow you to control where the property is grouped on the Properties display and the default value of the property.
[DefaultValue(System::Drawing::Color::typeid, "System::Drawing::SystemColors::Control")]
[Description("Background Color for button"), Category("Appearance")]
property virtual Color BackColor
{
Color get() override;
void set(Color value) override;
}
Handling the OnPaint event for the control
As with Native mode programming, most of the work is handled in the OnPaint
(or OnPaintBackground
) event of the control. In the case of the control, DrawRoundRectangle
does most of the heavy-lifting. In order to implement rounded corners, I make heavy use of gp->AddArc(arc, angle1, angle2)
where GraphicsPath ^gp = gcnew GraphicsPath()
.
Summary
The button implements many of the properties of a .NET 2.0 button. It was a lot of fun figuring out how to add the painting of the "hot" border. I had to use my tenth grade geometry to calculate the correct center of the arcs when drawing the rounded corners. This is the basic diagram I used for developing the rounder corner algorithm:
I was also able to implement gradient drawing of the background, which allows for some very cool effects when there is a mouse over or click event for the button. The following enum illustrates the gradient types that are implemented:
enum class GradientStyles
{
None,
Horizontal,
Vertical,
ForwardDiagonal,
BackwardDiagonal
};
The control is far from complete. I've added attributes for all of the properties. However, there is considerable work to be done with the XML documentation tags. I also have not implemented themes in this version of the control.
Things you should know
There are a few items that I found to be a lot easier in managed and mixed mode C++. First of all, you do not have to worry about importing API calls. You include any header files from the Platform SDK, and Visual C++ will take care of the call for you. This is a huge help, and can save a lot of time when working with API calls. Moving back and forth between wchar_t
and String
is a lot simpler in a managed C++ project.
VS 2005 does not have the nice wizards for C++ that C# has. When you select a Windows Control project, that is what you get. No designer. No bells and whistles. You'll have to consult the documentation often in the early going. However, I found that I learned a lot more about my environment doing it the hard way. Before I knew it, I wasn't consulting the documentation very often.
Adding references to projects is different between C++ and C#. You use the project properties page to add references. Just navigate to Common Properties, and click the Add New Reference... button.
Specifying namespaces and resolving referenced names: in Managed C++, you'll use "::" instead of "." when resolving names.
using namespace System::Windows::Forms;
History
- Version 1.0 - April 11, 2008 - Initial release of the control.