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.
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.
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.
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).
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 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.)
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 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.
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 renders text on the target object. Here is the Print method's syntax:
The text argument represents the text to be printed. It
can be any string.
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.
You can clear the painting area by using the Cls method. The Cls method takes no arguments; its syntax is:
To see how this works, add a command button to your example form. In the command button's Click event, add the code:
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.
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:
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.
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:
Private mlngForeColor As Long
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
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
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.
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.
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.
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:
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
Private Sub UserControl_AmbientChanged(PropertyName As String) If PropertyName = "DisplayAsDefault" Then Refresh End If End Sub
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:
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.
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."
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.