In this lesson, you will re-implement the OneArmedBandit ActiveX control using ATL. By doing this, you will be able to compare the differences between MFC and ATL control development techniques and assess which technology is most suited to your needs.
After this lesson, you will be able to:Estimated lesson time: 50 minutes
- Describe how to develop ActiveX controls using ATL.
- Describe how to add properties and methods to your control.
- Describe how to implement a control property page and make properties persistent.
- Describe how to use ATL's implementation of the COM connection point architecture to add events to your control.
As with any ATL COM development, you start by creating an ATL project and then add individual COM objects using the ATL Object Wizard. In the following exercises, you will create a DLL project to host your control, and then you'll add your ATL ActiveX control.
Figure 11.5 ATL Object Wizard control options
The ATL Object Wizard displays a number of different types of controls that you can create, including:
Figure 11.6 Selecting stock properties in the ATL Object Wizard
In ClassView, you see that the wizard has added the IATLBandit interface and the CATLBandit implementation class. The CATLBandit class is derived from the ATL CComControl class (as well as many others). Open the ATLBandit.h file and locate the COM map structure. You see that the CATLBandit class supports the many interfaces that are required by ActiveX control containers.
Directly beneath the COM map, you will see this property map structure:
BEGIN_PROP_MAP(CATLBandit) 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("ForeColor", DISPID_FORECOLOR, CLSID_StockColorPage) // Example entries // PROP_ENTRY("Property Description", dispid, clsid) // PROP_PAGE(CLSID_StockColorPage) END_PROP_MAP() |
ATL creates the property map structure to make implementing property persistence easy. You can see in the code just shown that entries for the stock properties, including the dimensions of the control, have already been added to the property map. Note that the PROP_ENTRY macros allow you to associate a property page with a property, and that the color stock property page has already been specified for your ForeColor and BackColor properties.
You will now add the NumberOfSymbols custom property to the control class.
Remember that when you are working with ATL, you must define a member variable to hold the data and provide implementations of the Get and Put functions so that you can pass data to and from this member variable.
STDMETHODIMP CATLBandit::get_NumberOfSymbols(short *pVal) { *pVal = m_numberOfSymbols; return S_OK; } STDMETHODIMP CATLBandit::put_NumberOfSymbols(short newVal) { newVal = newVal < 3 ? 3 : newVal; newVal = newVal > 7 ? 7 : newVal; m_numberOfSymbols = newVal; SetDirty(TRUE); return S_OK; } |
You should note that the put_ NumberOfSymbols() function ensures that the NumberOfSymbols property is set to a valid value. Note also the call to the SetDirty() function. This function should be called when the value of a persistent property is changed, so that the container can ask the user whether to save or abandon changes to the control's state. You will implement persistence for the NumberOfSymbols property after you have added a property page that allows access to the property value.
You will now implement the Click and Jackpot events for your control. To help you understand how COM events are implemented, open the project IDL file, OneArmedBanditATL.idl, and look at the type library definition, which is shown below.
[ uuid(39623002-6FAA-11D3-935D-0080C7FA0C3E), version(1.0), helpstring("OneArmedBanditATL 1.0 Type Library") ] library ONEARMEDBANDITATLLib { importlib("stdole32.tlb"); importlib("stdole2.tlb"); [ uuid(39623010-6FAA-11D3-935D-0080C7FA0C3E), helpstring("_IATLBanditEvents Interface") ] dispinterface _IATLBanditEvents { properties: methods: }; [ uuid(3962300F-6FAA-11D3-935D-0080C7FA0C3E), helpstring("ATLBandit Class") ] coclass ATLBandit { [default] interface IATLBandit; [default, source] dispinterface _IATLBanditEvents; }; }; |
The definition of the ONEARMEDBANDITATLLib type library contains the definition of a dispatch interface named _IATLBanditEvents. This interface is declared inside the ATLBandit coclass block—indicating that the ATLBandit control exposes the _IATLBanditEvents interface. Notice, however, that the in-terface is declared with the [source] IDL attribute, which indicates that the _IATLBanditEvents interface is a source of events. This kind of interface is known as a connection point. COM objects that expose connection points implement the IConnectionPointContainer interface, which manages the connection of source interfaces to a corresponding client object known as a sink. A sink object implements methods defined by the source object. Through the connection point mechanism, a pointer to the sink object's interface is passed to the source object. This pointer provides the source with access to the sink's implementation of its methods. To fire an event, the source object calls the corresponding method on the sink interface.
The first stage of defining an event is to add a method to the source interface to represent the event.
After you have added the event methods to the source interface, you use an ATL Wizard to implement a connection point. ATL uses information from the type library to achieve this implementation, which means that the type library must first be compiled.
Now you can use the ATL Wizard to implement the connection point.
Figure 11.7 The Implement Connection Point Wizard
The wizard creates a proxy class (nothing to do with the proxy objects used in marshaling), which contains proxy functions that implement the event methods you added to your event interface. If you look in ClassView, you will see that the CProxy_IATLBanditEvents class has been added to your project, and that it provides the functions Fire_Click() and Fire_Jackpot() that you use to fire control events. ATL alters the definition of the CATLBandit class so that it derives from the CProxy_IATLBanditEvents class, making the Fire_Click() and Fire_Jackpot() methods accessible as members of your control class.
The Implement Connection Point Wizard also creates the following connection point map within your class.
BEGIN_CONNECTION_POINT_MAP(CATLBandit) CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink) CONNECTION_POINT_ENTRY(DIID__IATLBanditEvents) END_CONNECTION_POINT_MAP() |
Make sure that the connection point map appears exactly as just shown—ATL sometimes fails to implement this map correctly, causing compilation errors.
You are now in a position to use the event proxy functions to fire events from your control. To demonstrate this, you will implement a handler function that fires a Click() event when you click inside the control.
Fire_Click(); |
You will now add the Play() method to the control interface and provide the code to implement the method.
TCHAR m_symbols[3]; |
To the CATLBandit constructor, add the following line to initialize the string:
_tcscpy(m_symbols, _T("JJJ")); |
(This code can be found in CH11_03.cpp, installed from the companion CD.)
STDMETHODIMP CATLBandit::Play() { srand((unsigned)time(NULL)); _tcscpy(m_symbols, _T("JJJ")); for(int i = 0; i < 3; i++) m_symbols[i] += UINT(rand() % m_numberOfSymbols); // repaint control m_spInPlaceSite->InvalidateRect(NULL, TRUE); if(m_symbols[0] == m_symbols[1] && m_symbols[1] == m_symbols[2]) Fire_Jackpot(); return S_OK; } |
#include <time.h> |
You will now implement the Symbols property page, which will enable users to set the value of the NumberOfSymbols custom property. ATL sets up each page of a control's property sheet as a separate object, implemented by a class derived from IPropertyPage. In the following exercise, you will insert a new property page class into your project.
If you switch back to ClassView, you will see that the CBanditPP class has been added to the project. The ATL Object Wizard has provided an implementation of the IPropertyPageImpl::Apply() function for this class. Apply() is executed when the user of the property page clicks the OK or Apply button. You will add code to provide an implementation of this function.
Locate the CBanditPP::Apply() function in the BanditPP.h file. Replace the example code with the following implementation:
STDMETHOD(Apply)(void) { CComQIPtr<IATLBandit> pIBandit(m_ppUnk[0]); pIBandit->put_NumberOfSymbols(GetDlgItemInt(IDC_NUMSYMBOLS)); m_bDirty = FALSE; return S_OK; } |
This function sets the control's NumberOfProperties property with the integer value retrieved from the edit control. Notice the use of the ATL smart pointer class CComQIPtr to retrieve an IATLBandit pointer. The member variable m_ppUnk is an array of IUnknown pointers to the objects associated with the property page.
The Apply() function stores the value in the edit control to the persistent IATLBandit::NumberOfSymbols property. You will also need to create a function to initialize the edit control with the property value. This function is essentially the Apply() function in reverse.
LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { CComQIPtr<IATLBandit> pIBandit(m_ppUnk[0]); short i = 0; pIBandit->get_NumberOfSymbols(&i); SetDlgItemInt(IDC_NUMSYMBOLS, i); return 0; } |
At this point, you should also add a notification function to indicate that the property page has been modified. This function should handle the EN_CHANGE event that fires when the user changes the value in the edit control.
SetDirty(TRUE); |
Now that you have fully implemented the property page, you can add an entry to the control class property map to make the NumberOfSymbols property persistent.
BEGIN_PROP_MAP(CATLBandit) PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4) PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4) PROP_ENTRY("NumberOfSymbols", 1, CLSID_BanditPP) PROP_ENTRY("BackColor", DISPID_BACKCOLOR, CLSID_StockColorPage) PROP_ENTRY("ForeColor", DISPID_FORECOLOR, CLSID_StockColorPage) END_PROP_MAP() |
The second parameter to the PROP_ENTRY macro is the DISPID, which can be found in the IDL file. The CLSID_BanditPP constant is defined in the OneArmedBanditATL_i.c file.
All that remains is to implement the drawing code. Like the MFC COleControl class, the CComControl class provides an OnDraw() function to contain all the drawing code. The CComControl::OnDraw() function receives a reference to an ATL_DRAWINFO structure that contains, among other things, a handle to a device context and a pointer to a RECT structure that denotes the bounding rectangle of the control. The device context is an HDC, the raw Windows data type that is wrapped by the MFC class CDC. This means that you have to use the GDI API functions to render your control. The GDI functions are similar to their CDC counterparts, the main difference being that they receive the HDC as the first argument. If you compare the implementation of CATLBandit::OnDraw() with the COneArmedBanditCtrl::OnDraw() function in the previous lesson, you will get a pretty good idea of how it all works.
Replace the function in ATLBandit.h with the following version:
(This code can be found in CH11_04.cpp, installed from the companion CD.)
HRESULT OnDraw(ATL_DRAWINFO& di) { const RECT& rc = *reinterpret_cast<const RECT*>(di.prcBounds); HDC dc = di.hdcDraw; COLORREF colorBack, colorFore; OleTranslateColor(m_clrForeColor, NULL, &colorFore); OleTranslateColor(m_clrBackColor, NULL, &colorBack); // Get dimensions of control float ctrlWidth = float(rc.right - rc.left); float ctrlHeight = float(rc.bottom - rc.top); // Set up DC HBRUSH brush = CreateSolidBrush(colorBack); HBRUSH oldBrush = static_cast<HBRUSH>(SelectObject(dc, brush)); HPEN pen = CreatePen(PS_SOLID, 3, colorFore); HPEN oldPen = static_cast<HPEN>(SelectObject(dc, pen)); HFONT SymbolFont = CreateFont(long(ctrlHeight / 1.1), long(ctrlWidth/6), 0, 0, 0, 0, 0, 0, SYMBOL_CHARSET, 0, 0, 0, 0,"WingDings"); HFONT oldFont = static_cast<HFONT>(SelectObject(dc, SymbolFont)); // Draw bounding rectangle Rectangle(dc, rc.left, rc.top, rc.right, rc.bottom); SetBkMode(dc, TRANSPARENT); // Draw text SetTextColor(dc, colorFore); RECT rect; CopyRect(&rect, &rc); TCHAR strDisplay[5]; _stprintf(strDisplay, "%c %c %c", m_symbols[0], m_symbols[1], m_symbols[2]); DrawText(dc, strDisplay, 5, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER ); // Draw vertical bars long onethird = long(ctrlWidth / 3); POINT ptTop = { rc.left, rc.top }; POINT ptBtm = { rc.left, rc.bottom }; ptTop.x += onethird; ptBtm.x += onethird; MoveToEx(dc, ptTop.x, ptTop.y, NULL); LineTo(dc, ptBtm.x, ptBtm.y); ptTop.x += onethird; ptBtm.x += onethird; MoveToEx(dc, ptTop.x, ptTop.y, NULL); LineTo(dc, ptBtm.x, ptBtm.y); // Restore device context SelectObject(dc, oldFont); SelectObject(dc, oldPen); SelectObject(dc, oldBrush); DeleteObject(brush); DeleteObject(pen); DeleteObject(SymbolFont); return S_OK; } |
You can now build the ATLBandit control. Load the control into the ActiveX Control Test Container to test the control display, the property pages, the property persistence, the Play() method, and the Click and Jackpot events.
By now you should have a good idea of the differences between MFC and ATL ActiveX control development. MFC makes it very easy to develop controls, shielding you from most of the complexities of COM programming. Although developing controls with ATL involves more work, as your COM expertise develops you might come to appreciate the greater degree of control that ATL affords.
ActiveX controls written in ATL are smaller than their MFC counterparts, and if written properly will perform better. A self-contained (MinDependency) build of the ATLBandit project results in a control that is 100 KB in size. The OneArmedBandit.ocx, when statically linked to the MFC libraries, is about 264 KB. When the component is dynamically linked to the MFC libraries, its size drops to about 36 KB, making MFC an attractive proposition when developing for target platforms where you can expect the MFC DLLs to be installed.
ATL can be used to create small, high-performance ActiveX controls that are suitable for use in any ActiveX control container. In ATL, ActiveX controls are implemented by a class that inherits from the CComControl class. ActiveX controls also implement the many COM interfaces that are required by ActiveX control containers. ATL control classes implement a property map structure to implement property persistence, a message map to handle Windows messages, and COM connection points to implement ActiveX events. You can insert a property page COM object into your project to implement a property page for your control. You draw your control using GDI API functions inside the CComControl::OnDraw() function.