ActiveX controls

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.

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.

“To iterate is human, to recurse divine” - L. Peter Deutsch