Properties are the most commonly used elements of an ActiveX control's interface. They're also fairly easy to implement in Visual Basic.
Much of the behavior of Visual Basic 5.0 properties is not new; the concept of properties existed in Visual Basic 4.0 as an element of classes. However, there are a number of new considerations, new language features, and additional tricks and traps to watch out for as you write properties for your ActiveX control.
You create a property in your control by following these steps:
After you've written Property Let and Property Get procedures, you'll take care of these additional tasks:
The example you'll use in this chapter is the LightBulb control, which you'll find on the CD-ROM accompanying this book. The group file this project is based on is called LightBulbGroup.vbg. The version of LightBulbGroup.vbg in the \Before folder contains a minimal version of the LightBulb control, with a visual design only and no code. The version of LightBulbGroup.vbg in the \After folder contains a version of the LightBulb control that includes all the code examples in this chapter. If you want to step through the examples in this chapter one at a time as a tutorial, start by opening the version of LightBulbGroup.vbg in the \Before folder.
Creating a property generally involves declaring a variable and
creating Property Get and Property Let procedures to read from
and write to the variable.
NOTE |
Properties that are delegated to constituent controls don't require a variable declaration; the state of their properties is stored in the constituent control's property. This concept was introduced in Chapter 2and is covered in more detail later in this chapter. |
This example shows you how to create an Illuminated property for the LightBulb control. The Illuminated property is stored in a Boolean variable, mIlluminated. When the user sets the Illuminated property to True, the LightBulb control displays the illuminated light bulb graphic. When the Illuminated property is set to False, the LightBulb displays the dimmed light bulb graphic.
To implement the Illuminated property, begin by declaring a variable to store the state of the property in the Declarations section of the LightBulb code designer:
Private mIlluminated As Boolean
Declare property variables as Private. This is because you don't
want external procedures to get access to the value in the property.
Rather than giving external procedures access to the variable
directly, you give them access through Property Get and
Property Let procedures. This lets you perform validation
and other actions (such as changing in the graphical portion of
the control) when the variable is accessed or changed.
TIP |
If you're coming to Visual Basic from another programming language, you might be familiar with the terms "reader function" and "writer function." You can think of Property Get as a way to declare a reader function and Property Let and Property Set as ways to declare writer functions |
After you've declared a variable to store your property, you next need to write Property Let and Property Get procedures. (Properties that can be set to object variables require a Property Set instead of a Property Let, but the syntax is essentially the same.)
Here is the skeletal syntax of a Property Let declaration:
Property Let propertyname ([argument_list,] value) . . . Exit Property . . . End Property
Property Let and Property Get statements can be declared as Public, Private, Friend, or Static. For more information on declaring procedures as Friend, see Chapter 16, "Object-Oriented Programming."
Here is the syntax of the Property Get declaration:
Property Get propertyname [(argument_list)] [As data_type] . . . [propertyname = expression] [Exit Property] . . . [propertyname = expression] End Property
Note that properties can be declared as any Visual Basic data type, including object variable types and user-defined types. But remember that if your property type is an object variable, you need to use Property Set instead of Property Let to set the value of the property.
Additionally, if you declare a property to be of a particular data type in its Property Let declaration, make sure that its Property Get is of the same type. Don't mix types, as you see here:
Public Property Let Wattage(ByVal NewValue As Wattage) ' Wattage is an mWattage = NewValue ' enumerated PropertyChanged "Wattage" ' data type. End Property Public Property Get Wattage() As Integer ' Integers are not good for Wattage = mWattage ' children and other living End Property ' things
This code will produce a compile error when you attempt to instantiate the control. To fix the problem, change the type declaration of the Property Get from Integer to Wattage.
The concept of enumerated properties, such as Wattage, is introduced later in this chapter. For now, you can write the code now and make the declaration it depends on later: .
' Declarations - We'll define the Wattage ' type later Dim mWattage As Wattage Public Property Let Wattage(ByVal NewValue As Wattage) mWattage = NewValue PropertyChanged "Wattage" End Property Public Property Get Wattage() As Wattage Wattage = mWattage End Property Public Property Get Illuminated() As Boolean Illuminated = mIlluminated End Property Public Property Let Illuminated(ByVal bNewValue As Boolean) If IsNumeric(bNewValue) Then If bNewValue = True Then picMain.Picture = picOn.Picture mIlluminated = bNewValue Else ' false picMain.Picture = picOff.Picture End If PropertyChanged "Illuminated" End If End Property
Don't test this code yet, because the Wattage property won't work until you declare the Wattage enumeration. You'll do that in the next section.
You know from working with controls in Visual Basic in the past that some properties have the ability to limit the user to a specific range of settings. For example, consider the Alignment property of a TextBox control. This property can be set to one of three numbers: zero, 1, or 2. You can't choose another value in the Property window at runtime, because Visual Basic provides a combo box for the Alignment property that limits you to these three choices.
Fortunately, you don't have to memorize what the values zero, 1, or 2 mean in order to set this property, because the numeric values are associated with textual values in the design-time environment, as illustrated in Figure 4.1. A property that provides a list of choices for the user at design time is called an enumerated property.
Figure 4.1 : Example of an enumerated property.
You can provide a pre-defined list of legal property values for your user by using an enumeration. An enumeration is a new programming feature in Visual Basic 5.0 that enables you to define a related set of constant values.
You define an enumeration in a block of code inserted at the Declarations section of a module using the Enum statement. A typical enumeration looks like this:
Public Enum HappyHourStatus WorkTime = 0 HappyHour = 1 NightTime = 2 Holiday = 3 Weekend = 4 End Enum
NOTE |
Remember, an enumeration is a type of constant, so the values you declare in an Enum statement can't be altered at runtime. Also, remember that you can use Enums for purposes other than supplying a list of valid properties; you can use an enumerated variable anywhere you'd use a normal constant. |
A cool thing about enumerated constants is that you don't have to declare their values explicitly. If you don't set them to a value, then their values are set automatically, starting at zero.
For example, the following code is functionally identical to the preceding example:
Public Enum HappyHourStatus WorkTime HappyHour NightTime Holiday Weekend End Enum
You can have two enumerated values with the same name defined in different enumerations. When you do this, you must refer to the variables by a fully qualified name of the form
EnumName.ConstantName
For example, say you have an application that contains enumerations for both HappyHourStatus and CalendarDay. The CalendarDay enumeration looks like this:
Public Enum CalendarDay WorkDay Weekend Holiday Vacation SickDay End Enum
There's a big problem here-the Holiday and Weekend constants exist in both the HappyHour and CalendarDay enumerations. What's more, they are equal to different values. Holiday is equal to 3 in the HappyHour enumeration, while it's equal to 2 in the CalendarDay enumeration.
You can refer to the identically named constants in different enumerations by using code like this:
iToday = HappyHour.Holiday ' iToday = 2 iTomorrow = CalendarDay.Holiday ' iTomorrow = 3
You may be shrieking with horror at the prospect of collisions between different enumerated constants, and rightly so. Failing to plan ahead to avoid conflicting enumerations will make your code as confusing as heck. How confusing is heck? That's the point. Try to avoid such conflicts in the first place so you won't have to jump through hoops to deal with them.
One alternative is to provide a unique prefix for each enumerated constant, like this:
Public Enum CalendarDay cdWorkDay cdWeekend cdHoliday cdVacation cdSickDay End Enum Public Enum HappyHourStatus hhWorkTime hhHappyHour hhNightTime hhHoliday hhWeekend End Enum
Declaring enumerations this way almost ensures you won't run into problems with two enumerated constants crashing into each other, bursting into flames, and making a mess on your carpet.
One small drawback is that your user will see the prefix when selecting the value in the combo box dropdown Property window, but that seems like a small price to pay. Function over form, and all that.
Here's the code that establishes the Wattage enumeration in the HappyHour example project:
Public Enum Wattage VeryDim FairlyDim Bright VeryBright End Enum
Obviously, the values in the Wattage enumeration are oversimplified to make the example clearer. You can still test these enumerations by doing the following:
Figure 4.2 : Wattage property enumeration.
You can implement a Boolean property by declaring the control's Property Let procedure As Boolean. When you declare a Property Let in this way, the user is presented with two choices: True and False. You don't have to do anything special to make it happen this way; the Visual Basic IDE realizes that a particular property is Boolean and at design time provides a place in the Properties window to select the values True and False.
The Illuminated property of the LightBulb control is an example of this kind of property. To see how it works, try changing the Illuminated property of a LightBulb control on an EXE project form in the Properties window. You should be able to see that only two options are available, True and False.
There are a few properties for which there are standard property sheets. When you specify that a property, such as a font or color property, is of a predefined enumeration, Visual Basic displays a predefined Property window for you.
This is similar to the way the Picture property, introduced in Chapter 3 displays a common Windows File Open dialog box when the user attempts to change the control. You don't need to do anything special to get Visual Basic to display a file dialog box for the Picture property; it is enough to declare your Property Set procedure As Picture.
You can provide this functionality in your control's properties by using predefined enumerations. Table 4.1 lists some of these system-supplied enumerations.
Table 4.1 is only a partial list of enumerated properties available to you when you're developing properties in your controls. For information on all the enumerations that are available, open the Object Browser (using the menu command View, Object Browser or the function key F2) and browse the list of enumerated properties displayed there, as in Figure 4.3.
Figure 4.3 : Object browser displaying property enumeration.
Property Type | Declaration | Description |
Checked | OLE_TRISTATE | The state of a check box (can be either checked, unchecked, or gray) |
Color (BackColor, ForeColor, etc.) | OLE_COLOR | A standard color (stored as a Long) |
MousePointer | MousePointerConstants | The icon associated with the mouse pointer (arrow, cross, I-beam, custom icon, etc.). |
Value | OLE_OPTEXCLUSIVE | Used by controls that act as grouped option buttons. When the Value property is declared as OLE_OPTEXCLUSIVE, only one such control in a group can have the value of True. |
Align | AlignConstants | Used for controls (such as the PictureBox) that can align themselves to the top, bottom, left to right sides of their containers |
Alignment | AlignmentConstants | The alignment of text left, right, or center, such as that found in a TextBox control |
BorderStyle | BorderStyleConstants | The graphical border around a control; it is either None or Fixed Single. |
FillStyle | FillStyleConstants | The graphical fill of a control; it is either solid, transparent, or one of a number of shades, such as horizontal line. |
You'll notice that the Object Browser also displays the name and values of the enumerations you've created yourself. To see this:
Figure 4.4 : Object Browser display of enumerated constant.
TIP |
Using Object Browser to keep track of the enumerations you've written can be a great help, because it keeps you from having to hunt through your code when you're trying to remember whether you called something xyzFlag or pdqFlag. It also helps you quickly look up the values of enumerated constants You can tell at a glance that a particular constant is an element of your project (as opposed to an element of a standard VB type library) by looking at the bottom of the Object Browser; this area is called the Details Pane. |
To give an example of a system-provided enumerated constant, you'll add a BorderColor property to the LightBulb control. For simplicity's sake, the BorderColor property of the LightBulb will delegate to the BackColor property of the UserControl. To do this:
Public Property Let BorderColor(ByVal NewColor As OLE_COLOR) UserControl.BackColor = NewColor PropertyChanged "BorderColor" End Property Public Property Get BorderColor() As OLE_COLOR BorderColor = UserControl.BackColor End Property
Figure 4.5 : The BorderColor dropdown list box.
Property Let and Property Set procedures are the place where you'll write validation procedures. Such procedures should reject all invalid property values, either by raising some kind of error or by ignoring the attempt to change the property value.
From an error-trapping perspective, Boolean properties are nice, because you don't have to do numeric boundary checking. For example, consider the Illuminated property of a LightBulb control.
LightBulbs are either Illuminated or not; that is, their Illuminated properties are either True or False. But because Visual Basic defines False as zero and True as any nonzero value, the code
Lightbulb1.Illuminated = 3.14159
will work fine, triggering no error (LightBulb1's Illuminated property will be set to True). However, the code
Lightbulb1.Illuminated = "ringbo"
generates an error (13-Type Mismatch). You can avoid error in this case by testing the value supplied by the user with an IsNumeric function, like this:
If IsNumeric (NewValue) Then ' perform the property change . . . Else ' raise an error . . . End If
This technique is known as validation, and it's important to do. If a run-time error occurs in your control, there's no way for your user to trap it.
You have the option of exiting a Property Let in situations where the input is just too weird to deal with. For example, say you provide a BorderColor property for your control. Windows colors are expressed as long integers. But if your control encounters the code:
LightBulb1.BorderColor = "václav"
the user gets a Type Mismatch error, because the BorderColor property expects a long integer.
Let's say that instead of raising an error, you want the BorderColor property to ignore, or "eat" the error. To make this happen, you can change your code to look like the following:
Public Property Let BorderColor(ByVal NewColor As OLE_COLOR) If IsNumeric(NewColor) Then UserControl.BackColor = NewColor PropertyChanged "BorderColor" Else Exit Property End If End Property
The preceding code was just to introduce the Exit property statement to you; in general, your procedures should eat errors as seldom as possible. The preferred option for validation is to raise an error in your Property Let procedure. For more information on raising errors in your control, see Chapter 15, "Debugging and Error Trapping."
You use the PropertyBag object to store the properties set for your control by a programmer at design time. The PropertyBag object has only two methods: ReadProperty and WriteProperty.
You read from the PropertyBag in the ReadProperties event and write to the PropertyBag in the WriteProperties event. The syntax of the WriteProperty method is:
PropertyBag.WriteProperty "property_name", value [, default_value]
The parameter property_name is a string that denotes which property you're saving to the property bag. Value is the value of the property you're saving. The parameter default_value is optional; it exists only to tell Visual Basic not to save the property unless the user has changed the property from its default. This makes for clearer and faster loading .CTL files.
Here is an example of the WriteProperty method used in the WriteProperties event of a UserControl:
Private Sub UserControl_WriteProperties(PropBag As PropertyBag) PropBag.WriteProperty "Wattage", mWattage, 0 PropBag.WriteProperty "Illuminated", mIlluminated, False End Sub
The syntax of the ReadProperty method is:
PropertyBag.ReadProperty "property_name" [, default_value]
Just as with WriteProperty, the ReadProperty method has an optional default_value parameter that tells Visual Basic whether the property needs to be read from disk or not. This accelerates loading the control.
Here is an example of the ReadProperty method in the ReadProperties event:
Private Sub UserControl_ReadProperties(PropBag As PropertyBag) Wattage = PropBag.ReadProperty("Wattage", 0) Illuminated = PropBag.ReadProperty("Illuminated", False) End Sub
Where Design-Time Property Data Is Stored
Officially, you aren't supposed to be concerned with what happens to data once it's put in the PropertyBag. This is because the PropertyBag object is an abstraction, standing between the programmer and the property's storage and shielding you from its complexity. But you might need to know what these files are and what they do (especially when you're backing up your project, using version control, or moving your project from one disk to another).
For forms, most design-time property settings are saved to a .FRM file. This is also true for controls, except the settings are saved in the .CTL file. You can inspect the format of these files by opening a .FRM or .CTL in a text editor; just make sure not to make any changes to these files, or Visual Basic won't be able to read them.
Some properties, such as the Picture property of a PictureBox control, can't be stored in standard form or control files, because they are too large and consequently need to be stored in a binary format. When you set a property such as Picture in a form or control designer, Visual Basic creates a binary file that has the same name as your file, but with a different extension. This file is created and updated when the user saves the corresponding form or user control file; under normal circumstances, Visual Basic programmers never work with these files directly. Table 4.2 summarizes the files' extensions.
You learned in Chapter 3how to pass through, or delegate, the property of your UserControl to a constituent control. In general, the code you use to read and write the properties of constituent controls to your UserControl is very simple. Given a UserControl called LightBulb with a constituent PictureBox control called picMain, the code to read and write the control's Picture property looks like this:
Public Property Get Picture() As Picture Picture = picMain.Picture End Property Public Property Set Picture(ByVal NewPic As Picture) picMain.Picture = NewPic PropertyChanged "Picture" End Property
This is a code cliché-a piece of code you'll write dozens if not hundreds of times in your career as a Visual Basic control developer. (The code is so straightforward, in fact, it makes more sense for you to let the ActiveX Control Interface Wizard write it for you. The ActiveX Control Interface Wizard is introduced in Chapter 2)
When using constituent controls, there is a caveat you must bear
in mind. When you place a constituent control on your UserControl
designer, the constituent control is in run-time mode, even though
no code is being executed. That means you will not be able to
gain access to runtime-only properties of the constituent control
(such as the Sorted property of the ComboBox control).
File Type | ||
Forms | ||
Controls | ||
Property pages |
Fortunately, the number of runtime-only properties of standard Windows control are few, so hopefully this shortcoming won't hinder you too often. But it is something to bear in mind as you build controls comprised of constituent controls.
There are a number of properties that your control is supposed to always provide. This being a free country and all, you don't have to provide any properties you don't want to. If your control is unusual (for example, it's invisible at runtime), then you obviously would.
The properties your control should provide are:
You should also provide properties for controls that are similar to your control. Make sure your control's property interface make sense, and you will avoid legions of users throwing rocks through the windows of your home in the middle of the night.
One helpful new feature of the VB5 IDE is the procedure description of properties. When you click on a property in the Properties window, a (hopefully) helpful piece of text appears at the bottom of the Properties window (see Figure 4.6) telling you what the property is used for.
Figure 4.6 : Example of a procedure description.
You can implement procedure descriptions in properties you create. To do this:
Figure 4.7 : Procedure Attributes dialog box.
To test your property description:
Figure 4.8 : Procedure description in the Properties window.
Most controls have a default property. If you choose to designate a default property in your control, the user will not have to explicitly type the name of the property when referring to it in code. Denoting a default property in your control saves the user time at the expense of clarity. For example, the default property of a TextBox is its text property. If you have a TextBox called Text1, you can either type
MyString = Text1.Text
or
MyString = Text1
and the two lines of code will mean the exact same thing.
NOTE |
The default property is not to be confused with the Default property (also referred to in VB5 as the user interface default). The Default property is a property of command buttons and similar controls. When the Default property is set to true, striking the Enter key triggers the command button's Click event |
To designate a property of your control as its default property, do the following:
You can test your new default property by entering code in the Immediate window while the form is running. To do this:
Print LightBulb1
LightBulb1 illuminates.LightBulb1 = True
Figure 4.9 : The completed Procedure Attributes dialog box.
Figure 4.10: The toolbar Break button.
You can organize properties into groups in order to make it easier for users to find them in the Properties windows. It's particularly useful to do this if your control exposes a large number of properties.
For example, you might want to create a bunch of electricity-related properties together in one group. The Wattage and Illuminated properties would be more easily accessible if they belonged to this group. To place the LightBulb control's Illuminated property in a property group, do the following:
To see that the Illuminated property has been assigned to a category, do the following:
Figure 4.11: Custom property category.
Figure 4.12: Electrical category.
You can use the AmbientProperties object to gain access to information about the properties of your control's container. You do this in order to synchronize your control's properties with those of its container.
For example, when you set a form's Font property to Times New Roman 12 Bold and then place a Label control on the form, the Font property of the label is automatically set to Times New Roman 12 Bold as well. The default properties of the Label are synchronized with the properties of its container.
When you write properties, consider whether it is appropriate to synchronize properties of your control with properties of its container. For more information on the container, including an example of how to use the AmbientProperties object, see Chapter 7, "Interacting with the Container."
A custom property page can go a long way toward making the properties of your control easier to manipulate at design time. This is particularly true if your control has numerous properties or an otherwise complicated interface. You can assign a property page to your entire control or to a particular property in your control.
In this section you'll step through the construction of a simple property page using a wizard, then you'll write code to create a property page manually.
You can use the Property Page Wizard as a quick way to set up a basic property page for your control. After you've created a property page using the wizard, you can further customize the property page using the same visual design techniques you'd use to create Visual Basic applications and ActiveX controls.
Begin by setting up a minimal property page for the LightBulb control.
To do this:
Figure 4.13: The Property Page Name window is where you name a new property page.
Figure 4.14: Property Page Wizard summary report.
Your property page is now functional. To test it:
Figure 4.15: The Properties Pages showing the LightBulb tab.
You should also be able to see that a new file has been added to your project, a custom property page called LightBulbPage. Locate it in the Project Explorer and double-click it to open it.
You can see that at design time, a property page looks not unlike a control designer; in fact, they are called property page designers. You can manipulate this designer and add controls and code to it just like a control designer or a form. You'll do that in the next section.
The PropertyPage object is a fully programmable Visual Basic object
similar to a Form or a UserControl object. Tables 4.3 and 4.4
describe the important properties and events of the PropertyPage.
Property | Description |
ActiveControl | This is the control that has the focus. You can use this property to determine which type of control the user has selected when the PropertyPage is displayed. |
Changed | This is a flag that indicates whether the user changed a property through the PropertyPage. |
SelectedControls | This is a collection containing all the controls selected when the PropertyPage was activated. |
Event | When Triggered |
ApplyChanges | This is triggered when the user clicks on the Apply or OK button or switches tabs in a PropertyPage comprised of multiple pages. |
EditProperty | When the user clicks on the ellipsis button in the Properties window, the property page is opened. It exists so you can set the focus to the appropriate control. |
SelectionChanged | This is triggered when the PropertyPage is first opened and when you select or deselect one or more controls while the page is still open. |
Unload | This is triggered when the PropertyPage is about to be unloaded (usually as a result of the user closing the page). This is similar to the Unload event of a form. |
Terminate | This event occurs after the Unload event. All references to the PropertyPage control go out of scope (or are set to Nothing). |
Property pages created by the Property Page Wizard are generally adequate for most types of properties. But you might want to take your control's properties sheet further by applying additional code and custom controls to the property page's interface.
To demonstrate this, you'll convert the check box that controls the Illuminated property to a group of option buttons with graphical representations of the "on" and "off" states of the light bulb. To do this:
Property | Value |
Picture | slite-off.bmp (on the CD-ROM) |
AutoSize | True |
Property | Value |
Picture | slite-on.bmp (also on your CD-ROM) |
AutoSize | True |
Property | Value |
Caption | Off |
Value | True |
Property | Value |
Caption | On |
Value | False |
Property | Value |
Caption | Illuminated |
Figure 4.16: New Property page.
Private Sub optBulbOff_Click() Changed = True End Sub Private Sub optBulbOn_Click() Changed = True End Sub
This code flags the property page as "dirty," meaning that the user has altered a property in the page. When the property page is dirty, the Apply button is enabled; the property page then knows to apply the property changes to the control when the user clicks on Apply or OK.
Next you'll need to alter the ApplyChanges event of the PropertyPage so the page applies the property change appropriately. To do this, make the following changes to the code in the property page's ApplyChanges event:
Private Sub PropertyPage_ApplyChanges() SelectedControls(0).Illuminated = optBulbOn.Value End Sub
Finally, you'll have to alter the property page's SelectionChanged
event so it accurately reflects the state of the selected control
when the property page was opened. This code is in the SelectionChanged
event because this event is triggered when the property page is
first opened. It is also triggered when the user changes the control
that is selected. However, the event is also triggered
when you click on the property sheet's Apply button, which is
bad, because when you change the property to Off and press Apply,
the code will change the property sheet to indicate that the property
has been set to On. We'll include a workaround for this puzzling
anomaly by placing a flag in the ApplyChanges event.
TIP |
It's important to remember that the set of selected controls can change after the user has brought up the Property sheet. This is the case because property pages are modeless (meaning that users can access windows in the background while the Property page is still being displayed). |
To make this change, start by creating the flag in the Declarations section of the code:
Private mApplyingFlag As Boolean
Next, alter the ApplyChanges event so it sets the flag. Its code should look like this:
Private Sub PropertyPage_ApplyChanges() mApplyingFlag = True SelectedControls(0).Illuminated = optBulbOn.Value End Sub
Finally, alter the code in the SelectionChanged event as follows:
Private Sub PropertyPage_SelectionChanged() If mApplyingFlag = False Then optBulbOn.Value = SelectedControls(0).Illuminated Else ' do nothing mApplyingFlag = False End If End Sub
To test your new Property page, do the following:
This chapter discussed how to create properties in the ActiveX controls you create. Using an example project, you created properties, wrote validation procedures, and developed custom property pages.
In the next chapter, you'll learn about another new element of control interfaces: custom events.