Chapter 9. Building Custom Windows Controls

Just as you can design custom classes, you can use Visual Studio to design custom controls. The process is very similar, in the sense that custom controls have properties, methods, and events, which are implemented with code that's identical to the code you'd use to implement these members with classes. The difference is that controls have a visual interface and interact with the user. In short, you must provide the code to draw the control's surface as well as react to selected user actions from within the control's code.

In this chapter, you'll learn how to enhance the functionality of existing controls, a common practice among developers, as well as how to build custom controls from scratch. Specifically, you will learn how to do the following:

  • Extend the functionality of existing Windows Forms controls with inheritance

  • Build compound custom controls that combine multiple existing controls

  • Build custom controls from scratch

  • Customize the rendering of the items in a ListBox control

On Designing Windows Controls

Before I get to the details of how to build custom controls, I want to show you how they relate to other types of projects. I'll discuss briefly the similarities and differences among Windows controls, classes, and Windows projects. This information will help you get the big picture and put together the pieces of the following sections.

An application interacts with the user through its interface. The developer decides how the forms interact with the user, and the user has to follow the rules. Something similar happens with custom controls. The custom control provides a well-defined interface, which consists of properties and methods. This is the only way to manipulate the control. Just as users of your applications don't have access to the source code and can't modify the application, developers can't see the control's source code and must access it through the interface exposed by the control. After an instance of the custom control is placed on the form, the developer can manipulate it through its properties and methods, but you never get to see the code.

In preceding chapters, you learned how to implement interfaces consisting of properties and methods and how to raise events from within a class. This is how you build the interface of a custom Windows control: You implement properties with Property procedures, and you implement methods as Public procedures. Although a class can provide a few properties and any number of methods, a control must provide a large number of properties. A developer who places a custom control on a form expects to see the properties that are common to all the controls (properties to set the control dimensions, the color, and the text font; the Index and Tag properties; and so on). Fortunately, many of the standard properties are exposed automatically. The developer also expects to be able to program all the common events, such as the mouse and keyboard events, as well as some events that are unique to the custom control.

The design of a Windows control is similar to the design of a form. You place controls on a form-like object, called UserControl, which is the control's surface. It provides nearly all the methods of a standard form, and you can adjust its appearance with the drawing methods. In other words, you can use familiar programming techniques to draw a custom control or you can use existing controls to build a custom control.

The major difference between forms and custom controls is that custom controls can exist in two runtime modes. When the developer places a control on a form, the control is actually running. When you set properties through the Properties window, something happens to the control — its appearance changes or the control rejects the changes. It means that the code of the custom control is executing, even though the project in which the control is used is in design mode. When the developer starts the application, the custom control is already running. However, the control must be able to distinguish when the project is in design or execution mode and behave accordingly. Here's the first property of the UserControl object you will be using quite frequently in your code: the DesignMode property. When the control is positioned on a form and used in the Designer, the DesignMode property is True. When the developer starts the project that contains the control, the DesignMode property is False.

This dual runtime mode of a Windows control is something you'll have to get used to. When you design custom controls, you must also switch between the roles of Windows control developer (the programmer who designs the control) and application developer (the programmer who uses the control).

In summary, a custom control is an application with a visible user interface as well as an invisible programming interface. The visible interface is what the developer sees when an instance of the control is placed on the form, and it's also what the user sees on the form when the project is placed in runtime mode. The developer using the control can manipulate it through its properties and methods. The control's properties can be set at both design time and runtime, whereas methods must be called from within the code of the application that uses the control. The properties and methods constitute the control's invisible interface (or the developer interface, as opposed to the user interface). You, the control developer, will develop the visible user interface on a UserControl object, which is almost identical to the Form object; it's like designing a standard application. As far as the control's invisible interface goes, it's like designing a class.

Enhancing Existing Controls

The simplest type of custom Windows control you can build is one that enhances the functionality of an existing control. Fortunately, they're the most common types of custom controls, and many developers have their own collections of enhanced Windows controls. The Windows controls are quite functional, but you won't be hard-pressed to come up with ideas to make them better.

The TextBox control, for example, is a text editor on its own, and you have seen how easy it is to build a text editor by using the properties and methods exposed by this control. Many programmers add code to their projects to customize the appearance and the functionality of the TextBox control.

Let's say you're building data-entry forms composed of many TextBox controls. To help the user identify the current control on the form, it would be nice to change its color while it has the focus. If the current control has a different color from all others, users will quickly locate it.

Another thing you can do with the TextBox control is format its contents as soon as it loses focus. Let's consider a TextBox control that must accept dollar amounts. After the user enters a numeric value, the control could automatically format the numeric value as a dollar amount and perhaps change the text's color to red for negative amounts. When the control receives the focus again, you can display the amount without any special formatting so that users can edit it quickly. As you will see, it's not only possible but actually quite easy to build a control that incorporates all the functionality of a TextBox and some additional features that you provide through the appropriate code. You already know how to add features such as the ones described here to a TextBox from within the application's code. But what if you want to enhance multiple TextBox controls on the same form or reuse your code in multiple applications?

The best approach is to create a new Windows control with all the desired functionality and then reuse it in multiple projects. To use the proper terminology, you can create a new custom Windows control that inherits the functionality of the TextBox control. The derived control includes all the functionality being inherited from the control, plus any new features you care to add to it. This is exactly what we're going to do in this section.

Building the FocusedTextBox Control

Let's call our new custom control FocusedTextBox. Start a new project, and in the New Project dialog box, select the template Windows Forms Control Library. Name the project FocusedTextBox. The Solution Explorer for this project contains a single item, the UserControl1 item. UserControl1 (see Figure 9.1) is the control's surface — in a way, it's the control's form. This is where you'll design the visible interface of the new control using the same techniques you would use to design a Windows form.

