Alex Stockton
Microsoft Corporation
May 1999
Note This article assumes some familiarity with the Component Object Model (COM) and the Active Template Library (ATL).
Summary: Discusses how to build COM property pages with the Active Template Library. (18 printed pages) Includes a demonstration on how to set some of the properties of a text document within the Microsoft® Visual C++® editor.
Introduction
The Property Page Contract
The Eventful Life of a Property Page
Reusable—Not Tightly Bound
Implementing Property Pages
Example: Implementing a Property Page
Property Pages and Controls
Example: Specifying Property Pages
Example: Displaying Specified Property Pages
Summary
COM property pages provide a user interface for setting the properties (or calling the methods) of one or more COM objects. Property pages are used extensively by Microsoft ActiveX® controls for providing rich user interfaces that allow control properties to be set at design time, but property pages are not limited to that use. For example, many of the tabbed pages displayed as part of the user interface of Microsoft Visual C++ are written as COM property pages, including those displayed by the ATL Object Wizard. COM property pages provide a standard way of allowing a user to configure any COM object, not just controls. As an example, in this article I'll show you how to create a property page that allows the user to set some of the properties of a text document within the Visual C++ editor.
First, however, I'll describe how the IPropertyPage interface defines the contract for displaying property pages. I'll show you the sequence of method calls that property page containers make on the interface of a COM property page. Once you understand the essence of the property page contract, I'll move on to show you how the Active Template Library (ATL) provides help for developers creating and using COM property pages.
Each property page is a COM object that implements the IPropertyPage or IPropertyPage2 interface (see Tables 1 and 2). For simplicity, we'll just consider IPropertyPage in this article.
Table 1. The IPropertyPage Interface
IPropertyPage methods | Description |
SetPageSite | Initializes a property page and provides the page with a pointer to the IPropertyPageSite interface through which the property page communicates with the property page site. |
Activate | Creates the window for the property page. |
Deactivate | Destroys the window created with Activate. |
GetPageInfo | Returns information about the property page. |
SetObjects | Provides the property page with an array of IUnknown pointers for objects associated with this property page. |
Show | Makes the property page window visible or invisible. |
Move | Positions and resizes the property page window. |
IsPageDirty | Indicates whether the property page has changed since activated or since the most recent call to Apply. |
Apply | Applies current property page values to the underlying objects specified through SetObjects. |
Help | Invokes help in response to a user request. |
TranslateAccelerator | Provides a pointer to a MSG structure that specifies a keystroke to process. |
Table 2. The IPropertyPage2 Interface
IPropertyPage2 methods | Description |
EditProperty | Specifies which field is to receive the focus when the property page is activated. |
These interfaces provide methods that allow the page to be associated with a site (a COM object that implements the IPropertyPageSite interface and represents the container of the page) and one or more objects (COM objects whose methods will be called in response to changes made by the user of the property page).
The property page container is responsible for calling methods on the property page interface to tell the page when to show or hide its user interface, and when to apply the changes made by the user to the underlying objects. The property page is responsible for letting the site know when the status of a page has changed, and for updating the objects when requested.
Let's take a look at a typical sequence of events in the life of a property page. We'll start our examination at the point at which a client application has pointers to interfaces on one or more COM objects for which it wishes to display property pages. We'll assume the client already has access to the CLSIDs of these property pages; anything before that point is application-specific.
Precisely what the application will do in response to this information is application-specific. Typically, the application will display the property pages in a modal dialog box with a tabbed look. The page size is used to ensure the dialog box is large enough to hold the largest of the pages, and the page titles are usually used to label the tabs displayed above each page.
However, there's no contract governing the way property pages must be presented to the user. The application could display multiple property pages side by side, in individual windows, or embedded in the interface of the application if appropriate for the application and the developer was willing to write the code. Most applications use the standard tabbed modal dialog box because there are two system-supplied APIs, OleCreatePropertyFrame and OleCreatePropertyFrameIndirect, that display property pages in this way. Displaying pages in any other way would require you to write some code.
Typically, a property page uses a gray dialog box window of a standard size for its user interface. This allows property pages written by individual developers to be displayed together without incongruity. However, there are no fundamental requirements placed on the look and feel of a property page. Specific applications may choose to create property pages with a unique look if that makes sense given the expected use for that page.
The part of the user interface that allows the user to apply their changes is provided by the application and not by the property page. Typically, property pages are displayed in a modal dialog box with OK, Cancel, and Apply buttons. These buttons are all supplied by the page container (usually by the OleCreatePropertyFrame). They allow a single action on the part of the user to apply changes on multiple property pages.
Note that this contract doesn't couple a property page to any particular class, nor to any other property page. All a property page needs is an understanding of a particular interface (or set of interfaces) on the objects it gets passed in the call to SetObjects. A property page might require the objects to support the IFile interface, if it were designed to display information about files, or it might require the presence of an ITextDocument interface, if it were designed to display information about text documents.
As long as the objects support the interface(s) expected by the property page, it couldn't care less where the objects came from or how they were created. Once the property page has been written, any application that needs to display information about COM objects implementing that particular interface can reuse that page.
Consider an application that displays information about a particular type of file. This application could represent each file as a COM object internally, using custom interfaces for the application-specific features of these files and FileSystemObject interfaces for the generic features. If the user asked for detailed information about a file, the application could display a property page that understood the IFile interface alongside another page that understood the interfaces specific to that application. Property pages provide plenty of opportunity for user-interface reuse.
Now that you've seen the property page interface contract, let's look at how you can create property pages with ATL. Recall that property pages are simply COM objects that implement the IPropertyPage or IPropertyPage2 interface. ATL provides support for implementing property pages through the Property Page item in the Controls category of the ATL Object Wizard.
To create a property page using ATL
The Object Wizard will generate a class derived from IPropertyPageImpl and CDialogImpl. If you want to host ActiveX controls in the user interface of your property page, you will need to change the derivation of your wizard-generated class from CDialogImpl<CYourClass> to CAxDialogImpl<CYourClass>.
IPropertyPageImpl method | Override to | Notes |
SetObjects | Perform basic sanity checks on the number of objects being passed to your page and the interfaces they support. | Execute your own code before calling the base class implementation. If the objects being set don't conform to your expectations, you should fail the call as soon as possible. |
Activate | Initialize your page's user interface (for example, set dialog box controls with current property values from objects, create controls dynamically, or perform other initializations). | Call the base class implementation before your code so the base class has a chance to create the dialog box window and all the controls before you try to update them. |
Apply | Validate the property settings and update the objects. | There is no need to call the base class implementation, because it doesn't do anything apart from trace the call. |
Deactivate | Clean up window-related items. | The base class implementation destroys the dialog box representing the property page. If you need to clean up before the dialog box is destroyed, you should add your code before calling the base class. |
You can override other IPropertyPage methods if you need to (it's a COM interface, so all methods are virtual), but the methods in the preceding table should be sufficient for the majority of property pages.
Now let's look at an example property page implementation that displays (and allows you to change) properties of the ITextDocument interface. This interface provides a number of methods and properties related to text documents and is exposed by TextDocument objects in the Microsoft Developer Studio® object model (although the property page that you'll create won't care where the objects it manipulates come from as long as they support this interface). This example follows the steps just described.
First create a new ATL project for a DLL server called ATLPages. Now use the ATL Object Wizard to generate a property page. Give the property page a short name of TextDocumentProperties, and then switch to the Strings page to set property page-specific items, as shown in Figure 1.
Figure 1. Setting property page-specific items in the ATL Object Wizard
The values that you set on this page of the wizard will be returned to the property page container when it calls IPropertyPage::GetPageInfo. What happens to the strings after that is dependent on the container, but typically they will be used to identify your page to the user. The Title will usually appear in a tab above your page and the Doc String may be displayed in a status bar or tool tip (although the standard property frame provided by OleCreatePropertyFrame doesn't use this string at all).
Note The strings that you set here are stored as string resources in your project by the wizard. You can easily edit these strings using the resource editor if you need to change this information after the code for your page has been generated.
To have the wizard generate your property page, choose OK.
Now that your property page has been generated, you'll need to add a few controls to the dialog box resource representing your page. Add an edit box, a static text control, and a check box, and set their IDs as shown in Figure 2.
Figure 2. Adding controls to the dialog box resource
These controls will be used to display the file name of the document and its read-only status. The ITextDocument interface provides a number of other interesting properties, but to keep the code simple, so as not to obscure the main points of this example, we'll ignore those other properties.
Note The dialog box resource does not include a frame or command buttons, nor does it have the familiar tabbed look. Recall that these features are provided by a property page frame such as the one created by calling OleCreatePropertyFrame.
With the controls in place, you can add message handlers to update the dirty status of the page when the value of either of the controls changes.
BEGIN_MSG_MAP(CTextDocumentProperties)
COMMAND_HANDLER(IDC_NAME, EN_CHANGE, OnUIChange)
COMMAND_HANDLER(IDC_READONLY, BN_CLICKED, OnUIChange)
CHAIN_MSG_MAP(IPropertyPageImpl<CTextDocumentProperties>)
END_MSG_MAP()
LRESULT OnUIChange(WORD wNotifyCode, WORD wID, HWND hWndCtl,
BOOL& bHandled)
{
SetDirty(true);
return 0;
}
This code responds to changes made to the edit control or check box by calling IPropertyPageImpl::SetDirty, which informs the page site that the page has changed and stores the current dirty state of the page in the m_bDirty member. Typically, the page site will respond by enabling or disabling an Apply button on the property page frame.
Remember that we should only update the object when the Apply method is called, so there's no need for any other code in this message handler. When the time comes, we'll get the latest values from the controls on our property page and update the text document with those values.
Here I've just taken the simple approach. In your own property pages, you might need to keep track of precisely which properties have been altered by the user so you can avoid updating properties that haven't been changed. If you do need to do that, it's a simple matter to write the code yourself.
Note The CHAIN_MSG_MAP entry is part of the code generated by the ATL Object Wizard. This entry is used to ensure the base class gets a chance to handle messages for the property page.
Now add a couple of #includes to TextDocumentProperties.h so the compiler knows about the ITextDocument interface. The definitions you need are in the TextGuid.h and TextAuto.h files supplied with Visual C++.
#include <ObjModel\TextGuid.h>
#include <ObjModel\TextAuto.h>
You'll also need to refer to the IPropertyPageImpl base class in the code that follows, so to make things easier add the following typedef to the CTextDocumentProperties class:
typedef IPropertyPageImpl<CTextDocumentProperties> PPGBaseClass;
The first IPropertyPageImpl method you need to override is SetObjects. Here you'll add code to check that only a single object has been passed and that it supports the ITextDocument interface you're expecting.
STDMETHOD(SetObjects)(ULONG nObjects, IUnknown** ppUnk)
{
HRESULT hr = E_INVALIDARG;
if (nObjects == 1)
{
CComQIPtr<ITextDocument> pDoc(ppUnk[0]);
if (pDoc)
hr = PPGBaseClass::SetObjects(nObjects, ppUnk);
}
return hr;
}
Here we use CComQIPtr to query the first element of the ppUnk array for the ITextDocument interface. If the object doesn't support that interface, or if the number of objects doesn't equal 1, we return E_INVALIDARG. If there is a single object that supports the ITextDocument interface, we call the base class implementation of SetObjects, which will store the array of objects in the m_ppUnk member and the count in m_nObjects.
Note For simplicity, we're only going to support a single object for this page because we will allow the user to set the file name of the object, and only one file can exist at any one location. In your own pages, you might need to take account of multiple objects: Perhaps there are elements of your property page's user interface that you will disable or change when multiple objects are passed. Don't assume that your page will always be passed just a single object. Check and return an error if the number of objects doesn't match your page's capabilities.
The next step is to initialize the property page with the property values of the underlying object when the page is first created. The base class implementation of the Activate method is responsible for creating the dialog box and its controls, so you can override this method and add your own initialization after calling the base class.
STDMETHOD(Activate)(HWND hWndParent, LPCRECT prc, BOOL bModal)
{
// Call the base class implementation
HRESULT hr = PPGBaseClass::Activate(hWndParent, prc, bModal);
if (FAILED(hr))
return hr;
// Get the ITextDocument pointer
CComQIPtr<ITextDocument> pDoc(m_ppUnk[0]);
if (!pDoc)
return E_UNEXPECTED;
// Get the FullName property
ComBSTR bstr;
hr = pDoc->get_FullName(&bstr);
if (FAILED(hr))
return hr;
// Set the text box so that the user can see the document name USES_CONVERSION;
SetDlgItemText(IDC_NAME, W2CT(bstr));
// Get the ReadOnly property
VARIANT_BOOL bReadOnly = VARIANT_FALSE;
hr = pDoc->get_ReadOnly(&bReadOnly);
if (FAILED(hr))
return hr;
// Set the check box to show the document's read-only status
CheckDlgButton(IDC_READONLY,
bReadOnly ? BST_CHECKED : BST_UNCHECKED);
return hr;
}
This code uses the COM methods of the ITextDocument interface to get the properties that we're interested in. It then uses the Microsoft Win32® API wrappers provided by CDialogImpl and its base classes to display the property values to the user.
When the user wants to apply their changes to the objects, the property page site will call the Apply method. This is the place to do the reverse of the code in Activate. Whereas Activate took values from the object and pushed them into the controls on the property page, Apply takes values from the controls on the property page and pushes them into the object.
STDMETHOD(Apply)(void)
{
// Check whether we need to update the object
// (Property frame may call Apply when it doesn't need to)
if (!m_bDirty)
return S_OK;
HRESULT hr = E_UNEXPECTED;
// Get a pointer to the document
CComQIPtr<ITextDocument> pDoc(m_ppUnk[0]);
if (!pDoc)
return hr;
// Set the read-only property
hr = pDoc->put_ReadOnly(IsDlgButtonChecked(IDC_READONLY)? VARIANT_TRUE : VARIANT_FALSE);
if (FAILED(hr))
return hr;
// Get the filename and save the document
CComBSTR bstrName;
GetDlgItemText(IDC_NAME, bstrName.m_str);
DsSaveStatus status;
hr = pDoc->Save(CComVariant(bstrName), CComVariant(), &status);
if (FAILED(hr))
return hr;
// Clear the dirty status of the property page
SetDirty(false);
return S_OK;
}
The check against m_bDirty at the top of this implementation avoids unnecessary updates of the objects if Apply is called more than once.
Note ITextDocument exposes FullName as a read-only property. To update the file name of the document based on changes made to the property page, you have to use the Save method to save the file with a different name. You can see that the code in a property page doesn't have to limit itself to getting or setting properties.
To display this page, you need to create a simple helper object that can display a property page. The helper object will provide a method that simplifies the OleCreatePropertyFrame API for displaying a single page connected to a single object.
OleCreatePropertyFrame takes an array of property page CLSIDs, which it uses to create property page instances and display them in tabbed form on a modal dialog box. It also takes an array of IUnknown pointers that it passes to each of the pages via IPropertyPage::SetObjects. OleCreatePropertyFrame is responsible for creating all the necessary page sites and managing the interaction between the pages and the frame, so it's a great deal simpler than writing that code yourself.
Note OleCreatePropertyFrame is extremely useful, but it has limitations. You can't use it to display a modeless dialog box, the property pages must always have the tabbed look, and you can't pass different objects to each of the pages displayed in the dialog box. If you need more flexibility than this API provides, you'll need to write your own property frame and property page site. Unfortunately, ATL doesn't give you any help in this area (beyond what it provides for any COM development).
This helper will be designed so that OleCreatePropertyFrame can be used from scripting languages. We need this object because we can easily get an ITextDocument pointer for the currently active document using a Microsoft Visual Basic® Scripting Edition (VBScript) macro running in the Visual C++ development environment, but without the helper we can't call the OleCreatePropertyFrame API from VBScript.
Note A helper object that allowed an array of pages and an array of objects to be passed would be preferable to our simplified version, but rather than swamping you with lots of ugly safearray manipulation, I'll keep this example clean by only allowing a single page and a single object. Enhancing the example is left as an exercise for the reader.
Create a new simple object using the ATL Object Wizard and use Helper as its short name. Once created, add a method, as shown in Figure 3.
Figure 3. Adding a method to a simple object interface
The bstrCaption parameter is the caption to be displayed as the title of the dialog box. The bstrID parameter is a string representing either a CLSID or a ProgID of the property page to display. The pUnk parameter will be the IUnknown pointer of the object whose properties will be configured by the property page.
Implement the method as shown here:
STDMETHODIMP CHelper::ShowPage(BSTR bstrCaption, BSTR bstrID,
IUnknown* pUnk)
{
if (!pUnk)
return E_INVALIDARG;
// First, assume bstrID is a string representing the CLSID
CLSID theCLSID = {0};
HRESULT hr = CLSIDFromString(bstrID, &theCLSID);
if (FAILED(hr))
{
// Now assume bstrID is a ProgID
hr = CLSIDFromProgID(bstrID, &theCLSID);
if (FAILED(hr))
return hr;
}
// Use the system-supplied property frame
return OleCreatePropertyFrame(
NULL, // Parent window of the property frame
0, // Horizontal position of the property frame
0, // Vertical position of the property frame
bstrCaption, // Property frame caption
1, // Number of objects
&pUnk, // Array of IUnknown pointers for objects
1, // Number of property pages
&theCLSID, // Array of CLSIDs for property pages
NULL, // Locale identifier
0, // Reserved - 0
NULL // Reserved - 0
);
}
To allow this helper to be used from script, you need to mark it as safe. You can do this by supporting the IObjectSafety interface. Add IObjectSafetyImpl as a base class as shown here:
public IObjectSafetyImpl<CHelper, INTERFACESAFE_FOR_UNTRUSTED_CALLER>
Also add an entry to the COM map for this interface:
COM_INTERFACE_ENTRY(IObjectSafety)
Once you've built the ATLPages project, you can test the property page and the helper object using a simple VBScript macro that you can create and run in the Visual C++ development environment. This macro will create a helper object and then call its ShowPage method using the ProgID of the TextDocumentProperties property page and the IUnknown pointer of the document currently active in the Visual C++ editor. To create this macro, use the Tools | Macro... menu item to display the Macro dialog box, type ATLPagesTest into the Name box, and use the Edit button to generate the skeleton of the macro. Add the following code and save the file:
Sub ATLPagesTest()
Dim Helper
Set Helper = CreateObject("ATLPages.Helper.1")
On Error Resume Next
Helper.ShowPage ActiveDocument.Name, _
"ATLPages.TextDocumentProperties.1", _
ActiveDocument
End Sub
When you execute this macro, the property page will be displayed showing the file name and read-only status of the currently active document. (Note that the read-only state of the document only reflects the ability to write to the document in the Visual C++ environment; it doesn't affect the read-only attribute of the file on disk.)
If you execute the macro without a document being active, the On Error Resume Next statement will ensure that nothing happens.
So far we haven't said much about property pages and controls. That's because it makes no real difference to the implementer of a property page whether the objects that are being manipulated are controls or not. The objects passed to a property page only need to support the particular interface(s) the page is interested in; there's no requirement beyond that.
From a control developer's point of view, there's not much they need to do once they've written (or discovered) a property page that understands the interface supported by their control. The only thing they do need to do is implement an interface, ISpecifyPropertyPages, that allows interested containers to find out which property pages can be used to set the control's properties.
A container will call ISpecifyPropertyPages::GetPages and the control will return an array of CLSIDs (in the form of a CAUUID structure) indicating the pages supported by that control. That's all there is to it from the control's perspective.
Implementing ISpecifyPropertyPages using ATL is incredibly easy. Just take the following steps:
ISpecifyPropertyPagesImpl provides an implementation of GetPages that loops through the entries in the property map and returns the array of CLSIDs without any coding effort on your part. In fact, if you're generating a full control (Full Control, DHTML Control, or Composite Control) using the ATL Object Wizard, you don't even need to worry about the first two steps—the wizard will generate the necessary code. All you have to do is add the PROP_PAGE entries to the property map.
Well-behaved containers will display the specified property pages in the same order as the PROP_PAGE entries in the property map. Generally, you should put standard property page entries after the entries for your custom pages in the property map, so that users see the pages specific to your class first.
Figure 4. Implementing the ISpecifyPropertyPages interface
The following class for a calendar uses the ISpecifyPropertyPages interface to tell containers that its properties can be set using a custom date page and the stock color page:
class ATL_NO_VTABLE CCalendar :
// ...
public ISpecifyPropertyPagesImpl<CCalendar>,
// ...
{
BEGIN_COM_MAP(CCalendar)
// ...
COM_INTERFACE_ENTRY(ISpecifyPropertyPages)
// ...
END_COM_MAP()
//...
BEGIN_PROP_MAP(CCalendar)
// ...
PROP_PAGE(CLSID_DatePage)
PROP_PAGE(CLSID_StockColorPage)
// ...
END_PROP_MAP()
};
Note that there's no indication that this class is actually a control. It could be, but it doesn't have to be. Any ATL COM class is capable of supporting ISpecifyPropertyPages using ATL's implementation of this interface. If you need this functionality in your noncontrol components, follow the three steps just described to add this functionality to your class.
Note If you are designing an application that needs to discover at run time which property pages a noncontrol object supports, consider using registration information that is external to the object, rather than requiring the objects to implement ISpecifyPropertyPages. For many applications the coupling provided by ISpecifyPropertyPages between an object and the property pages used to manipulate it is too strong, and unnecessarily adds to the amount of code in each object.
To finish things off, here's some code that shows how you can use the ISpecifyPropertyPages interface to get the array of supported property pages and display them using OleCreatePropertyFrame. Once again, there's nothing control-specific about this code beyond the need for the pUnk object to support the ISpecifyPropertyPages interface (which, as I've pointed out, isn't control-specific at all).
STDMETHODIMP CHelper::ShowSpecifiedPages(BSTR bstrCaption,
IUnknown* pUnk)
{
// Ensure that we can take the address of pUnk
if (!pUnk)
return E_INVALIDARG;
// Query object for ISpecifyPropertyPages interface
CComQIPtr<ISpecifyPropertyPages> spSpec(pUnk);
if (!spSpec)
return E_NOINTERFACE;
// Get list of property pages
CAUUID cauuid = {0};
HRESULT hr = spSpec->GetPages(&cauuid);
// Use the system-supplied property frame
if (SUCCEEDED(hr) && (cauuid.cElems != 0))
{
hr = OleCreatePropertyFrame(
NULL, // Parent window of the property frame
0, // Horizontal position of the property frame
0, // Vertical position of the property frame
bstrCaption,// Property frame caption
1, // Number of objects
&pUnk, // Array of IUnknown pointers for objects
cauuid.cElems, // Number of property pages
cauuid.pElems, // Array of CLSIDs for property pages
NULL, // Locale identifier
0, // Reserved - 0
NULL // Reserved - 0
);
// Free array allocated by GetPages
CoTaskMemFree(cauuid.pElems);
}
return hr;
}
The only things to watch out for when calling ISpecifyPropertyPages::GetPages are that you should initialize the CAUUID structure you pass to the method, and that you need to call CoTaskMemFree on the pElems member when you've finished using the structure. The rest of the code is just a call to OleCreatePropertyFrame, which handles all the hard work.
In this article, you've seen how to use ATL to help implement your property page functionality. You've seen that ATL provides two main classes related to property pages: IPropertyPageImpl, which works with one of the ATL dialog box classes to provide a basic implementation of a property page, and ISpecifyPropertyPagesImpl, which you can use in your ActiveX controls (or other classes) to return an array of relevant property page CLSIDs. The example we developed, although simple, showed that property pages have a use beyond ActiveX controls.
Alex Stockton is a programmer/writer in the Visual C++ group. He has co-authored two books, Beginning ATL COM Programming and Ivor Horton's Introduction to Microsoft Visual C++ 6.0, both published by Wrox Press.