ActiveX Controls, Property Pages, and the Container

 < Free Open Study > 



We have already seen two ways to tweak the properties of the ShapesControl: at design time (using the VB properties window) or run time (by writing code to call the members of the [default] interface). A production level control typically provides a third way to allow the users of the control to modify its state: property pages. As you recall, a property page is a COM object supporting IPropertyPage, and optionally IPropertyPage2. Like any COM object, property pages have CLSIDs that are used to identify them. To allow your container to grab the list of CLSIDs which represent the pages used by your control, the control must implement the ISpecifyPropertyPages interface. This standard COM interface has a single method called GetPages(), which returns an array of CAUUID structures:

// Return the set of CLSIDs for the control's pages. [object, uuid(B196B28B-BAB4-101A-B69C-00AA00341D07)] interface ISpecifyPropertyPages : IUnknown {      typedef struct tagCAUUID {           ULONG cElems;           [size_is(cElems)] GUID * pElems;      } CAUUID;      HRESULT GetPages( [out] CAUUID * pPages ); };

ATL provides a default implementation of this interface from its ISpecifyProperty- PagesImpl<> template, which can be seen from your control's inheritance list:

// ISpecifyPropertyPages is used by a client to grab the CLSIDs of your control's // property page objects. class ATL_NO_VTABLE CShapesControl : ...      public ISpecifyPropertyPagesImpl<CShapesControl>, ... {}; 

The actual implementation of GetPages() is delegated to a helper function called (appropriately enough) GetPagesHelper(). This method iterates over your class's property map, reading the pclsidPropPage field of each ATL_PROPMAP_ENTRY structure. If a non-NULL entry is discovered, it will fill the given index of the CAUUID structure with the correct CLSID. Here is the essence of this function:

// Fill up the CAUUID array with the GUIDS of our control's pages. HRESULT GetPagesHelper(CAUUID* pPages, ATL_PROPMAP_ENTRY* pMap) { ...      // Does this property have a page? CLSID_NULL means it does not.      if (!InlineIsEqualGUID(*pMap[i].pclsidPropPage, CLSID_NULL))      {           BOOL bFound = FALSE;           // Search through array we are building up to see if it is already in there.           for (int j=0; j<nCnt; j++)           {           if (InlineIsEqualGUID(*(pMap[i].pclsidPropPage), pPages->pElems[j]))           {                // It's already there, so no need to add it again                bFound = TRUE;                break;           }      }      // If we didn't find it in there, then add it.      if (!bFound)           pPages->pElems[nCnt++] = *pMap[i].pclsidPropPage; ... }

When the end user opens up your control's property sheet, a dialog box is created to host each page (representing a tabbed dialog). This dialog maintains an individual page site (an object implementing IPropertyPageSite) for each page used by the control. When the container obtains the list of CLSIDs from the control, it will pass the CAUUID structure into the dialog, allowing it to call CoCreateInstance() for each one, and ask each page for its IPropertyPage interface. Here is the definition for IPropertyPage:

// Each property page implements this interface, which is used by the property page // to communicate with the site: [object,uuid(B196B28D-BAB4-101A-B69C-00AA00341D07)] interface IPropertyPage : IUnknown {      typedef struct tagPROPPAGEINFO {           ULONG cb;           LPOLESTR pszTitle;           SIZE size;           LPOLESTR pszDocString;           LPOLESTR pszHelpFile;           DWORD dwHelpContext;      } PROPPAGEINFO;      HRESULT SetPageSite([in] IPropertyPageSite * pPageSite);      HRESULT Activate([in] HWND hWndParent, [in] LPCRECT pRect,                      [in] BOOL bModal);      HRESULT Deactivate();      HRESULT GetPageInfo([out] PROPPAGEINFO * pPageInfo);      HRESULT SetObjects([in] ULONG cObjects,                        [in, size_is(cObjects)] IUnknown ** ppUnk);      HRESULT Show([in] UINT nCmdShow);      HRESULT Move([in] LPCRECT pRect);      HRESULT IsPageDirty();      HRESULT Apply();      HRESULT Help();      HRESULT TranslateAccelerator( [in] MSG * pMsg); };