A custom control in design mode

Figure 9.1. A custom control in design mode

Start by renaming the UserControl1 object to FocusedTextBox. Then save the project by choosing File

A custom control in design mode
Partial Public Class FocusedTextBox
Inherits System.Windows.Forms.UserControl

The first statement says that the entire file belongs to the FocusedTextBox class; it's the part of the class that contains initialization code and other statements that the user does not need to see because it's left unchanged in most cases. To design an inherited control, we must change the second statement to the following:

Inherits System.Windows.Forms.TextBox

This statement tells the compiler that we want our new control to inherit all the functionality of the TextBox control. You must also modify the InitializeComponent method in the FocusedTextBox.Designer.vb file by removing the statement that sets the control's AutoScaleMode property. This statement applies to the generic UserControl object but not to the TextBox control.

As soon as you specify that your custom control inherits the TextBox control, the UserControl object will disappear from the Designer. The Designer knows exactly what the new control must look like (it will look and behave exactly like a TextBox control), and you're not allowed to change its appearance.

If you switch to the FocusedTextBox.vb file, you'll see that it's a public class called FocusedTextBox. The Partial class by the same name is part of this class; it contains the code that was generated automatically by Visual Studio. When compiled, both classes will produce a single DLL file. Sometimes we need to split a class's code into two files, and one of them should contain the Partial modifier. This keyword signifies that the file contains part of the class. The FocusedTextBox.vb file is where you will insert your custom code. The Partial class contains the code emitted by Visual Studio, and you're not supposed to touch it. Inherited controls are an exception to this rule because we have to be able to modify the Inherits statement, but the role of the Partial modifier is to enable us to split classes into two separate files.

Let's test the control and verify that it exposes the same functionality as the TextBox control. Figure 9.2 shows the IDE while you're developing an inherited control. Notice that the FocusedTextBox control has inherited all the properties of the TextBox control, such as the MaxLength and PasswordChar properties.

To test the control, you must add it to a form. A control can't be executed outside the context of a host application. Add a new project to the solution (a Windows Forms Application project) with the File

A custom control in design mode
The IDE during the design of an inherited control

Figure 9.2. The IDE during the design of an inherited control

To test the control you just designed, you need to place an instance of the custom control on the form of the test project. First, you must build the control. Select the FocusedTextBox item in the Solution Explorer, and from the Build menu, select the Build FocusedTextBox command (or right-click the FocusedTextBox component in the Solution Explorer and select Build from the context menu). The build process will create a DLL file with the control's executable code in the Bin folder under the project's folder.

Then switch to the test project's main form and open the Toolbox. You will see a new tab, the FocusedTextBox Components tab, which contains all the custom components of the current project. The new control has already been integrated into the design environment, and you can use it as you would any of the built-in Windows controls. Every time you edit the code of the custom control, you must rebuild the control's project for the changes to take effect and update the instances of the custom control on the test form. The icon that appears before the custom control's name is the default icon for all custom Windows controls. You can associate a different icon with your custom control, as explained in the section "Classifying the Control's Properties," later in this chapter.

Place an instance of the FocusedTextBox control on the form and check it out. It looks, feels, and behaves just like a regular TextBox. In fact, it is a TextBox control by a different name. It exposes all the members of the regular TextBox control: You can move it around, resize it, change its Multiline and WordWrap properties, set its Text property, and so on. It also exposes all the methods and events of the TextBox control.

Adding Functionality to Your Custom Control

As you can see, it's quite trivial to create a new custom control by inheriting any of the built-in Windows controls. Of course, what good is a control that's identical to an existing one? The idea is to add some extra functionality to the TextBox control, so let's do it. Switch to the control project and view the FocusedTextBox object's code. In the code editor's pane, expand the Objects list and select the item FocusedTextBox Events. This list contains the events of the TextBox control because it is the base control for our custom control.

Expand the Events drop-down list and select the Enter event. The following event handler declaration will appear:

Private Sub FocusedTextBox_Enter(...) Handles Me.Enter

End Sub

This event takes place every time our custom control gets the focus. To change the color of the current control, insert the following statement in the event handler:

Me.BackColor = Color.Cyan

