Programming Windows with MFC, Second Edition
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.
Running ControlWizard
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.
- Activates When Visible—Sets a flag in the control's miscellaneous status bits informing the container that the control wants to be active whenever it's visible. Disabling this option gives the container the option of disabling the control, which it might do to conserve resources or speed start-up time. If you uncheck this box, you should check the Mouse Pointer Notifications When Inactive box described below if your control processes WM_MOUSEMOVE or WM_SETCURSOR messages.
- Invisible At Runtime—Sets a flag in the control's miscellaneous status bits indicating that the control wants to be visible in design mode but invisible in user mode. In other words, the control should be visible in a design-time environment such as the Visual C++ dialog editor, but invisible when the application that uses the control is running. One example of a control that might choose to exercise this option is a timer control that fires events at specified intervals. The control doesn't need to be seen at run time, but it should be visible at design time so that the user can display its property sheet.
- Available In "Insert Object" Dialog—Because most ActiveX controls implement a functional superset of the interfaces required to act as object linking and embedding servers, most of them can, if asked, masquerade as object linking and embedding servers. When this option is selected, the control gets registered not only as an ActiveX control but also as an OLE server, which causes it to appear in the Insert Object dialog box found in Microsoft Word, Microsoft Excel, and other OLE containers. Checking this box is generally a bad idea because most OLE containers don't know how to interact with ActiveX controls. Except in isolated cases, the best strategy is to forget that this option even exists.
- Has An "About" Box—If checked, adds a method named AboutBox to the control that displays an About dialog box. Select this option if you'd like developers using your control to be able to learn more about it and its creator from an About box. ControlWizard creates a simple dialog resource for you; it's up to you to add a professional touch.
- Acts As A Simple Frame Control—Tells ControlWizard to add an ISimpleFrameSite interface to the control, and sets a flag in the miscellaneous status bits identifying this as a "simple frame" control. A simple frame control is one that hosts other controls but delegates much of the work to its own control container. Use this option for controls, such as group box controls, whose primary purpose is to provide a site for (and visual grouping of) other controls.
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:- Windowless Activation—Makes the control a windowless control. If the container doesn't support windowless activation, the control will be instantiated in a window as if it were a regular windowed control. Windowless controls are discussed at length later in this chapter.
- Unclipped Device Context—According to the documentation, this option, if selected, speeds redraws ever so slightly by preventing COleControl::OnPaint from calling CDC::IntersectClipRect on the device context passed to the control to prevent the control from inadvertently painting outside its own window. Despite what the documentation says, this option has no effect whatsoever on the control's behavior in MFC 6.0.
- Flicker-Free Activation—Most ActiveX controls are activated when they're created and remain active indefinitely. If a container deactivates an active control, however, the container repaints the control. And if an inactive control becomes active, the control repaints itself. For controls that look the same whether active or inactive, this repainting is unnecessary and can cause unsightly flicker. The flicker-free activation option eliminates redrawing induced by state transitions in containers that support it.
- Mouse Pointer Notifications When Inactive—Allows containers to forward WM_SETCURSOR and WM_MOUSEMOVE messages to inactive controls via an MFC-provided implementation of IPointerInactive. This option is typically used with controls that don't use the Activates When Visible option but want to alter the appearance of the mouse cursor or respond to mouse movements even while inactive.
- Optimized Drawing Code—When IViewObjectEx::Draw is called to draw a windowless control, the control is responsible for leaving the device context in the same state in which it found it. Some containers free the control from this obligation, in which case the control can speed repainting by reducing the number of GDI calls. To take advantage of this feature in supportive containers, select this option and call COleControl::IsOptimizedDraw each time OnDraw is called. If IsOptimizedDraw returns nonzero, there's no need to clean up the device context.
- Loads Properties Asynchronously—Indicates that this control supports datapath properties. Unlike standard control properties, datapath properties are downloaded asynchronously, typically from a URL. For controls designed to sit in Web pages, implementing properties that encapsulate large volumes of data as datapath properties can improve performance dramatically. MFC makes implementing datapath properties relatively easy, but (in my opinion, anyway) controls designed for the Internet should be written with the Active Template Library, not with MFC. For more information about implementing datapath properties in MFC, see the article "Internet First Steps: ActiveX Controls" in the online documentation.
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:
- A COleControlModule-derived class representing the control's OCX.
- A COleControl-derived class representing the control. ControlWizard overrides OnDraw, DoPropExchange, and other virtual functions in the derived class, so you don't have to. The control class also includes essential infrastructure such as a COM class factory and dispinterfaces for methods, properties, and events.
- A COlePropertyPage-derived class and a dialog resource representing the control's property page.
- An ODL file that ClassWizard will later modify as methods, properties, and events are added and from which the control's type library will be generated.
- A toolbar button bitmap that will represent the control on toolbars in design-time environments such as Visual Basic.
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.
Implementing OnDraw
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:
- A windowed control receives a WM_PAINT message.
- IViewObjectEx::Draw is called on an inactive control (or one that's about to become inactive) to retrieve a metafile for the control container. If you'd like to draw the control differently when it's inactive, override COleControl::OnDrawMetafile. The default implementation calls OnDraw.
- IViewObjectEx::Draw is called on a windowless control to ask it to paint itself into the container's window.
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:
- Assume nothing about the state of the device context passed in OnDraw's parameter list. You shouldn't assume, for example, that a black pen or a white brush is selected in. Prepare the device context as if its initial attributes were all wrong.
- Leave the device context in the same state you found it in, which means not only selecting out the GDI objects you selected in, but also preserving the drawing mode, text color, and other attributes of the device context. As an alternative, you can check the Optimized Drawing Code box in ControlWizard to advertise the control's intent not to preserve the state of the device context. But because many containers don't support this option, you must call COleControl::IsOptimizedDraw inside OnDraw to find out whether it's OK.
- Limit your drawing to the rectangular area described by the rcBounds parameter included in OnDraw's parameter list. For a windowed control, rcBounds' upper left corner will be (0,0). For a windowless control, these coordinates can be nonzero because they describe an area inside the container's window.
- Begin OnDraw by erasing the control's background—the rectangle described by rcBounds. This is typically accomplished by creating a brush of the desired color and calling CDC::FillRect. If the control is windowless, you can effect a transparent background by skipping this step.
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.
Using Ambient Properties
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 Methods
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 Properties
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.
Making Properties Persistent
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.
Customizing a Control's Property Sheet
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.
Adding Pages to a Control's Property Sheet
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.
Adding Events
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:
- Invoke ClassWizard, and go to the ActiveX Events page. (See Figure 21-8.)
- Click the Add Event button.
- In the Add Event dialog box (shown in Figure 21-9), enter the event's name (External Name), the name of the member function that you'd like to call to fire the event (Internal Name), and, optionally, the arguments that accompany the event. Because an event is an Automation method implemented by a container, events can have parameter lists.
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.
Event Maps
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.
Building an ActiveX Control
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.
- Create a new MFC ActiveX ControlWizard project named Calendar. Accept the default options in ControlWizard's Step 1 and Step 2 dialog boxes.
- Add three int member variables named m_nYear, m_nMonth, and m_nDay to CCalendarCtrl. CCalendarCtrl is the class that represents the control. The member variables that you added will store the control's current date.
- Add the following code to CCalendarCtrl's constructor to initialize the member variables:
- Add the following variable declaration to the CCalendarCtrl in CalendarCtrl.h:
- Add the following protected member function to CCalendarCtrl:
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.
- Add a BackColor property to the control by clicking the Add Property button on ClassWizard's Automation page and selecting BackColor from the External Name list in the Add Property dialog box. (See Figure 21-10.)
- Modify the property page map in CalendarCtrl.cpp as shown below to add a stock color page to the control's property sheet. Users will use this property page to customize the control's background color:
- Fill in the Add Property dialog box as shown in Figure 21-11 to add a custom member variable property named RedSundays. In response, ClassWizard will add a member variable named m_redSundays (which you can then change to m_bRedSundays) and a notification function named OnRedSundaysChanged to the control class. Follow up by adding the following statement to the notification function so that the control will automatically repaint when the property value changes:
- Add the following statement to CCalendarCtrl::DoPropExchange to make RedSundays persistent and to assign it a default value equal to TRUE:
- Switch to ResourceView, and add a checkbox control to the dialog resource whose ID is IDD_PROPPAGE_CALENDAR. (See Figure 21-12.) This is the resource that represents the control's property page. Assign the check box the ID IDC_REDSUNDAYS and the text "Show Sundays in &red."
- On ClassWizard's Member Variables page, select the property page's class name ( CCalendarPropPage) in the Class Name box, click the Add Variable button, and fill in the Add Member Variable dialog box as shown in Figure 21-13. This will connect the check box control to the property named RedSundays.
- Implement the control's OnDraw function. See the CalendarCtrl.cpp listing in Figure 21-18 for the finished code. Notice that OnDraw uses GetBackColor to retrieve the value of the BackColor property and then uses that value to paint the control's background. Also notice that it checks the value of m_bRedSundays and sets the text color to red before drawing a date corresponding to a Sunday if m_bRedSundays is nonzero. This explains how the two properties that you added affect the control's appearance.
- Add methods named GetDate and SetDate. To add a method, click the Add Method button on ClassWizard's Automation page. Pick DATE as GetDate's return type (as in Figure 21-14) and BOOL as SetDate's return type. Include three parameters in SetDate's parameter list: a short named nYear, a short named nMonth, and a short named nDay (as in Figure 21-15). See Figure 21-18 for the method implementations.
- Add a NewDay event to the control by clicking the Add Event button on ClassWizard's ActiveX Events page and filling in the Add Event dialog box as shown in Figure 21-16.
- Add a WM_LBUTTONDOWN handler to the control class that sets the current date to the date that was clicked on the calendar. You add a message handler to a control the same way you add a message handler to a conventional MFC application. Refer to Figure 21-18 for the implementation of OnLButtonDown. Notice the call to FireNewDay near the end of the function.
- In ResourceView, customize the control's toolbar button bitmap to look like the one shown in Figure 21-17. You'll find the button bitmap under the project's list of bitmap resources. The bitmap's resource ID is IDB_CALENDAR.
- Build the control.
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 // 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 DECLARE_MESSAGE_MAP() // Dispatch maps //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 }; }; // // 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 ON_OLEVERB(AFX_IDS_VERB_PROPERTIES, OnProperties) END_MESSAGE_MAP() /////////////////////////////////////////////////////////////////////////// // Dispatch map BEGIN_DISPATCH_MAP(CCalendarCtrl, COleControl) //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 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); } |
Testing and Debugging an ActiveX Control
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.
Registering an ActiveX Control
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.