ATL Tutorial

With ATL, you can create efficient, flexible, lightweight controls. This tutorial leads you through the creation of a control and demonstrates some ATL fundamentals in the process.

The ATL control that you create in this seven-step tutorial draws a circle and also draws a filled polygon inside the circle. You will add a control to your project, add a Sides property to indicate how many sides the polygon will have, and add drawing code to display your control when the property changes. Then, you will make your control respond to click events, add a property page to your control, and put your control on a web page.

The tutorial is divided into seven steps. Do them in order because later steps depend on tasks you have completed in earlier steps.


Step 1: Creating the Project

First you will create the initial ATL project using the ATL COM AppWizard.

  1. In the Developer Studio environment, click New on the File menu, click Project Workspace, and click OK.
  2. Choose ATL COM AppWizard as your application type.
  3. Type Polygon as the project name and click Create.

The ATL COM AppWizard presents a dialog box offering several choices to configure the initial ATL project.

Because you are creating a control, leave the Server Type as a DLL, since a control must be an in-process server. All the default options are fine, so click Finish. A dialog box appears that lists the main files that will be created. These files are listed below, along with a description of each file that the ATL COM AppWizard generates.

FileDescription
Polygon.cppContains the implementation of DllMain, DllCanUnloadNow, DllGetClassObject, DllRegisterServer and DllUnregisterServer. Also contains the object map, which is a list of the ATL objects in your project. This is initially blank, since you haven't created an object yet.
Polygon.defThe standard Windows module definition file for the DLL.
Polygon.idlThe interface definition language file, which describes the interfaces specific to your objects.
Polygon.rcThe resource file, which initially contains the version information and a string containing the project name.
Resource.hThe header file for the resource file.
Polygonps.makThe make file that can be used to build a proxy/stub DLL. You will not need to use this.
Polygonps.defThe module definition file for the proxy/stub DLL.
StdAfx.cppThe file that will #include the ATL implementation files.
StdAfx.hThe file that will #include the ATL header files.

To make the Polygon DLL useful, you need to add a control, using the ATL Object Wizard.

On to Step 2


Step 2: Adding a Control

To add an object to an ATL project, you use the ATL Object Wizard:

  1. Click Component on the Insert menu to open the Component Gallery.
  2. Click the ATL tab.
  3. Select the ATL Object Wizard, then click the Insert button. The ATL Object Wizard opens.

In the first ATL Object Wizard dialog box, select the category of object you want to add to your current ATL project. Some of the options you can select are a basic COM object, a control tailored to work in Internet Explorer, and a property page. In this tutorial, you are going to create a standard control, so set the category as Controls on the left, then on the right select Full Control. Finally, click Next.

A set of property pages is displayed that allow you to configure the control you are inserting into your project. Type “PolyCtl” as the short name.

The Class field shows the C++ class name created to implement the control. The .H File and .CPP File show the files created containing the definition of the C++ class. The CoClass is the name of the component class for this control, and Interface is the name of the interface on which your control will implement its custom methods and properties. The Type is a description for the control, and the ProgID is the readable name that can be used to look up the CLSID of the control.

Now enable support for rich error information for your control:

  1. Click on the Attributes tab.
  2. Click the Support ISupportErrorInfo check box.

You're going to color in the polygon when you draw it, so add a Fill Color stock property:

  1. Click on the Stock Properties tab. You see a list box with all the possible stock properties you can enter.
  2. Scroll down the list, then double-click Fill Color to move it to the Supported list.

You are finished selecting options for your control. Click OK.

When you created your control, several code changes and additions were made. The following files were created:

FileDescription
PolyCtl.hContains most of the implementation of the C++ class CPolyCtl.
PolyCtl.cppContains the remaining parts of CPolyCtl.
PolyCtl.rgsA text file that contains the registry script used to register the control.
PolyCtl.htmAn HTML file that contains the source of a web page that contains a reference to the newly created control, so that you can try it out in Internet Explorer immediately.

The following code changes were also performed by the Wizard:

The file PolyCtl.h is the most interesting because it contains the main code that implements your control. The code for PolyCtl.h is described in the Appendix of this tutorial.

You are now ready to build your control:

  1. On the Build menu click Build Polygon.dll. Once your control has finished building, click OLE Control Test Container on the Tools menu. The Test Container is launched.
  2. In Test Container, choose Insert Ole Control from the Edit menu. The Insert Ole Control dialog box appears.
  3. From the list of available controls in the Insert Ole Control dialog box, choose PolyCtl class. You should see a rectangle with the text "ATL 2.0" in the middle.
  4. Close Test Container.

Next, you will add a custom property to the control.

Back to Step 1 | On to Step 3


Step 3: Adding a Property to the Control

IPolyCtl is the interface that contains your custom methods and properties. To add a property, modify the definition of IPolyCtl in the Polygon.idl file. The code you add is in bold.

