Chapter 9

User-Drawn Controls


CONTENTS

Until now, all of the example controls in this book have been comprised of one or more constituent controls. But that does not mean that all your ActiveX controls must be based on existing controls. Using Visual Basic's graphics methods, you have the ability to create controls that have totally unique graphical appearances. It's possible that you may already be familiar with these graphics methods; you'll find that implementing them in the context of a UserControl is quite straightforward.

A control that does not use constituent controls is referred to as a user-drawn control. (This is something of a misnomer, since the control isn't technically drawn by the user, it's drawn by your code, but we'll let that slide for now.)

When your control project is user-drawn, there are a number of things to watch out for. This chapter will take a look at those considerations and summarize the Visual Basic graphic methods available to you when you're creating your user-drawn control.

Graphics Methods

You can use Visual Basic's graphics methods to draw the interface and appearance of your control.

The graphics methods discussed in this section apply to forms and form-like objects, such as property pages, as well as your UserControl object. You can also use these graphics methods with the PictureBox control. In this chapter, I'll refer to any component of the Visual Basic interface that can be drawn on as a Painting object.

NOTE
For experienced users of Visual Basic, much of this section will be review, but I'm including it here because I wanted all the important stuff to be in the same place, thereby satisfying my need for tidiness and organization. If you understand VB's graphics methods, you may wish to skim this section and skip to the middle of the chapter, where I'll relate it all back to control creation.

Addressing the Coordinate System

When you're using any of these graphics methods, you are drawing in a coordinate system. Everything you do in this coordinate system must be addressed to a point in the system. In Visual Basic, the coordinate system of any Painting object has its origin in the upper-left corner of the Painting object; coordinates increase as you go down and to the right. Horizontal dimensions are expressed along the X axis, while vertical dimensions are expressed along the Y axis. This is illustrated in Figure 9.1.

Figure 9.1 : Visual Basic coordinate system.

So, for example, to draw a line from the upper left corner of the form to the lower right corner of the form, you'd instruct the graphics method to draw a line from point (0,0) (that is, zero units on the X axis, and zero units on the Y axis) to point (Me.Width, Me.Height). If the Painting object were 2000 units wide and 3000 units high, the destination point for your line would be (2000, 3000).

NOTE
In Visual Basic, the keyword Me refers to the currently executing form (or other class). In the code examples in this book, it's invariably used as shorthand in situations where you don't care to specify (or don't know) the name of the form that contains the control.

Visual Basic's standard method of measurement is the twip. There are 1,440 twips to the inch, although the actual size of a twip on your screen will vary according to the resolution of your screen and the size of your monitor.

Since a twip is much smaller than the resolution of a pixel on any computer monitor you're likely to run across in your lifetime, it makes sense to express on-screen graphics methods in another measurement system. Visual Basic gives you the ability to express units on the coordinate system in inches, points (there are 72 points to the inch), millimeters, and so forth.

You can change the measurement system of a Painting object by using its ScaleMode property. For example, the code:

Me.ScaleMode = vbCentimeters

sets the coordinate system of the current form to centimeters.

Visual Basic Painting objects also provide graphics properties (TwipsPerPixelX and TwipsPerPixelY) that enable you to convert between twips and pixels. For simplicity's sake, in this chapter I'll use pixels (signified by the Visual Basic ScaleMode constant vbPixels).

The Line Method

The Line method draws a line between two points. The syntax of this method is:

object.Line (startX, startY) - (endX, endY)[, color, BF]

The parameters startX and startY designate the starting point of the line you're drawing. The values endX and endY indicate where the line ends. The optional color argument is a long integer corresponding to a Windows color. If you include the B argument, then the Line method will draw a box instead of a line. If you include the F argument, then the Line method will draw a filled box. (Of course, it's only meaningful to include the F argument if you also include the B argument.)

NOTE
The syntax of this method is a little kooky, as you might have noticed, mainly because it's a throwback to the early days of Basic. The funny syntax is retained for compatibility with earlier versions of the language.

To see how the Line method works, try this example. The code draws a simulated text box on the center of the form. You might find this code helpful as an example of how to create 3-D user interface effects for your controls.

To see how this works, create a command button on the EXE project form. In the button's Click event, type the following code:

Private Sub Command2_Click()

    Me.ScaleMode = vbPixels

    lngStartX = 20
    lngStartY = 20
    lngEndX = 200
    lngEndY = 35

' white box
Line (lngStartX, lngStartY)-(lngEndX, lngEndY), _
      RGB(255, 255, 255), BF

' ** black lines
' vertical
Line (lngStartX - 1, lngStartY - 1)-(lngStartX - 1, lngEndY + 1), _
      RGB(0, 0, 0)
' horizontal
Line (lngStartX - 1, lngStartY - 1)-(lngEndX + 1, lngStartY - 1), _
      RGB(0, 0, 0)

' ** dark grey lines
' vertical
Line (lngStartX - 2, lngStartY - 2)-(lngStartX - 2, lngEndY + 2), _
      RGB(128, 128, 128)
' horizontal
Line (lngStartX - 2, lngStartY - 2)-(lngEndX + 2, lngStartY - 2), _
      RGB(128, 128, 128)

' ** white lines
' vertical
Line (lngEndX + 2, lngStartY - 2)-(lngEndX + 2, lngEndY + 3), _
      RGB(255, 255, 255)
' horizontal
Line (lngStartX - 2, lngEndY + 2)-(lngEndX + 2, lngEndY + 2), _
      RGB(255, 255, 255)

End Sub

This code gives you a feel for the different flavors of the Line method. The first Line method takes the optional BF parameter, drawing a white box on the form. The remaining Line methods draw lines in black and gray around the box in order to give it that three-dimensional look that the kids are so crazy about these days.

The Circle Method

The Circle method draws a circle. Its syntax looks like this:

object.Circle (x, y), radius, [color, start, end, aspect]

The x and y arguments determine the midpoint of the circle. The radius argument sets the radius of the circle. The optional color argument is a long integer corresponding to a Windows color. The optional start and end arguments are single values that determine the start and end points for an arc (rather than a complete circle). The optional aspect argument determines the aspect ratio for the circle. Setting an aspect ratio other than 1 will produce an ellipse rather than a perfect circle.

To see how the Circle method works, try the following code. This code draws a bulls-eye on the center of the a form:

Private Sub Command3_Click()

    Me.ScaleMode = vbTwips

    lngCenterX = Me.Width / 2
    lngCenterY = Me.Height / 2
    Me.FillStyle = vbFSSolid   ' constant from VB's object library

    Me.FillColor = RGB(0, 0, 255)
    Circle (lngCenterX, lngCenterY), Me.Width / 5, Me.FillColor

    Me.FillColor = RGB(255, 255, 255)
    Circle (lngCenterX, lngCenterY), Me.Width / 10, Me.FillColor

    Me.FillColor = RGB(255, 0, 0)
    Circle (lngCenterX, lngCenterY), Me.Width / 20, Me.FillColor

End Sub

When you run this code and click on the button, the form should look something like the one in Figure 9.2.

Figure 9.2 : Example of Circle method.

The PSet Method

You can use the PSet method to draw an individual pixel on an object. The syntax of the PSet method looks like this:

object.PSet (x, y) [, color]

The x argument represents a horizontal position of the point in the coordinate system. The y argument represents the vertical position. The optional color argument is a long integer corresponding to a Windows color.

To test how the PSet method works, create an EXE project form with a command button. In the command button's Click event, type the following code:

Private Sub Command1_Click()

intMaxX = Me.Width
intMaxY = Me.Height

For x = 1 To 5000
    intX = Int(intMaxX - 1) * Rnd
    intY = Int(intMaxY - 1) * Rnd
    Me.PSet (intX, intY)
Next x

End Sub

This code demonstrates the PSet method by painting the form with random pixels. To see how it works, run the EXE project, then click on the button. The form should look like the one shown in Figure 9.3.

Figure 9.3 : Example of PSet method.

Because of the way this code is written, the density of the pixels drawn on your screen will be a function of the dimensions of your Form1.

The Print Method

The Print method renders text on the target object. Here is the Print method's syntax:

object.Print text

The text argument represents the text to be printed. It can be any string.

TIP
There are additional, seldom-used arguments to the Print method that are included primarily for compatibility with older versions of Visual Basic. For example, the Print method provides support for printing tabulated lists in columns. See the Print method topic in Visual Basic online help for more information on these arguments.

Here is an example of code that uses the Print method. This code displays a word on the form over and over, in a range of colors (or, rather, shades of gray):

Private Sub Command5_Click()
    Me.FontBold = True
    Me.Font = "Arial"
    Me.FontSize = 36

    Randomize Timer

    For x = 1 To 255
        Green = Int(255 * Rnd + 1)
        Blue = Int(255 * Rnd + 1)
        Me.CurrentX = x
        Me.CurrentY = x
        Me.ForeColor = RGB(x, Green, Blue)
        Print "Spoon!"
Next x

End Sub

The effect this code produces when run is illustrated in Figure 9.4

Figure 9.4: An example of the Print method.

The font face and style used by the Print method is a function of the Drawing object's font properties (such as FontSize and FontBold). These properties must be set before you use the Print method, because you can't change the way the text is rendered once it's been placed on the painting object.

The Cls Method

You can clear the painting area by using the Cls method. The Cls method takes no arguments; its syntax is:

object.Cls

To see how this works, add a command button to your example form. In the command button's Click event, add the code:

Me.Cls

Then run the EXE project. Click on one of the buttons that generates graphics on the form, then click on the Cls button. You should be able to see that the Cls method clears all the graphics on the form.

The Paint Event

In a user-drawn control, the graphics methods that comprise the control's appearance are placed in the control's Paint event.

Here are some things to watch out for when writing code in the Paint event of a UserControl:

Example of a User-Drawn Control: The Hexagon Control

Let's put all that together in an example. The Hexagon control is similar to the Shape control that comes with Visual Basic, except it draws a regular, six-sided figure. It is a completely user-drawn control; the code to draw the hexagon is in the UserControl's Paint event. The code for this control is on the CD-ROM that accompanies this book. To create the Hexagon control, insert the following code in a control designer called Hexagon:

' Declarations section

Private lngSideLength As Long
Private lngXPoint0 As Long, lngXPoint1 As Long
Private lngXPoint2 As Long, lngXPoint3 As Long
Private lngYPoint0 As Long, lngYPoint1 As Long
Private lngYPoint2 As Long

' The business end of the code

Private Sub UserControl_Paint()

    lngSideLength = (UserControl.Width / 2)
    lngXPoint0 = 0
    lngXPoint1 = lngXPoint0 + (lngSideLength / 2)
    lngXPoint2 = lngXPoint1 + lngSideLength
    lngXPoint3 = lngXPoint2 + lngXPoint1 - 10

    lngYPoint0 = 0
    lngYPoint1 = CLng(lngSideLength * (Sqr(3) / 2))
    lngYPoint2 = lngYPoint1 * 2

    DrawWidth = 1
    Line (lngXPoint1, lngYPoint0)-(lngXPoint2, lngYPoint0)
    Line (lngXPoint2, lngYPoint0)-(lngXPoint3, lngYPoint1)
    Line (lngXPoint3, lngYPoint1)-(lngXPoint2, lngYPoint2)
    Line (lngXPoint2, lngYPoint2)-(lngXPoint1, lngYPoint2)
    Line (lngXPoint1, lngYPoint2)-(lngXPoint0, lngYPoint1)
    Line (lngXPoint0, lngYPoint1)-(lngXPoint1, lngYPoint0)

End Sub


Private Sub UserControl_Resize()
    ' Make sure the control always
    ' fits dimensions of the hexagon
    UserControl.Height = UserControl.Width * (Sqr(3) / 2) + 20
End Sub

You can see that the Paint event is responsible for drawing the appearance of the control.

One cool thing about this code is that because the drawing in the Paint event is based on the dimensions of the UserControl, the hexagon always fills the available area of the control. If you resize the control, the hexagon redraws so it's exactly the right size.

The Refresh Method

Anytime you change the appearance of your user-drawn control, the control must execute the Refresh method. The Refresh method causes the code in your control's Paint event to run, thereby redrawing the control.

For example, let's say you want to enable the Hexagon control to draw in a color chosen by the user. To do this, you create a ForeColor property for the control and execute the Refresh method in the ForeColor's Property Let procedure. Here are the steps to implementing this feature in the Hexagon control:

  1. Add the following code to the declarations section of the Hexagon control. This variable stores the state of the control's foreground color.
Private mlngForeColor As Long
  1. Alter the code in the control's Paint event so that it takes advantage of the new property. The code should look like this:
Private Sub UserControl_Paint()
    .
    .
    .
Line (lngXPoint1, lngYPoint0)-(lngXPoint2, lngYPoint0), mlngForeColor
Line (lngXPoint2, lngYPoint0)-(lngXPoint3, lngYPoint1), mlngForeColor
Line (lngXPoint3, lngYPoint1)-(lngXPoint2, lngYPoint2), mlngForeColor
Line (lngXPoint2, lngYPoint2)-(lngXPoint1, lngYPoint2), mlngForeColor
Line (lngXPoint1, lngYPoint2)-(lngXPoint0, lngYPoint1), mlngForeColor
Line (lngXPoint0, lngYPoint1)-(lngXPoint1, lngYPoint0), mlngForeColor
  1. Next, add Property Let and Property Get procedures for the new property:
Public Property Get ForeColor() As OLE_COLOR
    ForeColor = mlngForeColor
End Property

Public Property Let ForeColor(ByVal NewValue As OLE_COLOR)
    mlngForeColor = NewValue
    PropertyChanged "ForeColor"
    Refresh   ' this redraws the control with the new color
End Property

NOTE
Don't forget to declare color properties as type OLE_COLOR so a color palette is displayed in the Properties window when the user changes the ForeColor property.

If you place an instance of the Hexagon control onto an EXE project form and then change its ForeColor property, you should be able to see that you can change the color of the control to any Windows color. The control should look like Figure 9.5.

Figure 9.5 : Colorized Hexagon control.

Displaying Your Control As Disabled

If your control has an Enabled property and that property has been set to False, you should provide some graphical indication that the control is disabled. You do this by providing logic in the control's Paint method.

There is no standardized way of graphically indicating that a control is disabled, but in general, drawing a disabled control involves graying out the colored portions of the control. For ideas on how to do that, take a look at some existing controls. Figure 9.6 shows some standard Windows controls in their disabled state.

Figure 9.6 : Disabled controls.

In order to implement a graphical display of Enabled = False, you need to inspect the Enabled property in the Paint event using an If...Then statement. If Enabled is False, the Paint event draws the disabled version of the control. If Enabled is True, the Paint event draws the enabled version of the control.

Displaying the Default Property

A control is said to be the default control when its Default property is set to True. This control will always be given the focus when the form it resides on is first displayed.

You see this most frequently in situations where the user is confronted with a dialog box containing OK and Cancel buttons; assuming the user does not move the focus to some other control in the dialog box, the user can either click on OK or press the Enter key to quickly confirm the dialog box settings.

You should draw a thick black line around your control when all of the following things are true:

The tricky part about this is determining whether another control residing on the same form as your control has the focus. Fortunately, Visual Basic helps you out here, through the DisplayAsDefault property of the AmbientProperties object. The DisplayAsDefault property is a Boolean property that tells your control whether it should draw itself as the default button.

NOTE
Exactly how thick the line should be is an aesthetic choice you'll make depending on what your control looks like; take a look at some existing controls for hints. In a user-drawn control, you'll use Visual Basic graphics methods to draw the border.

As an example, let's say you want to change the Hexagon control into a hexagonal button control. To do this:

  1. Open the control's designer. In the Properties window, change the DefaultCancel property to True. This tells Visual Basic that the control is capable of acting as a default or cancel button. Because the Cancel and Default properties are provided by the container, you don't have to write any code to implement these properties; they appear automatically when you set the DefaultCancel property to True.
  2. Change the control's Paint property as follows:
Line (lngXPoint1, lngYPoint0)-(lngXPoint2, lngYPoint0), _
    RGB(255, 255, 255)
Line (lngXPoint2, lngYPoint0)-(lngXPoint3, lngYPoint1), _
    RGB(128, 128, 128)
Line (lngXPoint3, lngYPoint1)-(lngXPoint2, lngYPoint2), _
    RGB(128, 128, 128)
Line (lngXPoint2, lngYPoint2)-(lngXPoint1, lngYPoint2), _
    RGB(128, 128, 128)
Line (lngXPoint1, lngYPoint2)-(lngXPoint0, lngYPoint1), _
    RGB(128, 128, 128)
Line (lngXPoint0, lngYPoint1)-(lngXPoint1, lngYPoint0), _
    RGB(255, 255, 255)

If Extender.Default = True Then
    Line (0, 0)-(Width - 20, Height - 20), 0, B
End If
  1. To redraw the control when the user changes the Default property at design time, insert the following code in the AmbientChanged event of the UserControl:
Private Sub UserControl_AmbientChanged(PropertyName As String)
    If PropertyName = "DisplayAsDefault" Then
        Refresh
    End If
End Sub
  1. To trap the user action when the Default or Cancel properties have been set to True, use the following code:
Private Sub UserControl_AccessKeyPress(KeyAscii As Integer)

    Select Case KeyAscii
        Case 13  ' user hit enter when Default property True
        MsgBox "Default."
        
        Case 27  ' user cancelled when Cancel property True
        MsgBox "Cancel."
    End Select
        
End Sub

You will, of course, want to replace the MsgBox statements in the AccessKeyPress event with something more meaningful. Typically, when the AccessKeyPress event of a command button detects that the user has pressed Enter when the Default property is True, it triggers the Click event.

To test the new version of the Hexagon control, do the following:

  1. Switch to the EXE project form that contains an instance of the Hexagon control.
  2. In the Properties window, set the Default property of Hexagon1 to True.
  3. Run the EXE project by pressing F5.
  4. After the program runs, press Enter. The MsgBox statement in the UserControl's AccessKeyPressed event is triggered, producing the message box illustrated in Figure 9.7.

Figure 9.7 : Demonstration of the Default property.

The fact that the control has been set as the default means that it responds to the user pressing the Enter key. In this case, pressing Enter causes the message box to be displayed.

Showing That Your Control Has the Focus

If your control can take the focus, then it should graphically display that is has the focus. Standard Windows controls show that they have the focus by drawing a thin, dotted line around themselves. In Figure 9.8, the command button with the caption "Martini" has the focus.

Figure 9.8 : Command button with focus.

The thin dotted line drawn around a control to indicate that it has the focus is called the focus rectangle. You can write custom graphics methods to draw the focus rectangle, or you can use a standard Windows API function called DrawFocusRect. This function only works for rectangular controls; if you create a non-rectangular control (such as our hexagonal button), you must manage the focus rectangle yourself.

For more information on making Windows API calls, see Chapter 11, "Making Windows API and DLL Calls."

Summary

This chapter explored the triumphs and pitfalls of rendering your control's appearance using Visual Basic graphics methods. In addition, we covered how you can use VB's graphics methods to render your control, including methods to display controls as disabled and in focus.

In the next chapter, you'll delve into a mixed bag of miscellaneous control features, effects, and tricks to give your control the kind of full-featured interface that users expect.