Introduction to Building Windows Custom Controls...
Recently, I've been working on windows development projects, and I've been learning about application and control development. I wanted to write a custom control article showing how windows controls work, from a web developer's point of view. There are many differences between these environments for web developers to be aware of. Whereas the web is a stateless environment, the windows environment isn't. Any postbacks and viewstate mechanisms aren't needed. The Session object isn't needed also; however, a caching mechanism is available through the Enterprise Library framework.
Because of these changes, the windows environment is even more event-driven than the web environment. Note that Form_Load, a highly used event in web development, is still used; however, it only runs when the form initially is loaded. In addition, there are events for detecting mouse clicks and movements, key presses, and other events that were harder to capture in the web environment.
Properties work differently too. In the web environment, all changes to property values aren't rendered until the render event; you can change the properties as many times as you like before that event and the newest value will be reflected. Windows, however, requires manually invoking rendering the control through the Invalidate method. A call to Invalidate calls "painting" for the control or form, which refreshes the changes made to it, which leads into my next point.
Windows controls are "painted," not rendered per se, which is the terminology used with the overridden OnPaint method for custom controls. All code to render the appearance of the control is performed in this method. To do this, it requires an understanding of GDI+. I will only write down the necessary knowledge for the task at hand; however, for more information see this web site: http://windowssdk.msdn.microsoft.com/en-us/library/ms536336.aspx. In the future, I'll post more about GDI+.
In the web control's render method, HTML code is rendered in the browser; however, windows paints the control to the form. This painting is done by using a mathematical-based approach to drawing lines and shapes, and filling them, creating the control's interface. The Graphics object is the key object to do this; it has all of the drawing and filling mechanisms to draw lines, curves, polygons, etc. to the screen, and fill them with colors.
Note: Another difference to be aware of is that where the web handled certain events for you (posting back and other javascript events), you have to control all of this manually in your control.
The example control for this article is a Quote custom control that renders a quote with the reference in the format of: "This is my quote" (Mains). The period, quotes, and reference are all optional, and can be disabled/enabled through properties. Let's talk about the properties, which are:
The definitions of the properties are shown below:
[
Category ( "Appearance" ),
DefaultValue ( true ),
Description ( "Whether to add the periods at the end" )
]
public bool AddPeriod
{
get { return _addPeriod; }
set
{
if (_addPeriod != value)
{
_addPeriod = value;
this.Invalidate ( );
}
}
}
[
Category ( "Appearance" ),
DefaultValue ( true ),
Description ( "Whether to include the quotation marks around the quote" )
]
public bool IncludeQuotation
{
get { return _includeQuotation; }
set
{
if (_includeQuotation != value)
{
_includeQuotation = value;
this.Invalidate ( );
}
}
}
[
Category ( "Data" ),
DefaultValue ( "" ),
Description ( "The reference for the quote (who said it, reference list)" )
]
public string Reference
{
get { return _reference; }
set
{
if (_reference != value)
{
_reference = value;
this.Invalidate ( );
}
}
}
public override string Text
{
get { return base.Text; }
set
{
if (base.Text != value)
{
base.Text = value;
this.Invalidate ( );
}
}
}
Each of the properties uses the Category, DefaultValue, and Description attributes to handle design-time rendering of the properties. If you aren't familiar with attributes, they add functionality to the designer. For instance, when displaying properties by category in the property grid, the Category attribute specifies the name of the category that property belongs to. If that category doesn't exist, a new one is added. The description below the properties in the property grid is set by the Description attribute.
Note: The only variant from this approach is Text; the Text property is already defined in the base class. However, the problem is it doesn't invalidate the control and repaint the control surface whenever it changes. I always override it to do refresh the control, and because of that I don't want to override the base class attributes.
You will notice something different about these properties. First, each property is assigned a value only when it is different. If different, the Invalidate method is called, the Paint event is raised, and the custom painting code in OnPaint runs. Look at our custom painting code below:
protected override void OnPaint ( PaintEventArgs e )
{
base.OnPaint ( e );
string text = string.Empty;
bool referenceExists = !string.IsNullOrEmpty ( this.Reference );
//Must have text to work with, that is not the ID of the control
if (!string.IsNullOrEmpty ( this.Text ) && this.Text != this.Name)
{
//Get the text
text = this.Text;
//If including quotations, wrap them around the text
if (this.IncludeQuotation)
{
if (referenceExists)
text = '"' + text + '"';
else
{
text = '"' + text;
if (this.AddPeriod)
text += '.';
text += '"';
}
}
//If a reference exists, add it
if (referenceExists)
{
text += string.Format ( " ({0})", this.Reference );
//If the text doesn't contain a period, add one at the end
if (this.AddPeriod)
text += '.';
}
}
//If in design mode, render a sample
else if (this.DesignMode)
{
text = "'Sample quotation' (Mains).";
}
//Only draw when not empty
if (!string.IsNullOrEmpty ( text ))
e.Graphics.DrawString ( text, this.Font, new SolidBrush ( this.ForeColor ), this.ClientRectangle );
}
There are many parts to this method, but it isn't overtly complicated. Remember, you have to do a lot more computations and property analysis, to render the control correctly. The reason is that you see the control as you build it, and some properties may be assigned, while others may not be. There isn't a way to tell this except through analysis. So, at first, I check to see if the text property exists, and doesn't equal the name of the control. If it does, then I want to see if the IncludeQuotation property was set. This is a Boolean value, wrapping quotes around the text. If the reference exists (meaning not null), then render the text, because the reference will be added next; otherwise, render the text, and put the period (if the period is meant to be added) before the last quotes.
if (this.IncludeQuotation)
{
if (referenceExists)
text = '"' + text + '"';
else
{
text = '"' + text;
if (this.AddPeriod)
text += '.';
text += '"';
}
}
For something so simple, you can see what I mean by when I said you need to perform a lot of analysis in windows controls. If the reference exists, the period goes after the reference parenthesis; however, if the reference doesn't exist, it goes before the last quotes. That isn't as easy as you think!
The next task is to output the reference. If it exists, it is rendered with parenthesis around it. I use the Format method of the string class for ease of use. Also, if adding a period determined by the property, then the period is added at the end.
if (referenceExists)
{
text += string.Format(" ({0})", this.Reference);
if (this.AddPeriod)
text += '.';
}
There is a special case if the text of the quote doesn't exist and we are in design mode. A sample quote renders in the control in this case. This happens because of second part of the main if statement:
else if (this.DesignMode)
{
text = "'Sample quotation' (Mains).";
}
The last statement in the method handles the rendering, which is how the text gets placed on the screen. Let's look again at the DrawString method:
if (!string.IsNullOrEmpty(text))
e.Graphics.DrawString(text, this.Font, new SolidBrush(this.ForeColor), this.ClientRectangle);
The DrawString method takes several parameters to render the text. It takes the text and the font to render with (this.Font specifies the font defined for the control; if none explicitly specified, it inherits from the Form). The last property determines the shape to render it in. For instance, the control can render in a limited width and height, which in this example it is represented by the ClientRectangle property. ClientRectangle is a property of the Control class that represents the area the text can be rendered in. DrawString handles the actual physical rendering in the control's region and is shown on the form it is displayed in. The result (of three samples I setup) is:
As you can see, the quotes are displayed in the form, based on the values of the properties assigned to it. If you look in the code, you can see how it is defined.