| Tenouk C & C++ | MFC Home | ATL and ActiveX Controls 5 | ATL and ActiveX Controls 7 | Download | Site Index |


 

 

 

 

 

ATL and ActiveX Controls 6

 

 

 

 

 

 

Program examples compiled using Visual C++ 6.0 compiler on Windows XP Pro machine with Service Pack 2. Topics and sub topics for this tutorial are listed below. Don’t forget to read Tenouk’s small disclaimer. The supplementary note for this tutorial is control class.

  1. Property Pages

 

Property Pages

 

Since ActiveX controls are usually UI gadgets meant to be mixed into much larger applications, they often find their homes within places such as 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 calling certain functions on the control's incoming interface functions. However, when an ActiveX control is in design mode, accessing the properties through the interface functions isn't always practical. It would be unkind to tool developers to force them to go through the interface functions all the time just to tweak some properties in the control. Why should the tool vendor who is creating the client have to provide UI for managing control properties? That's what property pages are for. Property pages are sets of dialogs implemented by the control for manipulating properties. That way, the tool vendors don't have to keep re-creating dialog boxes for tweaking the properties of an ActiveX control.

 

How Property Pages Are Used. Property pages are usually used in one of two ways. The first way is through the control's IOleObject interface. The client can call IOleObject's DoVerb() function, passing in the properties verb identifier (named OLEIVERB_PROPERTIES and defined as the number -7) to ask the control to show its property pages. The control then displays a dialog, or property frame, that contains all the control's property pages. For example, Figure 57 shows the Property Pages dialog containing the property pages for the Microsoft FlexGrid 6.0 control.

 

Figure 57:  The Microsoft FlexGrid 6.0 control executing the properties verb.

 

Figure 57:  The Microsoft FlexGrid 6.0 control executing the properties verb.

 

 

Property pages are a testament to the power of COM. As it turns out, each single property page is a separate COM object (named using a GUID and registered like all the other COM classes on your system). When a client asks an ActiveX control to show its property pages via the properties verb, the control passes its own list of property page GUIDs into a system API function named OleCreatePropertyFrame(). OleCreatePropertyFrame() enumerates the property page GUIDs, calling CoCreateInstance() for each property page. The property frame gets a copy of an interface so that the frame can change the properties within the control. OleCreatePropertyFrame() calls back to the control when the user clicks the OK or Apply button.

The second way clients use property pages is when the client asks the control for a list of property page GUIDs. Then the client calls CoCreateInstance() on each property page and installs each property page in its own frame. Figure 58 shows an example of how Visual C++ uses the Microsoft FlexGrid property pages in its own property dialog frame.