interface IPolyCtl : IDispatch
{
   [propput, id(DISPID_FILLCOLOR)]
   HRESULT FillColor([in]OLE_COLOR clr);
   [propget, id(DISPID_FILLCOLOR)]
   HRESULT FillColor([out, retval]OLE_COLOR* pclr);
   [propget, id(1)] HRESULT Sides([out, retval] short *pVal);
   [propput, id(1)] HRESULT Sides([in] short newVal);
};

MIDL (the program that compiles .idl files) defines a get method and a put method by prepending get_ and put_ to the property name. You need to add two member functions, get_Sides and put_Sides, to the CPolyCtl class.

First open PolyCtl.h and add the lines in bold, near the end of the class declaration:

// IPolyCtl
public:
   STDMETHOD(get_Sides)(short *pVal);
   STDMETHOD(put_Sides)(short newVal);
   HRESULT OnDraw(ATL_DRAWINFO& di);
   OLE_COLOR m_clrFillColor;
   short     m_nSides;
};

Now open PolyCtl.cpp and add the following implementations:

STDMETHODIMP CPolyCtl::get_Sides(short *pVal) 
{ 
   *pVal = m_nSides; 
   return S_OK; 
} 
STDMETHODIMP CPolyCtl::put_Sides(short newVal) 
{ 
   if (newVal > 2 && newVal < 101) 
   { 
      m_nSides = newVal; 
      return S_OK; 
   } 
   else 
      return Error(_T("Shape must have between 3 and 100 sides")); 
} 

The get_Sides function simply returns the current value of the Sides property through the pVal pointer. In the put_Sides method, you make sure the user is setting the Sides property to an acceptable value. You need more than 2 sides, and since you will be storing an array of points for each side later on, 100 is a reasonable limit for a maximum value. If an invalid value is passed you use the ATL Error function to set the details in the IErrorInfo interface. This is useful if your container needs more information about the error than the returned HRESULT.

The last thing you need to do for the property is initialize m_nSides. Make a triangle the default shape by adding a line to the constructor in PolyCtl.h:

CPolyCtl() 
{ 
   m_nSides = 3; 
} 

You now have a property called Sides. It's not much use until you do something with it, so next you will change the drawing code to use it.

Back to Step 2 | On to Step 4


Step 4: Changing the Drawing Code

In the drawing code you will use sin and cos functions to calculate the polygon points, so add include math.h at the top of PolyCtl.h:

#include <math.h>
#include "resource.h" // main symbols

Note for Release builds only When the ATL COM AppWizard generates the default project, it defines the macro _ATL_MIN_CRT. This macro is defined so that you don't bring the C Run-Time Library into your code if you don't need it. The polygon control needs the C Run-Time Library start-up code to initialize the floating-point functions. Therefore, you need to remove the _ATL_MIN_CRT macro if you want to build a Release version. To remove the macro, click Settings on the Build menu. Hold the ctrl key while selecting all Release configurations. On the C/C++ tab, choose the General category, then remove _ATL_MIN_CRT from the Preprocessor definitions edit box.

Once the polygon points are calculated, you store the points by adding an array of type POINT to the end of the class definition in PolyCtl.h:

OLE_COLOR m_clrFillColor; 
short m_nSides; 
POINT m_arrPoint[100]; 

Now change the OnDraw function in PolyCtl.cpp to match the one below. Note that you remove the calls to the Rectangle and DrawText functions. You also explicitly get and select a black pen and white brush. You need to do this in case your control is running windowless. If you don't have your own window, you can't make assumptions about the device context you'll be drawing in.

The completed OnDraw looks like this:

HRESULT CPolyCtl::OnDraw(ATL_DRAWINFO& di) 
{ 
   RECT& rc = *(RECT*)di.prcBounds; 
   HDC hdc = di.hdcDraw; 
   COLORREF colFore; 
   HBRUSH hOldBrush, hBrush; 
   HPEN hOldPen, hPen; 
   // Translate m_colFore into a COLORREF type 
   OleTranslateColor(m_clrFillColor, NULL, &colFore);
   //Create and select the colors to draw the circle 
   hPen = (HPEN)GetStockObject(BLACK_PEN); 
   hOldPen = (HPEN)SelectObject(hdc, hPen); 
   hBrush = (HBRUSH)GetStockObject(WHITE_BRUSH); 
   hOldBrush = (HBRUSH)SelectObject(hdc, hBrush); 
   const double pi = 3.14159265358979; 
   POINT ptCenter; 
   double dblRadiusx = (rc.right - rc.left) / 2; 
   double dblRadiusy = (rc.bottom - rc.top) / 2; 
   double dblAngle = 3 * pi / 2;   // Start at the top 
   double dblDiff = 2 * pi / m_nSides;  // Angle each side will make 
   ptCenter.x = (rc.left + rc.right) / 2; 
   ptCenter.y = (rc.top + rc.bottom) / 2; 
   // Calculate the points for each side 
   for (int i = 0; i < m_nSides; i++) 
   { 
      m_arrPoint[i].x = (long)(dblRadiusx * cos(dblAngle) +
                               ptCenter.x + 0.5); 
      m_arrPoint[i].y = (long)(dblRadiusy * sin(dblAngle) + 
                               ptCenter.y + 0.5); 
      dblAngle += dblDiff; 
   } 
   Ellipse(hdc, rc.left, rc.top, rc.right, rc.bottom); 
   // Create and select the brush that will be 
   // used to fill the polygon 
   hBrush = CreateSolidBrush(colFore); 
   SelectObject(hdc, hBrush); 
   Polygon(hdc, &m_arrPoint[0], m_nSides); 
   // Select back the old pen and brush and delete 
   // the brush we created 
   SelectObject(hdc, hOldPen); 
   SelectObject(hdc, hOldBrush); 
   DeleteObject(hBrush); 
   return S_OK; 
} 

