This article is somewhat like an extension to POLYGON tutorial provided in Microsoft’s online documentation. So I would not be explaining the basic steps involved to create the project and adding control to the project. I would only explain the steps where I diverted form POLYGON tutorial. To completely follow the example code of this article we will have to do the following steps.
Add the necessary controls and corresponding variables to the property page classes to manipulate the values corresponding to these controls. Since at the start of this article I told you that I didn’t add any MFC support to the project, therefore we will not be making use of CPropertPage or other MFC classes for these property page. We will use Win32 API calls to manipulate all the controls. I know it sucks after using the MFC for our applications. If you don’t want to go my way then you add the MFC support to this control and make use of all the control classes. Here is part of our code that initializes the controls.
LRESULT CAppearanceProp::OnInitDialog(UINT uMsg, WPARAM wParam,
LPARAM lParam,
BOOL& bHandled)
{
USES_CONVERSION;
if ((m_hBorderWidthEdit = GetDlgItem (IDC_BORDERWIDTH_EDIT))
!= NULL)
{
::SendMessage (m_hBorderWidthEdit, UDM_SETRANGE, 0,
MAKELONG (10, 0));
}
if ((m_hBorderWidthSpin = GetDlgItem (IDC_BORDERWIDTH_SPIN))
!= NULL)
{
::SendMessage (m_hBorderWidthSpin, UDM_SETRANGE, 0,
MAKELONG (10, 0));
}
m_hAppearanceCombo = GetDlgItem (IDC_APPEARANCE_COMBO);
m_hFillStyleCombo = GetDlgItem (IDC_FILLSTYLE_COMBO);
m_hFillPatternCombo = GetDlgItem (IDC_PATTERN_COMBO);
m_hDrawStyleCombo = GetDlgItem (IDC_DRAWSTYLE_COMBO);
m_hEnableCheck = GetDlgItem (IDC_ENABLE_CHECK);
m_hBorderVisibleCheck = GetDlgItem (IDC_BORDERVISIBLE_CHECK);
m_hTabStopCheck = GetDlgItem (IDC_TABSTOP_CHECK);
.
.
All the control variables have been defines as HWND.
Control Implementation
Now we have done enough work to prepare skeleton structure for our control. Compile the project and test it by inserting it in ActiveX Control Test Container provided in the Tool menu of VC++ IDE. Ofcourse you will see nothing else but a rectangle with the label "ATL 2.0". Now we will fill the function bodies to draw the control the way we want it to be. Before I start discussing these functions, here is one thing that I did to the source code files. Object wizard puts most of the function bodies in header file. To separate the interface definitions and their implementation I moved all the function implementations to source files. Therefore if you see any difference between your files and the example code files, this could be the reason.
OnDraw ()
An OnDraw method is automatically added to your control class when you create your control with the ATL Object Wizard. POLYGON tutorial has put code for drawing simple polygon. We will add code to draw circular control, but a fancy one. Our control implements circle filled with fill patterns and you can change the font to one of your choice. Here is part of code in OnDraw function that fills the control with pattern you specify
switch (m_nFillPattern)
{
case fillBDiagonal:
hBrush = CreateHatchBrush (HS_BDIAGONAL, colorFill);
break;
case fillCrossHatch:
hBrush = CreateHatchBrush (HS_CROSS, colorFill);
break;
case fillDiagXHatch:
hBrush = CreateHatchBrush (HS_DIAGCROSS, colorFill);
break;
case fillFDiagonal:
hBrush = CreateHatchBrush (HS_FDIAGONAL, colorFill);
break;
case fillHorizontal:
hBrush = CreateHatchBrush (HS_HORIZONTAL, colorFill);
break;
case fillVertical:
hBrush = CreateHatchBrush (HS_VERTICAL, colorFill);
break;
}
The font property is implemented through IFontDisp interface. So we will make use of it to get, change and release the font object handle.
CComQIPtr<IFont, &IID_IFont> pFont (m_pFont);
if (pFont != NULL)
{
pFont->get_hFont (&hFont);
pFont->AddRefHfont (hFont);
hOldFont = (HFONT)SelectObject (hdc, hFont);
}
And as a good Win32 programming practice, do delete al the GDI objects like HPEN, HBRUSH, HFONT, etc. after these have been used. And don’t forget to restore the font, pen and brush in the device context before you exit the function.
if (pFont != NULL)
{
pFont->ReleaseHfont (hFont);
}
SelectObject (hdc, hOldFont);
SelectObject (hdc, hOldBrush);
DeleteObject (hBrush);
SelectObject (hdc, hOldPen);
DeleteObject (hPen);
Ambient Properties Change
If you want your control to respond to any change in the ambient properties of the container that is hosting the control, we will have to implement the OnAmbientPropertyChange method of IOleControl interface. Our control has already has IObjectControl in the inheritance list. If its not, then add it to the list of classes from which our control is derived from. And add corresponding entry in the BEGIN_COM_MAP. In the header file you need to add the following lines for this
class ATL_NO_VTABLE CShapeCtl :
.
public IOleControlImpl<CShapeCtl>,
.
.
BEGIN_COM_MAP(CShapeCtl)
.
COM_INTERFACE_ENTRY(IOleControl)
.
Add the following interface definition to CShapeCtl.h file.
STDMETHOD(OnAmbientPropertyChange)(DISPID dispid);
.
And in the source file we will handle the property we want to respond to. In the example code I have handled the change in Background color.
STDMETHODIMP CShapeCtl::OnAmbientPropertyChange (DISPID dispid)
{
switch (dispid)
{
case DISPID_AMBIENT_BACKCOLOR:
if (SUCCEEDED (GetAmbientBackColor (m_clrBackColor)))
{
FireViewChange ();
}
break;
case DISPID_AMBIENT_DISPLAYNAME:
case DISPID_AMBIENT_FONT:
.
Control Redraw
Whenever some control property changes you would like to redraw the control. Call FireViewChange method of CComControl class. The following code shows how the control handles the change in radius.
STDMETHODIMP CShapeCtl::put_Radius(short newVal)
{
if (newVal <= 0)
{
return (Error (_T ("Radius value can't be zero!")));
}
else if (newVal > RectHt || newVal > RectWd)
{
return (Error (_T ("Radius can't exceed control's extents!")));
}
m_nRadius = newVal;
FireViewChange ();
return S_OK;
}
Event Firing
While creating control we added three methods to handle mouse button down and move events. For firing event for mouse click we check the location where the click took place. If its inside the control region, we fire ClickIn event otherwise we fire ClickOut event.
LRESULT CShapeCtl::OnLButtonDown(UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL& bHandled)
{
HRGN rgn;
WORD xPos = LOWORD (lParam);
WORD yPos = HIWORD (lParam);
rgn = CreateEllipticRgn((CenterPt.x - m_nRadius),
(CenterPt.y - m_nRadius),
(CenterPt.x + m_nRadius),
(CenterPt.y + m_nRadius));
if (PtInRegion (rgn, xPos, yPos))
{
Fire_ClickIn (xPos, yPos);
}
else
{
Fire_ClickOut (xPos, yPos);
}
DeleteObject (rgn);
return 0;
}
Use CreateEllipticRgn API call with the radius of control and then use PtInRegion API call to check if the click was inside this region or not. Don’t forget to delete the created region object.
Control Persistence
Here is the most important part of COM objects. What do we do with the control we have created? How should I save the properties of this control? How will the container, which uses this control, reload the control in the state it was saved? And you know this is the part that is not explained in most of the examples published in books. And this was the part which took me couple of weeks to get the things straight. There are different types of persistence interfaces provided in COM to accomplish this task. But the interfaces which are of main interest to ActiveX controls are IPersistStream, IPersistStorage, and IPersistPropertyBag. A container implements only one type of persistence interface. But if you want to make your control useable by any kind of container, implement each one of these interfaces. Why do we have to implement each one on the interface? And this was the same question I asked myself when I was experimenting with this control and trying to insert it in different kinds on containers. Here are couple of hints to decide which interface will be used by the container.
- VB 6.0 needs only IPersistStreamInit or IPersistStream. And when VB6.0 saves the control in a form, it uses IPersistPropertyBag interface to save and load the control.
- IE 4.0 will only use IPersistProprtyBag to load the control.
- VisualInterDev uses IPersistPropertyBag to save the state of the control
- MS Office applications use IPersistStorage interface to save and load the control
- When you insert the control in VC++ dialog box, it uses IPersistStreamInit interface to save and load the Control State.
If you look in the resource file of the Container App project, you will see the following code
IDD_CONTAINERAPP_DIALOG DLGINIT
BEGIN
IDC_SHAPECTL, 0x376, 129, 0
0x0000, 0x0000, 0x0060, 0x8080, 0x0000, 0x0080, 0x0080, 0xcdcd,
0xcdcd, 0xcdcd, 0xcdcd, 0x00ff, 0x0000, 0xcdcd, 0xcdcd, 0xffff,
0xffff, 0xcdcd, 0xcdcd, 0x0000, 0x0000, 0x0008, 0x0000, 0xffff,
0xffff, 0x0002, 0x0000, 0x0018, 0x8000, 0xcdcd, 0xcdcd, 0x0002,
0x0000, 0x0022, 0x0000, 0x004e, 0x0061, 0x0076, 0x0065, 0x0065,
0x006e, 0x0027, 0x0073, 0x0020, 0x0043, 0x006f, 0x006e, 0x0074,
0x0072, 0x006f, 0x006c, 0x0000, 0x0001, 0x0600, 0x02bc, 0xd4c0,
0x0001, 0x530e, 0x7263, 0x7069, 0x2074, 0x544d, 0x4220, 0x6c6f,
"\144"
0
END
This is how IPersistStreamInit interface saved the control sate. Next time you load the dialog box, the Control State will be loaded from this stream.
All the persistence interfaces have same methods, but with different argument types, to save and load the control state. For implementation of these three persistence interfaces, make sure that we have these in the inheritance list and the PROP_MAP_ENTRY. If not, then add these.
class ATL_NO_VTABLE CShapeCtl :
.
public IPersistStreamInitImpl<CShapeCtl>,
.
public IPersistStorageImpl<CShapeCtl>,
,
public IPersistPropertyBagImpl<CShapeCtl>,
.
In the property map add following entries if not present.
BEGIN_COM_MAP(CShapeCtl)
.
COM_INTERFACE_ENTRY(IPersistStreamInit)
COM_INTERFACE_ENTRY2(IPersist, IPersistStreamInit)
.
COM_INTERFACE_ENTRY(IPersistPropertyBag)
.
COM_INTERFACE_ENTRY(IPersistStorage)
.
END_COM_MAP()
And add the following interface definitions to CShapeCtl.h.
STDMETHOD(Load)(IStream *pStream);
STDMETHOD(Save)(IStream *pStream, BOOL fClearDirty);
STDMETHOD(GetSizeMax)(ULARGE_INTEGER* pcbSize);
STDMETHOD(Load)(IStorage *Storage);
STDMETHOD(Save)(IStorage *pStorage, BOOL fSameAsLoad);
STDMETHOD(Load)(IPropertyBag *pPropBag, IErrorLog* pErrorLog);
STDMETHOD(Save)(IPropertyBag *pPropBag, BOOL fClearDirty,
BOOL fSaveAllProperties);
Now we will have to provide the interface implementation in CShapeCtl.cpp file. I wouldn’t be going into details of implementation of each interface. I have tried to explain each and every detail of these interfaces in the CShapeCtl.cpp file While implementing these interfaces make sure that data is loaded in the same order in which it was saved. And you use the same stream and storage names. IPersistPropertyBag persistence has some problems. The default ATL implementation persist data in property bag if its of VARIANT type. So we will need some method to save user-defined data types. Therefore the simplest way would be to convert custom data types to one of the types defined in VARIANT type. And then save it in the property bag. And while loading reconvert it to user defined data type. For more explanation see "Professional ATL COM Programming", Wrox Press Ltd, by Dr. Richard Grimes. The code used in the example has been taken from this book.
void CShapeCtl::Insert (BSTR *pStream, void *pData, DWORD size)
{
CComBSTR bstr;
bstr.Attach (*pStream);
LPBYTE ptr = static_cast<LPBYTE>(pData);
TCHAR strTemp[3] = {0};
for (DWORD j = 0; j < size; ++j)
{
BYTE ch = *ptr;
strTemp[1] = (ch & 0xf) > 9 ?
(ch & 0xf);
ch = ch >> 4;
strTemp[0] = (ch & 0xf) > 9 ?
(ch & 0xf);
ptr++;
bstr += strTemp;
}
*pStream = bstr.Detach ();
}
void CShapeCtl::Extract (LPCWSTR *pStream, void *pData, DWORD size)
{
LPCWSTR str = *pStream;
LPBYTE ptr = static_cast<LPBYTE>(pData);
for (DWORD j = 0; j < size; ++j)
{
BYTE ch = (str[0] >= L
ch *= 16;
ch += (str[1] >= L
*ptr = ch;
ptr++;
str += 2;
}
*pStream = str;
}
This code converts your data type to string type and save as VARIANT and while reloading convert from string to user defined type. For more explanation see the comments in the code.
Control Category and Safety
If you insert this control in a web page and load the page, a warning message warning about unsafe ActiveX control gets displayed. It only gets displayed if you click OK. So we should mark our controls safe or unsafe so that when o container wants to load it, it knows about it. There are two ways of doing this. First way is to include the following category map in your control. I didn’t use this method. Instead I added the component category creation code in ActiveXCtl.cpp. During the registration of control, category gets created and registered in the registry. And when control is unregistered, this category is removed form the system registry. This way you can define your own control category types. Look in DllRegisterServer and DllUnregisterServer function bodies for implementation.
BEGIN_CATEGORY_MAP (CshapeCtl)
IMPLEMENT_CATEGORY(CATID_SafeForScripting)
IMPLEMENT_CATEGORY(CATID_SafeForInitializing)
END_CATEGORY_MAP ()
Second way is to Implement IObjectSafety interface. I have implemented this interface too. There was no need to implement both methods, but I did it for sake of getting experience of implementation of these methods. Add the following interface definition in the inheritance list, property map.
class ATL_NO_VTABLE CShapeCtl :
.
public IObjectSafetyImpl<CShapeCtl,
INTERFACESAFE_FOR_UNTRUSTED_CALLER |
INTERFACESAFE_FOR_UNTRUSTED_DATA>
.
BEGIN_COM_MAP(CShapeCtl)
.
COM_INTERFACE_ENTRY(IObjectSafety)
.
END_COM_MAP()
Add these interface definitions to CShapeCtl.h file
STDMETHOD(GetInterfaceSafetyOptions)
(REFIID riid, DWORD *pdwSupportedOptions,
DWORD *pdwEnabledOptions);
STDMETHOD(SetInterfaceSafetyOptions)
(REFIID riid, DWORD dwOptionSetMask,
DWORD dwEnabledOptions);
Look in code for implementation details of the interface.
Testing Control
Figure 3

I have included ContainerApp which is a simple dialog based MFC application. In the dialog box right click and choose Insert Activex control option. Select ShapeCtl control. Right click on the control and manipulate the control properties from the property pages we added.
To include this control in web page use any of the editor or use the ActiveXPage.html file included in the ComApps folder.
References:
- Professional ATL COM Programming, Wrox Press Ltd, by Dr. Richard Grimes.
- Active Template Library, IDG Books Worldwide Inc, by Tom Armstrong
- The Essence of COM with ActiveX, Prentice Hall, by David S. Platt
- Inside OLE second Ed., Microsoft Press, by Kraig Brockscmidt
- POLYGON tutuorial supplied with VC++ 6.0
- Microsft knowledgebase articles
Compilation
The example code has been compiled with VC+ 6.0 (SP-1) on Windows NT 4.0 (SP-4). If you compile the code, you will get 9 warnings. These warnings are there because I didn’t implement the dispatch interfaces for font, picture and mouse icon property of the control. I didn’t do this to keep the implementation of control as simple as I could. See Microsoft Knowledgebase Article ID : Q166472 for details on their implementation.
Downloads