ActiveX controls

Returning UDTs

ActiveX controls can expose properties and methods that return user-defined types or that accept UDTs as arguments. Because ActiveX controls are in-process COM components, you can always marshal UDTs regardless of the operating system version. For more details, see the "Passing Data Between Applications" section of Chapter 16.

This feature hasn't been completely ironed out, however. You can't use a property that returns a UDT in a With block without crashing the Visual Basic environment. I hope this bug will be fixed in a future service pack.

Special OLE data types

Properties can also return a few special data types. For example, the Wizard declares all the color properties using the OLE_COLOR type, as in this code:

Public Property Get BackColor() As OLE_COLOR
    BackColor = Text1.BackColor
End Property

When a property is declared as returning an OLE_COLOR value, programmers can pick its value from a palette of colors in the Properties window, exactly as they can with the ForeColor and BackColor properties of Visual Basic's own controls. For any other purpose, an OLE_COLOR property is treated internally as a Long.

Visual Basic supports three other special data types:

  • OLE_TRISTATE is used for CheckBox-like controls that can be in three states. This enumerated property can return the values 0-Unchecked, 1Checked, and 2-Gray.

  • OLE_OPTEXCLUSIVE is used for OptionButton-like controls. When you build an ActiveX control that must behave like an OptionButton, you should have it expose a Value property of type OLE_OPTEXCLUSIVE and make it the default property for the control. The container ensures that when the Value property of one control in a group is assigned the True value, the Value properties of all other controls in the group are automatically set to False. (You need to call the PropertyChanged method in the property's Property Let procedure to have this mechanism work correctly.)

  • OLE_CANCELBOOL is used for the Cancel argument in event declarations when you want to give clients the opportunity to cancel the event notification.

Procedure IDs

A few ActiveX control properties have special meanings. You define such special properties by assigning specific procedure IDs in the Advanced section of the Procedure Attributes dialog box.

As I already explained in the "Attributes" section of Chapter 6, you can make a property or a method the default member of a class by typing 0 (zero) or by selecting the (default) option from the list in the Procedure ID field. An OLE_ OPTEXCLUSIVE property must be the default property to have the ActiveX control correctly behave like an OptionButton control.

If you have a Text or Caption property, you should assign it the Text or Caption procedure ID, respectively. These settings make these properties behave as they do in Visual Basic: When the programmer types their values in the Properties window, the control is immediately updated. Behind the scenes, the Properties window calls the Property Let procedure at each key press instead of calling it only when the programmer presses the Enter key. You can use these procedure IDs for any property, regardless of its name. However, your control can't have more than two properties that behave in this way.

Tip #1

Because you can select only one item in the procedure ID field, it seems to be impossible to duplicate the behavior of Visual Basic's TextBox and Label controls, which expose a Text or Caption property that's immediately updated by the Properties window and is the default property at the same time. You can work around this problem by defining a hidden property, make it the default property, and have it delegate to the Text or Caption property:

' Make this property the default property, and hide it.
Public Property Get Text_() As String
    Text_ = Text
End Property

Public Property Let Text_(ByVal newValue As String)
    Text = newValue
End Property

You should assign the Enabled procedure ID to the Enabled property of your ActiveX control so that it works correctly. This is a necessary step because the Enabled property behaves differently from any other property. When you disable a form, the form also disables all its controls by setting their Extender's Enabled property to False (so that controls appear disabled to the running code), but without setting their inner Enabled properties to False (so that controls repaint themselves as if they were enabled). To have Visual Basic create an Extender's Enabled property, your UserControl module must expose a Public Enabled property marked with the Enabled procedure ID:

Public Property Get Enabled() As Boolean
    Enabled = Text1.Enabled
End Property

Public Property Let Enabled(ByVal New_Enabled As Boolean)
    Text1.Enabled() = New_Enabled
    PropertyChanged "Enabled"
End Property

The ActiveX Control Interface Wizard correctly creates the delegation code, but you have to assign the Enabled procedure ID manually.

Finally, you can create an About dialog box for displaying copyright information about your control by adding a Public Sub in its UserControl module and assigning the AboutBox procedure ID to it:

Sub ShowAboutBox()
    MsgBox "The SuperTextBox control" & vbCr _
        & "(C) 1999 Francesco Balena", vbInformation
End Sub

When the ActiveX control exposes a method with this procedure ID, an (About)item appear in the Properties window. It's common practice to hide this item so that programmers aren't encouraged to call it from code.

The Procedure Attributes dialog box

A few more fields in the Procedure Attributes dialog box are useful for improving the friendliness of your ActiveX controls. Not one of these setting affects the functionality of the control.

I've already described the Don't Show In Property Browser field in the "Design-Time and Run-Time Properties" section earlier in this chapter. When this check box is selected, the property won't appear in the Properties window at design time or in the Locals window at run time.

The Use This Page In The Property Browser combo box lets you associate the property with one generic property page provided by Visual Basic (namely StandardColor, StandardDataFormat, StandardFont, and StandardPicture) or with a property page that's defined in the ActiveX control project. When a property is associated with a property page, it appears in the Properties window with a button that, when clicked, brings up the property page. Property pages are described later in this chapter.

Use the Property Category field to select the category under which you want the property to appear in the Categorized tab of the Properties window. Visual Basic provides several categories-Appearance, Behavior, Data, DDE, Font, List, Misc, Position, Scale, and Text-and you can create new ones by typing their names in the edit portion of this combo box.

The User Interface Default attribute can have different meanings, depending on whether it's applied to a property or to an event. The property marked with this attribute is the one that's selected in the Properties window when you display it after creating the control. The event marked with the User Interface Default attribute is the one whose template is built for you by Visual Basic in the code window when you double-click the ActiveX control on the form's surface.

Limitations and workarounds

Creating ActiveX controls based on simpler constituent controls is an effective approach, but it has its limits as well. The one that bothers me most is that there's no simple way to create controls that expand on TextBox or ListBox controls and correctly expose all of their original properties. Such controls have a few properties-for example, MultiLine, ScrollBars, and Sorted-which are read-only at run time. But when you place an ActiveX control on a form at design time, the ActiveX control is already running, so you can't modify those particular properties in the Properties window of the application that's using the control.

You can use a few tricks to work around this problem, but none of them offers a definitive solution. For example, sometimes you can simulate the missing property with code, such as when you want to simulate a ListBox's Sorted property. Another well-known trick relies on an array of constituent controls. For example, you can implement the MultiLine property by preparing both a single-line and multiline TextBox controls and make visible only the one that matches the current property setting. The problem with this approach is that the number of needed controls grows exponentially when you need to implement two or more properties in this way. You need 5 TextBox controls to implement the MultiLine and ScrollBars properties (one for single-line TextBox controls and 4 for all the possible settings of the ScrollBar property), and 10 TextBoxes if you also want to implement the HideSelection property.

A third possible solution is to simulate the control that you want to implement with simpler controls. For example, you can manufacture a ListBox-like ActiveX control based on a PictureBox and a companion VScrollBar. You simulate the ListBox with graphic methods of the PictureBox, so you're free to change its graphic style, add a horizontal scroll bar, and so on. Needless to say, this solution isn't often simple.

I want merely to hint of a fourth solution, undoubtedly the most complex of the lot. Instead of using a Visual Basic control, you create a control from thin air using the CreateWindowEx API function. This is the C way, and following this approach in Visual Basic is probably even more complicated than working in C because the Visual Basic language doesn't offer facilities, such as pointers, that are helpful when you're working at such a low level.

After hearing all these complaints, you'll be happy to know Visual Basic 6 has elegantly solved the problem. In fact, the new Windowless control library (described in Chapter 9) doesn't expose a single property that's read-only at run time. The only drawback of this approach is that in that library controls don't expose an hWnd property, so you can't augment their functionality using API calls, which I describe in the Appendix.

Container Controls

You can create ActiveX controls that behave like container controls, as PictureBox and Frame controls do. To manufacture a container control, all you have to do is set the UserControl's ControlContainer property to True. Keep in mind, however, that not all host environments support this feature. If the container doesn't support the ISimpleFrame interface, your ActiveX control won't be able to contain other controls, even if it works normally as far as other features are concerned. Visual Basic's forms support this interface, as do PictureBox and Frame controls. In other words, you can place an ActiveX control that works as a container inside a PictureBox or Frame control, and it will work without a glitch.

You can place controls on a container control both at design time (using drag-and-drop from the ToolBox) or at run time (through the Container property). In both cases, the ActiveX control can find out which controls are placed on its surface by querying its ContainedControls property. This property returns a collection that holds references to the Extender interface of the contained controls.

On the companion CD, you'll find a simple container ActiveX control named Stretcher, which automatically resizes all the contained controls when it's resized. The code that implements this capability is unbelievably simple:

' These properties hold the previous size of the control.
Private oldScaleWidth As Single
Private oldScaleHeight As Single

' To initialize the variables, you need to trap both these events.
Private Sub UserControl_InitProperties()
    oldScaleWidth = ScaleWidth
    oldScaleHeight = ScaleHeight
End Sub

Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
    oldScaleWidth = ScaleWidth
    oldScaleHeight = ScaleHeight
End Sub

Private Sub UserControl_Resize()
    ' When the UserControl resizes, move and resize all container controls.
    Dim xFactor As Single, yFactor As Single
    ' Exit if this is the first resize.
    If oldScaleWidth = 0 Then Exit Sub
    ' This accounts for controls that can't be resized.
    On Error Resume Next
    ' Determine the zoom or factor along both axis.
    xFactor = ScaleWidth / oldScaleWidth
    yFactor = ScaleHeight / oldScaleHeight
    oldScaleWidth = ScaleWidth
    oldScaleHeight = ScaleHeight
    
    ' Resize all controls accordingly.
    Dim ctrl As Object
    For Each ctrl In ContainedControls
        ctrl.Move ctrl.Left * xFactor, ctrl.Top * yFactor, _
            ctrl.Width * xFactor, ctrl.Height * yFactor
    Next
End Sub

The ContainedControls collection includes only the contained controls that had been placed directly on the UserControl's surface. For example, if the ActiveX control contains a PictureBox, which in turn contains a TextBox, the PictureBox appears in the ContainedControls collection but the TextBox doesn't. Using Figure 17-8 as a reference, this means that the preceding code stretches or shrinks the Frame1 control contained in the Stretcher ActiveX control, but not the two OptionButton controls inside it. To have the resizing code work as well for the innermost controls, you need to modify the code in the UserControl_Resize event procedure as follows (added statements are in boldface):

Dim ctrl As Object, ctrl2 As Object
    For Each ctrl In ContainedControls
        ctrl.Move ctrl.Left * xFactor, ctrl.Top * yFactor, _
            ctrl.Width * xFactor, ctrl.Height * yFactor
        For Each ctrl2 In Parent.Controls
            ' Look for controls on the form that are contained in Ctrl.
            If ctrl2.Container Is ctrl Then
                ctrl2.Move ctrl2.Left * xFactor, ctrl2.Top * yFactor,_
                    ctrl2.Width * xFactor, ctrl2.Height * yFactor
            End If
        Next
    Next


Figure 17-8.
The Stretcher ActiveX control resizes all its contained controls, both at design time and at run time.

You should know a few other bits of information about container ActiveX controls authored in Visual Basic:

  • If the host application doesn't support container controls, any reference to the ContainedControls property raises an error. It's OK to return errors to the client, except from within event procedures-such as InitProperties or Show-because they would crash the application.

  • The ContainedControls collection is distinct from the Controls collection, which gathers all the constituent controls on the UserControl. If a container ActiveX control contains constituent controls, they'll appear on the background, below all the controls that the developer put on the UserControl's surface at design time.

  • Don't use a transparent background with container controls because this setting makes contained controls invisible. (More precisely, contained controls will be visible only on the areas where they overlap a constituent control.)

A problem with container controls is that the UserControl module doesn't receive any events when a control is added or removed at design time. If you need to react to these actions-for example, to automatically resize the contained control-you must use a Timer control that periodically queries the ContainedControls.Countcollection. While this approach isn't elegant or efficient, you usually need to activate the Timer only at design time, and therefore you experience no impact on the run-time performance.

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.

“God could create the world in six days because he didn't have to make it compatible with the previous version.”