As you can see, IPropertyPage defines a handy structure (PROPPAGEINFO) which identifies all the necessary information for a given page (e.g., the tab's caption, online help information). Among the numerous methods of IPropertyPage, the ones to keep focused on are Apply() and Show().

The Apply() method is called in response to the end user selecting the dialog box's Apply button. This signals to the dialog box that it is time to update the associated control with new values contained in the page's GUI widgets (i.e., call the correct [propput] methods of the control's [default] interface).

Show() is called when a given page object is about to be made visible to the user. This method is important because this is our chance to make sure the data placed in the dialog box widgets is up to date (i.e., call the correct [propget] functions in the control's [default] interface). For example, if we have an edit box used to set a control's Age property, the correct value should be placed into the box when it comes to life.

So to summarize the interaction of the key players, the container obtains a list of CLSIDs from the control using ISpecifyPropertyPages, to understand which pages work with the given control. The pages are hosted in a tabbed dialog box that sets up a page site (which is a COM object implementing IPropertyPageSite) for each page. So, what exactly is your responsibility as an ATL developer? Create the individual pages using the ATL Object Wizard. The framework takes care of the communications between each player.

Building Property Pages with ATL

Building a set of property pages for your ActiveX control is relatively painless. To begin the process, you access the ATL Object Wizard. Choose Controls from the Category section, and insert a new Property Page object as seen in Figure 14-34:

click to expand
Figure 14-34: Inserting a new property page à la ATL.

When you insert a new page, you will be presented with a unique configuration dialog. The Names and Attributes tabs are the same as always. The only difference is that you will not be adding any custom interfaces to a property page object, and thus the Interface edit field is disabled (typically, the only interface a property page will implement is the standard IPropertyPage). The Strings tab (Figure 14-35) allows you to set the text that will be used to identify the caption of this page in the tabbed dialog box, as well as any context- sensitive help paths:

click to expand
Figure 14-35: The text in the Title selection will be the string used on the tab for your custom page.

When you finish inserting your new page using the ATL Object Wizard, you will be given a dialog resource to assemble your GUI and a new class representing the page. For this example, we will construct a user interface which allows the user to enter text for our stock Caption property, as well as select the current shape:

click to expand
Figure 14-36: The GUI of our custom property page.

Now that we have a visual front end, we can examine how to move the values into our control's properties. As you can guess, we need to suck out the text in the edit box and send it into put_Caption(). Likewise, we need to discover the current shape and send that value into put_ShapeType(). The code behind this process will take place in the Apply() method of your new C++ class wrapping this dialog resource template.

We will also need to configure the page in such a way that the current values of the control's properties are shoved into our GUI widgets before the page is made visible. To make these code adjustments, let's get to know IPropertyPageImpl<>.

Framework Support: IPropertyPageImpl<>

When you insert a new property page, you will be given a C++ class deriving from a few ATL base classes. First, you inherit basic IUnknown support from CComObject- RootEx<>. Your class factory is provided via CComCoClass<>. As well, you gather in the core windowing and dialog support provided by ATL, as you are also derived from CDialogImpl<>. Most importantly, every ATL property page will derive from IPropertyPageImpl<>, which provides the implementation details of IPropertyPage.

Note 

As a property page is a valid COM object, you will see a new coclass statement within your project's IDL file.

Here is the initial class definition:

// Initial code for an ATL property page. class ATL_NO_VTABLE CCoCustomPage :      public CComObjectRootEx<CComSingleThreadModel>,      public CComCoClass<CCoCustomPage, &CLSID_CoCustomPage>,      public IPropertyPageImpl<CCoCustomPage>,      public CDialogImpl<CCoCustomPage> { public:      CCoCustomPage()      {           m_dwTitleID = IDS_TITLECoCustomPage;           m_dwHelpFileID = IDS_HELPFILECoCustomPage;           m_dwDocStringID = IDS_DOCSTRINGCoCustomPage;      }      enum {IDD = IDD_COCUSTOMPAGE}; DECLARE_REGISTRY_RESOURCEID(IDR_COCUSTOMPAGE) DECLARE_PROTECT_FINAL_CONSTRUCT() BEGIN_COM_MAP(CCoCustomPage)      COM_INTERFACE_ENTRY(IPropertyPage) END_COM_MAP() BEGIN_MSG_MAP(CCoCustomPage)      CHAIN_MSG_MAP(IPropertyPageImpl<CCoCustomPage>) END_MSG_MAP()      STDMETHOD(Apply)(void)      {           ATLTRACE(_T("CCoCustomPage::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;      } }; 

IPropertyPageImpl<> defines three DWORD data members which are used to hold the resource IDs for the custom strings used for this page, based on your selections from the String tab. We have an initial COM map supporting IPropertyPage, and a message map which gathers default messages from IPropertyPageImpl<>. The only other item of note is the Apply() method which has been defined in your class (with some suggested comments to boot). Recall that this method of IPropertyPage will be called when the user wishes to commit the given modifications. Your job is to send the new data to the control.

Programming the Apply() Method

The logic behind the Apply method will always have the same general flavor: Obtain the [default] interface of the control and call the correct [propput] methods to assign the new values gathered from the GUI widgets. In many IDEs, it is possible to use a property page to apply changes to any number of selected objects. For example, this VB form has four ShapesControls selected (Figure 14-37); any new values that were specified in the property page should apply to each of them. IPropertyPageImpl<> maintains an array of IUnknown pointers, which represent each control currently selected by the end user at design time.

click to expand
Figure 14-37: The effects of clicking the Apply button should affect all selected controls.

You will also need to add code to your IPropertyPageImpl<> derived class to enable or disable the Apply button maintained by the host dialog. For example, we need to enable the Apply button whenever a radio button has been selected or when any new text has been entered into the edit box. IPropertyPageImpl<> provides the SetDirty() method, which sets the value of an underlying BOOL (m_bDirty). Based on the truth or falsity of this data item, the framework will enable or disable the button accordingly. With this in mind, we need to handle an EN_CHANGE message for the edit box, and BN_CLICKED messages for each radio button. Each of these handlers will call SetDirty(TRUE) to enable the Apply button. Beyond this, each of the BN_CLICKED handlers will set the current value of the shape (assume our property page class maintains a private CURRENTSHAPE data member).

To ensure the end user's changes affect each selected control, the Apply() logic should iterate over each of the IUnknown pointers (the m_ppUnk array maintained by IProperty- PageImpl<>) and call put_Caption() and put_ShapeType() using the new values. Here, then, are all the necessary changes to program our custom property page Apply() method (with the correct command handlers):

// CCoCustomPage #include "shapescontrol.h" class ATL_NO_VTABLE CCoCustomPage : ... { ... // Inserted with the Add Message Handler Wizard. BEGIN_MSG_MAP(CCoCustomPage)      CHAIN_MSG_MAP(IPropertyPageImpl<CCoCustomPage>)      COMMAND_HANDLER(IDC_RADIOCIRCLE, BN_CLICKED,                     OnClickedRadioCircle)      COMMAND_HANDLER(IDC_RADIORECT, BN_CLICKED,                     OnClickedRadioRect)      COMMAND_HANDLER(IDC_RADIOSQUARE, BN_CLICKED,                     OnClickedRadioSquare)      COMMAND_HANDLER(IDC_EDITCAPTION, EN_CHANGE,                     OnChangeEditCaption) END_MSG_MAP()      STDMETHOD(Apply)(void)      {           // Loop over each selected control.           for (UINT i = 0; i < m_nObjects; i++)           {                // First set the caption.                IShapesControl* pSC = NULL;                CComBSTR temp;                GetDlgItemText( IDC_EDITCAPTION, temp.m_str);                m_ppUnk[i]->QueryInterface(IID_IShapesControl,                                       (void**)&pSC);                pSC->put_Caption(temp);                // Now set the shape.                pSC->put_ShapeType(m_currShape);                pSC->Release();           }           m_bDirty = FALSE;          // Changes are made; set dirty flag to false.           return S_OK;      }      LRESULT OnClickedRadioCircle(WORD wNotifyCode, WORD wID,                                HWND hWndCtl, BOOL& bHandled)      {           m_currShape = shapeCIRCLE;           SetDirty(TRUE);           return 0;      }      LRESULT OnClickedRadioRect(WORD wNotifyCode, WORD wID, HWND                                 hWndCtl, BOOL& bHandled)      {           m_currShape = shapeRECTANGLE;           SetDirty(TRUE);           return 0;      }      LRESULT OnClickedRadioSquare(WORD wNotifyCode, WORD wID, HWND                               hWndCtl, BOOL& bHandled)      {           m_currShape = shapeSQUARE;           SetDirty(TRUE);           return 0;      }      LRESULT OnChangeEditCaption(WORD wNotifyCode, WORD wID, HWND                               hWndCtl, BOOL& bHandled)      {           SetDirty(TRUE);           return 0;      } private:      CURRENTSHAPE m_currShape; }; 

We now must go back into the control's property map, and associate the ShapeType and Caption property to this page. The ATL Object Wizard has already defined a CLSID constant for this purpose, which is listed at the beginning of your property page's header file. Here are the changes:

// Rigging our properties to the correct page. BEGIN_PROP_MAP(CShapesControl)      PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4)      PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4)      PROP_ENTRY("BackColor", DISPID_BACKCOLOR, CLSID_StockColorPage)      PROP_ENTRY("Caption", DISPID_CAPTION, CLSID_CoCustomPage)      PROP_ENTRY("Font", DISPID_FONT, CLSID_StockFontPage)      PROP_ENTRY("ShapeType", 1, CLSID_CoCustomPage) END_PROP_MAP()

With this, we can now load up our pages using the VB (Custom) selection from the property window (Figure 14-38), and use the page to alter the appearance of the selected controls.

click to expand
Figure 14-38: The custom page in action.

Going the Other Direction: Prepping the GUI Widgets

The Apply() method is used to call the methods of your control's [default] interface using the new values container in the GUI widgets. In other words, the flow of communication is from the page site to the control. When your page is about to be displayed, it would be nice if the edit box really did contain the correct string, and if the correct radio box was indeed focused. Currently, when we pull up our custom page, the text is empty and no radio button is selected. What we need is a way to reverse the flow of communication from the object to the page sink. To do so requires overriding the Show() method maintained by IPropertyPageImpl<>, or alternatively handling a WM_INITDIALOG message in the C++ page wrapper class.

Given that the end user may have selected any number of controls before launching your property pages, you will still be making use of the held array of IUnknown pointers. However, under this context, you will need to decide which of the selected controls will be used to fill the values of the property page. IPropertyPageImpl<> holds the current count within the m_ppUnk array using the m_nObjects data point. I have chosen to use the first object selected to fill the values of the GUI items, but you may choose otherwise. Add a handler for WM_INITDIALOG, and add the following code:

// Fill the values of the edit box and radio selection using the // IShapesControl interface of the first selected control. LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam,                    BOOL& bHandled) {      USES_CONVERSION;      // Show the values from the first selected item.      if (m_nObjects > 0)      {           // Get the first item selected.           IShapesControl* pSC = NULL;           CComBSTR temp;           m_ppUnk[0]->QueryInterface(IID_IShapesControl, (void**)&pSC);           // Set the caption.           pSC->get_Caption(&temp.m_str);           SetDlgItemText(IDC_EDITCAPTION, W2A(temp.m_str));           // Now set focus to the correct radio item.           CURRENTSHAPE curShape;           pSC->get_ShapeType(&curShape);           CheckRadioButton(IDC_RADIOCIRCLE, IDC_RADIORECT,                          IDC_RADIOCIRCLE + curShape);           // All done!           pSC->Release();           SetDirty(FALSE);      }      return 0; }

ShapesControl now has a custom property page which can be used to edit the properties of the [default] interface. This wraps up the complete implementation of our ShapesControl tester. In the next (and final) lab of this book, you will add a custom page to the AXCar control developed in the previous lab. Before we get there, let's see what it would take to get ShapesControl (or any ActiveX control) up on a web page.



 < Free Open Study > 



Developer's Workshop to COM and ATL 3.0
Developers Workshop to COM and ATL 3.0
ISBN: 1556227043
EAN: 2147483647
Year: 2000
Pages: 171

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net