This is the most complicated but most flexible type of control. A user-drawn control consists of a UserControl object with no constituent controls. You are responsible for updating the control’s visible area with the appropriate code, which must appear in the control’s OnPaint method. (This method is invoked automatically every time the control’s surface must be redrawn.)
To demonstrate the design of user-drawn controls, we’ll develop the Label3D control, which is an enhanced Label control and is shown in Figure 8.5. It provides all the members of the Label control plus the capability to render its caption in three-dimensional type. The new custom control is called Label3D, and its project is the FlexLabel project. It contains the Label3D project (which is a Windows Control Library project) and the usual test project (which is a Windows Application project).
At this point, you’re probably thinking about the code that aligns the text and renders it as carved or raised. A good idea is to start with a Windows project, which displays a string on a form and aligns it in all possible ways. A control is an application packaged in a way that allows it to be displayed on a form instead of on the Desktop. As far as the functionality is concerned, in most cases it can be implemented on a regular form. Conversely, if you can display 3D text on a form, you can do so with a custom control.
Designing a Windows form with the same functionality is fairly straightforward. You haven’t seen the drawing methods yet, but this control doesn’t involve any advanced drawing techniques. All we need is a method to render strings on the control. To achieve the 3D effect, you must display the same string twice, first in white and then in black on top of the white. The two strings must be displaced slightly, and the direction of the displacement determines the effect (whether the text will appear as raised or carved). The amount of displacement determines the depth of the effect. Use a displacement of 1 pixel for a light effect, and a displacement of 2 pixels for a heavy one.
The Label3D Control Example
The first step of designing a user-drawn custom control is to design the control’s interface: what it will look like when placed on a form (its visible interface) and how developers can access this functionality through its members (the programmatic interface). Sure, you’ve heard the same advice over and over, and many of you still start coding an application without spending much time designing it. In the real world, especially if you are not a member of a programming team, people design as they code (or the other way around). (Download the Project files)
The situation is quite different with Windows controls. Your custom control must provide properties, which will be displayed automatically in the Properties window. The developer should be able to adjust every aspect of the control’s appearance by manipulating the settings of these properties. In addition, developers expect to see the standard properties shared by most controls (such as the background color, the text font, and so on) in the Properties window. You must carefully design the methods so that they expose all the functionality of the control that should be accessed from within the application’s code, and the methods shouldn’t overlap. Finally, you must provide the events necessary for the control to react to external events. Don’t start coding a custom control unless you have formulated a clear idea of what the control will do and how developers will use it at design time.
Label3D Control Specifications
The Label3D control displays a caption like the standard Label control, so it must provide a Font property, which lets the developer determine the label’s font. The UserControl object exposes its own Font property, so we need not implement it in our code. In addition, the Label3D control can align its caption both vertically and horizontally. This functionality will be exposed by the Alignment property,whose possible settings are themembers of the Align enumeration: TopLeft, TopMiddle, TopRight, CenterLeft, CenterMiddle, CenterRight, BottomLeft, BottomMiddle, and BottomRight. The (self-explanatory) values are the names that will appear in the drop-down list of the Alignment property in the Properties window.
Similarly, the text effect is manipulated through the Effect property, whose possible settings are the members of the Effect3D custom enumeration: None, Carved, CarvedHeavy, Raised, and RaisedHeavy. There are basically two types of effects (raised and carved text) and two variations on each effect (normal and heavy).
In addition to the custom properties, the Label3D control should also expose the standard properties of a Label control, such as Tag, BackColor, and so on. Developers expect to see standard properties in the Properties window, and you should implement them. The Label3D control doesn’t have any custom methods, but it should provide the standard methods of the Label control, such as the Move method. Similarly, although the control doesn’t raise any special events, it must support the standard events of the Label control, such as the mouse and keyboard events. Most of the custom control’s functionality exists already, and there should be a simple technique to borrow this functionality from other controls instead of implementing it from scratch. This is indeed the case: The UserControl object, from which all user-drawn controls inherit, exposes a large number of members.
Designing the Custom Control
Start a new project of the Windows Control Library type, name it FlexLabel, and then rename the UserControl1 object to Label3D. Open the UserControl object’s code window and change the name of the class from UserControl1 to Label3D.
Every time you place a Windows control on a form, it’s named according to the UserControl object’s name and a sequence digit. The first instance of the custom control you place on a formwill be named Label3D1, the next one will be named Label3D2, and so on. Obviously, it’s important to choose a meaningful name for your UserControl object.
As you will soon see, the UserControl is the “form” on which the custom control will be designed. It looks, feels, and behaves like a regular VB form, but it’s called a UserControl. UserControl objects have additional unique properties that don’t apply to a regular form, but to start designing new controls, think of them as regular forms.
You’ve set the scene for a new user-drawn Windows control. Start by declaring the Align and Effect3D enumerations, as shown in Listing 8.6.
Listing 8.6: Align and Effect3D Enumerations
Code language: PHP (php)
Public Enum Align TopLeft TopMiddle TopRight CenterLeft CenterMiddle CenterRight BottomLeft BottomMiddle BottomRight End Enum Public Enum Effect3D None Raised RaisedHeavy Carved CarvedHeavy End Enum
The next step is to implement the Alignment and Effect properties. Each property’s type is an enumeration; Listing 8.7 shows the implementation of the two properties.
Listing 8.7: Alignment and Effect Properties
Code language: PHP (php)
Private Shared mAlignment As Align Private Shared mEffect As Effect3D Public Property Alignment() As Align Get Alignment = mAlignment End Get Set(ByVal Value As Align) mAlignment = Value Invalidate() End Set End Property Public Property Effect() As Effect3D Get Effect = mEffect End Get Set(ByVal Value As Effect3D) mEffect = Value Invalidate() End Set End Property
The current settings of the two properties are stored in the private variables mAlignment and mEffect. When either property is set, the Property procedure’s code calls the Invalidate method of the UserControl object to force a redraw of the string on the control’s surface. The call to the Invalidate method is required for the control to operate properly in design mode. You can provide a method to redraw the control at runtime (although developers shouldn’t have to call a method to refresh the control every time they set a property), but this isn’t possible at design time. In general, when a property is changed in the Properties window, the control should be able to update itself and reflect the new property setting, and this is done with a call to the Invalidate method. Shortly, you’ll see an even better way to automatically redraw the control every time a property is changed.
Finally, you must add one more property, the Caption property, which is the string to be rendered on the control. Declare a private variable to store the control’s caption (the mCaption variable) and enter the code from Listing 8.8 to implement the Caption property.
Listing 8.8: Caption Property Procedure
Private mCaption As String Property Caption() As String Get Caption = mCaption End Get Set(ByVal Value As String) mCaption = Value Invalidate() End Set End Property
The core of the control’s code is in the OnPaint method, which is called automatically before the control repaints itself. The same event’s code is also executed when the Invalidate method is called, and this is why we call this method every time one of the control’s properties changes value. The OnPaint method enables you to take control of the paint process and supply your own code for painting the control’s surface. The single characteristic of all user-drawn controls is that they override the default OnPaint method. This is where you must insert the code to draw the control’s surface — that is, draw the specified string, taking into consideration the Alignment and Effect properties. The OnPaint method’s code is shown in Listing 8.9.
Listing 8.9: UserControl Object’s OnPaint Method
Code language: PHP (php)
Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs) Dim lblFont As Font = Me.Font Dim lblBrush As New SolidBrush(Color.Red) Dim X, Y As Integer Dim textSize As SizeF = e.Graphics.MeasureString(mCaption, lblFont) Select Case Me.mAlignment Case Align.BottomLeft X = 2 Y = Convert.ToInt32(Me.Height - textSize.Height) Case Align.BottomMiddle X = CInt((Me.Width - textSize.Width) / 2) Y = Convert.ToInt32(Me.Height - textSize.Height) Case Align.BottomRight X = Convert.ToInt32(Me.Width - textSize.Width - 2) Y = Convert.ToInt32(Me.Height - textSize.Height) Case Align.CenterLeft X = 2 Y = Convert.ToInt32((Me.Height - textSize.Height) / 2) Case Align.CenterMiddle X = Convert.ToInt32((Me.Width - textSize.Width) / 2) Y = Convert.ToInt32((Me.Height - textSize.Height) / 2) Case Align.CenterRight X = Convert.ToInt32(Me.Width - textSize.Width - 2) Y = Convert.ToInt32((Me.Height - textSize.Height) / 2) Case Align.TopLeft X = 2 Y = 2 Case Align.TopMiddle X = Convert.ToInt32((Me.Width - textSize.Width) / 2) Y = 2 Case Align.TopRight X = Convert.ToInt32(Me.Width - textSize.Width - 2) Y = 2 End Select Dim dispX, dispY As Integer Select Case mEffect Case Effect3D.None : dispX = 0 : dispY = 0 Case Effect3D.Raised : dispX = 1 : dispY = 1 Case Effect3D.RaisedHeavy : dispX = 2 : dispY = 2 Case Effect3D.Carved : dispX = -1 : dispY = -1 Case Effect3D.CarvedHeavy : dispX = -2 : dispY = -2 End Select lblBrush.Color = Color.White e.Graphics.DrawString(mCaption, lblFont, lblBrush, X, Y) lblBrush.Color = Me.ForeColor e.Graphics.DrawString(mCaption, lblFont, lblBrush, X + dispX, Y + dispY) Dim dimFont As New Font("Comic Sans MS", 12, FontStyle.Bold) Dim dispSize As SizeF = e.Graphics.MeasureString("Design Time", dimFont) If Me.DesignMode Then e.Graphics.DrawString("DesignTime", dimFont, _ New SolidBrush(Color.DarkOrange), _ 0, 0) e.Graphics.DrawString("DesignTime", dimFont, _ New SolidBrush(Color.DarkOrange), _ Me.Width - dispSize.Width - 5, _ Me.Height - dispSize.Height) Else e.Graphics.DrawString("RunTime", dimFont, _ New SolidBrush(Color.DarkOrange), _ 0, 0) e.Graphics.DrawString("RunTime", dimFont, _ New SolidBrush(Color.DarkOrange), _ dispSize.Width - 5, _ Me.Height - dispSize.Height) End If End Sub
This subroutine calls for a few explanations. The Paint method passes a PaintEventArgs argument (the ubiquitous e argument). This argument exposes the Graphics property, which represents the control’s surface. The Graphics object exposes all the methods you can call to create graphics on the control’s surface. The Graphics object is discussed in detail in Chapter 18, ‘‘Drawing and Painting with Visual Basic 2008,” but for this chapter all you need to know is that the MeasureString method returns the dimensions of a string when rendered in a specific font, and the DrawString method draws the string in the specified font. The first Select Case statement calculates the coordinates of the string’s origin on the control’s surface, and these coordinates are calculated differently for each type of alignment. Then another Select Case statement sets the displacement between the two strings, so that when superimposed they produce a three dimensional effect. Finally, the code draws the string of the Caption property on the Graphics object. It draws the string in white first, then in black. The second string is drawn dispX pixels to the left and dispY pixels below the first one to give the 3D effect. The values of these two variables are determined by the setting of the Effect property.
The event handler of the sample project contains a few more statements that are not shown here. These statements print the strings DesignTime and RunTime in a light color on the control’s background, depending on the current status of the control. They indicate whether the control is currently in design (if the DesignMode property is True) or runtime (if DesignMode is False), and you will remove them after testing the control.
Testing Your New Control
To test your new control, you must first add it to the Toolbox and then place instances of it on the test form. You can add a form to the current project and test the control, but you shouldn’t add more components to the control project. It’s best to add a new project to the current solution.
Add the TestProject to the current solution and place on its main form a Label3D control, as well as the other controls shown earlier in Figure 8.5. If the Label3D icon doesn’t appear in the Toolbox, build the control’s project, and a new item will be added to the FlexLabel Components tab of the ToolBox.
Now double-click the Label3D control on the form to see its events. Your new control has its own events, and you can program them just as you would program the events of any other control. Enter the following code in the control’s Click event:
Code language: PHP (php)
Private Sub Label3D1_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Label3D1.Click MsgBox("My properties are " & vbCrLf & _ "Caption = " & Label3D1.Caption & vbCrLf & _ "Alignment = " & Label3D1.Alignment & vbCrLf & _ "Effect = " & Label3D1.Effect) End Sub
To run the control, press F5 and then click the control. You will see the control’s properties displayed in a message box.
The other controls on the test form allow you to set the appearance of the custom control at runtime. The two ComboBox controls are populated with the members of the appropriate enumeration when the form is loaded. In their SelectedIndexChanged event handler, you must set the corresponding property of the FlexLabel control to the selected value, as shown in the following code:
Code language: PHP (php)
Private Sub AlignmentBox_SelectedIndexChanged(ByVal _ sender As System.Object, _ ByVal e As System.EventArgs) _ Handles AlignmentBox.SelectedIndexChanged Label3D1.Alignment = AlignmentBox.SelectedItem End Sub Private Sub EffectsBox_SelectedIndexChanged(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles EffectsBox.SelectedIndexChanged Label3D1.Effect = EffectsBox.SelectedItem End Sub
The TextBox control at the bottom of the form stores the Caption property. Every time you change this string, the control is updated because the Set procedure of the Caption property calls the Invalidate method.
A Quick Way to Test Custom Windows Controls
Visual Studio 2008 introduced a new, simple method of testing custom controls. Instead of using a test project, you can press F5 to “run” the Windows Control project. Right-click the name of the Label3D project (the Windows Control project in the solution) in Solution Explorer and from the context menu choose Set As Startup Project. Then press F5 to start the project. A dialog box (shown in the following figure) will appear with the control at runtime and its Properties window.
In this dialog box, you can edit any of the control’s properties and see how they affect the control at runtime. If the control reacts to any user actions, you can see how the control’s code behaves at runtime.
You can’t test the control’s methods, or program its events, but you’ll get an idea of how the control will behave when placed on a form. Use this dialog box while you’re developing the control’s interface to see how it will behave when placed on a test form and how it reacts when you change its properties. When you’re happy with the control’s interface, you should test it with a Windows project, from which you can call its methods and program its events.
The UserControl object exposes many of the events you need to program the control, such as the key and mouse events. In addition, you can raise custom events. The Windows controls raise an event every time a property value is changed. If you examine the list of events exposed by the Label3D control, you’ll see the FontChanged and SizeChanged events. These events are provided by the UserControl object. As a control developer, you should expose similar events for your custom properties, the OnAlignmentChanged, OnEffectChanged, and OnCaptionChanged events.
This isn’t difficult to do, but you must follow a few steps. Start by declaring an event handler for each of the Changed events:
Code language: PHP (php)
Private mOnAlignmentChanged As EventHandler Private mOnEffectChanged As EventHandler Private mOnCaptionChanged As EventHandler
Then declare the actual events and their handlers:
Code language: PHP (php)
Public Event AlignmentChanged(ByVal sender As Object, ByVal ev As EventArgs) Public Event EffectChanged(ByVal sender As Object, ByVal ev As EventArgs) Public Event CaptionChanged(ByVal sender As Object, ByVal ev As EventArgs)
When a property changes value, you must call the appropriate method. In the Set section of the Alignment property procedure, insert the following statement:
Code language: CSS (css)
And finally, invoke the event handlers from within the appropriate OnEventName method:
Code language: PHP (php)
Protected Overridable Sub OnAlignmentChanged(ByVal E As EventArgs) Invalidate() If Not (mOnAlignmentChanged Is Nothing) Then mOnAlignmentChanged.Invoke(Me, E) End Sub Protected Overridable Sub OnEffectChanged(ByVal E As EventArgs) Invalidate() If Not (mOnEffectChanged Is Nothing) Then mOnEffectChanged.Invoke(Me, E) End Sub Protected Overridable Sub OnCaptionChanged(ByVal E As EventArgs) Invalidate() If Not (mOnCaptionChanged Is Nothing) Then mOnCaptionChanged.Invoke(Me, E) End Sub
As you can see, the OnPropertyChanged events call the Invalidate method to redraw the control when a property’s value is changed. As a result, you can now remove the call to the Invalidate method from the Property Set procedures. If you switch to the test form, you will see that the custom control exposes the AlignmentChanged, EffectChanged, and CaptionChanged events. The OnCaptionChanged method is executed automatically every time the Caption property changes value, and it fires the CaptionChanged event. The developer using the Label3D control shouldn’t have to program this event.