(Or use any other color you like; just make sure it mixes well with the form's default background color. You can also use the members of the SystemColors enumeration, to help ensure that it mixes well with the background color.) We must also program the Leave event so that the control's background color is reset to white when it loses the focus. Enter the following statement in the Leave event's handler:

Private Sub FocusedTextBox_Leave(...) Handles Me.Leave
   Me.BackColor = Color.White
End Sub

Having a hard time picking the color that signifies that the control has the focus? Why not expose this value as a property so that you (or other developers using your control) can set it individually in each project? Let's add the EnterFocusColor property, which is the control's background color when it has the focus.

Because our control is meant for data-entry operations, we can add another neat feature. Some fields on a form are usually mandatory, and some are optional. Let's add some visual indication for the mandatory fields. First, we need to specify whether a field is mandatory with the Mandatory property. If a field is mandatory, its background color will be set to the value of the MandatoryColor property, but only if the control is empty.

Here's a quick overview of the control's custom properties:

EnterFocusColor

When the control receives the focus, its background color is set to this value. If you don't want the currently active control to change color, set its EnterFocusColor property to white.

Mandatory

This property indicates whether the control corresponds to a required field if Mandatory is True or to an optional field if Mandatory is False.

MandatoryColor

This is the background color of the control if its Mandatory property is set to True. The MandatoryColor property overwrites the control's default background color. In other words, if the user skips a mandatory field, the corresponding control is painted with the color specified by the MandatoryColor property, and it's not reset to the control's default background color. Required fields behave like optional fields after they have been assigned a value.

If you read the previous chapter, you should be able to implement these properties easily. Listing 9.1 is the code that implements the four custom properties. The values of the properties are stored in the private variables declared at the beginning of the listing. Then the control's properties are implemented as Property procedures.

Example 9.1. The Property procedures of the FocusedTextBox custom control

Dim _mandatory As Boolean
Dim _enterFocusColor, _leaveFocusColor As Color
Dim _mandatoryColor As Color

Property Mandatory() As Boolean
    Get
        Mandatory = _mandatory
    End Get
    Set(ByVal value As Boolean)
        _mandatory = Value
    End Set
End Property

Property EnterFocusColor() As System.Drawing.Color
    Get
        Return _enterFocusColor
    End Get
    Set(ByVal value As System.Drawing.Color)
        _enterFocusColor = value
    End Set
End Property

Property MandatoryColor() As System.Drawing.Color
    Get
        Return _mandatoryColor
    End Get
    Set(ByVal value As System.Drawing.Color)
        _mandatoryColor = value
    End Set
End Property

The last step is to use these properties in the control's Enter and Leave events. When the control receives the focus, it changes its background color to EnterFocusColor to indicate that it's the active control on the form (the control with the focus). When it loses the focus, its background is restored to the usual background color, unless it's a required field and the user has left it blank. In this case, its background color is set to MandatoryColor. Listing 9.2 shows the code in the two focus-related events of the UserControl object.

Example 9.2. Enter and Leave events

Private _backColor As Color
Private Sub FocusedTextBox_Enter(...) Handles MyBase.Enter
   _backColor = Me.BackColor
   Me.BackColor = _enterFocusColor
End Sub

Private Sub FocusedTextBox_Leave(...) Handles MyBase.Leave
   If Trim(Me.Text).Length = 0 And _mandatory Then
      Me.BackColor = _mandatoryColor
   Else
      Me.BackColor = _backColor
   End If
End Sub

Testing the FocusedTextBox Control

Build the control again with the Build

Testing the FocusedTextBox Control
Custom properties of the FocusedTextBox control in the Properties window

Figure 9.3. Custom properties of the FocusedTextBox control in the Properties window

Pretty impressive, isn't it? Even if you have no use for an enhanced TextBox control, you'll agree that building it was quite simple. Next time you need to enhance one of the Windows controls, you'll know how to do it. Just build a new control that inherits from an existing control, add some custom members, and use it. Create a project with all the "enhanced" controls and use them regularly in your projects. All you have to do is add a reference to the DLL that implements the control in a new project, just like reusing a custom class.

Classifying the Control's Properties

Let's go back to the FocusedTextBox control — there are some loose ends to take care of. First, you must specify the category in the Properties window under which each custom property appears. By default, all the properties you add to a custom control are displayed in the Misc section of the Properties window. To specify that a property be displayed in a different section, use the Category attribute of the Property procedure. As you will see, properties have other attributes too, which you can set in your code as you design the control.

Properties have attributes, which appear in front of the property name and are enclosed in a pair of angle brackets. All attributes are members of the System.ComponentModel class, and you must import this class to the module that contains the control's code. The following attribute declaration in front of the property's name determines the category of the Properties window in which the specific property will appear:

<Category("Appearance")> Public Property

If none of the existing categories suits a specific property, you can create a new category in the Properties window by specifying its name in the Category attribute. If you have a few properties that should appear in a section called Conditional, insert the following attribute in front of the declarations of the corresponding properties:

<Category("Conditional")> Public Property

When this control is selected, the Conditional section will appear in the Properties window, and all the properties with this attribute will be under it.

Another attribute is the Description attribute, which determines the property's description that appears at the bottom of the Properties window when the property is selected. You can specify multiple attributes as follows:

<Description("Indicates whether the control can be left blank")>
<Category("Appearance")>
Property Mandatory() As Boolean
   '  the property procedure's code
End Property

The most important attribute is the DefaultValue attribute, which determines the property's default (initial) value. The DefaultValue attribute must be followed by the default value in parentheses:

<Description("Indicates whether the control can be left blank")>
<Category("Appearance"), DefaultValue(False)>
Property Mandatory() As Boolean
   '  the property procedure's code
End Property

Some attributes apply to the class that implements the custom controls. The DefaultProperty and DefaultEvent attributes determine the control's default property and event. To specify that Mandatory is the default property of the FocusedTextBox control, replace the class declaration with the following:

<DefaultProperty("Mandatory")> Public Class FocusedTextBox

Events are discussed later in the chapter, but you already know how to raise an event from within a class. Raising an event from within a control's code is quite similar. Open the FocusedTextBox project, examine its code, and experiment with new properties and methods.

As you may have noticed, all custom controls appear in the Toolbox with the same icon. You can specify the icon to appear in the Toolbox with the ToolboxBitmap attribute, whose syntax is as follows, where imagepath is a string with the absolute path to a 16×16-pixel bitmap:

<ToolboxBitmap(imagepath)> Public Class FocusedTextBox

The bitmap is actually stored in the control's DLL and need not be distributed along with the control.

Now we're ready to move on to something more interesting. This time, we'll build a control that combines the functionality of several controls, which is another common scenario. You will literally design its visible interface by dropping controls on the UserControl object, just like designing the visible interface of a Windows form.

Building Compound Controls

A compound control provides a visible interface that consists of multiple Windows controls. The controls that make up a compound control are known as constituent controls. As a result, this type of control doesn't inherit the functionality of any specific control. You must implement its properties and methods with custom code. This isn't as bad as it sounds, because a compound control inherits the UserControl object, which exposes quite a few members of its own (the Anchoring and Docking properties, for example, are exposed by the UserControl object, and you need not implement these properties — thank Microsoft). You will add your own members, and in most cases you'll be mapping the properties and methods of the compound controls to a property or method of one of its constituent controls. If your control contains a TextBox control, for example, you can map the custom control's WordWrap property to the equivalent property of the TextBox. The following property procedure demonstrates how to do it:

Property WordWrap() As Boolean
   Get
      WordWrap = TextBox1.WordWrap
   End Get
   Set(ByVal Value As Boolean)
      TextBox1.WordWrap = Value
   End Set
End Property

You don't have to maintain a private variable for storing the value of the custom control's WordWrap property. When this property is set, the Property procedure assigns the property's value to the TextBox1.WordWrap property. Likewise, when this property's value is requested, the procedure reads it from the constituent control and returns it. In effect, the custom control's WordWrap property affects directly the functionality of one of the constituent controls.

The same logic applies to events. Let's say your compound control contains a TextBox and a ComboBox control, and you want to raise the TextChanged event when the user edits the TextBox control and the SelectionChanged event when the user selects another item in the ComboBox control. First, you must declare the two events:

Event TextChanged
Event SelectionChanged

Then, you must raise the two events from within the appropriate event handlers: the TextChanged event from the TextBox1 control's TextChanged event handler and the SelectionChanged event from the ComboBox1 control's SelectedIndexChanged event handler:

Private Sub TextBox1_TextChanged(...)
            Handles FocusedTextBox1.TextChanged
    RaiseEvent TextChanged()
End Sub

Private Sub ComboBox1_SelectedIndexChanged(...)
            Handles ComboBox1.SelectedIndexChanged
    RaiseEvent SelectionChanged()
End Sub

VB 2010 at Work: The ColorEdit Control

In this section, you're going to build a compound control that's similar to the Color dialog box. The ColorEdit control allows you to specify a color by adjusting its red, green, and blue components with three scroll bars or to select a color by name. The control's surface at runtime on a form is shown in Figure 9.4.

The ColorEdit control on a test form

Figure 9.4. The ColorEdit control on a test form

Create a new Windows Control Library project, the ColorEdit project. Save the solution and then add a new Windows Application project, the TestProject, and make it the solution's startup project, just as you did with the first sample project of this chapter.

Now, open the UserControl object and design its interface as shown in Figure 9.4. Place the necessary controls on the UserControl object's surface and align them just as you would do with a Windows form. The three ScrollBar controls are named RedBar, GreenBar, and BlueBar, respectively. The Minimum property for all three controls is 0; the Maximum for all three is 255. This is the valid range of values for a color component. The control at the top-left corner is a Label control with its background color set to black. (We could have used a PictureBox control in its place.) The role of this control is to display the selected color.

The ComboBox at the bottom of the custom control is the NamedColors control, which is populated with color names when the control is loaded. The Color class exposes 140 properties, which are color names (beige, azure, and so on). Don't bother entering all the color names in the ComboBox control; just open the ColorEdit project and you will find the AddNamedColors() subroutine, which does exactly that.

The user can specify a color by sliding the three ScrollBar controls or by selecting an item in the ComboBox control. In either case, the Label control's background color will be set to the selected color. If the color is specified with the ComboBox control, the three ScrollBars will adjust to reflect the color's basic components (red, green, and blue). Not all possible colors that you can specify with the three ScrollBars have a name (there are approximately 16 million colors). That's why the ComboBox control contains the Unknown item, which is selected when the user specifies a color by setting its basic components.

Finally, the ColorEdit control exposes two properties: NamedColor and SelectedColor. The NamedColor property retrieves the selected color's name. If the color isn't selected from the ComboBox control, the value Unknown will be returned. The SelectedColor property returns or sets the current color. Its type is Color, and it can be assigned any expression that represents a color value. The following statement will assign the form's BackColor property to the SelectedColor property of the control:

UserControl1.SelectedColor = Me.BackColor

You can also specify a color value with the FromARGB method of the Color object:

UserControl1.SelectedColor = Color.FromARGB(red, green, blue)

The implementation of the SelectedColor property (shown in Listing 9.3) is straightforward. The Get section of the procedure assigns the Label's background color to the SelectedColor property. The Set section of the procedure extracts the three color components from the value of the property and assigns them to the three ScrollBar controls. Then it calls the ShowColor subroutine to update the display. (You'll see shortly what this subroutine does.)

