ActiveX controls

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.

You might also like...

Comments

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“The first 90% of the code accounts for the first 90% of the development time. The remaining 10% of the code accounts for the other 90% of the development time.” - Tom Cargill