This second method is by far the most common way for a control's property pages to be used. Notice that the property sheet in Figure 58 contains a General tab in addition to the control's property pages, and that the General tab shown in Figure 55 has been renamed to the Control tab. The General property page in Figure 58 belongs to Visual C++. The Control, Style, Font, Color, and Picture property pages belong to the control (even though they're being shown within the context of Visual C++).

 

Figure 58:  Microsoft Visual C++ inserting the Microsoft FlexGrid 6.0 property pages into its own dialog box for editing resource properties.

 

Figure 58:  Microsoft Visual C++ inserting the Microsoft FlexGrid 6.0 property pages into its own dialog box for editing resource properties.

 

For a property page to work correctly, the control that the property page is associated with needs to implement ISpecifyPropertyPages and the property page object needs to implement an interface named IPropertyPage. With this in mind, let's examine exactly how ATL implements property pages.

 

Adding a Property Page to Your Control. You can use the Visual Studio ATL Object Wizard to create property pages in your ATL project. To create a property page, perform the following steps:

 

  1. Select New ATL Object from the Visual C++ Insert menu.

  2. From the ATL Object Wizard dialog, select Controls from the Category list.

  3. Select Property Page from the Objects list.

  4. Click Next.

  5. Fill in the required information on the ATL Object Wizard Properties dialog, and click OK.

 

ATL's Object Wizard generates a dialog template and includes it as part of a control's resources. In the dice control example, the two properties you're concerned with are the color of the dice and the number of times to roll the dice. The dialog template created by ATL's Object Wizard is blank, so you'll want to add a couple of controls to represent these properties. In this example, the user will be able to select the dice color from a combo box and enter the number of times the dice should roll in an edit control, as shown in Figure 59.

 

Figure 59. The property page dialog template.

 

Figure 59. The property page dialog template.

 

The ATL Object Wizard also creates a C++ class for you that implements the interface necessary for the class to behave as a property page. In addition to generating this C++ class, the ATL Object Wizard makes the class part of the project. The ATL Object Wizard adds the new property page class to the IDL file within the coclass section. In addition, the ATL Object Wizard appends the property page to the object map so that DllGetClassObject() can find the property page class. Finally, the ATL Object Wizard adds a new Registry script so that the DLL makes the correct Registry entries when the control is registered. Here is the header file created by the ATL Object Wizard for a property page named DiceMainPropPage:

#include "resource.h"  // main symbols

 

class ATL_NO_VTABLE CDiceMainPropPage :

    public CComObjectRootEx<CComSingleThreadModel>,

    public CComCoClass<CDiceMainPropPage, &CLSID_DiceMainPropPage>,

    public IPropertyPageImpl<CDiceMainPropPage>,

    public CDialogImpl<CDiceMainPropPage>

{

public:

    CDiceMainPropPage()

    {

        m_dwTitleID = IDS_TITLEDiceMainPropPage;

        m_dwHelpFileID = IDS_HELPFILEDiceMainPropPage;

        m_dwDocStringID = IDS_DOCSTRINGDiceMainPropPage;

    }

 

    enum {IDD = IDD_DICEMAINPROPPAGE};

 

DECLARE_REGISTRY_RESOURCEID(IDR_DICEMAINPROPPAGE)

DECLARE_PROTECT_FINAL_CONSTRUCT()

BEGIN_COM_MAP(CDiceMainPropPage)

    COM_INTERFACE_ENTRY(IPropertyPage)

END_COM_MAP()

BEGIN_MSG_MAP(CDiceMainPropPage)

    CHAIN_MSG_MAP(IPropertyPageImpl<CDiceMainPropPage>)

END_MSG_MAP()

 

STDMETHOD(Apply)(void)

{

    ATLTRACE(_T("CDiceMainPropPage::Apply\n"));

    for (UINT i = 0; i < m_nObjects; i++)

    {

        // Do something interesting here

        // ICircCtl* pCirc;

        // m_ppUnk[i]->QueryInterface(IID_ICircCtl, (void**)&pCirc);

        // pCirc->put_Caption(CComBSTR("something special"));

        // pCirc->Release();

    }

    m_bDirty = FALSE;

    return S_OK;

}

};

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Examining this property page listing reveals that 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.

As with most other COM classes created by ATL's Object Wizard, most of the code involved in getting a property page to work is boilerplate code. Notice that besides the constructor and some various maps, the only other function is one named Apply.

Before getting into the mechanics of implementing a property page, it's helpful to take a moment to understand how the property page architecture works. The code you need to type in to get the property pages working will then make more sense.

When the client decides it's time to show some property pages, a modal dialog frame needs to be constructed. The frame is constructed by either the client or by the control itself. If the property pages are being shown via the DoVerb() function, the control constructs the frame. If the property pages are being shown within the context of another application, as when Visual C++ shows the control's property pages within the IDE, the client constructs the dialog frame. The key to the dialog frame is that it holds property page sites (small objects that implement IPropertyPageSite) for each property page.

The client code (the modal dialog frame, in this case) then enumerates through a list of GUIDs, calling CoCreateInstance() on each one of them and asking for the IPropertyPage interface. If the COM object produced by CoCreateInstance() is a property page, it implements the IPropertyPage interface. The dialog frame uses the IPropertyPage interface to talk to the property page. Here's the declaration of the IPropertyPage interface:

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;

};