Now, initialize m_clrFillColor. Choose green as the default color and add this line to the CPolyCtl constructor in PolyCtl.h:

m_clrFillColor = RGB(0, 0xFF, 0);

The constructor now looks like this:

CPolyCtl() 
{ 
   m_nSides = 3; 
   m_clrFillColor = RGB(0, 0xFF, 0); 
} 

Now rebuild the control and try it again. Open OLE Control Test Container and insert the control. You should see a green triangle within a circle. Try changing the number of sides. To modify properties on a dual interface from within Test Container, use Invoke Methods:

  1. In Test Container, click Invoke Methods on the Edit menu.
    The Invoke Control Method dialog box is displayed.
  2. Click Sides from the Name list box and click 1: Put from the ID list box.
  3. Type 5 in the (Prop Value) edit box and click Invoke.

Notice that the control doesn't change. What is wrong? Although you changed the number of sides internally by setting the m_nSides variable, you didn't cause the control to repaint. If you switch to another application and then switch back to Test Container you will find that the control is repainted and now has the correct number of sides.

To correct this problem, you need to add a call to the FireViewChange function, which is defined in IViewObjectExImpl, after you set the number of sides. If the control is running in its own window, FireViewChange will call the InvalidateRect API directly. If the control is running windowless, the InvalidateRect method will be called on the container's site interface. This forces the control to repaint itself.

The new put_Sides method is as follows:

STDMETHODIMP CPolyCtl::put_Sides(short newVal) 
{ 
   if (newVal > 2 && newVal < 101) 
   { 
      m_nSides = newVal; 
      FireViewChange(); 
      return S_OK; 
   } 
   else 
      return Error(_T("Shape must have between 3 and 100 sides")); 
} 

After you've added FireViewChange, rebuild and try the control again. This time when you change the number of sides and click Invoke, you should see the control change immediately.

Next, you will add an event to the control.

Back to Step 3 | On to Step 5


Step 5: Adding an Event

Now you will add a ClickIn and a ClickOut event to your ATL control. You will fire the ClickIn event if the user clicks within the polygon and fire ClickOut if the user clicks outside.

To be able to fire events, you must first specify an event interface. Add the code declaring this interface to the library section in the Polygon.idl file. The resulting .idl file should appear as shown in the following code. The code that you add is in bold. Note that the GUIDs in your file will differ from the ones below. Do not change the code that is not bold. In particular, be careful not to overwrite the second GUID in the code below.

library POLYGONLib 
{ 
   importlib("stdole32.tlb"); 
   [ 
      uuid(4CBBC677-507F-11D0-B98B-000000000000), 
      helpstring("Event interface for PolyCtl") 
   ] 
   dispinterface _PolyEvents 
   { 
      properties: 
      methods: 
      [id(1)] void ClickIn([in]long x, [in] long y); 
      [id(2)] void ClickOut([in]long x, [in] long y); 
   }; 
   [ 
      uuid(4CBBC676-507F-11D0-B98B-000000000000), 
      helpstring("PolyCtl Class") 
   ] 
   coclass PolyCtl 
   { 
      [default] interface IPolyCtl; 
      [default, source] dispinterface _PolyEvents; 
   }; 
}; 

Note that you start the interface name with an underscore. This is a convention to indicate that the interface is an internal interface. Thus, programs that allow you to browse COM objects can choose not to display the interface to the user.

In the interface definition, you added the ClickIn and ClickOut methods that take the x and y coordinates of the clicked point as parameters. You also added a line to indicate that this is the default source interface. The source attribute indicates that the control is the source of the notifications, so it will call this interface on the container.

Now implement a connection point interface and a connection point container interface for your control. (In COM, events are implemented through the mechanism of connection points. To receive events from a COM object, a container establishes an advisory connection to the connection point that the COM object implements. Since a COM object can have multiple connection points, the COM object also implements a connection point container interface. Through this interface, the container can determine which connection points are supported.) The interface that implements a connection point is called IConnectionPoint and the interface that implements a connection point container is called IConnectionPointContainer.

To help implement IConnectionPoint, ATL provides a proxy generator. This proxy generator generates the IConnectionPoint interface by reading your type library and implementing a function for each event that can be fired. But before you can use it, you must generate your type library. To do this you can either rebuild your project or right click on the .idl file in the FileView and click Compile Polygon.idl. This will create the Polygon.tlb file, which is your type library.

