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