Example 9.3. SelectedColor property procedure

Property SelectedColor() As Color
   Get
      SelectedColor = Label1.BackColor
   End Get
   Set(ByVal Value As Color)
      HScrollBar1.Value = Value.R
HScrollBar2.Value = Value.G
      HScrollBar3.Value = Value.B
      ShowColor()
   End Set
End Property

The NamedColor property (see Listing 9.4) is read-only and is marked with the ReadOnly keyword in front of the procedure's name. This property retrieves the value of the ComboBox control and returns it.

Example 9.4. NamedColor property procedure

ReadOnly Property NamedColor() As String
   Get
      NamedColor = ComboBox1.SelectedItem
   End Get
End Property

When the user selects a color name in the ComboBox control, the code retrieves the corresponding color value with the Color.FromName method. This method accepts a color name as an argument (a string) and returns a color value, which is assigned to the namedColor variable. Then the code extracts the three basic color components with the R, G, and B properties. (These properties return the red, green, and blue color components, respectively.) Listing 9.5 shows the code behind the ComboBox control's SelectedIndexChanged event, which is fired every time a new color is selected by name.

Example 9.5. Specifying a color by name

Private Sub ComboBox1_SelectedIndexChanged(...) Handles ComboBox1.SelectedIndexChanged
   Dim namedColor As Color
   Dim colorName As String
   colorName = ComboBox1.SelectedItem
   If colorName <> "Unknown" Then
      namedColor = Color.FromName(colorName)
      HScrollBar1.Value = namedColor.R
      HScrollBar2.Value = namedColor.G
      HScrollBar3.Value = namedColor.B
      ShowColor()
   End If