Once a property page has been created, the property page and the client code need some channels to communicate back and forth with the control. After the property dialog frame successfully calls QueryInterface() for IPropertyPage on the property page objects, the frame calls IPropertyPage::SetPageSite on each IPropertyPage interface pointer it holds, passing in an IPropertyPageSite interface pointer. The property page sites within the property frame provide a way for each property page to call back to the frame. The property page site provides information to the property page and receives notifications from the page when changes occur. Here's the IPropertyPageSite interface:

interface IPropertyPageSite : public IUnknown

{

    public:

        virtual HRESULT OnStatusChange(DWORD dwFlags) = 0;

        virtual HRESULT GetLocaleID(LCID *pLocaleID) = 0;

        virtual HRESULT GetPageContainer(IUnknown *ppUnk) = 0;

        virtual HRESULT TranslateAccelerator(MSG *pMsg) = 0;

};

In addition to the frame and control connecting to each other through IPropertyPage and IPropertyPageSite, each property page needs a way to talk back to the control. This is usually done when the dialog frame calls IPropertyPage::SetObjects, passing in the control's IUnknown. Figure 60 illustrates the property page architecture.

Now that you see how ActiveX Control property pages work in general, understanding how they work within ATL will be a lot easier. You'll see how ATL's property pages work, in cases when the client code exercises the control's properties verb as well as in cases when environments like Visual C++ integrate a control's property pages into the IDE.

 

Figure 60:  How the property pages, the property frame, and the property page sites communicate.

 

Figure 60:  How the property pages, the property frame, and the property page sites communicate.

 

 

ATL and the Properties Verb. The first way in which an ActiveX control shows its property pages is when the client invokes the properties verb by calling IOleObject::DoVerb using the constant OLEIVERB_PROPERTIES. When the client calls DoVerb() in an ATL-based control, the call ends up in the function CComControlBase::DoVerbProperties, which 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 arranges them within the dialog frame. OleCreatePropertyFrame() uses each property page's IPropertyPage interface to manage the property page, as described in "How Property Pages Are Used"

 

ATL Property Maps. Of course, understanding how OleCreatePropertyFrame() works from within the ATL-based control begs the next question: where does the list of property pages actually come from? ATL uses macros to generate lists of property pages called property maps. 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. ATL includes several macros for implementing property maps: BEGIN_PROPERTY_MAP, PROP_ENTRY, PROP_ENTRY_EX, PROP_PAGE, and END_PROPERTY_MAP. Here are those macros in the raw:

struct ATL_PROPMAP_ENTRY

{

    LPCOLESTR szDesc;

    DISPID dispid;

    const CLSID* pclsidPropPage;

    const IID* piidDispatch;

    DWORD dwOffsetData;

    DWORD dwSizeData;

    VARTYPE vt;

};

 

#define BEGIN_PROPERTY_MAP(theClass) \

    typedef _ATL_PROP_NOTIFY_EVENT_CLASS __ATL_PROP_NOTIFY_EVENT_CLASS; \

        typedef theClass _PropMapClass; \

    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; \

}

When you decide to add property pages to a COM class using ATL's property page macros, according to the ATL documentation you should put these macros into your class's header file. For example, if you want to add property pages to the dice control, you'd add the following code to the C++ class:

class ATL_NO_VTABLE Cmyatldiceob :

 ...

 ...

 ...

{

    ...

    ...

    BEGIN_PROP_MAP(Cmyatldiceob)

        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()

};

ATL's property map macros set up the list of GUIDs representing property pages. ATL's property maps are composed of an array of ATL_PROPMAP_ENTRY structures. The BEGIN_PROPERTY_MAP macro declares a static variable of this structure. The PROP_PAGE macro inserts a GUID into the list of property pages. PROP_ENTRY inserts 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. When client code invokes the control's properties verb, the control just rips through this list of GUIDs and hands the list over to the OleCreatePropertyFrame() so that the property can create the property pages.

