If your custom control needs to draw its own interface, you should use the Control class as your starting point. Such a control gets a fair amount of base functionality from the Control class. A partial list of properties and methods of the Control class was included earlier in the chapter. These properties arrange for the control to automatically have visual elements such as background and foreground colors, fonts, window size, and so on.
However, such a control does not automatically use any of that information to actually display anything (except for a BackgroundImage, if that property is set). A control derived from the Control class must implement its own logic for painting the control’s visual representation. In all but the most trivial examples, such a control also needs to implement its own properties and methods to gain the functionality it needs.
The techniques used in the earlier example for default values and the ShouldSerialize and Reset methods all work fine with the controls created from the Control class, so that capability is not discussed again. Instead, this section focuses on the capability that is very different in the Control class - the logic to paint the control to the screen.
The base functionality used to paint visual elements for a custom control is in the part of .NET called GDI+. A complete explanation of GDI+ is too complex for this chapter, but an overview of some of the main concepts is needed.
GDI+ is an updated version of the old GDI (Graphics Device Interface) functions provided by the Windows API. GDI+ provides a new API for graphics functions, which then takes advantage of the Windows graphics library.
The GDI+ functionality can be found in the System.Drawing namespace and its subnamespaces. Some of the classes and members in this namespace will look familiar if you have used the Win32 GDI functions. Classes are available for such items as pens, brushes, and rectangles. Naturally, the System.Drawing namespace makes these capabilities much easier to use than the equivalent API functions.
With the System.Drawing namespace, you can manipulate bitmaps and use various structures for dealing with graphics such as Point, Size, Color, and Rectangle. In addition, there are a number of classes for use in drawing logic. The first three such classes you need to understand represent the surface on which drawing takes place, and the objects used to draw lines and fill shapes:
Graphics - Represents the surface on which drawing is done. Contains methods to draw items to the surface, including lines, curves, ellipses, text and so on.
Pen - Used for drawing line-based objects
Brush - Used for filling shapes (includes its subclasses)
The System.Drawing namespace includes many other classes and some subsidiary namespaces. Let’s look at the Graphics class in a bit more detail.
Many of the important drawing functions are members of the System.Drawing.Graphics class. Methods such as DrawArc, FillRectangle, DrawEllipse, and DrawIcon have self-evident actions. More than 40 methods provide drawing-related functions in the class.
Many drawing members require one or more points as arguments. A point is a structure in the System.Drawing namespace. It has X and Y values for horizontal and vertical positions, respectively. When a variable number of points are needed, an array of points may be used as an argument. The next example uses points.
The System.Drawing.Graphics class cannot be directly instantiated. That is, you can’t just enter code like the following to get an instance of the Graphics class:
Dim grfGraphics As New System.Drawing.Graphics() ' This does not work!!
This doesn’t work because the constructor for the class is not public. It is only supposed to be manipulated by objects that can set the Graphics class up for themselves. There are several ways to get a reference to a Graphics class, but the one most commonly used in the creation of Windows controls is to get one out of the arguments in a Paint event. That technique is used in a later example. For now, to understand the capabilities of GDI+ a little better, let’s do a quick example on a standard Windows Form.
Here is an example of a form that uses the System.Drawing.Graphics class to draw some graphic elements on the form surface. The example code runs in the Paint event for the form, and draws an ellipse, an icon (which it gets from the form itself), and two triangles: one in outline and one filled.
Start a Windows Application project in VB 2005. On the Form1 that is automatically created for the project, place the following code in the Paint event for the form:
' Need a pen for the drawing. We'll make it violet. Dim penDrawingPen As New _ System.Drawing.Pen(System.Drawing.Color.BlueViolet) ' Draw an ellipse and an icon on the form e.Graphics.DrawEllipse(penDrawingPen, 30, 100, 30, 60) e.Graphics.DrawIcon(Me.Icon, 90, 20) ' Draw a triangle on the form. ' First have to define an array of points. Dim pntPoint(2) As System.Drawing.Point pntPoint(0).X = 150 pntPoint(0).Y = 100 pntPoint(1).X = 150 pntPoint(1).Y = 150 pntPoint(2).X = 50 pntPoint(2).Y = 70 e.Graphics.DrawPolygon(penDrawingPen, pntPoint) ' Do a filled triangle. ' First need a brush to specify how it is filled. Dim bshBrush As System.Drawing.Brush bshBrush = New SolidBrush(Color.Blue) ' Now relocate the points for the triangle. ' We'll just move it 100 pixels to the right. pntPoint(0).X += 100 pntPoint(1).X += 100 pntPoint(2).X += 100 e.Graphics.FillPolygon(bshBrush, pntPoint)
Start the program. When it comes up, the form will look something like Figure 16-7.
Figure 16-7
To apply GDI+ to control creation, you create a custom control that displays a “traffic light,” with red, yellow, and green signals that can be displayed via a property of the control. GDI+ classes will be used to draw the traffic light graphics in the control.
Start a new project in VB 2005 of the Windows Control Library type and name it TrafficLight. The created module has a class in it named UserControl1. We want a different type of control class, so you need to get rid of this one. Right-click on this module in the Solution Explorer and select Delete.
Next, right-click on the project and select Add New Item. Select the item type of Custom Control and name it TrafficLight.vb.
As with the other examples in this chapter, it is necessary to include the Imports statement for the namespace containing the attribute you will use. This line should go at the very top of the code module for TrafficLight.vb:
Imports System.ComponentModel
The TrafficLight control needs to know which “light” to display. The control can be in three states: red, yellow, and green. An enumerated type will be used for these states. Add the following code just below the previous code:
Public Enum TrafficLightStatus statusRed = 1 statusYellow = 2 statusGreen = 3 End Enum
The example also needs a module-level variable and a property procedure to support changing and retaining the state of the light. The property is named Status.
To handle the Status property, first place a declaration directly under the last enumeration declaration that creates a module-level variable to hold the current status:
Private mStatus As TrafficLightStatus = TrafficLightStatus.statusGreen
Then, insert the following property procedure in the class to create the Status property:
<Description("Status (color) of the traffic light")> _ Public Property Status() As TrafficLightStatus Get Status = mStatus End Get Set(ByVal Value As TrafficLightStatus) If mStatus <> Value Then mStatus = Value Me.Invalidate() End If End Set End Property
The Invalidate method of the control is used when the Status property changes, and it forces a redraw of the control. Ideally, this type of logic should be placed in all of the events that affect the rendering of the control.
Now add procedures to make the property serialize and reset properly:
Public Function ShouldSerializeStatus() As Boolean If mStatus = TrafficLightStatus.statusGreen Then Return False Else Return True End If End Function Public Sub ResetStatus() Me.Status = TrafficLightStatus.statusGreen End Sub
Place code to do painting of the control, to draw the “traffic light” when the control repaints. We will use code similar to that used previously. The code generated for the new custom control will already have a blank OnPaint method inserted. You just need to insert the following highlighted code into that event, below the comment line that says Add your custom paint code here:
Protected Overrides Sub OnPaint(ByVal pe As _ System.Windows.Forms.PaintEventArgs) MyBase.OnPaint(pe) 'Add your custom paint code here Dim grfGraphics As System.Drawing.Graphics grfGraphics = pe.Graphics ' Need a pen for the drawing the outline. We'll make it black. Dim penDrawingPen As New _ System.Drawing.Pen(System.Drawing.Color.Black) ' Draw the outline of the traffic light on the control. ' First have to define an array of points. Dim pntPoint(3) As System.Drawing.Point pntPoint(0).X = 0 pntPoint(0).Y = 0 pntPoint(1).X = Me.Size.Width - 2 pntPoint(1).Y = 0 pntPoint(2).X = Me.Size.Width - 2 pntPoint(2).Y = Me.Size.Height - 2 pntPoint(3).X = 0 pntPoint(3).Y = Me.Size.Height - 2 grfGraphics.DrawPolygon(penDrawingPen, pntPoint) ' Now ready to draw the circle for the "light" Dim nCirclePositionX As Integer Dim nCirclePositionY As Integer Dim nCircleDiameter As Integer Dim nCircleColor As Color nCirclePositionX = Me.Size.Width * 0.02 nCircleDiameter = Me.Size.Height * 0.3 Select Case Me.Status Case TrafficLightStatus.statusRed nCircleColor = Color.OrangeRed nCirclePositionY = Me.Size.Height * 0.01 Case TrafficLightStatus.statusYellow nCircleColor = Color.Yellow nCirclePositionY = Me.Size.Height * 0.34 Case TrafficLightStatus.statusGreen nCircleColor = Color.LightGreen nCirclePositionY = Me.Size.Height * 0.67 End Select Dim bshBrush As System.Drawing.Brush bshBrush = New SolidBrush(nCircleColor) ' Draw the circle for the signal light grfGraphics.FillEllipse(bshBrush, nCirclePositionX, _ nCirclePositionY, nCircleDiameter, nCircleDiameter) End Sub
Build the control library by selecting Build from the Build menu. This will create a DLL in the /bin directory where the control library solution is saved.
Then, start a new Windows Application project. Drag a TrafficLight control from the top of the Toolbox onto the form in the Windows Application project. Notice that its property window includes a Status property. Set that to statusYellow. Note that the rendering on the control on the form’s design surface changes to reflect this new status. Change the background color of the TrafficLight control to a darker gray to improve its contrast. (The BackColor property for TrafficLight was inherited from the Control class.)
At the top of the code for the form, place the following line to make the enumerated value for the traffic light’s status available:
Imports TrafficLight.TrafficLight
Add three buttons (named btnRed, btnYellow, and btnGreen) to the form to make the traffic light control display as red, yellow, and green. The logic for the buttons looks something like the following:
Private Sub btnRed_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnRed.Click TrafficLight1.Status = TrafficLightStatus.statusRed End Sub Private Sub btnYellow_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnYellow.Click TrafficLight1.Status = TrafficLightStatus.statusYellow End Sub Private Sub btnGreen_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnGreen.Click TrafficLight1.Status = TrafficLightStatus.statusGreen End Sub
In the Solution Explorer, right-click your test Windows Application, and select Set as Startup Project. Then press F5 to run. When your test form comes up, you can change the “signal” on the traffic light by pressing the buttons. Figure 16-8 shows a sample screen.
Figure 16-8
Of course, you can’t see the color in a black-and-white screen shot, but as you might expect from its position, the circle is yellow. The “red light” displays at the top of the control, and the “green light” displays at the bottom. These positions are all calculated in the Paint event logic, depending on the value of the Status property.
For a complete example, it would be desirable for the control to allow the user to change the Status by clicking on a different part of the “traffic light.” That means including logic to examine mouse clicks, calculate whether they are in a given area, and change the Status property if appropriate. In the code available for download for this book, the TrafficLight example includes such functionality.