End Sub

The ShowColor() subroutine simply sets the Label's background color to the value specified by the three ScrollBar controls. Even when you select a color value by name, the control's code sets the three ScrollBars to the appropriate values. This way, we don't have to write additional code to update the display. The ShowColor() subroutine is quite trivial:

Sub ShowColor()
   Label1.BackColor = Color.FromARGB(255, HScrollBar1.Value,
           HScrollBar2.Value, HScrollBar3.Value)
End Sub

The single statement in this subroutine picks up the values of the three basic colors from the ScrollBar controls and creates a new color value with the FromARGB method of the Color object. The first argument is the transparency of the color (the alpha channel), and we set it to 255 for a completely opaque color. You can edit the project's code to take into consideration the transparency channel as well. If you do, you must replace the Label control with a PictureBox control and display an image in it. Then draw a rectangle with the specified color on top of it. If the color isn't completely opaque, you'll be able to see the underlying image and visually adjust the transparency channel.

Testing the ColorEdit Control

To test the new control, you must place it on a form. Build the ColorEdit control and switch to the test project (add a new project to the current solution if you haven't done so already). Add an instance of the new custom control to the form. You don't have to enter any code in the test form. Just run it and see how you specify a color, either with the scroll bars or by name. You can also read the value of the selected color through the SelectedColor property. The code behind the Color Form button on the test form does exactly that (it reads the selected color and paints the form with this color):

Private Sub Button1_Click(...) Handles Button1.Click
    Me.BackColor = ColorEdit1.SelectedColor
End Sub

Building User-Drawn Controls

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 you must insert 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 9.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.

The Label3D control is an enhanced Label control.

Figure 9.5. The Label3D control is an enhanced Label 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.

VB 2010 at Work: The Label3D Control

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 they are not a member of a programming team, people design as they code (or the other way around).

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 the members 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 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 form will 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. Note that although this custom control is basically a Label control, it won't inherit from an existing control. You must implement it from scratch in your code.

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 9.6.

Example 9.6. Align and Effect3D enumerations

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 9.7 shows the implementation of the two properties.

Example 9.7. Alignment and Effect properties

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 9.8 to implement the Caption property.

Example 9.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 9.9.

Example 9.9. UserControl object's OnPaint method

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 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)
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 briefly in Chapter 11 and in more detail in the tutorial "Drawing and Painting with Visual Basic 2010," 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 9.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:

Private Sub Label3D1_Click(...) Handles Label3D1.Click
   MsgBox("My properties are " & vbCrLf &
          "Caption = "  Label3D1.Caption.ToString & vbCrLf &
          "Alignment = "  Label3D1.Alignment.ToString & vbCrLf &
          "Effect = "  Label3D1.Effect.ToString)
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:

Private Sub AlignmentBox_SelectedIndexChanged(...)
            Handles AlignmentBox.SelectedIndexChanged
   Label3D1.Alignment = AlignmentBox.SelectedItem
End Sub

Private Sub EffectsBox_SelectedIndexChanged(...)
            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.

Changed Events

The UserControl object exposes many of the events you need to program the control, such as the key and mouse events, and you need not insert a single line of code in the custom control's code. 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:

Private mOnAlignmentChanged As EventHandler
Private mOnEffectChanged As EventHandler
Private mOnCaptionChanged As EventHandler

Then declare the actual events and their handlers:

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:

OnAlignmentChanged(EventArgs.Empty)

And finally, invoke the event handlers from within the appropriate OnEventName method:

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.

Raising Custom Events

When you select the custom control in the Objects drop-down list of the editor and expand the list of events for this control, you'll see all the events that the UserControl object may fire. Let's add a custom event for our control. To demonstrate how to raise events from within a custom control, we'll return for a moment to the ColorEdit control you developed a little earlier in this chapter.

Let's say you want to raise an event (the ColorClick event) when the user clicks the Label control displaying the selected color. To raise a custom event, you must declare it in your control and call the RaiseEvent method. Note that the same event may be raised from many different places in the control's code.

To declare the ColorClick event, enter the following statement in the control's code. This line can appear anywhere, but placing it after the private variables that store the property values is customary:

Public Event ColorClick(ByVal sender As Object, ByVal e As EventArgs)

To raise the ColorClick event when the user clicks the Label control, insert the following statement in the Label control's Click event handler:

Private Sub Label1_Click(...) Handles Label1.Click
   RaiseEvent ColorClick(Me, e)
End Sub

Raising a custom event from within a control is as simple as raising an event from within a class. It's actually simpler to raise a custom event than to raise the usual PropertyChanged events, which are fired from within the OnPropertyChanged method of the base control.

The RaiseEvent statement in the Label's Click event handler maps the Click event of the Label control to the ColorClick event of the custom control. If you switch to the test form and examine the list of events of the ColorEdit control on the form, you'll see that the new event was added. The ColorClick event doesn't convey much information. When raising custom events, it's likely that you'll want to pass additional information to the developer.

Let's say you want to pass the Label control's color to the application through the second argument of the ColorClick event. The EventArgs type doesn't provide a Color property, so we must build a new type that inherits all the members of the EventArgs type and adds a property: the Color property. You can probably guess that we'll create a custom class that inherits from the EventArgs class and adds the Color member. Enter the statements of Listing 9.10 at the end of the file (after the existing End Class statement).