Property Pages and Development Tools 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 such as Visual Basic and Visual C++ might want programmatic access to a control's property pages. For example, when using MFC to work on a dialog box containing an ActiveX control, right-clicking on the control to view the properties gives you a dialog frame produced by Visual C++ (as opposed to the dialog frame produced by OleCreatePropertyFrame()).

Visual C++ uses the control's ISpecifyPropertyPages interface to get the list of GUIDs (the list generated by the property page macros). Here's the ISpecifyPropertyPages interface definition:

interface ISpecifyPropertyPages : public IUnknown

{

    HRESULT GetPages(CAUUID *pPages);

};

 

typedef struct tagCAUUID

{

    ULONG     cElems;

    GUID FAR* pElems;

} CAUUID;

ATL implements the ISpecifyPropertyPages::GetPages function by cycling through the list of GUIDS (produced by the property map macros) and returning them within the CAUUID structure. Environments like Visual C++ use each GUID in a call to CoCreateInstance() to create a new property page. The property page site and the property page exchange interfaces. The property page site holds on to the property page's IPropertyPage interface, and the property page holds on to the property site's IPropertyPageSite interface. After the dialog frame constructs the property pages, it needs to reflect the current state of the ActiveX control through the dialog controls. For that you need to override the property page's Show() method.

 

Showing the Property Page. The property page's Show() method is called whenever the property page is about to be shown. A good thing for a property page to do at this time is fetch the values from the ActiveX control and populate the property page's controls. Remember that the property page holds on to an array of unknown pointers (they're held in the IPropertyPageImpl's m_ppUnk array.) To access the ActiveX control's properties, you need to call QueryInterface() on the unknown pointers and ask for the interface that exposes the properties. In this case, the interface is IMyatldiceobj. Once the property page has the interface, it can use the interface to fetch the properties and plug the values into the dialog box controls. Here's the overridden Show() method:

 

#include "atldicesrvr.h"

 

class ATL_NO_VTABLE CDiceMainPropPage :

    public CComObjectRootEx<CComSingleThreadModel>,

    public CComCoClass<CDiceMainPropPage, &CLSID_DiceMainPropPage>,

    public IPropertyPageImpl<CDiceMainPropPage>,

    public CDialogImpl<CDiceMainPropPage>

{

    ...

    ...

    ...

STDMETHOD(Show)( UINT nCmdShow )

{

        HRESULT hr;

        USES_CONVERSION;

 

        if(nCmdShow == SW_SHOW || nCmdShow == SW_SHOWNORMAL) {

            for (UINT i = 0; i < m_nObjects; i++)

            {

                CComQIPtr< IATLDieceObj, &IID_IATLDieceObj > pMyatldiceob(m_ppUnk[i]);

                short nColor = 0;

 

                if FAILED(pMyatldiceob->get_DiceColor(&nColor))

                {

                    CComPtr<IErrorInfo> pError;

                    CComBSTR            strError;

                    GetErrorInfo(0, &pError);

                    pError->GetDescription(&strError);

                    MessageBox(OLE2T(strError), _T("Error"), MB_ICONEXCLAMATION);

                    return E_FAIL;

                }

                HWND hWndComboBox = GetDlgItem(IDC_COLOR);

                ::SendMessage(hWndComboBox, CB_SETCURSEL, nColor, 0);

                short nTimesToRoll = 0;

 

                if FAILED(pMyatldiceob->get_TimesToRoll(&nTimesToRoll))

                {

                    CComPtr<IErrorInfo> pError;

                    CComBSTR            strError;

                    GetErrorInfo(0, &pError);

                    pError->GetDescription(&strError);

                    MessageBox(OLE2T(strError),

                        _T("Error"), MB_ICONEXCLAMATION);

                    return E_FAIL;

                }

                SetDlgItemInt(IDC_TIMESTOROLL, nTimesToRoll, FALSE);

            }

        }

        m_bDirty = FALSE;

        hr = IPropertyPageImpl<CDiceMainPropPage>::Show(nCmdShow);

        return hr;

    }

};

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