After compiling your type library, follow these steps:

  1. From the Insert menu select Component.
  2. Inside the Component Gallery, choose the ATL tab and double-click the ProxyGen icon. The ATL Proxy Generator dialog box appears.
  3. In the TypeLibrary name edit box, click the button labeled ... and select the Polygon.tlb file. The type library will be read and the two interfaces that you implemented (_PolyEvents and IPolyCtl) will appear in the Not selected box.
  4. To generate a connection point for the event interface, select _PolyEvents and click the -> button to move _PolyEvents to the Selected box .
    You want a connection point, so leave the Proxy Type as Connection Point.
  5. Click Insert.
  6. A standard Save dialog box appears and suggests CPPolygon.h as the filename. Accept this name and click Save.
  7. A message that the proxy has been successfully generated appears. Click OK.
  8. Now click Close, then click Close again to close the Component Gallery.

If you look at the generated CPPolygon.h file, you see it has a class called CProxy_PolyEvents that derives from IConnectionPointImpl. CPPolygon.h also defines the two methods Fire_ClickIn and Fire_ClickOut, which take the two coordinate parameters. These are the methods you call when you want to fire an event from your control.

Now include the CPPolygon.h file at the top of PolyCtl.h:

#include <math.h> 
#include "resource.h" // main symbols 
#include "CPPolygon.h" 

Now add the CProxy_PolyEvents class to the CPolyCtl class inheritance list in PolyCtl.h. You also need to implement IConnectionPointContainer. ATL supplies an implementation of this interface in the class IConnectionPointContainerImpl. Therefore, add these two lines to CPolyCtl class inheritance list in PolyCtl.h:

public CProxy_PolyEvents<CPolyCtl>, 
public IConnectionPointContainerImpl<CPolyCtl> 

Also, you need to make the interface _PolyEvents the default outgoing interface, so supply it as the second parameter to IProvideClassInfo2Impl in the CPolyCtl class inheritance list in PolyCtl.h:

public IProvideClassInfo2Impl<&CLSID_PolyCtl, &DIID__PolyEvents, 
                              &LIBID_POLYGONLib>, 

The CPolyCtl class declaration now looks like this:

class ATL_NO_VTABLE CPolyCtl : 
   public CComObjectRootEx<CComObjectThreadModel>, 
   public CComCoClass<CPolyCtl, &CLSID_PolyCtl>, 
   public CComControl<CPolyCtl>, 
   public CStockPropImpl<CPolyCtl, IPolyCtl, &IID_IPolyCtl, 
                         &LIBID_POLYGONLib>, 
   public IProvideClassInfo2Impl<&CLSID_PolyCtl, &DIID__PolyEvents, 
                                 &LIBID_POLYGONLib>, 
   public IPersistStreamInitImpl<CPolyCtl>, 
   public IPersistStorageImpl<CPolyCtl>, 
   public IQuickActivateImpl<CPolyCtl>, 
   public IOleControlImpl<CPolyCtl>, 
   public IOleObjectImpl<CPolyCtl>, 
   public IOleInPlaceActiveObjectImpl<CPolyCtl>, 
   public IViewObjectExImpl<CPolyCtl>, 
   public IOleInPlaceObjectWindowlessImpl<CPolyCtl>, 
   public IDataObjectImpl<CPolyCtl>, 
   public ISupportErrorInfo, 
   public ISpecifyPropertyPagesImpl<CPolyCtl>, 
   public CProxy_PolyEvents<CPolyCtl>, 
   public IConnectionPointContainerImpl<CPolyCtl> 

Next expose IConnectionPointContainer through your QueryInterface function by adding it to your COM map. Note that you don't need to expose IConnectionPoint through QueryInterface, since the client obtains this interface through the use of IConnectionPointContainer. Add the following line to the end of the COM map in PolyCtl.h:

COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer)

The COM map now looks like this:

BEGIN_COM_MAP(CPolyCtl) 
   COM_INTERFACE_ENTRY(IPolyCtl) 
   COM_INTERFACE_ENTRY(IDispatch) 
   COM_INTERFACE_ENTRY_IMPL(IViewObjectEx) 
   COM_INTERFACE_ENTRY_IMPL_IID(IID_IViewObject2, IViewObjectEx) 
   COM_INTERFACE_ENTRY_IMPL_IID(IID_IViewObject, IViewObjectEx) 
   COM_INTERFACE_ENTRY_IMPL(IOleInPlaceObjectWindowless) 
   COM_INTERFACE_ENTRY_IMPL_IID(IID_IOleInPlaceObject, 
                                IOleInPlaceObjectWindowless) 
   COM_INTERFACE_ENTRY_IMPL_IID(IID_IOleWindow, 
                                IOleInPlaceObjectWindowless) 
   COM_INTERFACE_ENTRY_IMPL(IOleInPlaceActiveObject) 
   COM_INTERFACE_ENTRY_IMPL(IOleControl) 
   COM_INTERFACE_ENTRY_IMPL(IOleObject) 
   COM_INTERFACE_ENTRY_IMPL(IQuickActivate) 
   COM_INTERFACE_ENTRY_IMPL(IPersistStorage) 
   COM_INTERFACE_ENTRY_IMPL(IPersistStreamInit) 
   COM_INTERFACE_ENTRY_IMPL(ISpecifyPropertyPages) 
   COM_INTERFACE_ENTRY_IMPL(IDataObject) 
   COM_INTERFACE_ENTRY(IProvideClassInfo) 
   COM_INTERFACE_ENTRY(IProvideClassInfo2) 
   COM_INTERFACE_ENTRY(ISupportErrorInfo) 
   COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer) 