Example 9.10. Declaring a custom event type

Public Class ColorEvent
   Inherits EventArgs
   Public color As Color
End Class

Then, declare the following event in the control's code:

Public Event ColorClick(ByVal sender As Object, ByVal e As ColorEvent)

And finally, raise the ColorClick event from within the Label's Click event handler (see Listing 9.11).

Example 9.11. Raising a custom event

Private Sub Label1_Click(...) Handles Label1.Click
   Dim clrEvent As ColorEvent
   clrEvent.color = Label1.BackColor
   RaiseEvent ColorClick(Me, clrEvent)
End Sub

Not all events fired by a custom control are based on property value changes. You can fire events based on external conditions or a timer, as discussed in Chapter 8.

Using the Custom Control in Other Projects

By adding a test project to the Label3D custom control project, we designed and tested the control in the same environment. A great help, indeed, but the custom control can't be used in other projects. If you start another instance of Visual Studio and attempt to add your custom control to the Toolbox, you won't see the Label3D entry there.

To add your custom component in another project, open the Choose Toolbox Items dialog box and then click the .NET Framework Components tab. Be sure to carry out the steps described here while the .NET Framework Components tab is visible. If the COM Components tab is visible instead, you can perform the same steps, but you'll end up with an error message (because the custom component is not a COM component).

Click the Browse button in the dialog box and locate the FlexLabel.dll file. It's in the Bin folder under the FlexLabel project's folder. The Label3D control will be added to the list of .NET Framework components, as shown in Figure 9.6. Select the check box in front of the control's name; then click the OK button to close the dialog box and add Label3D to the Toolbox. Now you can use this control in your new project.

Adding the Label3D control to another project's Toolbox

Figure 9.6. Adding the Label3D control to another project's Toolbox

Designing Irregularly Shaped Controls

The UserControl object has a rectangular shape by default. However, a custom control need not be rectangular. It's possible to create irregularly shaped forms too, but unlike irregularly shaped controls, an irregularly shaped form is still quite uncommon. Irregularly shaped controls are used in fancy interfaces, and they usually react to movement of the mouse. (They may change color when the mouse is over them or when they're clicked, for example.)

To change the default shape of a custom control, you must use the Region object, which is another graphics-related object that specifies a closed area. You can even use Bezier curves to make highly unusual and smooth shapes for your controls. In this section, we'll do something less ambitious: We'll create controls with the shape of an ellipse, as shown in the upper half of Figure 9.7. To follow the code presented in this section, open the NonRectangularControl project; the custom control is the RoundControl Windows Control Library project, and Form1 is the test form for the control.

A few instances of an ellipse-shaped control

Figure 9.7. A few instances of an ellipse-shaped control

You can turn any control to any shape you like by creating the appropriate Region object and then applying it to the Region property of the control. This must take place from within the control's Paint event. Listing 9.12 shows the statements that change the shape of the control.

Example 9.12. Creating a nonrectangular control

Protected Sub PaintControl(ByVal sender As Object,
              ByVal pe As PaintEventArgs) Handles Me.Paint
    pe.Graphics.TextRenderingHint = Drawing.Text.TextRenderingHint.AntiAlias
    Dim roundPath As New GraphicsPath()
    Dim R As New Rectangle(0, 0, Me.Width, Me.Height)
    roundPath.AddEllipse(R)
    Me.Region = New Region(roundPath)
End Sub

First, we retrieve the Graphics object of the UserControl; then we create a GraphicsPath object, the roundPath variable, and add an ellipse to it. The ellipse is based on the enclosing rectangle. The R object is used temporarily to specify the ellipse. The new path is then used to create a Region object, which is assigned to the Region property of the UserControl object. This gives our control the shape of an ellipse.

Listing 9.12 shows the statements that specify the control's shape. In addition, you must insert a few statements to display the control's caption, which is specified by the control's Caption property. The caption is rendered normally in yellow unless the mouse is hovering over the control, in which case the same caption is rendered with a 3D effect. You already know how to achieve this effect: by printing the same string twice in different colors with a slight displacement between them.

Listing 9.13 shows the code in the control's MouseEnter and MouseLeave events. When the mouse enters the control's area (this is detected by the control automatically — you won't have to write a single line of code for it), the currentState variable is set to State.Active (State is an enumeration in the project's code), and the control's caption appears in raised type. In the control's MouseLeave event handler, the currentState variable is reset to State.Inactive and the control's caption appears in regular font. In addition, each time the mouse enters and leaves the control, the MouseInsideControl and MouseOutsideControl custom events are fired.

Example 9.13. RoundButton control's MouseEnter and MouseLeave events

Private Sub RoundButton_MouseEnter(...) Handles MyBase.MouseEnter
    currentState = State.Active
    Me.Refresh()
    RaiseEvent MouseInsideButton(Me)
End Sub

Private Sub RoundButton_MouseLeave(...) Handles MyBase.MouseLeave
    currentState = State.Inactive
    Me.Refresh()
    RaiseEvent MouseOusideButton(Me)
End Sub

These two events set up the appropriate variables, and the drawing of the control takes place in the Paint event's handler, which is shown in Listing 9.14.

Example 9.14. RoundButton control's Paint event handler

Protected Sub PaintControl(ByVal sender As Object,
                           ByVal pe As PaintEventArgs) Handles Me.Paint
    pe.Graphics.TextRenderingHint = Drawing.Text.TextRenderingHint.AntiAlias
    Dim roundPath As New GraphicsPath()
    Dim R As New Rectangle(0, 0, Me.Width, Me.Height)
    roundPath.AddEllipse(R)
    Me.Region = New Region(roundPath)
