MFC simplifies the process of creating an ActiveX control. Using the MFC ActiveX ControlWizard, you can easily create a fully featured ActiveX control. In this lesson, you will use MFC to create an ActiveX control that implements a dispatch interface, fires events, and provides a property page that can be used to get and set persistent properties.
After this lesson, you will be able to:Estimated lesson time: 50 minutes
- Describe how to use the MFC ActiveX ControlWizard to create an ActiveX control.
- Describe how to use ClassWizard to add properties, methods, and events to your control.
- Describe how to implement a control property page.
- Describe how to make properties persistent.
- Describe how to use the ActiveX Control Test Container to test your control.
In this lesson, you will develop the OneArmedBandit ActiveX control, a software version of a Las Vegas-style slot machine. The control provides the single method Play(), which causes a random combination of symbols to appear in the control's three "reels." When the control displays three identical symbols, the Jackpot event is fired. When the user clicks inside the control, the Click event is fired. Figure 11.1 shows the OneArmedBandit control inside the ActiveX Control Test Container.
The OneArmedBandit control provides a property page that allows a user to set the values of the ForeColor and BackColor properties, and to set the custom control-specific NumberOfSymbols property. This property is used to set the number of possible symbols that the control can display, and thus increase or decrease the odds of winning the jackpot. All three of the control properties are persistent.
Figure 11.1 The OneArmedBandit ActiveX control
As with all MFC development, the first stage in creating an ActiveX control is to create a development project. In the following exercise, you will create the OneArmedBandit project.
Figure 11.2 Creating the OneArmedBandit project
There are a couple of interesting things to note about the information displayed in this dialog box. First, the DLL is created with an .ocx extension. The use of the .ocx extension is not required for ActiveX controls; it is simply a convention left over from the days when ActiveX controls were called OLE controls. OLE controls replaced the older Visual Basic Extension (VBX) controls. Second, the type library source code is in a file named OneArmedBandit.odl. This file contains Object Description Language (ODL) code. ODL was the predecessor to Interface Definition Language (IDL), and it is very similar to IDL in syntax. The Microsoft IDL (MIDL) compiler is capable of compiling ODL code.
The ActiveX ControlWizard creates classes to implement the control's DLL server, the control itself, and the control's property page. The COneArmedBanditCtrl control class is derived from the MFC class COleControl. This powerful class inherits all the functionality of the CWnd and CCmdTarget classes and provides a large number of member functions related to the operation of an ActiveX control. Using these functions, you can get and set a control's stock properties, retrieve ambient properties from the container, fire stock events, implement property persistence, and perform a number of operations related to siting and displaying a control.
Figure 11.3 OneArmedBandit project classes
The ControlWizard also defines two dispatch interfaces for your control: _DOneArmedBandit and _DOneArmedBanditEvents. You add methods and properties to the first of these and events to the second. Although you can make these additions by right-clicking the interface items and choosing the appropriate command from the shortcut menu, the preferred method is to use ClassWizard.
In the following exercises, you will be adding the stock ForeColor and BackColor properties and the custom NumberOfSymbols property to the COneArmedBanditCtrl class.
In ClassView, you will be able see the new properties listed underneath the _DOneArmedBandit interface item. Notice also that the member variable and the notification function have been added to the COneArmedBanditCtrl class. Double-click the OnNumberOfSymbolsChanged() function to view the source code in the OneArmedBanditCtl.cpp file. The default version of the function simply calls the COleControl::SetModifiedFlag() function.
Toward the top of the OneArmedBanditCtl.cpp file, you will find this code:
BEGIN_DISPATCH_MAP(COneArmedBanditCtrl, COleControl) //{{AFX_DISPATCH_MAP(COneArmedBanditCtrl) DISP_PROPERTY_NOTIFY(COneArmedBanditCtrl, "NumberOfSymbols", m_numberOfSymbols, OnNumberOfSymbolsChanged, VT_I2) DISP_STOCKPROP_BACKCOLOR() DISP_STOCKPROP_FORECOLOR() //}}AFX_DISPATCH_MAP END_DISPATCH_MAP() |
This code, along with the corresponding DECLARE_DISPATCH_MAP macro in the header file, implements an MFC dispatch map for the class. The dispatch map is very similar to the message map you learned about in Chapter 3, but instead of mapping Windows messages to class handler functions, the dispatch map maps Automation client requests to your control class's implementation of the properties and methods. For example, when a Visual Basic client requests the NumberOfSymbols property with the following code, the control uses the dispatch map (specifically the DISP_PROPERTY_NOTIFY macro) to retrieve the value contained in the m_numberOfSymbols variable and pass it back to the client:
Dim myobj As OneArmedBandit Set myobj = New OneArmedBandit MsgBox myobj.NumberOfSymbols |
As you can see from the dispatch map code, stock properties are supported with their own specific macros.
MFC makes it easy to make control properties persistent. The COleControl:: DoPropExchange() function serializes the control properties to and from a storage medium—usually the control's container. The ActiveX ControlWizard overloads the DoPropExchange() function for your control class. The framework passes this function a pointer to a CPropExchange object, which encapsulates the context of the property exchange, including its direction. The over- loaded version of DoPropExchange() calls the base class version to serialize the stock properties implemented by the control. It is your responsibility to add code to serialize any custom properties you want to make persistent.
MFC provides a number of functions that allow you to serialize properties of different types. The names of these functions begin with the prefix PX_. To serialize your NumberOfSymbols property, replace the //TODO comment in your control class's DoPropExchange() function with a call to the PX_Short() function, as follows:
void COneArmedBanditCtrl::DoPropExchange(CPropExchange* pPX) { ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor)); COleControl::DoPropExchange(pPX); PX_Short(pPX, "NumberOfSymbols", m_numberOfSymbols, 3); } |
The fourth argument to the PX_Short() function specifies a default value for the property that is used if the function is unable to retrieve a value from storage medium. The example code above ensures that the NumberOfSymbols property of a new control instance will be initialized with a default value of 3.
As well as providing access to control properties, the dispatch map also maps client method calls to class member function implementations. You use ClassWizard to add a method.
Notice that the Play() function has been added to the COneArmedBanditCtrl class. If you inspect the dispatch map, you will see that the following line has been added:
DISP_FUNCTION(COneArmedBanditCtrl, "Play", Play, VT_EMPTY, VTS_NONE) |
ClassWizard also automates the process of defining events fired by the control. In the following exercise, you will implement the Click stock event and the Jackpot custom event.
In ClassView, notice that the FireJackpot() function has been added to the COneArmedBanditCtrl class. You can use this function from within your code to fire the Jackpot event. ClassWizard has also added entries to the class's event map. The event map is a structure very similar to the dispatch map that is used to implement ActiveX control events. The following is the event map implementation for the COneArmedBanditCtrl class:
BEGIN_EVENT_MAP(COneArmedBanditCtrl, COleControl) //{{AFX_EVENT_MAP(COneArmedBanditCtrl) EVENT_CUSTOM("Jackpot", FireJackpot, VTS_NONE) EVENT_STOCK_CLICK() //}}AFX_EVENT_MAP END_EVENT_MAP() |
Now that you have implemented your ActiveX control's interface, you can add property pages to allow users of the control to get and set its properties. The framework uses the PROPPAGEID series of macros to specify an array of property page IDs for your control's property sheet. The default code generated by the ActiveX ControlWizard for the OneArmedBandit project can be found in the OneArmedBandit.cpp file, and is as follows:
BEGIN_PROPPAGEIDS(COneArmedBanditCtrl, 1) PROPPAGEID(COneArmedBanditPropPage::guid) END_PROPPAGEIDS(COneArmedBanditCtrl) |
Note that this structure, unlike the dispatch and event maps, is not maintained by ClassWizard. You have to maintain the code yourself.
In the code that is shown above, the single PROPPAGEID entry in the table refers to the default property page that is created for your control. The ActiveX ControlWizard creates a dialog template resource and a dialog class (based on the COlePropertyPage class) that you can edit and compile to create a property page that allows access to your control's custom properties.
In the following exercises, you will create a property page that allows a user to set the NumberOfSymbols custom property.
Figure 11.4 Implementing the custom property page
Give the edit control the ID IDC_NUMSYMBOLS. On the Styles tab of the Edit Properties dialog box, select the Number check box.
Look at the DoDataExchange() function of the COneArmedBanditPropPage class. Notice that in addition to the DDX and DDV functions, ClassWizard has added the following line:
DDP_Text(pDX, IDC_NUMSYMBOLS, m_numsymbols, _T("NumberOfSymbols")); |
This is one of a number of functions with the DDP_ prefix that is provided by MFC to transfer data between an ActiveX Control property page and the control properties. Note that the dispatch name of the control property is used, so you do not have to provide an additional code link between the COneArmedBanditPropPage class and the COneArmedBanditCtrl class.
Because you have added the ForeColor and BackColor properties to your control, you will need to implement a property page to allow the user to set them. MFC provides three stock property pages for use with ActiveX controls: CLSID_ CColorPropPage, CLSID_CFontPropPage, and CLSID_CPicturePropPage. These pages display a user interface for stock color, font, and picture properties, respectively. To add a stock property page, add another PROPPAGEID macro to the code in the OneArmedBandit.cpp file, as shown here:
BEGIN_PROPPAGEIDS(COneArmedBanditCtrl, 2) PROPPAGEID(COneArmedBanditPropPage::guid) PROPPAGEID(CLSID_CColorPropPage) END_PROPPAGEIDS(COneArmedBanditCtrl) |
Note that the second argument to the BEGIN_PROPPAGEIDS macro must be altered to match the number of property pages specified for the ActiveX control.
Controls created with the MFC ActiveX ControlWizard are user-interface controls. This means that much of the control-specific implementation that you provide is drawing code. As with an MFC document/view application, all the drawing code for a control is placed within a single function named OnDraw(). The following is the default implementation generated by the ControlWizard:
void COneArmedBanditCtrl::OnDraw( CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid) { // TODO: Replace the following code with your own drawing code. pdc->FillRect(rcBounds, CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH))); pdc->Ellipse(rcBounds); } |
The COleControl::OnDraw() function receives a pointer to an MFC device context object, and a rectangle that represents the control's on-screen area, specified in logical units. You should avoid using fixed values in your drawing code, and you should scale all drawing output to the dimensions of the bounding rectangle. In this way, you ensure that the entire control is always visible and that its internal proportions are always maintained. The OnDraw() function also receives a rectangle that represents the invalidated area of the control and can be used to optimize your drawing code.
Replace the code in your project with the following code:
(This code can be found in CH11_01.cpp, installed from the companion CD.)
void COneArmedBanditCtrl::OnDraw( CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid) { // Get colors of stock properties COLORREF colorBack = TranslateColor(GetBackColor()); COLORREF colorFore = TranslateColor(GetForeColor()); // Get dimensions of control float ctrlWidth = float(rcBounds.Width()); float ctrlHeight = float(rcBounds.Height()); // Setup DC CBrush brush(colorBack); CBrush * pOldBrush = pdc->SelectObject(&brush); CPen pen(PS_SOLID, 3, colorFore); CPen *pOldPen = pdc->SelectObject(&pen); CFont SymbolFont; CFont *pOldFont; if(SymbolFont. CreateFont(long(ctrlHeight / 1.1), long(ctrlWidth/6), 0, 0, 0, 0, 0, 0, SYMBOL_CHARSET, 0, 0, 0, 0,"WingDings")); pOldFont = pdc->SelectObject(&SymbolFont); else pOldFont = SelectStockFont(pdc); // Draw bounding rectangle pdc->Rectangle(rcBounds); pdc->SetBkMode(TRANSPARENT); // Draw text pdc->SetTextColor(colorFore); RECT rect; ::CopyRect(&rect, rcBounds); CString strDisplay; strDisplay.Format("%c %c %c", m_symbols[0], m_symbols[1], m_symbols[2]); pdc->DrawText(strDisplay, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER ); // Draw vertical bars long onethird = long(ctrlWidth / 3); CPoint ptTop(rcBounds.TopLeft()); CPoint ptBtm(rcBounds.left, rcBounds.bottom); ptTop.x += onethird; ptBtm.x += onethird; pdc->MoveTo(ptTop); pdc->LineTo(ptBtm); ptTop.x += onethird; ptBtm.x += onethird; pdc->MoveTo(ptTop); pdc->LineTo(ptBtm); // Restore device context pdc->SelectObject(pOldFont); pdc->SelectObject(pOldPen); pdc->SelectObject(pOldBrush); } |
This function simulates the slot machine window by displaying three characters from the Wingdings symbol font, as shown in Figure 11.1. The characters themselves are contained in the fixed-length string m_symbols, which is a member variable of the COneArmedBanditCtrl class. You will need to add this member before your code will compile.
TCHAR m_symbols[3]; |
_tcscpy(m_symbols, _T("JJJ")); |
All that remains is to implement the Play() method that simulates the spinning of the slot machine reels, displaying a random symbol in each window. You created a stub for the Play() implementation function when you added the method to the interface. Now you need to add code to randomly alter each of the characters in the COneArmedBanditCtrl::m_symbols string.
(This code can be found in CH11_02.cpp, installed from the companion CD.)
void COneArmedBanditCtrl::Play() { srand((unsigned)time(NULL)); _tcscpy(m_symbols, _T("JJJ")); for(int i = 0; i < 3; i++) m_symbols[i] += UINT(rand() % m_numberOfSymbols); InvalidateControl(); // repaints control if(m_symbols[0] == m_symbols[1] && m_symbols[1] == m_symbols[2]) FireJackpot(); } |
The use of the modulus operator (%) ensures that each character in the m_symbols string is incremented by a random number between zero and m_numberOfSymbols. When all three characters are the same, the Jackpot custom event is fired.
The ActiveX Control Test Container is a useful tool provided by Visual Studio that allows you to run and test any ActiveX control registered on your computer. In the following exercises, you will learn how to use the ActiveX Control Test Container to test the property pages, the events, and the Play() method of the OneArmedBandit ActiveX control.
In this lesson, you have seen just how easy it is to develop ActiveX controls using MFC. The ActiveX ControlWizard creates a set of classes that implement a DLL, a control and a property page. The ControlWizard defines dispatch interfaces for your control's properties, methods, and events. You use ClassWizard to design these interfaces. An MFC ActiveX control is based on the COleControl class, which provides many member functions that allow you to implement stock properties and events, retrieve ambient properties from the container, implement property persistence, and connect a control's properties to a property page. Apart from the implementations of the custom methods, most of the code that you need to write for your control is drawing code, placed within the control's OnDraw() function.