The missing pieces
Looking at the code that
the ActiveX Control User Interface wizard generates is a good starting point
for learning how ActiveX controls are implemented. Most of the time, you'll see
that a UserControl module isn't different from a regular class module. One important
note: The wizard adds several commented lines that it uses to keep track of how
members are implemented. You should follow the warnings that come along with
these lines and avoid deleting them or modifying them in any way.
Delegated properties,
methods, and events
As I already explained,
most of the code generated by the wizard does nothing but delegate the real action
to the inner constituent controls. For example, see how the Text property
is implemented:
Public Property Get Text() As String
Text = Text1.Text
End Property
Public Property Let Text(ByVal New_Text As String)
Text1.Text() = New_Text
PropertyChanged "Text"
End Property
The PropertyChanged
method informs the container environment-Visual Basic, in this case-that
the property has been updated. This serves two purposes. First, at design time
Visual Basic should know that the control has been updated and has to be saved
in the FRM file. Second, at run time, if the Text property is bound to
a database field, Visual Basic has to update the record. Data-aware ActiveX controls
are described in the "Data Binding"
section, later in this chapter.
The delegation mechanism
also works for methods and events. For example, see how the SuperTextBox module
traps the Text1 control's KeyPress event and exposes it to the outside,
and notice how it delegates the Refresh method to the UserControl object:
' The declaration of the event
Event KeyPress(KeyAscii As Integer)
Private Sub Text1_KeyPress(KeyAscii As Integer)
RaiseEvent KeyPress(KeyAscii)
End Sub
Public Sub Refresh()
UserControl.Refresh
End Sub
Custom properties
For all the public properties
that aren't mapped to a property of a constituent control, the ActiveX Control
Interface Wizard can't do anything but create a private member variable that
stores the value assigned from the outside. For example, this is the code generated
for the FormatMask custom property:
Dim m_FormatMask As String
Public Property Get FormatMask() As String
FormatMask = m_FormatMask
End Property
Public Property Let FormatMask(ByVal New_FormatMask As String)
m_FormatMask = New_FormatMask
PropertyChanged "FormatMask"
End Property
Needless to say, you decide
how such custom properties affect the behavior or the appearance of the SuperTextBox
control. In this particular case, this property changes the behavior of another
custom property, FormattedText, so you should modify the code generated
by the wizard as follows:
Public Property Get FormattedText() As String
FormattedText = Format$(Text, FormatMask)
End Property
The FormattedText property
had been defined as read-only at run time, so the wizard has generated its Property
Get procedure but not its Property Let procedure.
Custom methods
For each custom method you
have added, the wizard generates the skeleton of a Sub or Function procedure.
It's up to you to fill this template with code. For example, here's how you can
implement the Copy and Clear methods:
Public Sub Copy()
Clipboard.Clear
Clipboard.SetText IIf(SelText <> "", SelText, Text)
End Sub
Public Sub Clear()
If SelText <> "" Then SelText = "" Else Text = ""
End Sub
You might be tempted to
use Text1.Text and Text1.SelText instead of Text and SelText
in the previous code, but I advise you not to do it. If you use the public
name of the property, your code is slightly slower, but you'll save a lot of
time if you later decide to change the implementation of the Text property.
Custom events
You raise events from a
UserControl module exactly as you would from within a regular class module. When
you have a custom event that isn't mapped to any event of constituent controls,
the wizard has generated only the event declaration for you because it can't
understand when and where you want to raise it.
The SuperTextBox control
exposes the SelChange event, which is raised when either the SelStart
property or the SelLength property (or both) change. This event is
useful when you want to display the current column on the status bar or when
you want to enable or disable toolbar buttons depending on whether there's any
selected text. To correctly implement this event, you must add two private variables
and a private procedure that's called from multiple event procedures in the UserControl
module:
Private saveSelStart As Long, saveSelLength As Long
' Raise the SelChange event if the cursor moved.
Private Sub CheckSelChange()
If SelStart <> saveSelStart Or SelLength <> saveSelLength Then
RaiseEvent SelChange
saveSelStart = SelStart
saveSelLength = SelLength
End If
End Sub
Private Sub Text1_KeyUp(KeyCode As Integer, Shift As Integer)
RaiseEvent KeyUp(KeyCode, Shift)
CheckSelChange
End Sub
Private Sub Text1_Change()
RaiseEvent Change
CheckSelChange
End Sub
In the complete demonstration
project that you can find on the companion CD, the CheckSelChange procedure
is called from within Text1's MouseMove and MouseUp event procedures
and also from within the Property Let SelStart and Property Let SelLength
procedures.
Properties that
map to multiple controls
Sometimes you might need
to add custom code to correctly expose an event to the outside. Take, for example,
the Click and DblClick events: You mapped them to the Text1 constituent
control, but the UserControl module should raise an event also when the user
clicks on the Label1 control. This means that you have to manually write the
code that does the delegation:
Private Sub Label1_Click()
RaiseEvent Click
End Sub
Private Sub Label1_DblClick()
RaiseEvent DblClick
End Sub
You might also need to add
delegation code when the same property applies to multiple constituent controls.
Say that you want the ForeColor property to affect both the Text1 and
Label1 controls. Since the wizard can map a property only to a single control,
you must add some code (shown as boldface in the following listing) in the Property
Let procedure that propagates the new value to the other constituent controls:
Public Property Let ForeColor(ByVal New_ForeColor As OLE_COLOR)
Text1.ForeColor = New_ForeColor
Label1.ForeColor = New_ForeColor
PropertyChanged "ForeColor"
End Property
You don't need to modify
the code in the corresponding Property Get procedure, however.
Persistent properties
The ActiveX Control Interface
Wizard automatically generates the code that makes all the control's properties
persistent via FRM files. The persistence mechanism is identical to the one used
for persistable ActiveX components (which I explained in Chapter 16). In this
case, however, you never have to explicitly ask an ActiveX control to save its
own properties because the Visual Basic environment does it for you automatically
if any of the control's properties have changed during the editing session in
the environment
When the control is placed
on a form, Visual Basic fires its UserControl_InitProperties event. In
this event procedure, the control should initialize its properties to their default
values. For example, this is the code that the wizard generates for the SuperTextBox
control:
Const m_def_FormatMask = ""
Const m_def_FormattedText = ""
Private Sub UserControl_InitProperties()
m_FormatMask = m_def_FormatMask
m_FormattedText = m_def_FormattedText
End Sub
When Visual Basic saves
the current form to an FRM file, it asks the ActiveX control to save itself by
raising its UserControl_WriteProperties event:
Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
Call PropBag.WriteProperty("FormatMask", m_FormatMask, m_def_FormatMask)
Call PropBag.WriteProperty("FormattedText", m_FormattedText, _
m_def_FormattedText)
Call PropBag.WriteProperty("BackColor", Text1.BackColor, &H80000005)
Call PropBag.WriteProperty("ForeColor", Text1.ForeColor, &H80000008)
' Other properties omitted....
End Sub
The third argument passed
to the PropertyBag object's WriteProperty method is the default value
for the property. When you're working with color properties, you usually pass
hexadecimal constants that stand for system colors. For example, &H80000005
is the vbWindowBackground constant (the default background color), and &H80000008
is the vbWindowText constant (the default text color). Unfortunately, the wizard
doesn't generate symbolic constants directly. For a complete list of supported
system colors, use the Object Browser to enumerate the SystemColorConstants constants
in the VBRUN library.
When Visual Basic reloads
an FRM file, it fires the UserControl_ReadProperties event to let the
ActiveX control restore its own properties:
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
m_FormatMask = PropBag.ReadProperty("FormatMask", m_def_FormatMask)
m_FormattedText = PropBag.ReadProperty("FormattedText", _
m_def_FormattedText)
Text1.BackColor = PropBag.ReadProperty("BackColor", &H80000005)
Text1.ForeColor = PropBag.ReadProperty("ForeColor", &H80000008)
Set Text1.MouseIcon = PropBag.ReadProperty("MouseIcon", Nothing)
' Other properties omitted....
End Sub
Again, the last argument
passed to the PropertyBag object's ReadProperty method is the default
value of the property being retrieved. If you manually edit the code created
by the wizard, be sure that you use the same constant in the InitProperties,
WriteProperties, and ReadProperties event procedures.
The wizard does a good job
of generating code for properties persistence, but in some cases you might need
to fix it. For example, the preceding code directly assigns values to constituent
controls' properties. While this approach is OK in most cases, it fails when
the same property maps multiple controls, in which case you should assign the
value to the Public property name. On the other hand, using the Public property
name invokes its Property Let and Set procedures, which in turn
call the PropertyChanged method and cause properties to be saved again
even if they weren't modified during the current session. I'll show you how you
can avoid this problem later in this chapter.
Moreover, the wizard creates
more code than strictly necessary. For example, it generates the code that saves
and restores properties that aren't available at design time (SelStart,
SelText, SelLength, and FormattedText in this particular
case). Dropping the corresponding statements from the ReadProperties and
WriteProperties procedures makes your FRM files shorter and speeds up
save and load operations.
The UserControl's
Resize event
The UserControl object raises
several events during the lifetime of an ActiveX control, and I'll describe all
of them later in this chapter. One event, however, is especially important: the
Resize event. This event fires at design time when the programmer drops
the ActiveX control on the client form and also fires whenever the control itself
is resized. As the author of the control, you must react to this event so that
all the constituent controls move and resize accordingly. In this particular
case, the position and size of constituent controls depend on whether the SuperTextBox
control has a nonempty Caption:
Private Sub UserControl_Resize()
On Error Resume Next
If Caption <> "" Then
Label1.Move 0, 0, ScaleWidth, Label1.Height
Text1.Move 0, Label1.Height, ScaleWidth, _
ScaleHeight - Label1.Height
Else
Text1.Move 0, 0, ScaleWidth, ScaleHeight
End If
End Sub
The On Error statement
serves to protect your application from errors that occur when the ActiveX control
is shorter than the Label1 constituent control. The preceding code must execute
also when the Caption property changes, so you need to add a statement
to its Property Let procedure:
Public Property Let Caption(ByVal New_Caption As String)
Label1.Caption = New_Caption
PropertyChanged "Caption"
Call UserControl_Resize
End Property
The UserControl
Object
The UserControl object is
the container in which constituent controls are placed. In this sense, it's akin
to the Form object, and in fact it shares many properties, methods, and events
with the Form object. For example, you can learn its internal dimension using
the ScaleWidth and ScaleHeight properties, use the AutoRedraw
property to create persistent graphics on the UserControl's surface, and
add a border using the BorderStyle property. UserControl objects also
support all the graphic properties and methods that forms do, including Cls,
Line, Circle, DrawStyle, DrawWidth, ScaleX,
and ScaleY.
UserControls support most
of the Form object's events, too. For example, Click, DblClick,
MouseDown, MouseMove,and MouseUp events fire when
the user activates the mouse over the portions of UserControl's surface that
aren't covered by constituent controls. UserControl objects also support KeyDown,
KeyUp,and KeyPress events, but they fire only when no constituent
control can get the focus or when you set the UserControl's KeyPreview property
to True.