Dim Path As New GraphicsPath
    Path.AddEllipse(R)
    Dim grBrush As LinearGradientBrush
    If currentState = State.Active Then
        grBrush = New LinearGradientBrush(
                  New Point(0, 0),
                  New Point(R.Width, R.Height),
                  Color.DarkGray, Color.White)
    Else
        grBrush = New LinearGradientBrush(
                  New Point(R.Width, R.Height),
                  New Point(0, 0), Color.DarkGray,
                  Color.White)
    End If
    pe.Graphics.FillPath(grBrush, Path)
    Dim X As Integer =
            (Me.Width - pe.Graphics.MeasureString(
             currentCaption, currentFont).Width) / 2
    Dim Y As Integer = (Me.Height - pe.Graphics.MeasureString(
             currentCaption, currentFont).Height) / 2
    If currentState = State.Active Then
        pe.Graphics.DrawString(currentCaption,
                currentFont, Brushes.Black, X, Y)
        pe.Graphics.DrawString(currentCaption,
                currentFont,
                New SolidBrush(currentCaptionColor), X - 1, Y - 1)
    Else
        pe.Graphics.DrawString(currentCaption,
                currentFont,
                New SolidBrush(currentCaptionColor), X, Y)
    End If
End Sub

The OnPaint method uses graphics methods to fill the control with a gradient and center the string on the control. They're the same methods we used in the example of the user-drawn control earlier in this chapter. The drawing methods are discussed in detail in the tutorial "Drawing with VB 2010," which can be found at www.sybex.com/go/masteringvb2010.

The code uses the currentState variable, which can take on two values: Active and Inactive. These two values are members of the State enumeration, which is shown next:

Public Enum State
   Active
   Inactive
End Enum

The test form of the project shows how the RoundButton control behaves on a form. You can use the techniques described in this section to make a series of round controls for a totally different look and feel.

The Play button's Click event handler in the test form changes the caption of the button according to the control's current state. It also disables the other RoundButton controls on the test form. Here's the Click event handler of the Play button:

Private Sub bttnplay_Click(...) Handles bttnPlay.Click
    If bttnPlay.Caption = "Play" Then
        Label1.Text = "Playing..."
        bttnPlay.Caption = "STOP"
        bttnPlay.Color = Color.Red
        bttnRecord.Enabled = False
        bttnClose.Enabled = False
    Else
        Label1.Text = "Stopped Playing"
        bttnPlay.Caption = "Play"
        bttnPlay.Color = Color.Yellow
        bttnRecord.Enabled = True
        bttnClose.Enabled = True
    End If
End Sub

There are many methods for drawing shapes and paths, and you may wish to experiment with other oddly shaped controls. How about a progress indicator control that looks like a thermometer? Or a button with an LED that turns on or changes color when you press the button, like the buttons in the lower half of Figure 9.7? The two rectangular buttons are instances of the LEDButton custom control, which is included in the NonRectangularControl project. Open the project in Visual Studio and examine the code that renders the rectangular buttons emulating an LED in the left corner of the control.

Customizing List Controls

Next, I'll show you how to customize the list controls (such as the ListBox, ComboBox, and TreeView controls). You won't build new custom controls; actually, you'll hook custom code into certain events of a control to take charge of the rendering of its items.

Some of the Windows controls can be customized far more than it is possible through their properties. These are the list controls that allow you to supply your own code for drawing each item. You can use this technique to create a ListBox control that displays its items in different fonts, uses alternating background colors, and so on. You can even put bitmaps on the background of each item, draw the text in any color, and create items of varying heights. This is an interesting technique because without it, as you recall from our discussion of the ListBox control, all items have the same height and you must make the control wide enough to fit the longest item (if this is known at design time). The controls that allow you to take charge of the rendering process of their items are the ListBox, CheckedListBox, ComboBox, and TreeView controls.

To create an owner-drawn control, you must program two events: the MeasureItem and DrawItem events. In the MeasureItem event, you determine the dimensions of the rectangle in which the drawing will take place. In the DrawItem event, you insert the code for rendering the items on the control. Every time the control is about to display an item, it fires the MeasureItem event first and then the DrawItem event. By inserting the appropriate code in the two event handlers, you can take control of the rendering process.

These two events don't take place unless you set the DrawMode property of the control accordingly. Because only controls that expose the DrawMode property can be owner drawn, you have a quick way of figuring out whether a control's appearance can be customized with the techniques discussed in this section. The DrawMode property can be set to Normal (the control draws its own surface), OwnerDrawnFixed (you can draw the control, but the height of the drawing area remains fixed), or OwnerDrawnVariable (you can draw the control and use a different height for each item). The same property for the TreeView control has three different settings: None, OwnerDrawText (you provide the text for each item), and OwnerDrawAll (you're responsible for drawing each node's rectangle).

Designing Owner-Drawn ListBox Controls

The default look of the ListBox control will work fine with most applications, but you might have to create owner-drawn ListBoxes if you want to use different colors or fonts for different types of items or populate the list with items of widely different lengths.

The example you'll build in this section, shown in Figure 9.8, uses an alternating background color, and each item has a different height, depending on the string it holds. Lengthy strings are broken into multiple lines at word boundaries. Because you're responsible for breaking the string into lines, you can use any other technique — for example, you can place an ellipsis to indicate that the string is too long to fit on the control, use a smaller font, and so on. The fancy ListBox of Figure 9.8 was created with the OwnerDrawnList project.

An unusual, but quite functional, ListBox control

Figure 9.8. An unusual, but quite functional, ListBox control

