Armed with this knowledge of the ActiveX control architecture and the manner in which MFC encapsulates it, you're almost ready to build your first control. But first, you need to know more about the process of writing ActiveX controls with Visual C++ and MFC. The following sections provide additional information about the nature of ActiveX controls from an MFC control writer's perspective and describe some of the basic skills required to write a control—for example, how to add methods, properties, and events, and what impact these actions have on the underlying source code.
The first step in writing an MFC ActiveX control is to create a new project and select MFC ActiveX ControlWizard as the project type. This runs ControlWizard, which asks a series of questions before generating the project's source code files.
The first series of questions is posed in ControlWizard's Step 1 dialog box, shown in Figure 21-4. By default, the OCX generated when this project is built will contain just one control. If you'd rather it implement more, enter a number in the How Many Controls Would You Like Your Project To Have box. ControlWizard will respond by including multiple control classes in the project. Another option is Would You Like The Controls In This Project To Have A Runtime License? If you answer yes, ControlWizard builds in code that prevents the control from being instantiated in the absence of a valid run-time license. Implemented properly, this can be an effective means of preventing just anyone from using your control. But because ControlWizard's license-checking scheme is easily circumvented, enforcing run-time licensing requires extra effort on the part of the control's implementor. For details, see the section "Control Licensing" at the close of this chapter.
Figure 21-4. ControlWizard's Step 1 dialog box.
ControlWizard's Step 2 dialog box is shown in Figure 21-5. Clicking the Edit Names button displays a dialog box in which you can enter names for the classes ControlWizard will generate, the names of those classes' source code files, and ProgIDs for the control and its property page. If you'd like the control to wrap a built-in control type such as a slider control or a tree view control, choose a WNDCLASS name from the list attached to the Which Window Class, If Any, Should This Control Subclass box. The "Control Subclassing" section later in this chapter explains what this does to your source code and what implications it has for the code you write.
Figure 21-5. ControlWizard's Step 2 dialog box.
The options under Which Features Would You Like This Control To Have? can have profound effects on a control's appearance and behavior. The defaults are normally just fine, but it's hard to understand what these options really mean from the scant descriptions provided in the online help. Therefore, here's a brief synopsis of each one. The term miscellaneous status bits refers to a set of bit flags that communicate certain characteristics of the control to the control container. A container can acquire a control's miscellaneous status bits from the control itself or, if the control isn't running, from the registry.
You can access still more options by clicking the Advanced button in the Step 2 dialog box, which displays the window shown in Figure 21-6. All are relatively recent additions to the ActiveX control specification (most come directly from OCX 96), and none are universally supported by control containers. Nevertheless, they're worth knowing about, if for no other reason than the fact that ControlWizard exposes them to you.
Figure 21-6. ControlWizard's Advanced ActiveX Features dialog box.
Here's a brief summary of the options found in the Advanced ActiveX Features dialog box:When you select any of the advanced options—with the exception of Loads Properties Asynchronously—ControlWizard overrides a COleControl function named GetControlFlags in the derived control class and selectively sets or clears bit flags in the control flags that the function returns. For example, selecting Flicker-Free Activation ORs a noFlickerActivate flag into the return value. Some options prompt ControlWizard to make more extensive modifications to the source code. For example, selecting Optimized Drawing Code adds canOptimizeDraw to the control flags and inserts a call to IsOptimizedDraw into OnDraw. MFC calls GetControlFlags at various times to find out about relevant characteristics of the control.
When ControlWizard is done, you're left with an ActiveX control project that will actually compile into a do-nothing ActiveX control—one that has no methods, properties, or events, and does no drawing other than erase its background and draw a simple ellipse, but one that satisfies all the criteria for an ActiveX control. That project includes these key elements:
ControlWizard does nothing that you couldn't do by hand, but it provides a welcome jump start on writing an ActiveX control. I'm not a big fan of code-generating wizards, and there's much more I wish ControlWizard would do, but all things considered, it's a tool that would be hard to live without.
When a control needs repainting, MFC calls its OnDraw function. OnDraw is a virtual function inherited from COleControl. It's prototyped like this:
virtual void OnDraw (CDC* pDC, const CRect& rcBounds, const CRect& rcInvalid) |
pDC points to the device context in which the control should paint itself. rcBounds describes the rectangle in which painting should be performed. rcInvalid describes the portion of the control rectangle ( rcBounds) that is invalid; it could be identical to rcBounds, or it could be smaller. Use it to optimize drawing performance the same way you'd use GetClipBox in a conventional MFC application.
OnDraw can be called for three reasons:
Regardless of why it's called, OnDraw's job is to draw the control. The device context is provided for you in the parameter list, and you can use CDC output functions to do the drawing. Just be careful to abide by the following rules:
These rules exist primarily for the benefit of windowless controls, but it's important to heed them when writing controls that are designed to work equally well whether they're windowed or windowless. To determine at run time whether a control is windowed or windowless, check the control's m_bInPlaceSiteWndless data member. A nonzero value means the control is windowless.
Ambient properties allow a control to query its container for pertinent characteristics of the environment in which the control is running. Because ambient properties are Automation properties implemented by the container, they are read by calling IDispatch::Invoke on the container. COleControl simplifies the retrieval of ambient property values by supplying wrapper functions that call IDispatch::Invoke for you. COleControl::AmbientBackColor, for example, returns the ambient background color. The following table lists several of the ambient properties that are available, their dispatch IDs, and the corresponding COleControl member functions. To read ambient properties for which property-specific retrieval functions don't exist, you can call GetAmbientProperty and pass in the property's dispatch ID.
Ambient Properties
Property Name | Dispatch ID | COleControl Retrieval Function |
---|---|---|
BackColor | DISPID_AMBIENT_BACKCOLOR | AmbientBackColor |
DisplayName | DISPID_AMBIENT_ DISPLAYNAME | AmbientDisplayName |
Font | DISPID_AMBIENT_ FONT | AmbientFont |
ForeColor | DISPID_AMBIENT_ FORECOLOR | AmbientForeColor |
LocaleID | DISPID_AMBIENT_ LOCALEID | AmbientLocaleID |
MessageReflect | DISPID_AMBIENT_MESSAGEREFLECT | GetAmbientProperty |
ScaleUnits | DISPID_AMBIENT_SCALEUNITS | AmbientScaleUnits |
TextAlign | DISPID_AMBIENT_TEXTALIGN | AmbientTextAlign |
UserMode | DISPID_AMBIENT_USERMODE | AmbientUserMode |
UIDead | DISPID_AMBIENT_UIDEAD | AmbientUIDead |
ShowGrabHandles | DISPID_AMBIENT- | AmbientShow- |
_SHOWGRABHANDLES | GrabHandles | |
ShowHatching | DISPID_AMBIENT_SHOWHATCHING | AmbientShowHatching |
DisplayAsDefaultButton | DISPID_AMBIENT_DISPLAYASDEFAULT | GetAmbientProperty |
SupportsMnemonics | DISPID_AMBIENT- | GetAmbientProperty |
_SUPPORTSMNEMONICS | ||
AutoClip | DISPID_AMBIENT_AUTOCLIP | GetAmbientProperty |
Appearance | DISPID_AMBIENT_APPEARANCE | GetAmbientProperty |
Palette | DISPID_AMBIENT_PALETTE | GetAmbientProperty |
TransferPriority | DISPID_AMBIENT_TRANSFERPRIORITY | GetAmbientProperty |
The following code, which would probably be found in a control's OnDraw function, queries the container for the ambient background color and paints the control background the same color:
CBrush brush (TranslateColor (AmbientBackColor ())); pdc->FillRect (rcBounds, &brush); |
Notice the use of COleControl::TranslateColor to convert the OLE_COLOR color value returned by AmbientBackColor into a Windows COLORREF value. OLE_COLOR is ActiveX's native color data type.
If your OnDraw implementation relies on one or more ambient properties, you should override COleControl::OnAmbientPropertyChange in the derived control class. This function is called when the container notifies the control that one or more ambient properties have changed. Overriding it allows the control to respond immediately to changes in the environment surrounding it. A typical response is to repaint the control by calling InvalidateControl:
void CMyControl::OnAmbientPropertyChange (DISPID dispid) { InvalidateControl (); // Repaint. } |
The dispid parameter holds the dispatch ID of the ambient property that changed, or DISPID_UNKNOWN if two or more properties have changed. A smart control could check this parameter and refrain from calling InvalidateControl unnecessarily.
Adding a custom method to an ActiveX control is just like adding a method to an Automation server. The procedure, which was described in Chapter 20, involves going to ClassWizard's Automation page, selecting the control class in the Class Name box, clicking Add Method, filling in the Add Method dialog box, and then filling in the empty function body created by ClassWizard.
Adding a stock method is even easier. You once again click the Add Method button, but rather than enter a method name, you choose one from the drop-down list attached to the External Name box. COleControl provides the method implementation, so there's literally nothing more to do. You can call a stock method on your own control by calling the corresponding COleControl member function. The stock methods supported by COleControl and the member functions used to call them are listed in the following table.
Stock Methods Implemented by COleControl
Method Name | Dispatch ID | Call with |
---|---|---|
DoClick | DISPID_DOCLICK | DoClick |
Refresh | DISPID_REFRESH | Refresh |
When you add a custom method to a control, ClassWizard does the same thing it does when you add a method to an Automation server: it adds the method and its dispatch ID to the project's ODL file, adds a function declaration and body to the control class's H and CPP files, and adds a DISP_FUNCTION statement to the dispatch map.
Stock methods are treated in a slightly different way. ClassWizard still updates the ODL file, but because the function implementation is provided by COleControl, no function is added to your source code. Furthermore, rather than add a DISP_FUNCTION statement to the dispatch map, ClassWizard adds a DISP_STOCKFUNC statement. The following dispatch map declares two methods—a custom method named Foo and the stock method Refresh:
BEGIN_DISPATCH_MAP (CMyControl, COleControl) DISP_FUNCTION (CMyControl, "Foo", Foo, VT_EMPTY, VTS_NONE) DISP_STOCKFUNC_REFRESH () END_DISPATCH_MAP () |
DISP_STOCKFUNC_REFRESH is defined in Afxctl.h. It maps the Automation method named Refresh to COleControl::Refresh. A related macro named DISP_STOCKFUNC_DOCLICK adds the stock method DoClick to an ActiveX control.
Adding a custom property to an ActiveX control is just like adding a property to an MFC Automation server. ActiveX controls support member variable properties and get/set properties just like Automation servers do, so you can add either type.
You add a stock property by choosing the property name from the list that drops down from the Add Property dialog box's External Name box. COleControl supports most, but not all, of the stock properties defined in the ActiveX control specification. The following table lists the ones that it supports.
Stock Properties Implemented by COleControl
Property Name | Dispatch ID | Retrieve with | Notification Function |
---|---|---|---|
Appearance | DISPID_APPEARANCE | GetAppearance | OnAppearanceChanged |
BackColor | DISPID_BACKCOLOR | GetBackColor | OnBackColorChanged |
BorderStyle | DISPID_BORDERSTYLE | GetBorderStyle | OnBorderStyleChanged |
Caption | DISPID_CAPTION | GetText or InternalGetText | OnTextChanged |
Enabled | DISPID_ENABLED | GetEnabled | OnEnabledChanged |
Font | DISPID_FONT | GetFont or InternalGetFont | OnFontChanged |
ForeColor | DISPID_FORECOLOR | GetForeColor | OnForeColorChanged |
hWnd | DISPID_HWND | GetHwnd | N/A |
ReadyState | DISPID_READYSTATE | GetReadyState | N/A |
Text | DISPID_TEXT | GetText or InternalGetText | OnTextChanged |
To retrieve the value of a stock property that your control implements, call the corresponding COleControl get function. ( COleControl also provides functions for setting stock property values, but they're rarely used.) To find out when the value of a stock property changes, override the corresponding notification function in your derived class. Generally, it's a good idea to repaint the control any time a stock property changes if the control indeed uses stock properties. COleControl provides default notification functions that repaint the control by calling InvalidateControl, so unless you want to do more than simply repaint the control when a stock property value changes, there's no need to write a custom notification function.
Under the hood, adding a custom property to a control modifies the control's source code files as if a property had been added to an Automation server. Stock properties are handled differently. In addition to declaring the property in the ODL file, ClassWizard adds a DISP_STOCKPROP statement to the control's dispatch map. The following dispatch map declares a custom member variable property named SoundAlarm and the stock property BackColor:
BEGIN_DISPATCH_MAP (CMyControl, COleControl) DISP_PROPERTY_EX (CMyControl, "SoundAlarm", m_bSoundAlarm, VT_BOOL) DISP_STOCKPROP_BACKCOLOR () END_DISPATCH_MAP () |
DISP_STOCKPROP_BACKCOLOR is one of several stock property macros defined in Afxctl.h. It associates the property with a pair of COleControl functions named GetBackColor and SetBackColor. Similar macros are defined for the other stock properties that COleControl supports.
After adding a custom property to a control, the very next thing you should do is add a statement to the control's DoPropExchange function making that property persistent. A persistent property is one whose value is saved to some storage medium (usually a disk file) and later read back. When a Visual C++ programmer drops an ActiveX control into a dialog and modifies the control's properties, the control is eventually asked to serialize its property values. The dialog editor saves those values in the project's RC file so that they will "stick." The saved values are reapplied when the control is re-created. Controls implement persistence interfaces such as IPersistPropertyBag for this reason.
To make an MFC control's properties persistent, you don't have to fuss with low-level COM interfaces. Instead, you override the DoPropExchange function that a control inherits from COleControl and add statements to it—one per property. The statements are actually calls to PX functions. MFC provides one PX function for each possible property type, as listed in the following table.
PX Functions for Serializing Control Properties
Function | Description |
---|---|
PX_Blob | Serializes a block of binary data |
PX_Bool | Serializes a BOOL property |
PX_Color | Serializes an OLE_COLOR property |
PX_Currency | Serializes a CURRENCY property |
PX_DataPath | Serializes a CDataPathProperty property |
PX_Double | Serializes a double-precision floating point property |
PX_Float | Serializes a single-precision floating point property |
PX_Font | Serializes a CFontHolder property |
PX_IUnknown | Serializes properties held by another object |
PX_Long | Serializes a signed 32-bit integer property |
PX_Picture | Serializes a CPictureHolder property |
PX_Short | Serializes a signed 16-bit integer property |
PX_String | Serializes a CString property |
PX_ULong | Serializes an unsigned 32-bit integer property |
PX_UShort | Serializes an unsigned 16-bit integer property |
If your control implements a custom member variable property of type BOOL named SoundAlarm, the following statement in the control's DoPropExchange function makes the property persistable:
PX_Bool (pPX, _T ("SoundAlarm"), m_bSoundAlarm, TRUE); |
pPX is a pointer to a CPropExchange object; it's provided to you in DoPropExchange's parameter list. SoundAlarm is the property name, and m_bSoundAlarm is the variable that stores the property's value. The fourth parameter specifies the property's default value. It is automatically assigned to m_bSoundAlarm when the control is created.
If SoundAlarm were a get/set property instead of a member variable property, you'd need to retrieve the property value yourself before calling PX_Bool:
BOOL bSoundAlarm = GetSoundAlarm (); PX_Bool (pPX, _T ("SoundAlarm"), bSoundAlarm); |
In this case, you would use the form of PX_Bool that doesn't accept a fourth parameter. Custom get/set properties don't require explicit initialization because they are initialized implicitly by their get functions.
Which brings up a question. Given that custom properties are initialized either inside DoPropExchange or by their get functions, how (and when) do stock properties get initialized? It turns out that MFC initializes them for you using commonsense values. A control's default BackColor property, for example, is set equal to the container's ambient BackColor property when the control is created. The actual initialization is performed by COleControl::ResetStockProps, so if you want to initialize stock properties yourself, you can override this function and initialize the property values manually after calling the base class implementation of ResetStockProps.
When you create a control project with ControlWizard, DoPropExchange is overridden in the derived control class automatically. Your job is to add one statement to it for each custom property that you add to the control. There's no wizard that does this for you, so you must do it by hand. Also, you don't need to modify DoPropExchange when you add stock properties because MFC serializes stock properties for you. This serialization is performed by the COleControl::DoPropExchange function. That's why ControlWizard inserts a call to the base class when it overrides DoPropExchange in a derived control class.
One other detail you must attend to when adding properties to an ActiveX control is to make sure that all those properties, whether stock or custom, are accessible through the control's property sheet. The property sheet is displayed by the container, usually at the request of a user. For example, when a Visual C++ programmer drops an ActiveX control into a dialog, right-clicks the control, and selects Properties from the context menu, the dialog editor displays the control's property sheet.
To make its properties accessible through a property sheet, a control implements one or more property pages and makes them available through its ISpecifyPropertyPages interface. To display the control's property sheet, the container asks the control for a list of CLSIDs by calling its ISpecifyPropertyPages::GetPages method. Each CLSID corresponds to one property page. The container passes the CLSIDs to ::OleCreatePropertyFrame or ::OleCreatePropertyFrameIndirect, which instantiates the property page objects and inserts them into an empty property sheet. Sometimes the container will insert property pages of its own. That's why a control's property sheet will have extra pages in some containers but not in others.
MFC simplifies matters by implementing ISpecifyPropertyPages for you. It even gives you a free implementation of property page objects in the form of COlePropertyPage. ControlWizard adds an empty dialog resource representing a property page to the project for you; your job is to add controls to that page and link those controls to properties of the ActiveX control. You accomplish the first task with the dialog editor. You connect a control on the page to an ActiveX control property by using ClassWizard's Add Variable button to add a member variable to the property page class and specifying the Automation name of the ActiveX control property in the Add Member Variable dialog box's Optional Property Name field. (You'll see what I mean when you build a control later in this chapter.)
Under the hood, ClassWizard links a dialog control to an ActiveX control property by modifying the derived COlePropertyPage class's DoDataExchange function. The DDP_Check and DDX_Check statements in the following DoDataExchange function link the check box whose ID is IDC_CHECKBOX to an ActiveX control property named SoundAlarm:
void CMyOlePropertyPage::DoDataExchange(CDataExchange* pDX) { DDP_Check (pDX, IDC_CHECKBOX, m_bSoundAlarm, _T ("SoundAlarm")); DDX_Check (pDX, IDC_CHECKBOX, m_bSoundAlarm); DDP_PostProcessing (pDX); } |
DDP functions work hand in hand with their DDX counterparts to transfer data between property page controls and ActiveX control properties.
When ControlWizard creates an ActiveX control project, it includes just one property page. You can add extra pages by modifying the control's property page map, which is found in the derived control class's CPP file. Here's what a typical property page map looks like:
BEGIN_PROPPAGEIDS (CMyControl, 1) PROPPAGEID (CMyControlPropPage::guid) END_PROPPAGEIDS (CMyControl) |
The 1 in BEGIN_PROPPAGEIDS' second parameter tells MFC's implementation of ISpecifyPropertyPages that this control has just one property page; the PROPPAGEID statement specifies that page's CLSID. ( CMyControlPropPage::guid is a static variable declared by the IMPLEMENT_OLECREATE_EX macro that ControlWizard includes in the property page class's CPP file.)
Adding a property page is as simple as incrementing the BEGIN_PROPPAGEIDS count from 1 to 2 and adding a PROPPAGEID statement specifying the page's CLSID. The big question is, Where does that property page (and its CLSID) come from?
There are two possible answers. The first is a stock property page. The system provides three stock property pages that ActiveX controls can use as they see fit: a color page for color properties, a picture page for picture properties, and a font page for font properties. Their CLSIDs are CLSID_CColorPropPage, CLSID_CPicturePropPage, and CLSID_CFontPropPage, respectively. The most useful of these is the stock color page (shown in Figure 21-7), which provides a standard user interface for editing any color properties implemented by your control. The following property page map includes a color page as well as the default property page:
BEGIN_PROPPAGEIDS (CMyControl, 2) PROPPAGEID (CMyOlePropertyPage::guid) PROPPAGEID (CLSID_CColorPropPage) END_PROPPAGEIDS (CMyControl) |
Figure 21-7. The stock color property page.
The second possibility is that the PROPPAGEID statement you add to the property page map identifies a custom property page that you created yourself. Although the process for creating a custom property page and wiring it into the control isn't difficult, it isn't automatic either. The basic procedure is to add a new dialog resource to the project, derive a class from COlePropertyPage and associate it with the dialog resource, add the page to the property page map, edit the control's string table resource, and make a couple of manual changes to the derived property page class. I won't provide a blow-by-blow here because the Visual C++ documentation already includes one. See "ActiveX controls, adding property pages" in the online help for details.
Thanks to ClassWizard, adding a custom event to an ActiveX control built with MFC is no more difficult than adding a method or a property. Here's how you add a custom event:
Figure 21-8. ClassWizard's ActiveX Events page.
Figure 21-9. The Add Event dialog box.
For each custom event that you add to a control, ClassWizard adds a member function to the control class that you can use to fire events of that type. By default, the function name is Fire followed by the event name, but you can enter any name you like in the Add Event dialog box. These custom event-firing functions do little more than call COleControl::FireEvent, which uses a form of COleDispatchDriver::InvokeHelper to call Automation methods on the container's IDispatch pointer.
Adding a stock event is as simple as selecting an event name from the list attached to the Add Event dialog box's External Name box. The following table lists the stock events you can choose from, their dispatch IDs, and the COleControl member functions used to fire them.
Stock Events Implemented by COleControl
Event Name | Dispatch ID | Fire with |
---|---|---|
Click | DISPID_CLICK | FireClick |
DblClick | DISPID_DBLCLICK | FireDblClick |
Error | DISPID_ERROREVENT | FireError |
KeyDown | DISPID_KEYDOWN | FireKeyDown |
KeyPress | DISPID_KEYPRESS | FireKeyPress |
KeyUp | DISPID_KEYUP | FireKeyUp |
MouseDown | DISPID_MOUSEDOWN | FireMouseDown |
MouseMove | DISPID_MOUSEMOVE | FireMouseMove |
MouseUp | DISPID_MOUSEUP | FireMouseUp |
ReadyStateChange | DISPID_READYSTATECHANGE | FireReadyStateChange |
The Fire functions in this table are inline functions that call FireEvent with the corresponding event's dispatch ID. With the exception of FireReadyStateChange and FireError, these functions are rarely used directly because when you add a Click, DblClick, KeyDown, KeyUp, KeyPress, MouseDown, MouseUp, or MouseMove event to a control, MFC automatically fires the corresponding event for you when a keyboard or mouse event occurs.
Technically speaking, a COM interface that's implemented by a control container to allow a control to fire events is known as an event interface. Event interfaces are defined just like regular interfaces in both the Interface Definition Language (IDL) and the Object Description Language (ODL), but they're marked with the special source attribute. In addition to adding Fire functions for the custom events that you add to a control, ClassWizard also declares events in the project's ODL file. In ODL, an event is simply a method that belongs to an event interface. Here's how the event interface is defined in the ODL file for a control named MyControl that fires PriceChanged events:
[ uuid(D0C70155-41AA-11D2-AC8B-006008A8274D), helpstring("Event interface for MyControl Control") ] dispinterface _DMyControlEvents { properties: // Event interface has no properties methods: [id(1)] void PriceChanged(CURRENCY price); }; // Class information for CMyControl [ uuid(D0C70156-41AA-11D2-AC8B-006008A8274D), helpstring("MyControl Control"), control ] coclass MyControl { [default] dispinterface _DMyControl; [default, source] dispinterface _DMyControlEvents; }; |
The dispinterface block defines the interface itself; coclass identifies the interfaces that the control supports. In this example, _DMyControl is the IDispatch interface through which the control's methods and properties are accessed, and _DMyControlEvents is the IDispatch interface for events. The leading underscore in the interface names is a convention COM programmers often use to denote internal interfaces. The capital D following the underscore indicates that these are dispinterfaces rather than conventional COM interfaces.
Besides adding Fire functions and modifying the control's ODL file when events are added, ClassWizard also adds one entry per event (stock or custom) to the control's event map. An event map is a table that begins with BEGIN_EVENT_MAP and ends with END_EVENT_MAP. Statements in between describe to MFC what events the control is capable of firing and what functions are called to fire them. An EVENT_CUSTOM macro declares a custom event, and EVENT_STOCK macros declare stock events. The following event map declares a custom event named PriceChanged and the stock event Click:
BEGIN_EVENT_MAP(CMyControlCtrl, COleControl) EVENT_CUSTOM("PriceChanged", FirePriceChanged, VTS_CY) EVENT_STOCK_CLICK() END_EVENT_MAP() |
MFC uses event maps to determine whether to fire stock events at certain junctures in a control's lifetime. For example, COleControl's WM_LBUTTONUP handler fires a Click event if the event map contains an EVENT_STOCK_CLICK entry. MFC currently doesn't use the EVENT_CUSTOM entries found in a control's event map.
Now that you understand the basics of the ActiveX control architecture and MFC's support for the same, it's time to write an ActiveX control. The control that you'll build is the calendar control featured in Figure 21-1. It supports the following methods, properties, and events:
Name | Description |
---|---|
Methods | |
GetDate | Returns the calendar's current date |
SetDate | Sets the calendar's current date |
Properties | |
BackColor | Controls the calendar's background color |
RedSundays | Determines whether Sundays are highlighted in red |
Events | |
NewDay | Fired when a new date is selected |
Because Calendar is a full-blown ActiveX control, it can be used in Web pages and in applications written in ActiveX-aware languages such as Visual Basic and Visual C++. Following is a step-by-step account of how to build it.
CTime time = CTime::GetCurrentTime (); m_nYear = time.GetYear (); m_nMonth = time.GetMonth (); m_nDay = time.GetDay (); |
static const int m_nDaysPerMonth[]; |
Then add these lines to CalendarCtrl.cpp to initialize the m_nDaysPerMonth array with the number of days in each month:
const int CCalendarCtrl::m_nDaysPerMonth[] = { 31, // January 28, // February 31, // March 30, // April 31, // May 30, // June 31, // July 31, // August 30, // September 31, // October 30, // November 31, // December }; |
BOOL CCalendarCtrl::LeapYear(int nYear) { return (nYear % 4 == 0) ^ (nYear % 400 == 0) ^ (nYear % 100 == 0); } |
This function returns a nonzero value if nYear is a leap year, or 0 if it isn't. The rule is that nYear is a leap year if it's evenly divisible by 4, unless it's divisible by 100 but not by 400.
Figure 21-10. Adding the BackColor property.
BEGIN_PROPPAGEIDS (CCalendarCtrl, 2) PROPPAGEID (CCalendarCtrl::guid) PROPPAGEID (CLSID_CColorPropPage) END_PROPPAGEIDS (CCalendarCtrl) |
InvalidateControl (); |
Figure 21-11. Adding the RedSundays property.
PX_Bool (pPX, _T ("RedSundays"), m_bRedSundays, TRUE); |
Figure 21-12. The modified property page.
Figure 21-13. Associating the check box with RedSundays.
Figure 21-14. Adding the GetDate method.
Figure 21-15. Adding the SetDate method.
Figure 21-16. Adding the NewDay event.
Figure 21-17. The calendar control's toolbar button bitmap.
With that, you've just built your first ActiveX control. It probably didn't seem very complicated, but rest assured that's only because of the thousands of lines of code MFC supplied to implement all those COM interfaces. Selected portions of the finished source code appear in Figure 21-18.
Figure 21-18. The calendar control's source code.
CalendarCtl.h#if !defined( AFX_CALENDARCTL_H__68932D29_CFE2_11D2_9282_00C04F8ECF0C__INCLUDED_) #define AFX_CALENDARCTL_H__68932D29_CFE2_11D2_9282_00C04F8ECF0C__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 // CalendarCtl.h : Declaration of the CCalendarCtrl ActiveX Control class. /////////////////////////////////////////////////////////////////////////// // CCalendarCtrl : See CalendarCtl.cpp for implementation. class CCalendarCtrl : public COleControl { DECLARE_DYNCREATE(CCalendarCtrl) // Constructor public: CCalendarCtrl(); // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CCalendarCtrl) public: virtual void OnDraw(CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid); virtual void DoPropExchange(CPropExchange* pPX); virtual void OnResetState(); //}}AFX_VIRTUAL // Implementation protected: BOOL LeapYear(int nYear); static const int m_nDaysPerMonth[]; int m_nDay; int m_nMonth; int m_nYear; ~CCalendarCtrl(); DECLARE_OLECREATE_EX(CCalendarCtrl) // Class factory and guid DECLARE_OLETYPELIB(CCalendarCtrl) // GetTypeInfo DECLARE_PROPPAGEIDS(CCalendarCtrl) // Property page IDs DECLARE_OLECTLTYPE(CCalendarCtrl) // Type name and misc status // Message maps //{{AFX_MSG(CCalendarCtrl) afx_msg void OnLButtonDown(UINT nFlags, CPoint point); //}}AFX_MSG DECLARE_MESSAGE_MAP() // Dispatch maps //{{AFX_DISPATCH(CCalendarCtrl) BOOL m_bRedSundays; afx_msg void OnRedSundaysChanged(); afx_msg DATE GetDate(); afx_msg BOOL SetDate(short nYear, short nMonth, short nDay); //}}AFX_DISPATCH DECLARE_DISPATCH_MAP() afx_msg void AboutBox(); // Event maps //{{AFX_EVENT(CCalendarCtrl) void FireNewDay(short nDay) {FireEvent(eventidNewDay,EVENT_PARAM(VTS_I2), nDay);} //}}AFX_EVENT DECLARE_EVENT_MAP() // Dispatch and event IDs public: enum { //{{AFX_DISP_ID(CCalendarCtrl) dispidRedSundays = 1L, dispidGetDate = 2L, dispidSetDate = 3L, eventidNewDay = 1L, //}}AFX_DISP_ID }; }; //{{AFX_INSERT_LOCATION}} // Microsoft Visual C++ will insert additional declarations // immediately before the previous line. #endif // !defined( // AFX_CALENDARCTL_H__68932D29_CFE2_11D2_9282_00C04F8ECF0C__INCLUDED) |
CalendarCtl.cpp// CalendarCtl.cpp : Implementation of the // CCalendarCtrl ActiveX Control class. #include "stdafx.h" #include "Calendar.h" #include "CalendarCtl.h" #include "CalendarPpg.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif IMPLEMENT_DYNCREATE(CCalendarCtrl, COleControl) const int CCalendarCtrl::m_nDaysPerMonth[] = { 31, // January 28, // February 31, // March 30, // April 31, // May 30, // June 31, // July 31, // August 30, // September 31, // October 30, // November 31, // December }; /////////////////////////////////////////////////////////////////////////// // Message map BEGIN_MESSAGE_MAP(CCalendarCtrl, COleControl) //{{AFX_MSG_MAP(CCalendarCtrl) ON_WM_LBUTTONDOWN() //}}AFX_MSG_MAP ON_OLEVERB(AFX_IDS_VERB_PROPERTIES, OnProperties) END_MESSAGE_MAP() /////////////////////////////////////////////////////////////////////////// // Dispatch map BEGIN_DISPATCH_MAP(CCalendarCtrl, COleControl) //{{AFX_DISPATCH_MAP(CCalendarCtrl) DISP_PROPERTY_NOTIFY(CCalendarCtrl, "RedSundays", m_bRedSundays, OnRedSundaysChanged, VT_BOOL) DISP_FUNCTION(CCalendarCtrl, "GetDate", GetDate, VT_DATE, VTS_NONE) DISP_FUNCTION(CCalendarCtrl, "SetDate", SetDate, VT_BOOL, VTS_I2 VTS_I2 VTS_I2) DISP_STOCKPROP_BACKCOLOR() //}}AFX_DISPATCH_MAP DISP_FUNCTION_ID(CCalendarCtrl, "AboutBox", DISPID_ABOUTBOX, AboutBox, VT_EMPTY, VTS_NONE) END_DISPATCH_MAP() /////////////////////////////////////////////////////////////////////////// // Event map BEGIN_EVENT_MAP(CCalendarCtrl, COleControl) //{{AFX_EVENT_MAP(CCalendarCtrl) EVENT_CUSTOM("NewDay", FireNewDay, VTS_I2) //}}AFX_EVENT_MAP END_EVENT_MAP() /////////////////////////////////////////////////////////////////////////// // Property pages // TODO: Add more property pages as needed. // Remember to increase the count! BEGIN_PROPPAGEIDS(CCalendarCtrl, 2) PROPPAGEID(CCalendarPropPage::guid) PROPPAGEID (CLSID_CColorPropPage) END_PROPPAGEIDS(CCalendarCtrl) /////////////////////////////////////////////////////////////////////////// // Initialize class factory and guid IMPLEMENT_OLECREATE_EX(CCalendarCtrl, "CALENDAR.CalendarCtrl.1", 0xed780d6b, 0xcc9f, 0x11d2, 0x92, 0x82, 0, 0xc0, 0x4f, 0x8e, 0xcf, 0xc) /////////////////////////////////////////////////////////////////////////// // Type library ID and version IMPLEMENT_OLETYPELIB(CCalendarCtrl, _tlid, _wVerMajor, _wVerMinor) /////////////////////////////////////////////////////////////////////////// // Interface IDs const IID BASED_CODE IID_DCalendar = { 0x68932d1a, 0xcfe2, 0x11d2, { 0x92, 0x82, 0, 0xc0, 0x4f, 0x8e, 0xcf, 0xc } }; const IID BASED_CODE IID_DCalendarEvents = { 0x68932d1b, 0xcfe2, 0x11d2, { 0x92, 0x82, 0, 0xc0, 0x4f, 0x8e, 0xcf, 0xc } }; /////////////////////////////////////////////////////////////////////////// // Control type information static const DWORD BASED_CODE _dwCalendarOleMisc = OLEMISC_ACTIVATEWHENVISIBLE œ OLEMISC_SETCLIENTSITEFIRST œ OLEMISC_INSIDEOUT œ OLEMISC_CANTLINKINSIDE œ OLEMISC_RECOMPOSEONRESIZE; IMPLEMENT_OLECTLTYPE(CCalendarCtrl, IDS_CALENDAR, _dwCalendarOleMisc) /////////////////////////////////////////////////////////////////////////// // CCalendarCtrl::CCalendarCtrlFactory::UpdateRegistry - // Adds or removes system registry entries for CCalendarCtrl BOOL CCalendarCtrl::CCalendarCtrlFactory::UpdateRegistry(BOOL bRegister) { // TODO: Verify that your control follows apartment-model // threading rules. Refer to MFC TechNote 64 for more information. // If your control does not conform to the apartment-model rules, then // you must modify the code below, changing the 6th parameter from // afxRegApartmentThreading to 0. if (bRegister) return AfxOleRegisterControlClass( AfxGetInstanceHandle(), m_clsid, m_lpszProgID, IDS_CALENDAR, IDB_CALENDAR, afxRegApartmentThreading, _dwCalendarOleMisc, _tlid, _wVerMajor, _wVerMinor); else return AfxOleUnregisterClass(m_clsid, m_lpszProgID); } /////////////////////////////////////////////////////////////////////////// // CCalendarCtrl::CCalendarCtrl - Constructor CCalendarCtrl::CCalendarCtrl() { InitializeIIDs(&IID_DCalendar, &IID_DCalendarEvents); CTime time = CTime::GetCurrentTime (); m_nYear = time.GetYear (); m_nMonth = time.GetMonth (); m_nDay = time.GetDay (); } /////////////////////////////////////////////////////////////////////////// // CCalendarCtrl::~CCalendarCtrl - Destructor CCalendarCtrl::~CCalendarCtrl() { // TODO: Cleanup your control's instance data here. } /////////////////////////////////////////////////////////////////////////// // CCalendarCtrl::OnDraw - Drawing function void CCalendarCtrl::OnDraw( CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid) { // // Paint the control's background. // CBrush brush (TranslateColor (GetBackColor ())); pdc->FillRect (rcBounds, &brush); // // Compute the number of days in the month, which day of the week // the first of the month falls on, and other information needed to // draw the calendar. // int nNumberOfDays = m_nDaysPerMonth[m_nMonth - 1]; if (m_nMonth == 2 && LeapYear (m_nYear)) nNumberOfDays++; CTime time (m_nYear, m_nMonth, 1, 12, 0, 0); int nFirstDayOfMonth = time.GetDayOfWeek (); int nNumberOfRows = (nNumberOfDays + nFirstDayOfMonth + 5) / 7; int nCellWidth = rcBounds.Width () / 7; int nCellHeight = rcBounds.Height () / nNumberOfRows; int cx = rcBounds.left; int cy = rcBounds.top; // // Draw the calendar rectangle. // CPen* pOldPen = (CPen*) pdc->SelectStockObject (BLACK_PEN); CBrush* pOldBrush = (CBrush*) pdc->SelectStockObject (NULL_BRUSH); pdc->Rectangle (rcBounds.left, rcBounds.top, rcBounds.left + (7 * nCellWidth), rcBounds.top + (nNumberOfRows * nCellHeight)); // // Draw rectangles representing the days of the month. // CFont font; font.CreatePointFont (80, _T ("MS Sans Serif")); CFont* pOldFont = pdc->SelectObject (&font); COLORREF clrOldTextColor = pdc->SetTextColor (RGB (0, 0, 0)); int nOldBkMode = pdc->SetBkMode (TRANSPARENT); for (int i=0; i<nNumberOfDays; i++) { int nGridIndex = i + nFirstDayOfMonth - 1; int x = ((nGridIndex % 7) * nCellWidth) + cx; int y = ((nGridIndex / 7) * nCellHeight) + cy; CRect rect (x, y, x + nCellWidth, y + nCellHeight); if (i != m_nDay - 1) { pdc->Draw3dRect (rect, RGB (255, 255, 255), RGB (128, 128, 128)); pdc->SetTextColor (RGB (0, 0, 0)); } else { pdc->SelectStockObject (NULL_PEN); pdc->SelectStockObject (GRAY_BRUSH); pdc->Rectangle (rect); pdc->Draw3dRect (rect, RGB (128, 128, 128), RGB (255, 255, 255)); pdc->SetTextColor (RGB (255, 255, 255)); } CString string; string.Format (_T ("%d"), i + 1); rect.DeflateRect (nCellWidth / 8, nCellHeight / 8); if (m_bRedSundays && nGridIndex % 7 == 0) pdc->SetTextColor (RGB (255, 0, 0)); pdc->DrawText (string, rect, DT_SINGLELINE œ DT_LEFT œ DT_TOP); } // // Clean up and exit. // pdc->SetBkMode (nOldBkMode); pdc->SetTextColor (clrOldTextColor); pdc->SelectObject (pOldFont); pdc->SelectObject (pOldBrush); pdc->SelectObject (pOldPen); } /////////////////////////////////////////////////////////////////////////// // CCalendarCtrl::DoPropExchange - Persistence support void CCalendarCtrl::DoPropExchange(CPropExchange* pPX) { ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor)); COleControl::DoPropExchange(pPX); PX_Bool (pPX, _T ("RedSundays"), m_bRedSundays, TRUE); } /////////////////////////////////////////////////////////////////////////// // CCalendarCtrl::OnResetState - Reset control to default state void CCalendarCtrl::OnResetState() { COleControl::OnResetState(); // Resets defaults found in DoPropExchange // TODO: Reset any other control state here. } /////////////////////////////////////////////////////////////////////////// // CCalendarCtrl::AboutBox - Display an "About" box to the user void CCalendarCtrl::AboutBox() { CDialog dlgAbout(IDD_ABOUTBOX_CALENDAR); dlgAbout.DoModal(); } /////////////////////////////////////////////////////////////////////////// // CCalendarCtrl message handlers BOOL CCalendarCtrl::LeapYear(int nYear) { return (nYear % 4 == 0) ^ (nYear % 400 == 0) ^ (nYear % 100 == 0); } void CCalendarCtrl::OnRedSundaysChanged() { InvalidateControl (); SetModifiedFlag(); } DATE CCalendarCtrl::GetDate() { COleDateTime date (m_nYear, m_nMonth, m_nDay, 12, 0, 0); return (DATE) date; } BOOL CCalendarCtrl::SetDate(short nYear, short nMonth, short nDay) { // // Make sure the input date is valid. // if (nYear < 1970 œœ nYear > 2037) return FALSE; if (nMonth < 1 œœ nMonth > 12) return FALSE; int nNumberOfDays = m_nDaysPerMonth[m_nMonth - 1]; if (nMonth == 2 && LeapYear (nYear)) nNumberOfDays++; if (nDay < 1 œœ nDay > nNumberOfDays) return FALSE; // // Update the date, repaint the control, and fire a NewDay event. // m_nYear = nYear; m_nMonth = nMonth; m_nDay = nDay; InvalidateControl (); return TRUE; } void CCalendarCtrl::OnLButtonDown(UINT nFlags, CPoint point) { int nNumberOfDays = m_nDaysPerMonth[m_nMonth - 1]; if (m_nMonth == 2 && LeapYear (m_nYear)) nNumberOfDays++; CTime time (m_nYear, m_nMonth, 1, 12, 0, 0); int nFirstDayOfMonth = time.GetDayOfWeek (); int nNumberOfRows = (nNumberOfDays + nFirstDayOfMonth + 5) / 7; CRect rcClient; GetClientRect (&rcClient); int nCellWidth = rcClient.Width () / 7; int nCellHeight = rcClient.Height () / nNumberOfRows; for (int i=0; i<nNumberOfDays; i++) { int nGridIndex = i + nFirstDayOfMonth - 1; int x = rcClient.left + (nGridIndex % 7) * nCellWidth; int y = rcClient.top + (nGridIndex / 7) * nCellHeight; CRect rect (x, y, x + nCellWidth, y + nCellHeight); if (rect.PtInRect (point)) { m_nDay = i + 1; FireNewDay (m_nDay); InvalidateControl (); } } COleControl::OnLButtonDown(nFlags, point); } |
Now that you've built the control, you'll want to test it, too. Visual C++ comes with the perfect tool for testing ActiveX controls: the ActiveX Control Test Container. You can start it from Visual C++'s Tools menu or by launching Tstcon32.exe. Once the ActiveX Control Test Container is running, go to its Edit menu, select the Insert New Control command, and select Calendar Control from the Insert Control dialog box to insert your control into the test container, as shown in Figure 21-19.
Figure 21-19. The ActiveX Control Test Container.
Initially, the control's background will probably be white because MFC's implementation of the stock property BackColor defaults to the container's ambient background color. This presents a wonderful opportunity to test the BackColor property you added to the control. With the control selected in the test container window, select Properties from the Edit menu. The control's property sheet will be displayed. (See Figure 21-20.) Go to the Colors page, and select light gray as the background color. Then click Apply. The control should turn light gray. Go back to the property sheet's General page and toggle Show Sundays In Red on and off a time or two. The control should repaint itself each time you click the Apply button. Remember the OnRedSundaysChanged notification function in which you inserted a call to InvalidateControl? It's that call that causes the control to update when the property value changes.
Figure 21-20. The calendar control's property sheet.
You can test a control's methods in the ActiveX Control Test Container, too. To try it, select the Invoke Methods command from the Control menu. The Invoke Methods dialog box, which is pictured in Figure 21-21, knows which methods the control implements because it read the control's type information. (That type information was generated from the control's ODL file and linked into the control's OCX as a binary resource.) To call a method, select the method by name in the Method Name box, enter parameter values (if applicable) in the Parameters box, and click the Invoke button. The method's return value will appear in the Return Value box. Incidentally, properties show up in the Invoke Methods dialog box with PropGet and PropPut labels attached to them. A PropGet method reads a property value, and a PropPut method writes it.
Figure 21-21. The ActiveX Control Test Container's Invoke Methods dialog box.
The ActiveX Control Test Container also lets you test a control's events. To demonstrate, choose the Logging command from the Options menu and make sure Log To Output Window is selected. Then click a few dates in the calendar. A NewDay event should appear in the output window with each click, as in Figure 21-22. The event is fired because you included a call to FireNewDay in the control's OnLButtonDown function.
Figure 21-22. Events are reported in the ActiveX Control Test Container's output window.
If your control uses any of the container's ambient properties, you can customize those properties to see how the control reacts. To change an ambient property, use the Ambient Properties command in the Container menu.
What happens if your control doesn't behave as expected and you need to debug it? Fortunately, you can do that, too. Suppose you want to set a breakpoint in your code, see it hit, and single-step through the code. It's easy. Just open the control project in Visual C++ and set the breakpoint. Then go to the Build menu and select Start Debug-Go. When Visual C++ asks you for an executable file name, click the arrow next to the edit control and select ActiveX Control Test Container. Insert the control into the container and do something to cause the breakpoint to be hit. That should pop you into the Visual C++ debugger with the arrow on the instruction at the breakpoint. The same debugging facilities that Visual C++ places at your disposal for debugging regular MFC applications are available for debugging controls, too.
Like any COM object, an ActiveX control can't be used unless it is registered on the host system. Registering an ActiveX control means adding entries to the registry identifying the control's CLSID, the DLL that houses the control, and other information. When you build an ActiveX control with Visual C++, the control is automatically registered as part of the build process. If you give the control to another user, that user will need to register it on his or her system before it can be used. Here are two ways to register a control on another system.
The first way is to provide a setup program that registers the control programmatically. Because an OCX is a self-registering in-proc COM server, the setup program can load the OCX as if it were an ordinary DLL, find the address of its DllRegisterServer function, and call the function. DllRegisterServer, in turn, will register any and all of the controls in the OCX. The following code demonstrates how this is done if the OCX is named Calendar.ocx:
HINSTANCE hOcx = ::LoadLibrary (_T ("Calendar.ocx")); if (hOcx != NULL) { FARPROC lpfn = ::GetProcAddress (hOcx, _T ("DllRegisterServer")); if (lpfn != NULL) (*lpfn) (); // Register the control(s). ::FreeLibrary (hOcx); } |
To implement an uninstall feature, use the same code but change the second parameter passed to ::GetProcAddress from "DllRegisterServer" to "DllUnregisterServer."
To register an ActiveX control on someone else's system without writing a setup program, use the Regsvr32 utility that comes with Visual C++. If Calendar.ocx is in the current directory, typing the following command into a command prompt window will register the OCX's controls:
Regsvr32 Calendar.ocx |
By the same token, passing a /U switch to Regsvr32 unregisters the controls in an OCX:
Regsvr32 /U Calendar.ocx |
Regsvr32 isn't a tool you should foist on end users, but it's a handy utility to have when testing and debugging a control prior to deployment.