In addition to adding code to prepare to show the dialog box, you need to add code allowing users to set the control's properties. Whenever the user changes a property, the property dialog activates the Apply button, indicating that the user can apply the newly set properties. When the user presses the Apply button, control jumps to the property page's Apply function so you need to insert some code in here to make the Apply button work.

 

Handling the Apply Button. After the user finishes manipulating the properties, he or she clicks either the Apply button or the OK button to save the changes. In response, the client code asks the property page to apply the new properties to the control. Remember that the ActiveX control and the property page are separate COM objects, so they need to communicate via interfaces. Here's how the process works.

When you create a property page using the ATL Object Wizard, ATL overrides the Apply() function from IPropertyPage for you. The property page site uses this function for notifying the property page of changes that need to be made to the control. When the property page's Apply() function is called, it's time to synch up the state of the property page with the state of the control. Remember, the control's IUnknown interface was passed into the property page early in the game via a call to IPropertyPage::SetObjects. The interface pointers are stored in the property page's m_ppUnk array. Most property pages respond to the Apply() function by setting the state of the ActiveX control properties through the interface provided. In the case of our example ATL-based property page, this means examining the value in the combo box and the edit box and setting the new values inside the control itself, like this:

#include "atldicesrvr.h"

 

class ATL_NO_VTABLE CDiceMainPropPage :

    public CComObjectRootEx<CComSingleThreadModel>,

    public CComCoClass<CDiceMainPropPage, &CLSID_DiceMainPropPage>,

    public IPropertyPageImpl<CDiceMainPropPage>,

    public CDialogImpl<CDiceMainPropPage>

{

    ...

    ...

    STDMETHOD(Apply)(void)

    {

        USES_CONVERSION;

        ATLTRACE(_T("CDiceMainPropPage::Apply\n"));

        for (UINT i = 0; i < m_nObjects; i++)

        {

            CComQIPtr<IATLDieceObj, &IID_IATLDieceObj> pMyatldiceob(m_ppUnk[i]);

            HWND hWndComboBox = GetDlgItem(IDC_COLOR);

            short nColor = (short)::SendMessage(hWndComboBox, CB_GETCURSEL, 0, 0);

 

            if(nColor >= 0 && nColor <= 2)

            {

                if FAILED(pMyatldiceob->put_DiceColor(nColor))

                {

                    CComPtr<IErrorInfo> pError;

                    CComBSTR            strError;

                    GetErrorInfo(0, &pError);

                    pError->GetDescription(&strError);

                    MessageBox(OLE2T(strError), _T("Error"), MB_ICONEXCLAMATION);

                    return E_FAIL;

                }

            }

 

            short nTimesToRoll = (short)GetDlgItemInt(IDC_TIMESTOROLL);

            if FAILED(pMyatldiceob->put_TimesToRoll(nTimesToRoll))

            {

                CComPtr<IErrorInfo> pError;

                CComBSTR            strError;

                GetErrorInfo(0, &pError);

                pError->GetDescription(&strError);

                MessageBox(OLE2T(strError), _T("Error"), MB_ICONEXCLAMATION);

                return E_FAIL;

            }

        }

        m_bDirty = FALSE;

        return S_OK;

    }

 

 

 

 

 

 

Further reading and digging:

  1. DCOM at MSDN.

  2. COM+ at MSDN.

  3. COM at MSDN.

  4. Win32 process, thread and synchronization story can be found starting from Module R.

  5. MSDN MFC 7.0 class library online documentation.

  6. MSDN MFC 9.0 class library online documentation - latest version.

  7. MSDN Library

  8. Windows data type.

  9. Win32 programming Tutorial.

  10. The best of C/C++, MFC, Windows and other related books.

  11. Unicode and Multibyte character set: Story and program examples.

 

 

 

 


 

| Tenouk C & C++ | MFC Home | ATL and ActiveX Controls 5 | ATL and ActiveX Controls 7 | Download | Site Index |