To custom-draw the items in a ListBox control (or a ComboBox, for that matter), you use the MeasureItem event to calculate the item's dimensions and the DrawItem event to actually draw the item. Each item is a rectangle that exposes a Graphics object, and you can call any of the Graphics object's drawing methods to draw on the item's area. The drawing techniques we'll use in this example are similar to the ones we used in the previous section.

Each time an item is about to be drawn, the MeasureItem and DrawItem events are fired in this order. In the MeasureItem event handler, we set the dimensions of the item with the statements shown in Listing 9.15.

Example 9.15. Setting up an item's rectangle in an owner-drawn ListBox control

Private Sub ListBox1_MeasureItem(ByVal sender As Object,
                 ByVal e As System.Windows.Forms.MeasureItemEventArgs)
                 Handles ListBox1.MeasureItem
     If fnt Is Nothing Then Exit Sub
     Dim itmSize As SizeF
     Dim S As New SizeF(ListBox1.Width, 200)
     itmSize = e.Graphics.MeasureString(ListBox1.Items(e.Index).ToString, fnt, S)
     e.ItemHeight = itmSize.Height
     e.ItemWidth = itmSize.Width
  End Sub

The MeasureString method of the Graphics object accepts as arguments a string, the font in which the string will be rendered, and a SizeF object. The SizeF object provides two properties: the Width and Height properties, which you use to pass to the method information about the area in which you want to print the string. In our example, we'll print the string in a rectangle that's as wide as the ListBox control and as tall as needed to fit the entire string. I'm using a height of 200 pixels (enough to fit the longest string that users might throw at the control). Upon return, the MeasureString method sets the members of the SizeF object to the width and height actually required to print the string.

The two properties of the SizeF object are then used to set the dimensions of the current item (properties e.ItemWidth and e.ItemHeight). The custom rendering of the current item takes place in the ItemDraw event handler, which is shown in Listing 9.16. The Bounds property of the handler's e argument reports the dimensions of the item's cell as you calculated them in the MeasureItem event handler.

Example 9.16. Drawing an item in an owner-drawn ListBox control

Private Sub ListBox1_DrawItem(ByVal sender As Object,
               ByVal e As System.Windows.Forms.DrawItemEventArgs)
               Handles ListBox1.DrawItem
   If e.Index = −1 Then Exit Sub
   Dim txtBrush As SolidBrush
   Dim bgBrush As SolidBrush
   Dim txtfnt As Font
   If e.Index / 2 = CInt(e.Index / 2) Then
   ' color even numbered items
      txtBrush = New SolidBrush(Color.Blue)
      bgBrush = New SolidBrush(Color.LightYellow)
   Else
   ' color odd numbered items
      txtBrush = New SolidBrush(Color.Blue)
      bgBrush = New SolidBrush(Color.Cyan)
   End If
   If e.State And DrawItemState.Selected Then
' use red color and bold for the selected item
      txtBrush = New SolidBrush(Color.Red)
      txtfnt = New Font(fnt.Name, fnt.Size, FontStyle.Bold)
   Else
      txtfnt = fnt
   End If
   e.Graphics.FillRectangle(bgBrush, e.Bounds)
   e.Graphics.DrawRectangle(Pens.Black, e.Bounds)
   Dim R As New RectangleF(e.Bounds.X, e.Bounds.Y,
                           e.Bounds.Width, e.Bounds.Height)
   e.Graphics.DrawString(ListBox1.Items(e.Index).ToString, txtfnt, txtBrush, R)
   e.DrawFocusRectangle()
End Sub

To test the custom-drawn ListBox control, place two buttons on the form, as shown in Figure 9.8. The Add New Item button prompts the user for a new item (a string) and adds it to the control's Items collection. Listing 9.17 shows the code that adds a new item to the list. Note that the code is identical to the code you'd use to add items to a regular ListBox control.

Example 9.17. Adding an item to the list at runtime

Private Sub Button1_Click(...) Handles Button1.Click
    Dim newItem As String
    newItem = InputBox("Enter item to add to the list")
    ListBox1.Items.Add(newItem)
End Sub

The Bottom Line

Extend the functionality of existing Windows Forms controls with inheritance.

The simplest type of control you can build is one that inherits an existing control. The inherited control includes all the functionality of the original control plus some extra functionality that's specific to an application and that you implement with custom code.

Master It

Describe the process of designing an inherited custom control.

Build compound controls that combine multiple existing controls.

A compound control provides a visible interface that combines multiple Windows controls. As a result, this type of control doesn't inherit the functionality of any specific control; you must expose its properties by providing your own code. The UserControl object, on which the compound control is based, already exposes a large number of members, including some fairly advanced ones such as the Anchoring and Docking properties, and the usual mouse and key events.

Master It

How will you map certain members of a constituent control to custom members of the compound control?

Build custom controls from scratch.

User-drawn controls are the most flexible custom controls because you're in charge of the control's functionality and appearance. Of course, you have to implement all the functionality of the control from within your code, so it takes substantial programming effort to create user-drawn custom controls.

Master It

Describe the process of developing a user-drawn custom control.

Customize the rendering of items in a ListBox control.

The Windows controls that present lists of items display their items in a specific manner. The Framework allows you to take control of the rendering process and change completely the default appearance of the items on these controls. The controls that allow you to take charge of the rendering process of their items are the ListBox, CheckedListBox, ComboBox, and TreeView controls.

To create an owner-drawn control, you must set the DrawMode property to a member of the DrawMode enumeration and insert the appropriate code in the events MeasureItem and DrawItem. The MeasureItem event is where you decide about the dimensions of the rectangle in which the drawing will take place. The DrawItem event is where you insert the code for rendering the items on the control.

Master It

Outline the process of creating a ListBox control that wraps the contents of lengthy items.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.36.38