Property Pages
Just browsing the code produced by the wizard is sufficient
to understand how property pages work. The PropertyPage object is similar to
a form and supports many of the Form object's properties, methods, and events,
including Caption, Font, and all the keyboard and mouse events.
You might even implement property pages that work as drag-and-drop servers or
clients if you need to.
Property pages have their peculiarities, of course. For one,
you can control the size of the page using the StandardSize property,
which can be assigned one of the values 0-Custom (the size is determined by the
object), 1-Small (101-by-375 pixels), or 2-Large (179-by-375 pixels). Microsoft
suggests that you create custom-sized pages that aren't larger than the space
that you actually need because values other than 0-Custom might display incorrectly
at different screen resolutions.
You might notice in Figure 17-15 that the property page doesn't
include the OK, Cancel, and Apply buttons that you usually find on standard property
pages. Those buttons, in fact, are provided by the environment, and you don't
have to add them yourself. The communication between the property page and the
environment occurs through properties and events of the PropertyPage object.
If the project is associated with a help file, a Help button is also displayed.
When the page loads, the PropertyPage object receives the SelectionChanged
event. In this event, your code should load all the controls in the page
with the current values of the corresponding properties. The SelectedControls
collection returns a reference to all the controls in the form that are currently
selected and that will be affected by the property page. For example, this is
the code in the SelectionChanged event procedure for the General page
of the SuperListBox control:
Private Sub PropertyPage_SelectionChanged()
txtCaption.Text = SelectedControls(0).Caption
txtAllItems.Text = SelectedControls(0).AllItems
chkEnabled.Value = (SelectedControls(0).Enabled And vbChecked)
cboShowPopupMenu.ListIndex = SelectedControls(0).ShowPopupMenu
cboBoundPropertyName.Text = SelectedControls(0).BoundPropertyName
Changed = False
End Sub
When the contents of any field on the page is modified, the
code in its Change or Click event should set the PropertyPage's
Changed property to True, as in these examples:
Private Sub txtCaption_Change()
Changed = True
End Sub
Private Sub cboShowPopupMenu_Click()
Changed = True
End Sub
Setting the Change property to True automatically enables
the Apply button. When the user clicks on this button (or simply switches to
another property page), the PropertyPage object receives an ApplyChanges event.
In this event, you must assign the values on the property page to the corresponding
ActiveX control's properties, as in the following example:
Private Sub PropertyPage_ApplyChanges()
SelectedControls(0).Caption = txtCaption.Text
SelectedControls(0).AllItems = txtAllItems.Text
SelectedControls(0).Enabled = chkEnabled.Value
SelectedControls(0).ShowPopupMenu = cboShowPopupMenu.ListIndex
SelectedControls(0).BoundPropertyName = cboBoundPropertyName.Text
End Sub
One more custom event is associated with PropertyPage objects-the
EditProperties event. This event fires when the property page is displayed
because the developer clicked on the ellipsis button beside a property name in
the Properties window. (This button appears if the property has been associated
with a specific property page in the Procedure Attributes dialog box.) You usually
take advantage of this property to automatically move the focus on the corresponding
control on the property page:
Private Sub PropertyPage_EditProperty(PropertyName As String)
Select Case PropertyName
Case "Caption"
txtCaption.SetFocus
Case "AllItems"
txtAllItems.SetFocus
' etc. (other properties omitted...)
End Select
End Sub
You might also want to disable or hide all other controls on
the page, but this is rarely necessary or useful.
Working with multiple selections
The code produced by the Property Page Wizard accounts for
only the simplest situation-that is, when only one ActiveX control is selected
on the form. To build robust and versatile property pages, you should make them
work also with multiple controls. Keep in mind that property pages aren't modal,
and therefore the developer is allowed to select (or deselect) controls on the
form even when the page is already visible. Each time a new control is added
to or removed from the SelectedControls collection, a SelectionChanged event
fires.
The standard way to deal with multiple selections is as follows.
If the selected controls on the form share the same value for a given property,
you fill the corresponding field on the property page with that common value;
otherwise, you leave the field blank. This is a modified version of the SelectionChanged
that accounts for multiple selections:
Private Sub PropertyPage_SelectionChanged()
Dim i As Integer
' Use the property of the first selected control.
txtCaption.Text = SelectedControls(0).Caption
' If there are other controls, and their Caption property differs from
' the Caption of the first selected control, clear the field and exit.
For i = 1 To SelectedControls.Count - 1
If SelectedControls(i).Caption <> txtCaption.Text Then
txtCaption.Text = ""
Exit For
End If
Next
' The AllItems property is dealt with in the same way (omitted ...).
' The Enabled property uses a CheckBox control. If values differ, use
' the special vbGrayed setting.
chkEnabled.Value = (SelectedControls(0).Enabled And vbChecked)
For i = 1 To SelectedControls.Count - 1
If (SelectedControls(i).Enabled And vbChecked) <> chkEnabled.Value
Then
chkEnabled.Value = vbGrayed
Exit For
End If
Next
' The ShowPopupMenu enumerated property uses a ComboBox control.
' If values differ, set the ComboBox's ListIndex property to _1.
cboShowPopupMenu.ListIndex = SelectedControls(0).ShowPopupMenu
For i = 1 To SelectedControls.Count - 1
If SelectedControls(i).ShowPopupMenu <> cboShowPopupMenu.ListIndex
Then
cboShowPopupMenu.ListIndex = -1
Exit For
End If
Next
' The BoundPropertyName property is dealt with similarly (omitted ...).
Changed = False
txtCaption.DataChanged = False
txtAllItems.DataChanged = False
End Sub
The DataChange properties of the two TextBox controls
are set to False because in the ApplyChange event you must determine whether
the developer entered a value in either of those fields:
Private Sub PropertyPage_ApplyChanges()
Dim ctrl As Object
' Apply changes to Caption property only if the field was modified.
If txtCaption.DataChanged Then
For Each ctrl In SelectedControls
ctrl.Caption = txtCaption.Text
Next
End If
' The AllItems property is deal with in the same way (omitted ...).
' Apply changes to the Enabled property only if the CheckBox control
' isn't grayed out.
If chkEnabled.Value <> vbGrayed Then
For Each ctrl In SelectedControls
ctrl.Enabled = chkEnabled.Value
Next
End If
' Apply changes to the ShowPopupMenu property only if an item
' in the ComboBox control is selected.
If cboShowPopupMenu.ListIndex <> -1 Then
For Each ctrl In SelectedControls
ctrl.ShowPopupMenu = cboShowPopupMenu.ListIndex
Next
End If
' The BoundPropertyName property is dealt with similarly (omitted ...).
End Sub
Advanced techniques
I want to mention a few techniques that you can use with property
pages and that aren't immediately obvious. For example, you don't need to wait
for the ApplyChanges event to modify a property in selected ActiveX controls:
You can update a property right in the Change or Click event of
the corresponding control on the property page. You can therefore achieve in
the property page the same behavior that you can implement in the Properties
window by assigning a property the Text or Caption procedure ID.
Another easy-to-overlook feature is that the PropertyPage object
can invoke Friend properties and methods of the UserControl module because they're
in the same project. This gives you some additional flexibility: For example,
the UserControl module can expose one of its constituent controls as a Friend
Property Get procedure so that the Property Page can directly manipulate
its attributes, as you can see in the code at below.
' In the SuperListBox UserControl module
Friend Property Get Ctrl_List1() As ListBox
Set Ctrl_List1 = List1
End Property
A minor annoyance of this approach is that the PropertyPage
code accesses the UserControl through the SelectedControls collection, which
returns a generic Object, whereas Friend members can only be accessed through
specific object variables. You can work around this issue by casting the elements
of the collection to specific object variables:
' In the PropertyPage module
Dim ctrl As SuperListBox
' Cast the generic control to a specific SuperListBox variable.
Set ctrl = SelectedControls(0)
' Now it is possible to access Friend members.
ctrl.Ctrl_List1.AddItem "New Item"
The last technique that I'm showing you is likely to be useful
when you're developing complex UserControls with many properties and constituent
controls, such as the Customer ActiveX control that I introduced earlier in this
chapter. Surprisingly, it turns out that you can use the UserControl even on
a property page that's associated with itself. Figure 17-16 shows an example
of this technique: The General property page uses an instance of the Customer
ActiveX control to let the developer assign the properties of the Customer control
itself!
Figure 17-16.
A property page that uses an instance of the UserControl object defined in
its own project.
The beauty of this approach is how little code you need to
write in the PropertyPage module. This is the complete source code of the property
page shown in Figure 17-16:
Private Sub Customer1_Change(PropertyName As String)
Changed = True
End Sub
Private Sub PropertyPage_ApplyChanges()
' Read all properties in one loop.
Dim propname As Variant
For Each propname In Array("CustomerName", "Address", "City", _
"ZipCode", "Country", "Phone", "Fax")
CallByName SelectedControls(0), propname, VbLet, _
CallByName(Customer1, propname, VbGet)
Next
End Sub
Private Sub PropertyPage_SelectionChanged()
' Assign all properties in one loop.
Dim propname As Variant
For Each propname In Array("CustomerName", "Address", "City", _
"ZipCode", "Country", "Phone", "Fax")
CallByName Customer1, propname, VbLet, _
CallByName(SelectedControls(0), propname, VbGet)
Next
End Sub
Notice how the code takes advantage of the CallByName function
to streamline multiple assignments to and from the properties in the UserControl.