END_COM_MAP() 

There is one more thing to do for connection points and that is to tell the ATL implementation of IConnectionPointContainer which connection points are available. You do this through the use of a connection point map, which is simply a list of the interface identifiers for each supported connection point. Add the following three lines after the COM map in PolyCtl.h. Note that there are two underscore characters in the identifier name for the interface, since MIDL prepends DIID_ onto the interface name that you defined earlier, which starts with an underscore character.

BEGIN_CONNECTION_POINT_MAP(CPolyCtl) 
   CONNECTION_POINT_ENTRY(DIID__PolyEvents) 
END_CONNECTION_POINT_MAP() 

You are done implementing the code to support events. Now, add some code to fire the events at the appropriate moment. Remember, you are going to fire a ClickIn or ClickOut event when the user clicks the left mouse button in the control. To find out when the user clicks the button, first add a handler for the WM_LBUTTONDOWN message. In PolyCtl.h, add the following line to the message map:

MESSAGE_HANDLER(WM_LBUTTONDOWN, OnLButtonDown)

The message map now looks like this:

BEGIN_MSG_MAP(CPolyCtl) 
   MESSAGE_HANDLER(WM_PAINT, OnPaint) 
   MESSAGE_HANDLER(WM_GETDLGCODE, OnGetDlgCode) 
   MESSAGE_HANDLER(WM_SETFOCUS, OnSetFocus) 
   MESSAGE_HANDLER(WM_KILLFOCUS, OnKillFocus) 
   MESSAGE_HANDLER(WM_LBUTTONDOWN, OnLButtonDown) 
END_MSG_MAP() 

To supply the implementation of OnLButtonDown, add the following code after the OnDraw prototype in PolyCtl.h:

LRESULT OnLButtonDown(UINT uMsg, WPARAM wParam, LPARAM lParam,
                      BOOL& bHandled); 

Next, add the following code after the OnDraw implementation in PolyCtl.cpp:

LRESULT CPolyCtl::OnLButtonDown(UINT uMsg, WPARAM wParam, 
                                LPARAM lParam, BOOL& bHandled) 
{ 
   HRGN hRgn; 
   WORD xPos = LOWORD(lParam);  // horizontal position of cursor 
   WORD yPos = HIWORD(lParam);  // vertical position of cursor 
   // Create a region from our list of points 
   hRgn = CreatePolygonRgn(&m_arrPoint[0], m_nSides, WINDING);
   // If the clicked point is in our polygon then fire the ClickIn 
   // event otherwise we fire the ClickOut event 
   if (PtInRegion(hRgn, xPos, yPos)) 
      Fire_ClickIn(xPos, yPos); 
   else 
      Fire_ClickOut(xPos, yPos); 
   // Delete the region that we created 
   DeleteObject(hRgn); 
   return 0; 
} 

Since you have already calculated the points of the polygon in the OnDraw function, use them in OnLButtonDown to create a region. Then, use the PtInRegion API function to determine whether the clicked point is inside the polygon or not.

The uMsg parameter is the ID of the Windows message being handled. This allows you to have one function that handles a range of messages. The wParam and the lParam are the standard values for the message being handled. The parameter bHandled allows you to specify whether the function handled the message or not. By default, the value is set to TRUE to indicate that the function handled the message, but you can set it to FALSE. Doing so will cause ATL to continue looking for another message handler function to which to send the message.

Now try out your events. Build the control and start OLE Control Test Container again. This time open the event log window by clicking Event Log on the View menu. Now insert the control and try clicking in the window. Notice that ClickIn is fired if you click within the filled polygon and ClickOut is fired when you click outside it.

Next you will add a property page.

Back to Step 4 | On to Step 6


Step 6: Adding a Property Page

Property pages are implemented as separate COM objects, which allow property pages to be shared if required. To add a property page to your control you can use the ATL Object Wizard.

Start the ATL Object Wizard and select Controls as the category on the left. Select Property Page on the right, then click Next.

You again get the dialog box allowing you to enter the name of the new object. Call the object PolyProp and enter that name in the Short Name edit box.

Notice that the Interface edit box is grayed out. This is because a property page doesn't need a custom interface.

Click on the Strings tab to set the title of the property page. The title of the property page is the string that appears in the tab for that page. The Doc String is a description that a property frame could use to put in a status line or tool tip. Note that the standard property frame currently doesn't use this string, but you can set it anyway. You're not going to generate a Helpfile at the moment, so erase the entry in that text box. Click OK and the property page object will be created.

