Refining the Control
Adding a UserControl object
to the current project and placing some constituent controls on it is just the
first step toward the creation of a full-fledged, commercial-quality ActiveX
control. In this section, I'll show you how to implement a robust user interface,
add binding capabilities and property pages, create user-drawn controls, and
prepare your controls for the Internet.
Custom Properties
You've already seen how
you can add custom properties using pairs of property procedures. This section
explains how to implement some special types of properties.
Design-time
and run-time properties
Not all properties are available
both at design time and at run time, and it's interesting to see how you write
the code in the UserControl module to limit the visibility of properties. The
easiest way to create a run time-only property, such as the SelText property
of a TextBox or the ListIndex property of a ListBox, is by ticking the
Don't Show In Property Browser option in the Attributes section of the Procedure
Attributes dialog box. (You can access this dialog box by choosing it from the
Tools menu.) If this check box is selected, the property doesn't appear in the
Properties window at design time.
The problem with this simple
approach, however, is that it also hides the property in the other property browser
that Visual Basic provides, namely the Locals window. To have the property listed
in the Locals window at run time but not in the Properties window, you must raise
an error in the Property Get procedure at design time, as this code demonstrates:
Public Property Get SelText() As String
If Ambient.UserMode = False Then Err.Raise 387
SelText = Text1.SelText
End Property
Error 387 "Set not
permitted" is the error that by convention you should raise in this case,
but any error will do the trick. If Visual Basic-or more generally, the host
environment-receives an error when reading a value at design time, the property
isn't displayed in the properties browser, which is precisely what you want.
Creating a property that's unavailable at design time and read-only at run time
is even simpler because you need merely to omit the Property Let procedure,
as you would do with any read-only property. Visual Basic doesn't show such a
property in the Properties window because it couldn't be modified in any way.
Another common situation
concerns properties that are available at design time and read-only at run time.
This is similar to the MultiLine and ScrollBars properties of the
Visual Basic TextBox control. You can implement such properties by raising Error
382 "Set not supported at runtime" in their Property Let procedures,
as shown in the following code:
' This property is available at design time and read-only at run time.
Public Property Get ScrollBars() As Integer
ScrollBars = m_ScrollBars
End Property
Public Property Let ScrollBars(ByVal New_ScrollBars As Integer)
If Ambient.UserMode Then Err.Raise 382
m_ScrollBars = New_ScrollBars
PropertyChanged "ScrollBars"
End Property
When you have design-time
properties that are read-only at run time, you can't call the Property Let
procedure from within the ReadProperties event procedure because you
would get an error. In this case, you're forced to directly assign the private
member variable or the constituent control's property, or you have to provide
a module-level Boolean variable that you set to True on entering the ReadProperties
event and reset to False on exit. You then query this variable before raising
errors in the Property Let procedure. You can also use the same variable
to skip an unnecessary call to the PropertyChanged method, as in this
code example:
Public Property Let ScrollBars(ByVal New_ScrollBars As Integer)
' The ReadingProperties variable is True if this routine is being
' called from within the ReadProperties event procedure.
If Ambient.UserMode
And Not ReadingProperties Then Err.Raise 382
m_ScrollBars = New_ScrollBars
If Not ReadingProperties Then
PropertyChanged "ScrollBars"
End Property
Enumerated properties
You can define enumerated
properties using either Enum blocks in code or Visual Basic's own enumerated
types. For example, you can modify the code produced by the wizard and improve
the MousePointer property as follows:
Public Property Get MousePointer() As MousePointerConstants
MousePointer = Text1.MousePointer
End Property
Public Property Let MousePointer(ByVal New_MousePointer _
As MousePointerConstants)
Text1.MousePointer() = New_MousePointer
PropertyChanged "MousePointer"
End Property
Enumerated properties are
useful because their valid values appear in the Properties window in a combo
box, as shown in Figure 17-6. Keep in mind, however, that you should always protect
your ActiveX control from invalid assignments in code, so the previous routine
should be rewritten as follows:
Public Property Let MousePointer(ByVal New_MousePointer _
As MousePointerConstants)
Select Case New_MousePointer
Case vbDefault To vbSizeAll, vbCustom
Text1.MousePointer() = New_MousePointer
PropertyChanged "MousePointer"
Case Else
Err.Raise 380 ' Invalid Property Value error
End Select
End Property
Figure 17-6.
Use enumerated properties to offer a list of valid values in the Properties
window.
There's a good reason for not defining properties and arguments
using Visual Basic and VBA enumerated constants, though: If you use the control
with environments other than Visual Basic, these symbolic constants won't be
visible to the client application.
|
Tip #1
Sometimes you might want to add spaces and other symbols
inside an enumerated value to make it more readable in the Properties window.
For example, the FillStyle property includes values such as Horizontal
Line or Diagonal Cross. To expose similar values in your ActiveX
controls, you have to enclose Enum constants within square brackets, as in
the following code:
Enum MyColors
Black = 1
[Dark Gray]
[Light Gray]
White
End Enum
Tip #2
Here's another idea that you might find useful: If you use an enumerated
constant name whose name begins with an underscore, such as [_HiddenValue],
this value won't appear by default in the Object Browser. However, this value
does appear in the Properties window, so this trick is especially useful for
enumerated properties that aren't available at design time.
|
Picture and
Font properties
Visual Basic deals in a
special way with properties that return a Picture or Font object. In the former
instance, the Properties window shows a button that lets you select an image
from disk; in the latter, the Properties window includes a button that displays
a Font common dialog box.
When working with Font properties,
you should keep in mind that they return object references. For example, if two
or more constituent controls have been assigned the same Font reference, changing
a font attribute in one of them also changes the appearance of all the others.
For this reason, Ambient.Font returns a copy of the parent form's font
so that any subsequent change to the form's font doesn't affect the UserControl's
constituent controls, and vice versa. (If you want to keep your control's font
in sync with the form's font, you simply need to trap the AmbientChanged event.)
Sharing object references can cause some subtle errors in your code. Consider
the following example:
' Case 1: Label1 and Text1 use fonts with identical attributes.
Set Label1.Font = Ambient.Font
Set Text1.Font = Ambient.Font
' Case 2: Label1 and Text1 point to the *same* font.
Set Label1.Font = Ambient.Font
Set Text1.Font = Label1.Font
The two pieces of code look
similar, but in the first instance the two constituent controls are assigned
different copies of the same font, so you can change the font attributes of one
control without affecting the other. In the latter case, both controls are pointing
to the same font, so each time you modify a font attribute in either control
the other one is affected as well.
It's a common practice to
provide all the alternate, old-styled Fontxxxx properties, namely FontName,
FontSize, FontBold, FontItalic, FontUnderline, and
FontStrikethru. But you should also make these properties unavailable
at design time, and you shouldn't save them in the WriteProperties event
if you also save the Font object. If you decide to save individual Fontxxxx
properties, it's important that you retrieve them in the correct order (first
FontName, and then all the others).
One more thing to keep in
mind when dealing with font properties: You can't restrict the choices of the
programmer who's using the control to a family of fonts- for example, to nonproportional
fonts or to printer fonts-if the Font property is exposed in the Properties
window. The only way to restrict font selection is to show a Font Common Dialog
box from a Property Page. See the "Property Pages" section later in
this chapter for details about building property pages.
Font properties pose a special
challenge to ActiveX control programmers. If your control exposes a Font
property and the client code modifies one or more font attributes, Visual Basic
calls the Property Get Font procedure but not the Property Set Font
procedure. If the Font property delegates to a single constituent
control, this isn't usually a problem because the control's appearance is correctly
updated. Things are different in user-drawn ActiveX controls because in this
case your control gets no notification that it should be repainted. This problem
has been solved in Visual Basic 6 with the FontChanged event of the StdFontobject. Here's a fragment of code taken from a Label-like, user-drawn control
that correctly refreshes itself when the client modifies an attribute of the
Font property:
Private WithEvents UCFont As StdFont
Private Sub UserControl_InitProperties()
' Initialize the Font property (and the UCFont object).
Set Font = Ambient.Font
End Sub
Public Property Get Font() As Font
Set Font = UserControl.Font
End Property
Public Property Set Font(ByVal New_Font As Font)
Set UserControl.Font = New_Font
Set UCFont = New_Font ' Prepare to trap events.
PropertyChanged "Font"
Refresh ' Manually perform the first refresh.
End Property
' This event fires when the client code changes a font's attribute.
Private Sub UCFont_FontChanged(ByVal PropertyName As String)
Refresh ' This causes a Paint event.
End Sub
' Repaint the control.
Private Sub UserControl_Paint()
Cls
Print Caption;
End Sub
Object properties
You can create ActiveX controls
with properties that return objects, such as a TreeView-like control that exposes
a Nodes collection. This is possible because ActiveX control projects can include
PublicNotCreatable classes, so your control can internally create them using
the New operator and return a reference to its clients through a read-only property.
Object properties can be treated as if they were regular properties in most circumstances,
but they require particular attention when you need to make them persistent and
reload them in the WriteProperties and ReadProperties procedures.
Even if Visual Basic 6 does
support persistable classes, you can't save objects that aren't creatable, as
in this case. But nothing prevents you from manually creating a PropertyBag object
and loading it with all the properties of the dependent object. Let me demonstrate
this technique with an example.
Suppose that you have an
AddressOCX ActiveX control that lets the user enter a person's name and address,
as shown in Figure 17-7. Instead of many properties, this AddressOCX control
exposes one object property, named Address, whose class is defined inside
the same project. Rather than having the main UserControl module save and reload
the individual properties of the dependent object, you should create a Friend
property in the PublicNotCreatable class. I usually call this property AllProperties
because it sets and returns the values of all the properties in one Byte array.
To serialize the properties into an array, I use a private stand-alone PropertyBag
object. Following is the complete source code of the Address class module. (For
the sake of simplicity, properties are implemented as Public variables.)
' The Address.cls class module
Public Name As String, Street As String
Public City As String, Zip As String, State As String
Friend Property Get AllProperties() As Byte()
Dim PropBag As New PropertyBag
PropBag.WriteProperty "Name", Name, ""
PropBag.WriteProperty "Street", Street, ""
PropBag.WriteProperty "City", City, ""
PropBag.WriteProperty "Zip", Zip, ""
PropBag.WriteProperty "State", State, ""
AllProperties = PropBag.Contents
End Property
Friend Property Let AllProperties(value() As Byte)
Dim PropBag As New PropertyBag
PropBag.Contents = value()
Name = PropBag.ReadProperty("Name", "")
Street = PropBag.ReadProperty("Street", "")
City = PropBag.ReadProperty("City", "")
Zip = PropBag.ReadProperty("Zip", "")
State = PropBag.ReadProperty("State", "")
End Property
Rather than saving and reloading
all the individual properties in the WriteProperties and ReadProperties
event procedures of the main AddressOCX module, you simply save and restore
the AllProperties property of the Address object.
Figure 17-7.
An AddressOCX ActiveX control that exposes each of the Address properties
as an individual Address, PublicNotCreatableobject.
' The AddressOCX code module (partial listing)
Dim m_Address As New Address
Public Property Get Address() As Address
Set Address = m_Address
End Property
Public Property Set Address(ByVal New_Address As Address)
Set m_Address = New_Address
PropertyChanged "Address"
End Property
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
m_Address.AllProperties = PropBag.ReadProperty("Address")
End Sub
Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
Call PropBag.WriteProperty("Address", m_Address.AllProperties)
End Sub
All the individual constituent
controls must refer to the corresponding property in the Address object. For
example, this is the code in the Change event procedure of the txtName
control:
Private Sub txtName_Change()
Address.Name = txtName
PropertyChanged "Address"
End Sub
The ActiveX control should
also expose a Refresh method that reloads all the values from the Address
object into the individual fields. Alternatively, you might implement an event
that the Address object raises in the AddressOCX module when any of its properties
is assigned a new value. This problem is similar to the one I described in the
"Forms as Object Viewers" section of Chapter 9.