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