The following three files are created:

FileDescription
PolyProp.hContains the C++ class CPolyProp, which implements the property page.
PolyProp.cppIncludes the PolyProp.h file.
PolyProp.rgsThe registry script that registers the property page object.

The following code changes are also made:

Now add the fields that you want to appear on the property page. Switch to ResourceView, then open the dialog IDD_POLYPROP. Notice that it is empty except for a label that tells you to put your property page controls here. Delete that label and add one that contains the text “Sides:”. Next to the label add an edit box and give it an ID of IDC_SIDES.

Include Polygon.h at the top of the PolyProp.h file:

#include "Polygon.h" // definition of IPolyCtl

Now enable the CPolyProp class to set the number of sides in your object when the Apply button is pressed. Change the Apply function in PolyProp.h as follows.

STDMETHOD(Apply)(void) 
{ 
   USES_CONVERSION; 
   ATLTRACE(_T("CPolyProp::Apply\n")); 
   for (UINT i = 0; i < m_nObjects; i++) 
   { 
      CComQIPtr<IPolyCtl, &IID_IPolyCtl> pPoly(m_ppUnk[i]); 
      short nSides = (short)GetDlgItemInt(IDC_SIDES); 
      if FAILED(pPoly->put_Sides(nSides)) 
      { 
         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; 
} 

A property page could have more than one client attached to it at a time, so the Apply function loops around and calls put_Sides on each client with the value retrieved from the edit box. You are using the CComQIPtr class, which performs the QueryInterface on each object to obtain the IPolyCtl interface from the IUnknown (stored in the m_ppUnk array).

Check that setting the Sides property actually worked. If it fails, you get a message box displaying error details from the IErrorInfo interface. Typically, a container asks an object for the ISupportErrorInfo interface and calls InterfaceSupportsErrorInfo first, to determine whether the object supports setting error information. But since it's your control, you can forego that check.

CComPtr helps you by automatically handling the reference counting, so you don't need to call Release on the interface. CComBSTR helps you with BSTR processing, so you don't have to perform the final SysFreeString call. You also use one of the various string conversion classes, so you can convert the BSTR if necessary (this is why we add the USES_CONVERSION macro at the start of the function).

You also must set the property page's dirty flag to indicate that the Apply button should be enabled. This occurs when the user changes the value in the Sides edit box, so add this line to the message map in PolyProp.h:

COMMAND_HANDLER(IDC_SIDES, EN_CHANGE, OnSidesChange)

The property page message map now looks like this:

BEGIN_MSG_MAP(CPolyProp)
   COMMAND_HANDLER(IDC_SIDES, EN_CHANGE, OnSidesChange) 
   CHAIN_MSG_MAP(IPropertyPageImpl<CPolyProp>) 
END_MSG_MAP() 

Now add the OnSidesChange function after the Apply function:

LRESULT OnSidesChange(WORD wNotify, WORD wID, HWND hWnd, 
                      BOOL& bHandled) 
{ 
   SetDirty(TRUE); 
   return 0; 
} 

OnSidesChange will be called when a WM_COMMAND message is sent with the EN_CHANGE notification for the IDC_SIDES control. OnSidesChange then calls SetDirty and passes TRUE to indicate the property page is now dirty and the Apply button should be enabled.

Now, add the property page to your control. The ATL Object Wizard doesn't do this for you automatically, since there could be multiple controls in your project. Open PolyCtl.h and add this line to the property map:

PROP_ENTRY("Sides", 1, CLSID_PolyProp)

The control's property map now looks like this:

BEGIN_PROPERTY_MAP(CPolyCtl) 
   //PROP_ENTRY("Description", dispid, clsid) 
   PROP_ENTRY("Sides", 1, CLSID_PolyProp) 
   PROP_PAGE(CLSID_StockColorPage) 
END_PROPERTY_MAP() 

You could have added a PROP_PAGE macro with the CLSID of your property page, but if you use the PROP_ENTRY macro as shown, the Sides property value is also saved when the control is saved. The three parameters to the macro are the property description, the DISPID of the property, and the CLSID of the property page that has the property on it. This is useful if, for example, you load the control into Visual Basic and set the number of Sides at design time. Since the number of Sides is saved, when you reload your Visual Basic project the number of Sides will be restored.

Now build that control and insert it into OLE Control Test Container. Then follow the steps below:

  1. In Test Container, on the Edit menu click Embedded Object Functions.
  2. Click Properties on the submenu.

The property page appears.

The Apply button is initially disabled. Start typing a value in the Sides edit box and the Apply button will become enabled. After you have finished entering the value, click the Apply button. The control display changes and the Apply button is again disabled. Try entering an invalid value and you should see a message box containing the error description that you set from the put_Sides function.

Next you'll put your control on a web page.

Back to Step 5 | On to Step 7


Step 7: Putting the Control on a Web Page

Your control is now finished. To see your control work in a real-world situation, put it on a web page. When the ATL Object Wizard creates the initial control it also creates an HTML file that contains the control. You can open up the PolyCtl.htm file in Internet Explorer and you see your control on a Web page.

The control doesn't do anything yet, so change the Web page to respond to the events that you send. Open PolyCtl.htm in Developer Studio and add the lines in bold.

<HTML> 
<HEAD> 
<TITLE>ATL 2.0 test page for object PolyCtl</TITLE> 
</HEAD> 
<BODY> 
<OBJECT ID="PolyCtl" <
 CLASSID="CLSID:4CBBC676-507F-11D0-B98B-000000000000"> > 
</OBJECT> 
<SCRIPT LANGUAGE="VBScript"> 
<!-- 
Sub PolyCtl_ClickIn(x, y) 
   PolyCtl.Sides = PolyCtl.Sides + 1 
End Sub 
Sub PolyCtl_ClickOut(x, y) 
   PolyCtl.Sides = PolyCtl.Sides - 1 
End Sub 
--> 
</SCRIPT> 
</BODY> 
</HTML> 

You have added some VBScript code that gets the Sides property from the control, and increases the number of sides by one if you click inside the control. If you click outside the control you reduce the number of sides by one.

Start up Internet Explorer and make sure your Security settings are set to Medium:

  1. Click Options on the View menu.
  2. Select the Security tab and click Safety Level.
  3. Set the security to medium if necessary, then click OK.
  4. Click OK to close the Options dialog box.

Now open PolyCtl.htm. A Safety Violation dialog box informs you that Internet Explorer doesn't know if the control is safe to script.

What does this mean? Imagine if you had a control that, for example, displayed a file, but also had a Delete method that deleted a file. The control would be safe if you just viewed it on a page, but wouldn't be safe to script since someone could call the Delete method. This message is Internet Explorer's way of saying that it doesn't know if someone could do damage with this control so it is asking the user.

You know your control is safe, so click Yes. Now click inside the polygon; the number of sides increases by one. Click outside the polygon to reduce the number of sides. If you try to reduce the number of sides below three, you will see the error message that you set.

The following figure shows the control running in Internet Explorer after you have clicked inside the polygon twice.

Since you know your control is always safe to script, it would be good to let Internet Explorer know, so that it doesn't need to show the Safety Violation dialog box. You can do this through the IObjectSafety interface. ATL supplies an implementation of this interface in the class IObjectSafetyImpl.

To add the interface to your control, just add IObjectSafetyImpl to your list of inherited classes and add an entry for it in your COM map.

Add the following line to the end of the list of inherited classes in PolyCtl.h, remembering to add a comma to the previous line:

public IObjectSafetyImpl<CPolyCtl>

Then add the following line to the COM map in PolyCtl.h:

COM_INTERFACE_ENTRY_IMPL(IObjectSafety)

Now build the control. Once the build has finished, open PolyCtl.htm in Internet Explorer again. This time the web page should be displayed directly without the Safety Violation dialog box. Click inside and outside the polygon to confirm that the scripting is working.

Back to Step 6 | On to References


ATL References

This tutorial has demonstrated some basic concepts about using ATL.

To view the available ATL documentation, see:


Appendix

This appendix contains the PolyCtl.h and Poly.cpp code created by the ATL Object Wizard when generating a full control with the Support ISupportErrorInfo option chosen on the Attributes tab.

PolyCtl.cpp implements the InterfaceSupportsErrorInfo function for the ISupportErrorInfo interface and the OnDraw function.

PolyCtl.h shows how ATL uses multiple inheritance to implement the necessary interfaces. This provides a flexible way of implementing a COM object, since it allows you to add and remove interfaces easily. The list of interfaces that will be exposed through QueryInterface are specified in the COM map.

The DECLARE_REGISTRY_RESOURCEID macro simply specifies the resource ID containing the registry script and is used to register and unregister the control.

The property map indicates which properties of the object will persist, meaning they can be loaded and saved. The property map also identifies the property pages used by the object.

The message map indicates which function will be called to handle the various Windows messages.

// PolyCtl.h : Declaration of the CPolyCtl 
#ifndef __POLYCTL_H_ 
#define __POLYCTL_H_ 
#include "resource.h" // main symbols 
////////////////////////////////////////////////// 
// CPolyCtl 
class ATL_NO_VTABLE CPolyCtl : 
   public CComObjectRootEx<CComObjectThreadModel>, 
   public CComCoClass<CPolyCtl, &CLSID_PolyCtl>, 
   public CComControl<CPolyCtl>, 
   public CStockPropImpl<CPolyCtl, IPolyCtl, &IID_IPolyCtl,
                         &LIBID_TEMPLib>, 
   public IProvideClassInfo2Impl<&CLSID_PolyCtl, NULL,
                                 &LIBID_TEMPLib>, 
   public IPersistStreamInitImpl<CPolyCtl>, 
   public IPersistStorageImpl<CPolyCtl>, 
   public IQuickActivateImpl<CPolyCtl>, 
   public IOleControlImpl<CPolyCtl>, 
   public IOleObjectImpl<CPolyCtl>, 
   public IOleInPlaceActiveObjectImpl<CPolyCtl>, 
   public IViewObjectExImpl<CPolyCtl>, 
   public IOleInPlaceObjectWindowlessImpl<CPolyCtl>, 
   public IDataObjectImpl<CPolyCtl>, 
   public ISupportErrorInfo, 
   public ISpecifyPropertyPagesImpl<CPolyCtl> 
{ 
public: 
   CPolyCtl() 
   { 
   } 
   DECLARE_REGISTRY_RESOURCEID(IDR_PolyCtl) 
   DECLARE_POLY_AGGREGATABLE(CPolyCtl) 
   BEGIN_COM_MAP(CPolyCtl) 
      COM_INTERFACE_ENTRY(IPolyCtl) 
      COM_INTERFACE_ENTRY(IDispatch) 
      COM_INTERFACE_ENTRY_IMPL(IViewObjectEx) 
      COM_INTERFACE_ENTRY_IMPL_IID(IID_IViewObject2, IViewObjectEx) 
      COM_INTERFACE_ENTRY_IMPL_IID(IID_IViewObject, IViewObjectEx) 
      COM_INTERFACE_ENTRY_IMPL(IOleInPlaceObjectWindowless) 
      COM_INTERFACE_ENTRY_IMPL_IID(IID_IOleInPlaceObject, 
                                   IOleInPlaceObjectWindowless) 
      COM_INTERFACE_ENTRY_IMPL_IID(IID_IOleWindow, 
                                   IOleInPlaceObjectWindowless) 
      COM_INTERFACE_ENTRY_IMPL(IOleInPlaceActiveObject) 
      COM_INTERFACE_ENTRY_IMPL(IOleControl) 
      COM_INTERFACE_ENTRY_IMPL(IOleObject) 
      COM_INTERFACE_ENTRY_IMPL(IQuickActivate) 
      COM_INTERFACE_ENTRY_IMPL(IPersistStorage) 
      COM_INTERFACE_ENTRY_IMPL(IPersistStreamInit) 
      COM_INTERFACE_ENTRY_IMPL(ISpecifyPropertyPages) 
      COM_INTERFACE_ENTRY_IMPL(IDataObject) 
      COM_INTERFACE_ENTRY(IProvideClassInfo) 
      COM_INTERFACE_ENTRY(IProvideClassInfo2) 
      COM_INTERFACE_ENTRY(ISupportErrorInfo) 
   END_COM_MAP() 
   BEGIN_PROPERTY_MAP(CPolyCtl) 
      // PROP_ENTRY("Description", dispid, clsid) 
      PROP_PAGE(CLSID_StockColorPage) 
   END_PROPERTY_MAP() 
   BEGIN_MSG_MAP(CPolyCtl) 
      MESSAGE_HANDLER(WM_PAINT, OnPaint) 
      MESSAGE_HANDLER(WM_GETDLGCODE, OnGetDlgCode) 
      MESSAGE_HANDLER(WM_SETFOCUS, OnSetFocus) 
      MESSAGE_HANDLER(WM_KILLFOCUS, OnKillFocus) 
   END_MSG_MAP() 
   // IViewObjectEx 
   STDMETHOD(GetViewStatus)(DWORD* pdwStatus) 
   { 
      ATLTRACE(_T("IViewObjectExImpl::GetViewStatus\n")); 
      *pdwStatus = VIEWSTATUS_SOLIDBKGND|VIEWSTATUS_OPAQUE; 
      return S_OK; 
   } 
   // IPolyCtl 
public: 
   HRESULT OnDraw(ATL_DRAWINFO& di); 
}; 
#endif // __POLYCTL_H_ 

The following code is the PolyCtl.cpp file generated by the ATL Object Wizard when the Support ISupportErrorInfo option is chosen. You can see that the default drawing code simply draws a rectangle with the text “ATL 2.0” in the center.

// PolyCtl.cpp : Implementation of CPolyCtl 
#include "stdafx.h" 
#include "Polygon.h" 
#include "PolyCtl.h" 
////////////////////////////////////////////////// 
// CPolyCtl 
STDMETHODIMP CPolyCtl::InterfaceSupportsErrorInfo(REFIID riid) 
{ 
   static const IID* arr[] = 
   { 
      &IID_IPolyCtl, 
   }; 
   for (int i=0;i<sizeof(arr)/sizeof(arr[0]);i++) 
   { 
      if (InlineIsEqualGUID(*arr[i],riid)) 
         return S_OK; 
   } 
   return S_FALSE; 
} 
HRESULT CPolyCtl::OnDraw(ATL_DRAWINFO& di) 
{ 
   RECT& rc = *(RECT*)di.prcBounds; 
   Rectangle(di.hdcDraw, rc.left, rc.top, rc.right, rc.bottom); 
   DrawText(di.hdcDraw, _T("ATL 2.0"), -1, &rc, 
            DT_CENTER | DT_VCENTER | DT_SINGLELINE); 
   return S_OK; 
}