Dr. Dobb's Journal April 1998
In our February 1998 installment of "Undocumented Corner," we took a look at how Microsoft's Active Template Library (ATL) implements dual interfaces. We saw that ATL classes implementing dual interfaces derive from a template named IDispatchImpl. IDispatchImpl works by holding an ITypeInfo pointer representing the type information of the dual interface. When clients call into an ATL-based object's IDispatch interface, ATL simply delegates to the ITypeInfo pointer. This month, we'll take a look at how ATL implements property pages for ActiveX controls.
ActiveX controls are reusable software gadgets (mostly UI components) built using COM. While even today there's some confusion as to what really constitutes an ActiveX control, most folks generally agree that ActiveX controls are those COM objects that live within DLLs and implement a certain set of features. Those features include OLE's embedding protocol, regular incoming interfaces, outgoing interfaces that are connected using COM's connection point technology, and property pages.
In general, most ActiveX controls have both a presentation state and an internal state. The presentation state is the one reflected when the control draws itself. The control's internal state is a set of variables exposed to the outside world via one or more interfaces -- they're also known as properties.
For example, imagine a simple grid implemented as an ActiveX control. The grid has an external presentation state and a set of internal variables for describing the state of the grid. The properties of a grid control would probably include the number of rows in the grid, the number of columns in the grid, the color of the lines composing the grid, the kind of font to use, and so forth.
As ActiveX controls are usually UI gadgets meant to be mixed into much larger applications, they often find their homes within places like Visual Basic forms and MFC form views and dialogs. When a control is instantiated, the client code can usually reach into the control and manipulate its properties by exercising interface functions. However, when an ActiveX control is in design mode, it's usually not practical to access the properties through the interfaces. It would be unkind to tool developers to make them have to go through the interface functions just to tweak some properties in the control. Why should the tool vendor have to provide UI for managing properties? That's what property pages are for. Property pages are sets of dialogs implemented for manipulating properties. That way, the tool vendors don't have to keep recreating dialog boxes for tweaking the properties of an ActiveX control.
Client code usually accesses property pages in one of two ways. The first way is for the client to call IOleObject's DoVerb, passing in the property verb identifier (named OLEIVERB_PROPERTIES and defined as the number -7). The control then shows a dialog frame with all the control's property pages. For example, Figure 1 illustrates the property dialog for Microsoft's FlexGrid control.
Property pages are a testament to the power of COM. As it turns out, each single property page is a separate COM object (represented by GUIDs, of course). When a client asks control to show its property pages via the properties verb, the ActiveX control passes its own list of property page GUIDs into a function named OleCreatePropertyFrame, which enumerates the property page GUIDs, calling CoCreateInstance() for each property page. The property frame gets a copy of an interface for talking to the control (so the frame can change the properties within the control). OleCreateProperty frame calls back to the control when the user hits the OK or Apply button.
The second way for clients to use property pages is for the client to ask the control for a list of property page GUIDs. Then the client calls CoCreateInstance on each property page and installs each in its own frame. See Figure 2 for an example of how Visual C++ uses Microsoft's FlexGrid property sheets in its own property dialog frame.
This is by far the most common way for clients to use a control's property pages. Notice that the property sheet in Figure 2 contains a General tab in addition to the control's property pages. The "General" property page belongs to Visual C++. The "Control," "Style," "Font," "Color," "Picture," and "All" property pages belong to the control (even though they're being shown within the context of Visual C++.
For property pages to work correctly, the COM objects implementing property pages need to implement ISpecifyPropertyPages, and each property page object needs to implement an interface named IPropertyPage. With this in mind, let's take a look at exactly how ATL implements property sheets.
Visual Studio provides a wizard for creating property pages in your ATL project. To create one, just select New ATL Object from Visual C++'s Insert menu. The ATL ObjectWizard generates a CPP class for you that implements the functions and interfaces necessary for the class to behave as a property page. In addition to generating a C++ class that implements the correct property page interfaces, the ATL ObjectWizard makes the property page class part of the project. The ObjectWizard adds the new property page class to the IDL file within the coclass section. In addition, the ObjectWizard adds the property page to the object map so DllGetClassObject can find the property page classes. Finally, the ObjectWizard adds a new registry script (so the DLL makes the correct registry entries when the control is registered).
ATL's property page classes are composed of several ATL templates: CComObjectRootEx (to implement IUnknown), CComCoClass (the class object for the property page), IPropertyPageImpl (for implementing IPropertyPage), and CDialogImpl (for implementing the dialog-specific behavior).
ATL's property pages are relatively simple beasts. They really only implement one interface -- IPropertyPage. Listing One shows the IPropertyPage interface.
This interface is used by client code to manage the property page. For example, if the property page lives inside the frame created by OleCreatePropertyFrame, the property frame uses the interface to perform operations such as applying the new properties when the property frame's Apply button is pushed.
Let's examine how ATL implements the properties verb first. A control shows its property pages in response to the client issuing the properties verb by calling IOleObject::DoVerb using the number defined by OLEIVERB_PROPERTIES. When this happens, control ends up in CComControlBase::DoVerbProperties. CComControlBase::DoVerbProperties simply calls OleCreatePropertyFrame, passing in its own IUnknown pointer and the list of property page GUIDs. OleCreatePropertyFrame takes the list of GUIDs, calling CoCreateInstance on each one to create the property pages and arrange them within the dialog frame. OleCreatePropertyFrame uses each property pages's IPropertyPage interface to manage the property page.
Of course, understanding how OleCreatePropertyFrame works begs the next question -- where does the list of property pages come from? ATL generates lists of property pages through its property maps -- some macros to help manage property pages. Whenever you add a new property page to an ATL-based control, you need to set up the list of property pages through these macros. For implementing property pages, there are three macros: BEGIN_PROPERTY_MAP, PROP_ENTRY, PROP_ENTRY_EX, PROP_PAGE, and END_PROPERTY_MAP. Listing Two presents these macros.
When you decide to add property pages to a COM class using ATL, the ATL documentation tells you to put these macros into your class' header file. For example, if you wanted to add property pages to a class named CTestCtl, you could add Listing Three.
ATL's property map macros set up the list of GUIDs representing property pages. Remember that property pages are just COM objects. Notice that Listing Two shows a structure named ATL_PROPMAP_ENTRY and that BEGIN_PROPERTY_MAP declares a static variable of this structure. The PROP_PAGE macro simply inserts a GUID into the list of property pages. PROP_ENTRY insert a property page GUID into the list as well as associating a specific control property with the property page. The final macro PROP_ENTRY_EX lets you associate a certain dual interface to a property page.
Now let's see how ATL's property pages work with tools like Visual Basic and Visual C++.
Executing the properties verb isn't the only way for an ActiveX control to show its property pages. As we mentioned before, folks who write tools like Visual Basic and Visual C++ may want programmatic access to a control's property pages. For example, imagine working on a dialog box containing an ActiveX control. When you right mouse-click on the control to view the properties you get dialog frame produced by Visual C++ (as opposed to OleCreatePropertyFrame).
Visual C++ uses the control's ISpecifyPropertyPages interface to get the list of GUIDs; see Listing Four. ATL implements this interface by cycling through the list of GUIDs (produced by the property map macros) and returning them within the CAUUID structure. Then environments like Visual C++ use each GUID in a call to CoCreateInstance to create new property page.
The last interesting part of ATL's property page implementation is to see how they implement the Apply method. Once the client code has produced the set of pages (either by using OleCreateFrame or some other means), the client code may ask the property page to apply new properties. Remember that the ActiveX control and the property page are separate COM objects, so they need to communicate via interfaces.
When you create a property page using the ATL ObjectWizard, ATL overrides the Apply function from IPropertyPage. The ActiveX control has the necessary interfaces -- they were passed into the property page early in the game via a call to IPropertyPage::SetObjects. Most property pages respond to the Apply function by setting the state of the ActiveX control through the interface provided.
Property pages are fairly standard features that clients of ActiveX controls expect to be able to access. MFC has included a property page architecture since the introduction of OLE controls back in October 1994. ATL's property pages work in fundamentally the same way as MFC's. Each ActiveX control that wants to use property pages has to maintain a list of property page GUIDs. The DLL contains both the ActiveX control and the property pages. When the DLL is registered, all the components are registered together. Generally, there are two ways client code can use the property pages. First, the client can ask the control to show its property pages outright by calling IOleObject's DoVerb function. The second and more common way is for the client to query the control for all the property page GUIDs (through ISpecifyPropertyPages), call CoCreateInstance on each one, and host the property pages from within its own frame.
DDJ
interface IPropertyPage : public IUnknown { HRESULT SetPageSite(IPropertyPageSite *pPageSite) = 0;
HRESULT Activate(HWND hWndParent, LPCRECT pRect, BOOL bModal) = 0;
HRESULT Deactivate( void) = 0;
HRESULT GetPageInfo(PROPPAGEINFO *pPageInfo) = 0;
HRESULT SetObjects(ULONG cObjects, IUnknown **ppUnk) = 0;
HRESULT Show(UINT nCmdShow) = 0;
HRESULT Move(LPCRECT pRect) = 0;
HRESULT IsPageDirty( void) = 0;
HRESULT Apply( void) = 0;
HRESULT Help(LPCOLESTR pszHelpDir) = 0;
HRESULT TranslateAccelerator(MSG *pMsg) = 0;
};
struct ATL_PROPMAP_ENTRY{
LPCOLESTR szDesc;
DISPID dispid;
const CLSID* pclsidPropPage;
const IID* piidDispatch;
};
#define BEGIN_PROPERTY_MAP(theClass) \
typedef _ATL_PROP_NOTIFY_EVENT_CLASS __ATL_PROP_NOTIFY_EVENT_CLASS; \
static ATL_PROPMAP_ENTRY* GetPropertyMap()\
{\
static ATL_PROPMAP_ENTRY pPropMap[] = \
{
#define PROP_PAGE(clsid) \
{NULL, NULL, &clsid, &IID_NULL},
#define PROP_ENTRY(szDesc, dispid, clsid) \
{OLESTR(szDesc), dispid, &clsid, &IID_IDispatch},
#define PROP_ENTRY_EX(szDesc, dispid, clsid, iidDispatch) \
{OLESTR(szDesc), dispid, &clsid, &iidDispatch},
#define END_PROPERTY_MAP() \
{NULL, 0, NULL, &IID_NULL} \
}; \
return pPropMap; \
}
class ATL_NO_VTABLE CTestCtl : ... {
...
BEGIN_PROPERTY_MAP(CTestCtl)
PROP_ENTRY("Caption goes here...", 2,
CLSID_MainPropPage)
PROP_ENTRY_EX("Caption goes here...", 3,
CLSID_SecondPropPage,
DIID_SecondDualInterface)
PROP_PAGE(CLSID_StockColorPage)
END_PROPERTY_MAP()
};
interface ISpecifyPropertyPages : public IUnknown { HRESULT GetPages(CAUUID *pPages);
};
typedef struct tagCAUUID
{
ULONG cElems;
GUID FAR* pElems;
} CAUUID;