>
If I'm doing my job right, by now you should be beginning to feel comfortable with the way ActiveX controls fit together. You should understand the different states in which a control exists. You should be familiar with the main events in the life of a control and a few of the most important methods and properties.
Chapter 19, which deals with the way you define properties and events in your control, is going to be extremely important when it comes to implementing your controls. You can think of this chapter, perhaps, as a long introduction to the next one. It's a chapter in which we'll introduce the rest of the fundamental building blocks on which a control is based. For if the UserControl object forms the core of your ActiveX control, then surely the Extender and Ambient objects define the environment in which it lives.
Most of the properties of the Extender object are already familiar to you-they are found in almost every Visual Basic form or control. The purpose of this section is not to tell you about those properties or how they are used from the developer's perspective. Instead, the purpose is to show you how to deal with them from a control author's perspective.
To begin, try creating a new control, closing its designer, and adding it to a form. Do not add any properties-this is a completely empty control.
When you look at the properties of the control in the container's design time, depending on the property settings of the UserControl object, you will see the properties shown in the following table
.
Extender Properties for an Empty ActiveX Control | Comments |
Name | Standard |
Align | Only appears if the control's Alignable property is True |
Cancel | Standard-only appears if the control's DefaultCancel property is True |
DataBinding DataChanged DataField DataSource | Only appears if the control has a property that is bound to a database. See Chapter 19. |
Default | Standard-only appears if the control's DefaultCancel property is True |
DragIcon | |
DragMode | |
Enabled | Only appears if the control has an Enabled property-more on this later |
Height | |
HelpContextId | Only appears if the control's CanGetFocus property is True |
Index | |
Left | Standard |
Negotiate | Only appears if the control's Alignable property is True |
TabIndex | Only appears if the control's CanGetFocus property is True |
Tabstop | Only appears if the control's CanGetFocus property is True |
Tag | |
ToolTipText | |
Top | |
Visible | Standard |
WhatsThisHelpID | |
Width | Standard |
The following items about this table should be noted.
In addition to these properties, Visual Basic provides the following Extender properties that can be accessed by a developer at runtime.
Runtime-Only Extender Properties, Methods, and Events | Comments |
Container property | The container (typically a form) that contains the control |
Object property | References the underlying object, not the extender |
Parent property | Standard-the container of the control. The UserControl object also provides a Parent property, which is always available regardless of the container. |
Drag method | |
Move method | |
SetFocus method | |
ShowWhatsThis method | |
Zorder method | |
DragDrop event | |
GotFocus event | Only appears if control's CanGetFocus property is True |
LostFocus event | Only appears if control's CanGetFocus property is True |
DragOver event |
From the point of view of the developer (the person using your control), there is no difference between Control properties and Extender properties. They appear as a single interface. How is this possible? When you, as a developer, reference a control, you are in fact referencing the control's extender. The Extender object reflects the properties and methods of the object itself. This is shown in Figure 18.1.
Figure 18.1 : The relationship between the control and the extender.
There are some things you must keep in mind with respect to the Extender object:
The ch18tst1.vbg program group contains two projects: ch18ctls.vbp project contains a number of controls that demonstrate the techniques described in this chapter. The ch18tst1.vbp project is a test program that exercises those controls.
The ch18ctla control is completely empty except for two functions that allow you to access the control's internal Extender and Me objects using the following code:
Public Function InternalExtender() As Object Set InternalExtender = UserControl.Extender End Function Public Function InternalMe() As Object Set InternalMe = Me End Function
You should not generally expose these objects in this manner, but it is useful for test purposes. Place the control on a form, add a command button, and add the following code to its Click event:
Private Sub Command1_Click() If ch18CtlA1.Container Is ch18CtlA1.Parent Then Debug.Print "Parent and container are the same" Else Debug.Print "Parent and container are not the same" End If If ch18CtlA1 Is ch18CtlA1.Object Then Debug.Print "Control and control's Object are the same" Else Debug.Print "Control and control's Object are different" End If If ch18CtlA1.Object Is ch18CtlA1.InternalMe Then Debug.Print "Extender's object property is the control's Me" Else Debug.Print "Extender's object property is not the control's Me" End If If ch18CtlA1 Is ch18CtlA1.InternalExtender Then Debug.Print "Control is the control's extender" Else Debug.Print "Control is not the control's extender" End If End Sub
The results are as follows:
From this, we can generate Table 18.1, which shows how to access
the various properties as both a control author and a developer
using the control.
Object | Access as a Developer | Access as a Control Author |
Control myctl Extender | myctl | Usercontrol.Extender |
Control myctl object itself (not the extender) | myctl.Object | Me |
Container of control "myctl" | myctl.Parent myctl.Container | UserControl.Extender.Container UserControl.Extender.Parent UserControl.Parent |
You are already quite familiar with the general rules for scoping of variables, properties, and methods. When you define an item with the same name as an item that exists at a higher level, the other item is hidden. Thus if you have a global variable named x and define x in a procedure, the global one is hidden until the procedure exits.
The same principle applies with Control properties as they relate to Extender properties. For example: say you add a Tag property to your control. The VB extender already has a Tag property that it manages. Since the developer accesses the extender, that property takes priority. This is demonstrated in the ch18tst1.vbp project with the ch18ctlb.ctl control. This control implements its own Tag property and member variable using the following code:
Public Property Get Tag() As String Tag = m_Tag End Property Public Property Let Tag(ByVal vNewValue As String) m_Tag = vNewValue Debug.Print "Setting internal tag" Report End Property Public Sub Report() Debug.Print "Internal Tag variable is " & m_Tag Debug.Print "Internal Tag property is " & Tag Debug.Print "Extender Tag is " & Extender.Tag End Sub
The test code in form frmch18test1 is attached to a second command button as follows:
Private Sub Command2_Click() ch18CtlB1.Tag = "Setting tag Property" ch18CtlB1.Object.Tag = "Setting object tag property" End Sub
The results are as follows:
Setting internal tag Internal Tag variable is Setting object tag property Internal Tag property is Setting object tag property Extender Tag is Setting tag Property
From this we can conclude that there are two separate Tag variables-one
implemented and belonging to the control, the other by the container.
You can access these variables as shown in Table 18.2.
Tag Being Accessed | Access As Developer | Access As Author |
Extender Tag property managed by Visual Basic | ch18CtlB1.Tag | UserControl.Extender.Tag |
Internal Tag property managed by your control | ch18CtlB1.Object.Tag | Tag Me.Tag |
Why would you ever want to define a property, such as Tag, which is already implemented by the container? Because you may want to guarantee that people using your control have access to a Tag property for the control, and you cannot be sure that your control's container will always support a Tag property on its extender.
The biggest catch to using Extender properties is that you can never be certain whether or not they are actually present. This means that unless you want your control to be limited to Visual Basic or a subset of containers, you must take precautions when accessing Extender properties so your control degrades gracefully. There are two approaches for doing this.
The error trapping approach is obvious. The second approach can be handled in two ways. You can create a routine that attempts to access a property and records the results. The following code from the ch18ctlb.ctl control shows how this can be accomplished:
Private TagIsPresent As Boolean Private xyzIsPresent As Boolean Private zzzispresent As Boolean Private Sub CheckExtender() Dim testnumber As Integer Dim v As Variant TagIsPresent = True xyzIsPresent = True zzzispresent = True On Error GoTo itp1 testnumber = 0 v = Extender.Tag testnumber = 1 v = Extender.xyz testnumber = 2 v = Extender.zzz Exit Sub itp1: Select Case testnumber Case 0 TagIsPresent = False Case 1 xyzIsPresent = False Case 2 zzzispresent = False End Select Resume Next End Sub
The routine simply checks the desired properties one by one, setting a module level flag to indicate which ones fail. A simpler version of this routine could use a Boolean array, where each entry corresponds to a particular Extender property.
An even easier approach uses the apigid32.dll dynamic link library included with this book. This DLL was written originally for the Visual Basic Programmer's Guide to the Win32 API and contains a variety of useful low-level functions, one of them being the function called agIsValidName. The function is declared as follows:
Declare Function agIsValidName& Lib "apigid32.dll" (ByVal o As Object, ByVal_ lpname$)
Its use is demonstrated in the following code from the ch18ctlb.ctl control:
Debug.Print "Tag property present: " & agIsValidName(Extender, "Tag") Debug.Print "xyz property present: " & agIsValidName(Extender, "xyz")
This function is actually quite simple internally. It simply calls the GetIDsOfNames method on the object's dispatch interface to see if it gets a valid return value. As you may recall from Part 1 of this book, Visual Basic 5.0 requires that all interfaces it uses be either dispatch interfaces or dual interfaces that contain the dispatch interface functions along with those you define.
Which approach should you take? It's really up to you. I tend to use the latter because most of the controls I create take advantage of Desaware's tools (which is not surprising, since those tools were created primarily for me and my staff to be able to create more powerful controls in the first place). Since we're distributing apigid32.dll anyway (or one of its big brothers, which also contain the IsValidName function), there is no additional cost in terms of resources or performance in using the DLL function.
The Extender properties that are made available for a particular control may depend on settings in the control itself. These dependencies are entirely up to the container. The following are the most important of these dependencies that you should be aware of as a Visual Basic programmer.
The Enabled property is strange. To see why, load the EnTest1.vbp project in the Chapter 18 samples directory on your CD-ROM. This project contains a single control called EnCtl1.ctl, which contains a command button and a public Enabled property. The code for this control is as follows:
Option Explicit Public Property Get Enabled() As Boolean Enabled = UserControl.Enabled End Property Public Property Let Enabled(ByVal New_Enabled As Boolean) UserControl.Enabled() = New_Enabled PropertyChanged "Enabled" End Property 'Load property values from storage Private Sub UserControl_ReadProperties(PropBag As PropertyBag) UserControl.Enabled = PropBag.ReadProperty("Enabled", True) End Sub 'Write property values to storage Private Sub UserControl_WriteProperties(PropBag As PropertyBag) Call PropBag.WriteProperty("Enabled", UserControl.Enabled, True) End Sub
This code should be quite familiar to you by now-it is the standard way in which a public property is implemented and persisted. The UserControl object's internal Enabled property is set according to the public variable. A Report function has been added to the control. It prints the state of the Enabled property, the UserControl enabled property, and the Command1 enabled property to the Immediate window as follows:
Public Sub Report() Debug.Print "Within control code - " Debug.Print "Enabled property is: " & Enabled Debug.Print "UserControl.Enabled property is: " & _ UserControl.Enabled Debug.Print "Command1.Enabled is: " & Command1.Enabled Debug.Print "Extended Enabled is: " & Extender.Enabled Debug.Print "End within control code." End Sub
The project contains a single form, enForm1, which contains a timer and the EnCtl1 control. During the Timer control's Timer event, the following trace routine is called:
Private Sub Timer1_Timer() Debug.Print "Enabled property is: " & Enabled Debug.Print "Control's Enabled property is: " & _ EnableTestControl.Enabled Debug.Print "Report: "; EnableTestControl.Report End Sub
The EnCtl1 control has two properties set in the VB property window for the form: The Enabled property is set to True, and the Name property is set to EnableTestControl. The Enabled property of the form is set to False.
Now, before you run this sample, let's take a moment and review
all of the different Enabled properties we are actually dealing
with. In the control (control author's point of view) the properties
are these:
Syntax | What You Are Accessing |
Enabled | The public Enabled property for the control. |
UserControl.Enabled | The UserControl object Enabled property. |
Command1.Enabled | The Enabled property of the constituent command button on the control. |
Extender.Enabled | The Enabled property provided by the container as part of the Extender object. Due to the scoping rules described earlier, this is the property seen by developers using the control unless they explicitly access the control's Enabled property using the Object property and the syntax EnableTestControl.Object.Enabled. |
From the container (application developer's point of view):
Syntax | What You Are Accessing |
Enabled | The Enabled property for the form (actually, the Enabled property for the form's Extender object). |
EnableTestControl. Enabled | The Enabled property provided by the container as part of the Extender object for the control. |
EnableTestControl. Object.Enabled | The public Enabled property for the control. (Developers should never access this property.) |
When you run the application, you will see the following displayed in the Debug window.
Enabled property is: False Control's Enabled property is: False Report: Within control code - Enabled property is: True UserControl.Enabled property is: True Command1.Enabled is: True Extended Enabled is: False End within control code.
The Enabled property for the form is disabled. This makes sense because that is what was set for the form in the VB Property window.
The control's Enabled property, which is on the Extender object, is False. Keep in mind that it was set to True at design time. But the public Enabled property for the control, along with that of the UserControl object and the command button within the control, are all True! What's going on? Why is the control's Extender Enabled property False while the Control properties are actually enabled?
This occurs because of a convention specific to Visual Basic. First, consider how Windows works. When you disable a window, all of its child windows continue to appear as enabled. They will not receive keystrokes or mouse input because their parent form is disabled. If you want them to appear as disabled, you must disable them individually. This is similar to the situation we saw in Chapter 17 with regards to visibility, where controls on a form can have their Visible properties set to True, yet they are obviously hidden when their container is hidden.
In this example we disabled the form but did not disable the control. The developers of Visual Basic, for reasons that are probably lost in history and certainly undocumented, decided that when a container is disabled, the controls it contains should read as disabled as well (even though they have not been set explicitly to disabled). They could not, however, just disable the controls. Doing so would change their appearance, violating the standard behavior of Windows applications. So they let the controls remain enabled (as far as they were concerned), but rigged it so the Enabled property on the Extender object for controls on a disabled container would return False.
Now, in order for this trick to work, there is one important requirement. There has to be a way to let Visual Basic know which property is the Enabled property. It's not enough to assume that the property will actually be named Enabled-the control author may choose to localize the property name for different languages or use a different term entirely. How can you, as a control author, let Visual Basic know which of your properties is the Enabled property for the control?
The solution lies in one of the characteristics of the IDispatch interface discussed back in Part 1 of this book. Remember how each property in the interface has a unique dispatch identifier number (Dispatch ID or DispID)? You may also recall that negative Dispatch ID numbers are defined by COM to indicate certain standard properties. These values do not affect the behavior of the interface or property itself but may be used by a container to provide special handling for properties if it chooses to do so. Dispatch ID number -514 is the identifier number for the Enabled property.
You can use the VB Tools, Procedure Attributes menu command to bring up the Procedure Attributes dialog box. Select the Enabled property and click the Advanced button. In the Procedure ID combo box you will see that Enabled is selected. When the Enabled procedure ID is selected, Visual Basic automatically sets the Dispatch ID for the property to -514.
When you create a blank control with no properties, Visual Basic does not create an Enabled property on the Extender object by default. However, if you assign any of the control's properties to the Enabled procedure ID, Visual Basic will add an Enabled property to the extender for the control. When you set the Enabled property using code or the VB property window, Visual Basic will also set the property that you specified with this ID, regardless of the actual name of the property.
What happens if you add an Enabled property to the control but do not set the Procedure ID for the property to Enabled? Visual Basic does not create an Enabled property on the Extender! The Enabled property you see in the VB Property window for the control is the control's Enabled property.
If you set the Procedure ID for the EnCtl1.ctl control to (None) and run the program again, you will see the following debug messages in the Debug window (you may need to save and reload your project for the system to reset itself properly after this change):
Enabled property is: False Control's Enabled property is: True Report: Within control code - Enabled property is: True UserControl.Enabled property is: True Command1.Enabled is: True Extended Enabled is: True End within control code.
As you can see, there is no difference between the Enabled property as seen by the developer using the control and that seen by the control's author.
To conclude: If you want your control to exhibit the standard Visual Basic behavior for the Enabled property, you must do the following:
If you set the Alignable property in your control to True, the extender will display an Align property and possibly a Negotiate property as well (depending on the container).
Your control can read the extender's Align property to determine whether your control has been aligned. You need not take any action in this case; the container is responsible for repositioning your control.
If your control uses a Toolbar control to implement a toolbar, the Negotiate property is provided to allow the developer to determine how the toolbar is displayed on an MDI form. Refer to your Visual Basic documentation for information on using the Toolbar control provided with Visual Basic.
If you set the DefaultCancel property of your control to True, Visual Basic will add a Default and Cancel property to your control's extender. You can then read these Extender properties to determine whether your control has been set to be the Default or Cancel control for the container.
It is customary for a Default control to show a different appearance from a standard control of the same type. You could look at the default Extender property, but Visual Basic provides an easier way to determine when this is necessary. The Ambient object has a DisplayAsDefault property that is set to True when your control should appear in its default state. Why use the Ambient property instead of the extender?
The ch18CtlB.ctl control demonstrates this using the following code:
Private Sub UserControl_AmbientChanged(PropertyName As String) If PropertyName = "DisplayAsDefault" Then UserControl.Refresh End If End Sub Private Sub UserControl_GotFocus() Label2.Caption = "Has Focus" End Sub Private Sub UserControl_LostFocus() Label2.Caption = "Lost Focus" End Sub Private Sub UserControl_Paint() Dim s$ If Extender.Default Then s$ = "Default" End If If Extender.Cancel Then s$ = s$ & " Cancel" End If If s$ = "" Then s$ = "Normal" Label1.Caption = s$ End Sub
The control contains two constituent Label controls that are used to display the current state of the control. Open the frmCh18Test2 form in design mode and try changing the control's Default and Cancel property in the VB Property page (keep in mind that by doing so you are directly changing the Extender properties). When you change the Default property, the control will update immediately. When you change the Cancel property you will not see the change until the control is redrawn (try changing the size of the control after changing the Cancel property to see this).
Is there a way to determine when an Extender property has changed? I don't know of any, other than polling. If you find one, please let me know.
Allow me to close this section with two warnings. Visual Basic's Extender control and design environment is exceptionally robust. Other containers are likely to have fewer Extender properties and less sophisticated support for features you may have incorporated into your control. You should review all use of Extender properties to make sure that your control will still work if they are missing. There are a number of options for how to handle different containers with regard to Extender properties:
The container and parent properties give you access to the Container object and all of its properties and methods. This means your control can do the following:
I could probably go on for pages with variations on the possibilities. It is possible for controls to be terribly nasty to an application.
It is fundamental to the philosophy of components that the developer using your control be responsible for the container. The developer is also responsible for setting and using Extender properties and events (since those are provided by the container). As a control author, keep your hands off the container and other controls!
It is sometimes easy to get confused between Extender and Ambient properties. Like so many aspects of ActiveX control development, the difference between them is a matter of perspective. Extender properties are those that are added to your control by the container and are intended to be used primarily by developers who are using your control to create applications. Ambient properties are those that are provided by the container and are intended to be used by control authors.
Ambient properties are accessed through the AmbientProperties object, which is accessed via the Ambient property of the UserControl object for your control. The AmbientProperties object always includes a core set of standard properties, unlike the Extender object, whose properties vary based on the container and control configuration. However, this does not mean that all of the standard Ambient properties are, in fact, supported by each container. If an Ambient property is not supported by the container, the AmbientProperties object simply returns a default value for the property.
Consider, for example, the UserMode property. If a container does not support a design-time mode, it need not provide a UserMode ambient property. The AmbientProperties object sees that the property is missing in the container and always returns a default value of True for its own UserMode property.
A container can provide additional container-specific Ambient
properties. These properties are always late bound, and you should
use error trapping when accessing them. Table 18.3 shows the standard
properties for the Ambient object. The default value shown indicates
the value returned for the property when it is not supported by
the container. The UserControl object's AmbientChanged event is
triggered any time an Ambient property is changed.
Property | Comments | Default Value |
BackColor | Background color of the container. | &H80000005 |
DisplayAsDefault | If the control's DefaultCancel property is True, and the developer specifies that this is the default control, this property returns True. Refer to the description of the DefaultCancel property in Chapter 17. | False |
DisplayName | The name of the control assigned by the developer. | "" |
Font | The font of the container, or the default font recommended by the container for use by the control. Note that the container's FontTransparent property does not trigger an AmbientChanged event. | MS Sans Serif 8 |
ForeColor | Foreground color of the container. | &H80000008 |
LocaleID | See chapter text. | Current system default |
MessageReflect | See chapter text. | False |
Palette | A Picture property specifying the container-recommended palette (usually the palette in use by the container). | |
RightToLeft | Indicates that the control should draw text from right to left on Hebrew or Arabic versions of Windows. | False |
ScaleUnits | The name of the units in use by the container. See chapter text. | |
ShowGrabHandles | See chapter text. | True |
ShowHatching | See chapter text. | True |
SupportsMnemonics | See chapter text. | False |
TextAlign | Indicates the container's text alignment or the default text alignment for the control. | Zero |
UserMode | True indicates that the control is in end-user mode, which means runtime for Visual Basic. | True |
UIDead | See chapter text. | False |
The following Ambient properties are worthy of further comment.
The MessageReflect, ShowGrabHandles, ShowHatching, SupportsMnemonics, and UIDead ambient properties are important to controls but are handled entirely by Visual Basic's implementation of ActiveX controls. You can safely ignore them. Here is a short description of these properties in case you are interested:
This property can be used to determine the scale units of the container. When Visual Basic is the container, ScaleUnits can take the values User, Twip, Point, Pixel, Character, Inch, Millimeter, Centimeter; these are the constant names of the ScaleMode property.
The possible values of this property are not defined by the ActiveX specification. Thus, containers can use any strings they wish. This means that this property may not be reliable under all containers for determining the actual scale mode of the container. It is intended primarily for controls to be able to display the container coordinate system when necessary.
When Visual Basic is the container, you can also access the container's ScaleMode property directly through the Extender object's Container property.
A Locale is a 32-bit value that identifies the language and platform for the thread or system. Bits 0 through 15 (the low-order word) identify the language. Bits 16 through 19 specify how sorting works under this language. It is typically 0 but may be set to 1 for Far East Unicode environments. Figure 18.2 shows the structure of a Locale identifier.
Figure 18.2 : The Locale identifier.
The low-order word is divided into two parts. The low 10 bits (bits 0 through 9) indicate the language. The high 6 bits (bits 10 through 15) indicate a subset of the language-to differentiate, for example, between U.S. English and U.K. English. The values for the supported languages can be found in any good Win32 API declaration file: Look for the constants with the prefix LANG_. The subsets, or sublanguage constants, are identified with the prefix SUBLANG_. Many functions take the default system or user locales, which are given the special constant values
Public Const LOCALE_SYSTEM_DEFAULT = &H800 Public Const LOCALE_USER_DEFAULT = &H400
A great deal of information is associated with locales. For example, the country name, names of days and months (and their abbreviations), the symbol used for local currency, the format used for dates and times, and other information that tends to vary from country to country. This information can go a long way in helping you create truly international applications using Visual Basic. The subject of locales and the Win32 API functions that are available to work with them are discussed further in the Visual Basic 5.0 Programmer's Guide to the Win32 API.
The UserMode ambient property is probably the most important one. It is almost certainly the one that you will use most often. A setting of True for this property indicates that the control's container is in run mode or End-User mode. False indicates that the container is in a design-time or developer mode. Keep in mind that not every container supports a design-time environment.
The most common uses for this property are:
There are a number of approaches you can use when working with Ambient properties. In order to illustrate this, let's take a close look at the BackColor property.
This is another one of those properties that require you to keep
close track of which object you are considering. The ch18Test1
project group includes the ch18ctlD control, which contains three
constituent Text Box controls. This means that you, as the control
author, have to deal with six different BackColor properties on
six different objects, as shown in Table 18.4.
Property | Description |
Text1.BackColor | Author only-sets the background color of the Text1 control |
Text2.BackColor | Author only-sets the background color of the Text2 control |
Text3.BackColor | Author only-sets the background color of the Text3 control |
UserControl.BackColor | Author only-sets the background color of the ActiveX control |
BackColor | Public-Does whatever you the control author wishes |
Ambient.BackColor | Public-The current background color of the control's container |
This raises some interesting questions. Exactly what should this control look like? What determines the background colors of the various text boxes and the control itself? You have a number of options:
I would not dare to suggest which approach you should take. But I would strongly recommend that you ask yourself these questions when you are designing your control.
The ch18CtlD control demonstrates all of these approaches in a manner that is hopefully educational and definitely unattractive. The public BackColor property defines the background color for the control and the Text1 constituent control. The property functions are as follows:
Public Property Get BackColor() As OLE_COLOR BackColor = UserControl.BackColor End Property Public Property Let BackColor(ByVal New_BackColor As OLE_COLOR) UserControl.BackColor() = New_BackColor Text1.BackColor = UserControl.BackColor PropertyChanged "BackColor" End Property
There are two things to note in this code. First, the property type is OLE_COLOR. This property type is defined by referencing the standard OLE (stdole) library, which is referenced by default by Visual Basic 5.0. The OLE_COLOR variable is a 32-bit long, and variables of this type can be assigned directly to and from Visual Basic numeric variables. Why then should you use the OLE_COLOR type? Because Visual Basic is a very smart container, and when it sees a property with the OLE_COLOR type, it adds the color selection popup to the VB property window for that control.
The other point of interest is that two objects are "connected" to the BackColor property. When the property is set, both the UserControl and the Text1 control's BackColor property are set. The intent in this example is for these two objects to always have the same background color. The property is saved in the UserControl_WriteProperties function with the following code:
Call PropBag.WriteProperty("BackColor", UserControl.BackColor, &H8000000F)
It is read as follows:
UserControl.BackColor = PropBag.ReadProperty("BackColor", &H8000000F) Text1.BackColor = UserControl.BackColor
When writing the property, we can use the value from either the UserControl or the Text1 object, since they are the same. When reading the property we must be sure to set both to the new value. It is also necessary to initialize the property in the UserControl_InitProperties event. There are two approaches for doing so. One is
UserControl.BackColor = PropBag.ReadProperty("BackColor", &H8000000F) Text1.BackColor = UserControl.BackColor PropertyChanged "BackColor"
The other is to simply use
BackColor = &H8000000F
In this case the public BackColor setting serves exactly the same purpose. The advantage to using the public property in this manner is obvious-it allows you to reuse the same code and helps you avoid forgetting some crucial operation (such as calling PropertyChanged or setting all of the values that are necessary). There will be cases, however, where you cannot do this; for example, when you have code or error trapping in the public property setting routine that you do not want called when reading the property settings from a file.
The Text2 constituent control takes a different approach. It is set using an independent property called BackColor2. As such it is completely independent of the other background colors, and its property procedures are quite straightforward:
' Background color for text2 Public Property Get BackColor2() As OLE_COLOR BackColor2 = Text2.BackColor End Property Public Property Let BackColor2(ByVal vNewValue As OLE_COLOR) Text2.BackColor = vNewValue PropertyChanged "BackColor2" End Property
The Text3 constituent control is somewhat more complex. Its background color can be set to follow the container color or set independently using the BackColor3 property. This imposes some additional requirements on the control code:
Listing 18.1 shows the remainder of the control module. This example takes a simple approach to letting the developer choose between the different modes for the Text3 control. It uses a separate property called Text3Ambient, which is True when the Text3 control should follow the Ambient BackColor property value. The value for this property is stored in private variable m_Text3Ambient. A second private variable, m_Text3Backcolor, is used to hold the value of the BackColor3 property.
Another possible approach would be to define an "impossible" color value, meaning the control should return to the ambient background color mode. In this case, you would examine the BackColor3 property Let function parameter. If it was some invalid value, such as &HFFFFFFFF, you would set the m_Text3Ambient property to True instead of setting the m_Text3Backcolor variable. This approach is somewhat awkward, because it requires the developer to remember that the way to reset the control to its ambient background mode is to type -1 or &HFFFFFFFF into the BackColor3 property edit box in the VB Property window. Since colors are usually set using the pop-up palette values, this is not an intuitive approach. A separate variable to control the Text3 control mode is much easier for developers to remember.
Listing 18.1: Partial Listing for ch18ctlD.ctl
' Should we use the ambient or back3color value Private m_Text3Ambient As Boolean Private m_Text3Backcolor As OLE_COLOR Private Sub UserControl_AmbientChanged(PropertyName As String) If PropertyName = "BackColor" Then SetText3Color End If End Sub 'Load property values from storage Private Sub UserControl_ReadProperties(PropBag As PropertyBag) UserControl.BackColor = PropBag.ReadProperty("BackColor", &H8000000F) Text1.BackColor = UserControl.BackColor Text2.BackColor = PropBag.ReadProperty("BackColor2", &H80000005) m_Text3Backcolor = PropBag.ReadProperty("BackColor3", &H80000005) m_Text3Ambient = PropBag.ReadProperty("Text3Ambient", True) SetText3Color ' Update after read End Sub 'Write property values to storage Private Sub UserControl_WriteProperties(PropBag As PropertyBag) Call PropBag.WriteProperty("BackColor", UserControl.BackColor, &H8000000F) Call PropBag.WriteProperty("BackColor2", Text2.BackColor, &H80000005) Call PropBag.WriteProperty("BackColor3", m_Text3Backcolor, &H80000005) Call PropBag.WriteProperty("Text3Ambient", m_Text3Ambient, True) End Sub Public Property Get BackColor3() As OLE_COLOR If Ambient.UserMode Then ' At runtime, return the actual color in use BackColor3 = Text3.BackColor Else BackColor3 = m_Text3Backcolor End If End Property Public Property Let BackColor3(ByVal vNewValue As OLE_COLOR) m_Text3Backcolor = vNewValue PropertyChanged "BackColor3" SetText3Color End Property Public Property Get Text3Ambient() As Boolean Text3Ambient = m_Text3Ambient End Property Public Property Let Text3Ambient(ByVal vNewValue As Boolean) m_Text3Ambient = vNewValue PropertyChanged "Text3Ambient" SetText3Color End Property Private Sub SetText3Color() If m_Text3Ambient Then Text3.BackColor = Ambient.BackColor Else Text3.BackColor = m_Text3Backcolor End If End Sub
The actual setting of the Text3 background color is accomplished using the SetText3Color function. This function needs to be called any time the Text3 background color may change. This includes after the properties are read during the UserControl_ReadProperties event and any time the Text3Ambient or BackColor3 properties are set.
To see the effects of this code, bring up the frmCh18Test3 form in design mode and experiment with the various property values using the VB property window.
As you have seen, the Ambient and Extender objects interact closely with properties that you define for your ActiveX control. But as you will see in the next chapter, you have only begun to delve into issues relating to ActiveX control properties.