Localising ActiveX
The Ambient.LocaleID
property returns a Long value that corresponds to the locale of the program
that's hosting the ActiveX control. This value lets you display localized messages
in the language of the user-for example, by loading them from a string table,
a resource file, or a satellite DLL. But you must account for some rough edges.
When you compile your application,
the Visual Basic locale becomes the default locale for the application. But the
application that's hosting the control might automatically adapt itself to the
language of the user and change its locale accordingly. Inside the Initialize
event procedure of the UserControl, the siting procedure hasn't completed
yet, so the value returned by the LocaleID ambient property reflects the
default locale of the Visual Basic version that compiled it. For this reason,
if you want to use this property to load a table of localized messages, you should
follow this schema:
Private Sub UserControl_Initialize()
' Load messages in the default (Visual Basic's) locale.
LoadMessageTable Ambient.LocaleID
End Sub
Private Sub UserControl_InitProperties()
' Load messages in the user's locale.
LoadMessageTable Ambient.LocaleID
End Sub
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
' Load messages in the user's locale.
LoadMessageTable Ambient.LocaleID
End Sub
Private Sub UserControl_AmbientChanged(PropertyName As String)
' Load messages in the new user's locale.
If PropertyName = "LocaleID" Then LoadMessageTable Ambient.LocaleID
End Sub
Private Sub LoadMessageTable(LocaleID As Long)
' Here you load localized strings and resources.
End Sub
You need to load the message
in both the InitProperties and ReadProperties event procedures
because the former is invoked when the control is first placed on the form's
surface, whereas the latter is invoked any time the project is reopened or the
application is executed.
Other ambient
properties
The Ambient.ScaleMode
property returns a string corresponding to the unit measure currently used
in the container form (for example, twip). This value might be useful
within messages to the user or the developer. For a way to easily convert from
the form's and UserControl's units, see the section "Converting Scale Units".
The Ambient.DisplayAsDefault
property is useful only within user-drawn controls whose DefaultCancel
property is True. These controls must display a thicker border when their
Default extender property becomes True. You usually trap changes to this
property in the AmbientChanged event.
The Ambient.SupportsMnemonics
property returns True if the environment supports hot keys, such as those
that you indicate in a Caption property using the ampersand character.
Most containers support this feature, but you can improve the portability of
your control if you test this property in the Show event procedure and
filter out ampersand characters in your captions if you find that the environment
doesn't support hot keys.
The Ambient.RightToLeft
property specifies whether the control should display text from right to
left, as it might be necessary under Hebrew or Arabic versions of Windows. All
the remaining ambient properties-namely, MessageReflect, ShowGrabHandles,
ShowHatching, and UIDead-are of no practical use with controls
developed with Visual Basic and can be safely ignored.
Implementing
Features
The UserControl object exposes
many properties, methods, and events that have no equivalent in form modules.
In this section, I describe most of them and briefly hint at items that I examine
in depth later in the chapter.
Managing the
input focus
Understanding how UserControl
objects manage the input focus can be a nontrivial task. Several events are related
to input focus:
-
The UserControl object's
GotFocus and LostFocus events. These events can fire only if
the UserControl doesn't contain any constituent controls that can get the input
focus (typically, a user-drawn UserControl). In most cases, you don't have
to write any code for these events.
-
The constituent controls' GotFocus
and LostFocus events. These events fire when the focus enters or
exits a constituent control.
-
The UserControl's EnterFocus and
ExitFocus events. These events fire when the input focus enters or exits
the UserControl as a whole but don't fire when the focus moves from one constituent
control to another.
-
The Extender's GotFocus and LostFocus
events. These are the events that an ActiveX control activates in its container
application.
The simplest way to see
what actually happens at run time is to create a trace of all the events as they
occur when the user visits the constituent controls by pressing the Tab key.
I created a simple UserControl named MyControl1 with two TextBox constituent
controls on it-named Text1 and Text2-and then added Debug.Print
statements in all the event procedures related to focus management. This
is what I found in the Immediate window (with some remarks manually added later):
UserControl_EnterFocus ' The user has tabbed into the control.
MyControl1_GotFocus
Text1_GotFocus
Text1_Validate ' The user has pressed the Tab key a second time.
Text1_LostFocus
Text2_GotFocus
MyControl1_Validate ' The user has pressed the Tab key a third time.
Text2_LostFocus
UserControl_ExitFocus
MyControl1_LostFocus
... ' The user has pressed Tab several times
UserControl_EnterFocus ' until the focus reenters the UserControl
MyControl1_GotFocus ' and the sequence is repeated.
Text1_GotFocus
As you see, the UserControl
object gets an EnterFocus just before the ActiveX control raises a GotFocus
event in its parent form. Similarly, the UserControl receives an ExitFocus
one instant before the ActiveX control raises a LostFocus in the form.
When the focus shifts from
one constituent control to another, the control that loses the focus receives
a Validate event, but this doesn't happen when the focus leaves the UserControl
module. To force the Validate event of the last control in the UserControl,
you must explicitly call the ValidateControls method in the UserControl's
ExitFocus, which isn't really intuitive. If the ActiveX control includes
several controls, it sometimes doesn't make sense to validate them individually
in their Validate events. Moreover, if you use the ValidateControls
method, you might incorrectly force the validation of a constituent control
when the form is being closed (for example, when the user presses Cancel). For
all these reasons, it's much better to validate the contents of a multifield
ActiveX control only upon a request from the parent form, or more precisely,
in the Validate event that the ActiveX control raises in the parent form.
If the control is complex, you might simplify the life of programmers by providing
a method that performs the validation, as in the following piece of code:
Private Sub MyControl1_Validate(Cancel As Boolean)
If MyControl1.CheckSubFields = False Then Cancel = True
End Sub
|
Tip
The Visual Basic documentation omits an important detail
about focus management inside ActiveX controls with multiple constituent controls.
If the ActiveX control is the only control on the form that can receive the
focus and the user presses the Tab key on the last constituent control, the
focus won't automatically shift on the first constituent control as the user
would expect. So to have such an ActiveX control behave normally, you should
add at least one other control on the form. If you don't want to display another
control, you should resort to the following trick: Create a CommandButton
(or any other control that can get the focus), move it out of sight using
a large negative value for the Left or Top property, and then
add these statements in its GotFocus event procedure:
Private Sub Command1_GotFocus()
MyControl1.SetFocus ' Manually move the focus
' to the ActiveX control.
End Sub
|
Invisible controls
The InvisibleAtRuntime
property permits you to create controls that are visible only at design time,
as are the Timer and CommonDialog controls. When the InvisibleAtRuntime property
is True, the Extender object doesn't expose the Visible property. You
usually want the controls to have a fixed size at design time, and you ensure
this result by using the Size method in the UserControl's Resize event:
Private Sub UserControl_Resize()
Static Active As Boolean
If Not Active Then Exit Sub ' Avoid nested calls.
Active = True
Size 400, 400
Active = False
End Sub
Hot keys
If your ActiveX control
includes one or more controls that support the Caption property, you can
assign each of them a hot key using the ampersand character, as you would do
in a regular Visual Basic form. Such hot keys work as you expect, even if the
input focus isn't currently on the ActiveX control. As an aside, keep in mind
that it's considered bad programming practice to provide an ActiveX control with
fixed captions, both because they can't be localized and because they might conflict
with other hot keys defined by other controls on the parent form.
If your ActiveX control
doesn't include a constituent control with a Caption property, your control
responds to the hot keys assigned to the AccessKeys property. For example,
you might have a user-drawn control that exposes a Caption property and
you want to activate it if the user types the Alt+char key combination,
where char is the first character in the Caption. In this circumstance,
you must assign the AccessKeys property in the Property Let procedure
as follows:
Property Let Caption(New_Caption As String)
m_Caption = New_Caption
PropertyChanged "Caption"
AccessKeys = Left$(New_Caption, 1)
End Property
When the user presses a
hot key, an AccessKeyPressed event fires in the UserControl module. This
event receives the code of the hot key, which is necessary because you can associate
multiple hot keys with the ActiveX control by assigning a string of two or more
characters to the AccessKeys property:
Private Sub UserControl_AccessKeyPress(KeyAscii As Integer)
' User pressed the Alt + Chr$(KeyAscii) hot key.
End Sub
You can create ActiveX controls
that behave like Label controls by setting the ForwardFocus property to
True. When the control gets the input focus, it automatically moves it to the
control on the form that comes next in the TabIndex order. If the ForwardFocus
property is True, the UserControl module doesn't receive the AccessKeyPress
event.
Accessing the
parent's controls
An ActiveX control can access
other controls on its parent form in two distinct ways. The first approach is
based on the Controls collection of the Parent object, as this code example demonstrates:
' Enlarge or shrink all controls on the parent form except this one.
Sub ZoomControls(factor As Single)
Dim ctrl As Object
For Each ctrl In Parent.Controls
If Not (ctrl Is Extender) Then
ctrl.Width = ctrl.Width * factor
ctrl.Height = ctrl.Height * factor
End if
Next
End Sub
The items in the Parent.Controls
collection are all Extender objects, so if you want to sort out the ActiveX control
that's running the code you must compare each item with the Extender property,
not with the Me keyword. The problem with this approach is that it works
only under Visual Basic (more precisely, only under environments for which there
is a Parent object that exposes the Controls collection).
The second approach is based
on the ParentControls property. Unlike the Parent.Controlscollection,
this property is guaranteed to work with all containers. The items in the Parent.Controls
collection contain the parent form itself, but you can easily filter it out by
comparing each reference with the Parentobject (if there is one).
Converting scale
units
In the interaction with
the container application, the code in the ActiveX control often has to convert
values from the UserControl's coordinate system to the parent form's system by
using the ScaleX and ScaleY methods. This is especially necessary
in mouse events, where the container expects that the x and y coordinates
of the mouse are measured in its current ScaleMode. While you can use
the Parent.ScaleMode property to retrieve a Visual Basic form's ScaleMode,
this approach fails if the control is running inside another container-for example,
Internet Explorer. Fortunately, the ScaleX and ScaleY methods also
support the vbContainerPosition constant:
' Forward the MouseDown event to the container, but convert measure units.
Private Sub UserControl_MouseDown(Button As Integer, Shift As Integer, _
X As Single, Y As Single)
RaiseEvent MouseDown(Button, Shift, _
ScaleX(X, vbTwips, vbContainerPosition), _
ScaleY(Y, vbTwips, vbContainerPosition))
End Sub
When you're raising mouse
events from within a constituent control, things are a bit more complicated because
you also need to keep the control's offset from the upper left corner of the
UserControl's surface:
Private Sub Private Sub Text1_MouseDown(Button As Integer, _
Shift As Integer, X As Single, Y As Single)
RaiseEvent MouseDown(Button, Shift, _
ScaleX(Text1.Left + X, vbTwips, vbContainerPosition), _
ScaleY(Text1.Top + Y, vbTwips, vbContainerPosition))
End Sub
The ScaleX and ScaleY
methods support an additional enumerated constant, vbContainerSize, that
you should use when converting a size value (as opposed to a coordinate value).
The vbContainerPosition and vbContainerSize constants deliver different results
only when the container uses a custom ScaleMode. The ActiveX Control Interface
Wizard doesn't address these subtleties, and you must manually edit the code
that it produces.
Other properties
If the Alignable property
is True, the ActiveX control-more precisely, its Extender object-exposes the
Align property. Similarly, you should set DefaultCancel to True
if the control has to expose the Default and Cancel properties.
This setting is necessary when the ActiveX control should behave like a standard
CommandButton and works only if ForwardFocus is False. If the ActiveX
control's Default property is True and the user presses Enter, the click
will be received by the constituent control whose Default property is
also True. If there aren't any constituent controls that support the Default
or Cancel properties, you can trap the Enter or Escape key in the
AccessKeyPress event.
If the CanGetFocus is
False, the UserControl itself can't get the input focus and the ActiveX control
won't expose the TabStop property. You can't set this property to False
if one or more constituent controls can receive the focus. The opposite is also
true: You can't place constituent controls that can receive the focus on a UserControl
whose CanGetFocus property is False.
The EventsFrozen property
is a run-time property that returns True when the parent form ignores events
raised by the UserControl object. This happens, for instance, when the form is
in design mode. At run time, you can query this property to find out whether
your RaiseEvent commands will be ignored so that you can decide to postpone
them. Unfortunately, there's no safe way to find out when the container is again
ready to accept events, but you can learn when a paused program has restarted
by watching for a change in the UIDead property in the AmbientChanged
event.
You can create controls
that can be edited at design time by setting the EditAtDesignTime property
to True. You can right-click on such controls at design time and select the Edit
command to enter edit mode. While the control is in edit mode, it reacts exactly
as it does at run time although it doesn't raise events in its container. (The
EventsFrozen property returns True.) You exit edit mode when you click
anywhere on the form outside the control. In general, writing a control that
can be edited at design time isn't a simple task: for example, you must account
for all the properties that aren't available at design time and that raise an
error if used when Ambient.UserMode returns False.
The ToolboxBitmap property
lets you assign the image that will be used in the Toolbox window. You should
use 16-by-15-pixel bitmaps, but bitmaps of different size are automatically scaled.
You shouldn't use icons because they don't scale well to that dimension. The
lower left pixel in the bitmap defines its transparent color.
The ContainerHwnd property
is available only through code and returns the Windows handle of the ActiveX
control's container. If the control is hosted in a Visual Basic program, this
property corresponds to the value returned by the Extender.Container.hWnd
property.
The UserControl object exposes
a few other properties, which let you create windowless controls, container controls,
and transparent controls. I'll cover them later in this chapter.