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.