您正在查看: MFC 分类下的文章

Building ActiveX Controls

Building ActiveX Controls

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.)
  • Figure 21-8. ClassWizard's ActiveX Events page.

  • 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-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_ERROREVENTFireError
    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
    RedSundaysDetermines 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:
  • CTime time = CTime::GetCurrentTime ();
    m_nYear = time.GetYear ();
    m_nMonth = time.GetMonth ();
    m_nDay = time.GetDay ();
    

  • Add the following variable declaration to the CCalendarCtrl in CalendarCtrl.h:
  • 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
    };
    

  • Add the following protected member function to CCalendarCtrl:
  • 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.)
  • Figure 21-10. Adding the BackColor property.

  • 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:
  • BEGIN_PROPPAGEIDS (CCalendarCtrl, 2)
     PROPPAGEID (CCalendarCtrl::guid)
     PROPPAGEID (CLSID_CColorPropPage)
    END_PROPPAGEIDS (CCalendarCtrl)
    

  • 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:
  • InvalidateControl ();
    

    Figure 21-11. Adding the RedSundays property.

  • Add the following statement to CCalendarCtrl::DoPropExchange to make RedSundays persistent and to assign it a default value equal to TRUE:
  • PX_Bool (pPX, _T ("RedSundays"), m_bRedSundays, 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."
  • Figure 21-12. The modified property page.

  • 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.
  • Figure 21-13. Associating the check box with 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.
  • Figure 21-14. Adding the GetDate method.

    Figure 21-15. Adding the SetDate method.

  • 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.
  • Figure 21-16. Adding the NewDay event.

  • 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.
  • Figure 21-17. The calendar control's toolbar button bitmap.

  • Build the control.
  • 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);
    elsereturn 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 I

    Using ActiveX Controls in MFC Applications

    Using ActiveX Controls in mfc Applications

    Now you know how to write ActiveX controls. But what about control containers? Not just any window can host an ActiveX control; to do it, someone must implement the requisite COM interfaces. Fortunately, mfc will provide those interfaces for you. All you have to do is check a box in AppWizard and insert the control into the project. The control will then appear in the dialog editor's control toolbar, where it can be inserted into any MFC dialog.

    Here are the steps required to use an ActiveX control in an MFC application:

  • In AppWizard's Step 2 dialog box (for dialog-based applications) or Step 3 dialog box (for nondialog-based applications), check the ActiveX Controls box, as shown in Figure 21-23.
  • Figure 21-23. Checking AppWizard's ActiveX Controls box makes any MFC dialog an ActiveX control container.

  • When AppWizard is done, use Visual C++'s Project-Add To Project-Components And Controls command to insert the control into the project. This command displays the Components And Controls Gallery dialog box. The Registered ActiveX Controls folder contains a list of all the ActiveX controls installed on this system. (See Figure 21-24.)
  • Figure 21-24. The Components And Controls Gallery dialog box showing a list of registered ActiveX controls.

  • When the Confirm Classes dialog box (shown in Figure 21-25) appears, either edit the class name and file names or accept the defaults. Visual C++ will create a wrapper class that the container can use to interact with the control. Member functions in the wrapper class will provide access to the control's methods and properties. Visual C++ gets the information it needs to build the wrapper class from the control's type library.
  • Figure 21-25. The Confirm Classes dialog box.

  • Close the Components And Controls Gallery dialog box.
  • If you now switch to ResourceView and open a dialog resource, the dialog editor's controls toolbar will contain a button representing the control. Adding the control to a dialog is a simple matter of clicking the button and drawing the control into the dialog. You can display the control's property sheet by right-clicking the control and selecting Properties from the context menu. Any changes you make to the control's properties will be serialized into the project's RC file and reapplied when the dialog is displayed.

    Calling an ActiveX Control's Methods

    Can it really be that easy? You bet. But that's not all. You can program the control—call its methods and read and write its properties programmatically—using the wrapper class generated when the control was added to the project. First, though, you must instantiate the wrapper class and connect it to a running control. Here's how to do it:

  • Go to ClassWizard's Member variables page, and select the ActiveX control's ID in the Control IDs box.
  • Click the Add Variable button, and choose the wrapper class's name (for example, CCalendar) in the Variable Type box. Enter a name for the instantiated class in the Member Variable Name box, too.
  • Click OK.
  • After that, you can call a control method or access a control property by calling the corresponding member function on the object whose name you entered in the Member Variable Name box. For a calendar control object named m_ctlCalendar, the following statement calls the control's SetDate method to set the date to January 1, 2000:

    m_ctlCalendar.SetDate (2000, 1, 1);
    

    The next statement sets the control's background color to light gray:

    m_ctlCalendar.SetBackColor (OLE_COLOR (RGB (192, 192, 192)));
    

    It works because ClassWizard added a DDX_Control statement to the dialog's DoDataExchange function that connects m_ctlCalendar to the running ActiveX control. You could add this statement yourself, but regardless of how you choose to do it, the fact remains that accessing the control from your program's source code is now no more difficult than calling a C++ member function.

    Processing Events

    You might want to do one more thing with an ActiveX control in an MFC application: process events. In Chapter 1, you learned that MFC uses message maps to correlate messages to member functions. Similarly, it uses event sink maps to correlate events fired by ActiveX controls to member functions. Here's a simple event sink map that connects NewDay events fired by our calendar control to a CMyDialog member function named OnNewDay:

    BEGIN_EVENTSINK_MAP (CMyDialog, CDialog)
     ON_EVENT (CMyDialog, IDC_CALENDAR, 1, OnNewDay, VTS_I2)
    END_EVENTSINK_MAP ()
    

    The second parameter passed to ON_EVENT is the control ID. The third is the event's dispatch ID. The fourth is the member function that's called when the event is fired, and the final parameter specifies the types of parameters included in the event's parameter list—in this case, a 16-bit integer (VTS_I2). An event sink map can hold any number of ON_EVENT entries, making it a simple matter for an MFC container to respond to all manner of control events.

    How do you write event sink maps? You don't have to write them by hand because ClassWizard will write them for you. To write an event handler, go to ClassWizard's Message Maps page, select the class that hosts the control in the Class Name box, and select the control's ID in the Object IDs box. You'll see a list of events that the control is capable of firing in the Messages box. (See Figure 21-26.) Select an event, and click the Add Function button. Enter a name for the member function you want to be called when the event is fired, and ClassWizard will add the member function to the class and an entry to the event sink map. When an event of that type is fired, the member function will be called just as if it were an ordinary message handler.

    Figure 21-26. Adding an event handler with ClassWizard.

    The CalUser Application

    The CalUser application shown in Figure 21-1 is a dialog-based MFC application that hosts the MFC calendar control. Selecting a new month or year changes the calendar by calling its SetDate method. Clicking a square in the calendar pops up a message box that echoes the date that was clicked. The message box is displayed by an event handler named OnNewDay that's called each time the control fires a NewDay event. Relevant portions of CalUser's source code are reproduced in Figure 21-27.

    Figure 21-27. The CalUser application.

    CalUserDlg.h

    // CalUserDlg.h : header file
    //
    //{{AFX_INCLUDES()
    #include "calendar.h"
    //}}AFX_INCLUDES
    

    #if !defined(
    AFX_CALUSERDLG_H__85FDD589_470B_11D2_AC96_006008A8274D__INCLUDED_)
    #define AFX_CALUSERDLG_H__85FDD589_470B_11D2_AC96_006008A8274D__INCLUDED_

    #if _MSC_VER > 1000
    #pragma once
    #endif // _MSC_VER > 1000

    ///////////////////////////////////////////////////////////////////////////
    // CCalUserDlg dialog

    class CCalUserDlg : public CDialog
    {
    // Construction
    public:
    CCalUserDlg(CWnd* pParent = NULL); // standard constructor

    // Dialog Data
    //{{AFX_DATA(CCalUserDlg)
    enum { IDD = IDD_CALUSER_DIALOG };
    CComboBox m_ctlYear;
    CComboBox m_ctlMonth;
    CCalendar m_ctlCalendar;
    //}}AFX_DATA// ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(CCalUserDlg)
    protected:
    virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
    //}}AFX_VIRTUAL

    // Implementation
    protected:
    static const CString m_strMonths[];
    void InitListOfYears ();
    void InitListOfMonths ();
    HICON m_hIcon;// Generated message map functions
    //{{AFX_MSG(CCalUserDlg)
    virtual BOOL OnInitDialog();
    afx_msg void OnPaint();
    afx_msg HCURSOR OnQueryDragIcon();
    afx_msg void OnChangeDate();
    afx_msg void OnNewDay(short nDay);
    DECLARE_EVENTSINK_MAP()
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
    };

    //{{AFX_INSERT_LOCATION}}
    // Microsoft Visual C++ will insert additional declarations
    // immediately before the previous line.

    #endif
    // !defined(
    // AFX_CALUSERDLG_H__85FDD589_470B_11D2_AC96_006008A8274D__INCLUDED_)

    CalUserDlg.cpp

    // CalUserDlg.cpp : implementation file
    //
    

    #include "stdafx.h"
    #include "CalUser.h"
    #include "CalUserDlg.h"

    #ifdef _DEBUG
    #define new DEBUG_NEW
    #undef THIS_FILE
    static char THIS_FILE[] = FILE;
    #endif

    ///////////////////////////////////////////////////////////////////////////
    // CCalUserDlg dialog

    CCalUserDlg::CCalUserDlg(CWnd* pParent /=NULL/)
    : CDialog(CCalUserDlg::IDD, pParent)
    {
    //{{AFX_DATA_INIT(CCalUserDlg)
    //}}AFX_DATA_INIT
    // Note that LoadIcon does not require a subsequent// DestroyIcon in win32
    m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
    }

    void CCalUserDlg::DoDataExchange(CDataExchange* pDX)
    {
    CDialog::DoDataExchange(pDX);
    //{{AFX_DATA_MAP(CCalUserDlg)
    DDX_Control(pDX, IDC_YEAR, m_ctlYear);
    DDX_Control(pDX, IDC_MONTH, m_ctlMonth);
    DDX_Control(pDX, IDC_CALENDAR, m_ctlCalendar);
    //}}AFX_DATA_MAP
    }

    BEGIN_MESSAGE_MAP(CCalUserDlg, CDialog)
    //{{AFX_MSG_MAP(CCalUserDlg)
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    ON_CBN_SELENDOK(IDC_MONTH, OnChangeDate)
    ON_CBN_SELENDOK(IDC_YEAR, OnChangeDate)
    //}}AFX_MSG_MAP
    END_MESSAGE_MAP()

    const CString CCalUserDlg::m_strMonths[] = {
    _T ("January"),
    _T ("February"),
    _T ("March"),
    _T ("April"),
    _T ("May"),
    _T ("June"),
    _T ("July"),
    _T ("August"),
    _T ("September"),
    _T ("October"),
    _T ("November"),
    _T ("December")
    };
    ///////////////////////////////////////////////////////////////////////////
    // CCalUserDlg message handlers

    BOOL CCalUserDlg::OnInitDialog()
    {
    CDialog::OnInitDialog();SetIcon(m_hIcon, TRUE);// Set big icon
    SetIcon(m_hIcon, FALSE); // Set small icon//
    // Initialize the Month control.
    //
    COleDateTime date = m_ctlCalendar.GetDate ();
    int nMonth = date.GetMonth ();
    int nYear = date.GetYear ();InitListOfMonths ();
    m_ctlMonth.SetCurSel (nMonth - 1);//
    // Initialize the Year control.
    //
    InitListOfYears ();
    m_ctlYear.SetCurSel (nYear - 1970);return TRUE;
    }

    void CCalUserDlg::OnPaint()
    {
    if (IsIconic())
    {CPaintDC dc(this); // device context for painting SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0); // Center icon in client rectangleint cxIcon = GetSystemMetrics(SM_CXICON);int cyIcon = GetSystemMetrics(SM_CYICON);CRect rect;GetClientRect(&rect);int x = (rect.Width() - cxIcon + 1) / 2;int y = (rect.Height() - cyIcon + 1) / 2; // Draw the icondc.DrawIcon(x, y, m_hIcon);
    }
    else
    {CDialog::OnPaint();
    }
    }

    HCURSOR CCalUserDlg::OnQueryDragIcon()
    {
    return (HCURSOR) m_hIcon;
    }

    void CCalUserDlg::InitListOfMonths()
    {
    for (int i=0; i<12; i++)m_ctlMonth.AddString (m_strMonths[i]);
    }

    void CCalUserDlg::InitListOfYears()
    {
    for (int i=1970; i<=2037; i++) {CString string;string.Format (_T ("%d"), i);m_ctlYear.AddString (string);
    }
    }

    void CCalUserDlg::OnChangeDate()
    {
    int nMonth = m_ctlMonth.GetCurSel () + 1;
    int nYear = GetDlgitemInt (IDC_YEAR);
    ASSERT (nYear != 0);
    m_ctlCalendar.SetDate (nYear, nMonth, 1);
    }

    BEGIN_EVENTSINK_MAP(CCalUserDlg, CDialog)
    //{{AFX_EVENTSINK_MAP(CCalUserDlg)
    ON_EVENT(CCalUserDlg, IDC_CALENDAR, 1 /* NewDay */, OnNewDay, VTS_I2)
    //}}AFX_EVENTSINK_MAP
    END_EVENTSINK_MAP()

    void CCalUserDlg::OnNewDay(short nDay)
    {
    static const CString strDays[] = {_T ("Sunday"),_T ("Monday"),_T ("Tuesday"),_T ("Wednesday"),_T ("Thursday"),_T ("Friday"),_T ("Saturday"),
    };COleDateTime date = m_ctlCalendar.GetDate ();
    int nMonth = date.GetMonth ();
    int nYear = date.GetYear ();CTime time (nYear, nMonth, nDay, 12, 0, 0);
    int nDayOfWeek = time.GetDayOfWeek () - 1;CString string;
    string.Format (_T ("%s, %s %d, %d"), strDays[nDayOfWeek],m_strMonths[nMonth - 1], nDay, nYear);MessageBox (string);
    }

    Using ActiveX Controls in Nondialog Windows

    MFC and ClassWizard make it wonderfully easy to use ActiveX controls in dialogs, but what about nondialog windows? It turns out that MFC allows any CWnd object to host ActiveX controls. You can create ActiveX controls just about anywhere in an MFC application, but outside of dialog windows, you have to do some manual coding to make it happen.

    Here's how to add the MFC calendar control to a view:

  • Insert the control into the project. Name the wrapper class CCalendar.
  • Add a CCalendar member variable named m_ctlCalendar to the view.
  • Add the following statement to the view's OnCreate handler:
  • m_ctlCalendar.Create (NULL, WS_VISIBLE,
    CRect (0, 0, 400, 300), this, IDC_CALENDAR);
    

    When the view is created, the calendar control will be created in the view's upper left corner and assigned the control ID IDC_CALENDAR. Most of the work is done by CCalendar::Create, which calls the CreateControl function CCalendar inherits from CWnd. CWnd::CreateControl indirectly calls COleControlSite::CreateControl, which creates an ActiveX control and wires it up to its container.

    So far, so good. But what if you want the view to process control events, too? This is where it gets tricky. ClassWizard will add event handlers to dialogs, but not to nondialogs. So you code the event sink map by hand. That wouldn't be too bad if it weren't for the fact that an event's parameter list has to be coded into the ON_EVENT statement in the form of VTS flags. Some programmers get around this by doing the following:

  • Add a dummy dialog to the application.
  • Insert the ActiveX control into the dialog.
  • Use ClassWizard to write event handlers into the dialog.
  • Copy the event sink map from the dialog to the view.
  • Delete the dummy dialog.
  • I didn't say it was pretty. But it works. If you use this technique, don't forget to copy the DECLARE_EVENTSINK_MAP statement from the dialog's header file to the view's header file. DECLARE_EVENTSINK_MAP declares an event sink map just as DECLARE_MESSAGE_MAP declares a message map.

    All this assumes, of course, that you checked AppWizard's ActiveX Controls box when you created the project. If you didn't, you can add container support to the application after the fact by adding an

    AfxEnableControlContainer ();
    

    statement to InitInstance.

    Using ActiveX Controls in Web Pages

    One of the reasons ActiveX controls exist is to make Web content more interactive. An <OBJECT> tag in an html page denotes an ActiveX control. The control's methods and properties are accessible from within the html code, and events can be processed as well. The following HTML page displays this chapter's calendar control and responds to NewDay events by popping up a message box announcing which date was clicked:

    <HTML>
    <BODY>
    <OBJECT
     CLASSID="CLSID:ED780D6B-CC9F-11D2-9282-00C04F8ECF0C"
     WIDTH=400
     HEIGHT=300
     ID="Calendar"
    >
    <PARAM NAME="BackColor" VALUE=12632256>
    </OBJECT>
    </BODY>
    <SCRIPT LANGUAGE=VBScript>
    Sub Calendar_NewDay(day)
     dt = Calendar.GetDate
     yr = DatePart ("yyyy", dt)
     mon = DatePart ("m", dt)
     MsgBox (CStr (mon) + "/" + CStr (day) + "/" + CStr (yr))
    End Sub
    </SCRIPT>
    </HTML>
    

    You can try out these statements by typing them into an HTML file and opening the file with Internet Explorer. You'll have to modify the CLSID in the <OBJECT> tag if you create the control yourself because your control's CLSID will differ from mine. And remember that for the page to display properly, the control must be installed on your system. (In real life, the <OBJECT> tag would include a CODEBASE attribute and a URL telling Internet Explorer where to find the control if it's not already installed.) Notice the WIDTH and HEIGHT statements that specify the size of the control and the PARAM statement that sets the control's background color to light gray. The VBScript code in the SCRIPT block is called whenever a NewDay event is fired. It calls the control's GetDate method and displays the resultant date.

    Advanced Topics

    Advanced Topics

    No chapter can possibly cover everything there is to know about ActiveX controls. You've learned the essentials, but there are some additional issues that every control developer should be aware of. The following sections present three such issues.

    Windowless Controls

    Although ActiveX controls have existed in one form or another since 1994, 1996 was the year that they came of age. OCX 94 defined the baseline control architecture; OCX 96 introduced a number of enhancements designed to allow controls to run faster and more efficiently and consume fewer resources while doing so. One of those enhancements was the windowless control.

    OCX 94_style controls are always created with windows of their own. This is consistent with the behavior of Windows controls, which also have their own windows. That's fine when you're hosting just one or two controls, but if a container creates tens, perhaps hundreds, of ActiveX controls, giving each a window of its own introduces an appreciable amount of overhead in terms of resource requirements and instantiation time.

    It turns out that most controls don't really need windows of their own if their containers are willing to help out by performing a few small chores such as simulating the input focus and forwarding mouse messages. This interaction between the control and the container—the physical mechanisms that permit a windowless ActiveX control to borrow a portion of its container's window—was standardized in OCX 96.

    Here, in a nutshell, is how it works. A windowless control implements the COM interface IOleInPlaceObjectWindowless; a container that supports windowless controls implements IOleInPlaceSiteWindowless. When a windowless control is created, it has no window, which means it can't receive mouse or keyboard input. Therefore, the container forwards mouse messages and, if the control has the conceptual input focus, keyboard messages to the control by calling its IOleInPlaceObjectWindowless::OnWindowMessage method. Another IOleInPlaceObjectWindowless method named GetDropTarget permits the container to get a pointer to the control's IDropTarget interface if the control is an OLE drop target. (Recall from Chapter 19 that registering as an OLE drop target requires a CWnd pointer or a window handle.) To draw a windowless control, the container calls the control's IViewObjectEx::Draw method and passes in a device context for the container window. A windowless control receives no WM_PAINT messages, so it's up to the container to tell the control when to draw.

    Clearly, the container shoulders an extra burden when it supports windowless controls, but the control also has to do certain things differently, too. For example, if it wants to repaint itself, a windowless control can't just call Invalidate because it has no window to invalidate. Nor can it get a device context by calling GetDC. So instead it calls IOleInPlaceSiteWindowless::InvalidateRect on its container to invalidate itself, or IOleInPlaceSiteWindowless::GetDC to get a device context. There's more, but you probably get the picture.

    Obviously, windowlessness requires extra effort on the part of both the control and the control container. The good news is that COleControl supports both windowed and windowless controls, and it abstracts the differences between them so that you write to one programming model and the control will work either way. Many COleControl functions behave differently in a windowed control than they do in a windowless control. COleControl::InvalidateControl, for example, calls CWnd::Invalidate if the control is windowed or IOleInPlaceSiteWindowless::InvalidateRect if it isn't. COleControl::GetClientRect calls CWnd::GetClientRect for a windowed control, but for a nonwindowed control, it obtains the control rectangle from the control's m_rcPos data member.

    Another example demonstrating how COleControl masks the differences between windowed and windowless controls relates to the message map. When a container forwards a message to a windowless control via IOleInPlaceObjectWindowless::OnWindowMessage, COleControl routes the message through the message map. This means that if you register an OnLButtonDown handler in a control's message map, the handler will be called when a windowed control receives a WM_LBUTTONDOWN message or when a windowless control receives a pseudo-WM_LBUTTONDOWN message. In fact, COleControl does such a good job making windowless controls behave just like windowed controls that you almost have to deliberately set out to write COleControl code that won't work in windowless controls. Even so, you'll want to avoid these three common mistakes:

    • Don't use Invalidate to force a repaint; use InvalidateControl instead.
    • Don't trap messages directly; use the message map.
    • Don't rely on window-oriented messages such as WM_CREATE and WM_DESTROY; windowless controls don't receive them.

    Another common problem is using the rcBounds parameter passed to OnDraw or the rectangle returned by GetClientRect and forgetting that the upper left corner might not be (0,0) for a windowless control. The following code, which computes the center point of the control, is fine in a windowed control, but flawed in a windowless control:

    CRect rect;
    GetClientRect (&rect);
    int x = rect.Width () / 2;
    int y = rect.Height () / 2;
    

    Why is this code in error? Because if rect's upper left corner is anywhere other than (0,0), the calculation must take that into account. Here's the corrected code:

    CRect rect;
    GetClientRect (&rect);
    int x = rect.left + (rect.Width () / 2);
    int y = rect.top + (rect.Height () / 2);
    

    Windows programmers have been conditioned to treat client rectangles as if the upper left corner coordinates are always (0,0). You must break this habit if you want to write windowless controls that work.

    Don't forget that a control created using ControlWizard's default options is strictly a windowed control. To enable the windowless option, check the Windowless Activation box in ControlWizard's Advanced ActiveX Features dialog box. You can convert a windowed control to a windowless control after the fact by overriding GetControlFlags in the derived control class and implementing it like this:

    DWORD CMyControl::GetControlFlags ()
    {
     return COleControl::GetControlFlags () œ windowlessActivate;
    }
    

    If GetControlFlags is already overridden, simply add windowlessActivate to the list of control flags.

    What happens if you build a windowless control and it's instantiated in a container that doesn't support windowless controls? An mfc control will simply fall back and run in a window. Which brings up an interesting point: even though mfc has supported windowless controls for a while now, the test container that ships with Visual C++ didn't support windowless controls prior to version 6.0. Don't make the mistake I once did and attempt to test a windowless control's windowless behavior in the Visual C++ 5.0 test container, because the control will always be created in a window. You can selectively enable and disable windowless support in version 6.0's test container using the Options command in the Container menu.

    Control Subclassing

    One of the questions that ControlWizard asks you before creating an ActiveX control project is, Which Window Class, If Any, Should This Control Subclass? (See Figure 21-5.) The default is none, but if you select a WNDCLASS name, ControlWizard will create an ActiveX control that wraps a Windows control. This makes it relatively easy to write ActiveX controls that look and act like tree view controls, slider controls, and other built-in control types.

    Subclassing a control seems simple, but there's more to it than meets the eye. When a "subclassing" control is created, MFC automatically creates the corresponding Windows control. To do that, MFC must know the control's type—that is, its WNDCLASS. ControlWizard makes the WNDCLASS name you selected in the Which Window Class, If Any, Should This Control Subclass box known to MFC by overriding COleControl::PreCreateWindow and copying the class name to CREATESTRUCT's lpszClass data member. Programmers frequently modify ControlWizard's implementation to apply default styles to the Windows control, as shown here:

    BOOL CMyControl::PreCreateWindow (CREATESTRUCT& cs)
    {
     cs.lpszClass = _T ("SysTreeView32"); // Tree view control
     // Apply default control styles here.
     return COleControl::PreCreateWindow (cs);
    }
    

    Furthermore, at certain points in an ActiveX control's lifetime, MFC needs to know whether the control has subclassed a Windows control. Consequently, a subclassing control must override COleControl::IsSubclassedControl and return TRUE:

    BOOL CMyControl::IsSubclassedControl()
    {
     return TRUE;
    }
    

    ControlWizard writes this function for you if you check the Which Window Class, If Any, Should This Control Subclass box.

    When a subclassing control is created, MFC silently creates a "reflector" window that bounces notification messages emitted by the Windows control back to the ActiveX control. The message IDs change en route: WM_COMMAND becomes OCM_COMMAND, WM_NOTIFY becomes OCM_NOTIFY, and so on. To process these messages in your control, you must modify the derived control class's message map to direct OCM messages to the appropriate handling functions. The following code demonstrates how an ActiveX control that subclasses a push button control might turn button clicks into Click events by processing BN_CLICKED notifications:

    BEGIN_MESSAGE_MAP (CMyControl, COleControl)
     ON_MESSAGE (OCM_COMMAND, OnOcmCommand)
    END_MESSAGE_MAP ()...
    LRESULT CMyControl::OnOcmCommand (WPARAM wParam, LPARAM lParam)
    {
     WORD wNotifyCode = HIWORD (wParam);
     if (wNotifyCode == BN_CLICKED)FireClick ();
     return 0;
    }
    

    ControlWizard adds an empty OnOcmCommand handler similar to this one to the control class, but if you want to process other types of OCM messages, you must add the handlers and message map entries yourself.

    It's also your job to add any methods, properties, and events you would like the control to have. They're added to a subclassing control the same way they're added to a regular control. If you need to access the Windows control from the ActiveX control's method implementations, you can get to it through the m_hWnd data member. To illustrate, the following code fragment adds strings to a list box that has been subclassed by an ActiveX control:

    static const CString strMonthsOfTheYear[] = {
     _T ("January"), _T ("February"), _T ("March"),
     _T ("April"),_T ("May"),_T ("June"),
     _T ("July"), _T ("August"),_T ("September"),
     _T ("October"), _T ("November"), _T ("December")
    };
    

    for (int i=0; i<12; i++)
    ::SendMessage (m_hWnd, LB_ADDSTRING, 0,(LPARAM) (LPCTSTR) strMonthsOfTheYear[i]);

    Subclassing Windows controls has a dark side; it has to do with painting. ControlWizard writes an OnDraw function that looks like this:

    void CMyControl::OnDraw(CDC* pdc, const CRect& rcBounds,
     const CRect& rcInvalid)
    {
     DoSuperclassPaint(pdc, rcBounds);
    }
    

    DoSuperclassPaint is a COleControl function that paints a subclassed control by simulating a WM_PAINT message. It works fine as long as the ActiveX control is active. But if the control goes inactive, the Windows control it wraps is asked to paint itself into a metafile device context. Some controls don't render well into a metafile, which leaves you to write the code that paints an inactive control by overriding OnDrawMetafile. That can be quite a lot of work. As a rule, subclassing Windows common controls works better than subclassing classic controls—list boxes, combo boxes, and so on—because the common controls' internal painting logic is more metafile-friendly.

    You can mostly avoid this problem by checking ControlWizard's Activates When Visible box. If the container honors your request (remember, however, that this isn't guaranteed), the control will be active whenever it's visible on the screen. This means the control's metafile will never be seen. And if it's never seen, it doesn't matter how good (or bad) it looks.

    Incidentally, a similar set of issues arises when writing ActiveX controls that create child controls. Rather than open a new can of worms, I'll refer you to an excellent discussion of this matter in the "Converting a VBX and Subclassing Windows Controls" chapter of Adam Denning's ActiveX Controls Inside Out (1997, Microsoft Press).

    Control Licensing

    Most COM objects in the world today are accompanied by class factories that implement COM's IClassFactory interface. Anyone who has the DLL or EXE (or OCX) that houses such an object can create object instances by calling IClassFactory::CreateInstance or a wrapper function such as ::CoCreateInstance. This is exactly what happens under the hood when a container instantiates an ActiveX control.

    If you check the Yes, Please box in response to the question Would You Like The Controls In This Project To Have A Runtime License? in ControlWizard's Step 1 dialog box, ControlWizard inserts a special class factory that implements IClassFactory2 instead of IClassFactory. IClassFactory2 adds licensing methods to IClassFactory and affords the control's implementor veto power over instantiation requests. Exercised properly, this feature can be used to restrict the use of an ActiveX control to individuals (or applications) who are legally authorized to use it.

    Here's how licensing works in an MFC ActiveX control. When the licensing option is selected, ControlWizard overrides a pair of virtual functions named VerifyUserLicense and GetLicenseKey in the control's embedded class factory class. Exactly when these functions are called depends on the context in which the control is being used. Here's what happens when you drop a licensed control into a dialog in Visual C++'s dialog editor:

  • VerifyUserLicense is called. Its job is to verify that the control is licensed for use in a design-time environment. Returning 0 prevents the control from being instantiated; a nonzero return allows instantiation to proceed.
  • If VerifyUserLicense returns a nonzero value, GetLicenseKey is called. Its job is to return a licensing string that Visual C++ can embed in the compiled executable. This string becomes the control's run-time license.
  • Now suppose you compile and run the application. When the application is executed and the container attempts to instantiate the ActiveX control, a different series of events ensues:

  • The MFC-provided class factory's VerifyLicenseKey function is called and passed the licensing string embedded in the control container. Its job: to check the string and determine whether it represents a valid run-time license.
  • VerifyLicenseKey calls GetLicenseKey to retrieve the run-time licensing string from the control and compares that string to the one supplied by the container. If the strings don't match, VerifyLicenseKey returns 0 and the class factory will refuse to create the control. If the strings match, instantiation proceeds as normal.
  • VerifyLicenseKey is a third virtual function that plays a role in licensing. ControlWizard doesn't override it because the default implementation compares the strings and does the right thing, but you can override it manually if you want to implement a custom run-time verification algorithm.

    These semantics might vary somewhat from container to container, but the result is the same. In effect, the control supports two separate licenses: a design-time license and a run-time license. At first, this dichotomy might seem odd, but it makes terrific sense when you think about it. If a developer uses your control in an application, you can make sure that he or she has the right to do so. But once the application is built, you probably don't want to force individual users to be licensed, too. Design-time licenses and run-time licenses are treated separately for precisely this reason.

    The licensing scheme implemented by ControlWizard is exceedingly weak, so if you really want to build a licensed control, you must do some work yourself. Here's how ControlWizard implements VerifyUserLicense and GetLicenseKey:

    static const TCHAR BASED_CODE _szLicFileName[] = _T("License.lic");
    static const WCHAR BASED_CODE _szLicString[] = L"Copyright (c) 1999 ";
    BOOL CMyControl::CMyClassFactory::VerifyUserLicense()
    {
     return AfxVerifyLicFile(AfxGetInstanceHandle(), _szLicFileName,_szLicString);
    }
    

    BOOL CMyControl::CMyClassFactory::GetLicenseKey(DWORD dwReserved,
    BSTR FAR* pbstrKey)
    {
    if (pbstrKey == NULL)return FALSE;*pbstrKey = SysAllocString(_szLicString);
    return (*pbstrKey != NULL);
    }

    AfxVerifyLicFile is an MFC helper function that scans the first n characters in the file whose name is specified in the second parameter for the string passed in the third, where n is the string length. Visual C++ places a text file named License.lic in the same folder as the control's OCX. Inside that file is the text string "Copyright (c) 1999." If the file is present and it begins with the expected string, AfxVerifyLicFile returns a nonzero value, allowing the control to be instantiated. Otherwise, it returns 0. In other words, the control's design-time license amounts to a simple text file that anyone could create with Notepad.

    There are endless ways to modify ControlWizard's code to strengthen the licensing policy. You could, for example, have VerifyUserLicense display a dialog box prompting the developer to enter a password. Or you could have it check the registry for an entry created by the control's setup program. However you choose to do it, keep in mind that determined users will be able to circumvent just about any scheme you come up with, and many will steer clear of copy-protected products, period, if at all possible. For these reasons, most ActiveX control writers have opted not to license their controls. You be the judge.

    ActiveX Controls

    Chapter 21

    ActiveX Controls

    In the minds of most people, the term ActiveX conjures up visions of ActiveX controls displaying fancy animations and video streams in Web pages. In truth, ActiveX controls are just one piece of the puzzle called ActiveX. But as the penultimate COM technology and the most visible member of the ActiveX family, ActiveX controls enjoy a special distinction that sets them apart from run-of-the-mill COM objects.

    ActiveX controls began their life in 1994 as "OLE controls." The first version of the OLE control specification, which is now referred to as OCX 94, outlined the structure of what would one day be known as ActiveX controls and was intended to provide developers with a generic, COM-based architecture for building reusable Microsoft Windows controls. The OLE control of 1994 was a replacement for custom control DLLs and Visual Basic controls (VBXs). The specification was revised in 1996 (OCX 96), and later that same year, OLE controls were officially renamed ActiveX controls and Microsoft Internet Explorer gained the ability to host ActiveX controls in Web pages. To this day, ActiveX controls serve a dual purpose. (See Figure 21-1.) Application developers can use them to enhance their programs, and Web developers can use them to create interactive Web content—"interactive" because unlike Active Server Pages and Common Gateway Interface (CGI) scripts, ActiveX controls execute on the client side of an HTTP connection.

    Figure 21-1. Two instances of an ActiveX calendar control displayed side by side—in Internet Explorer and in an application program.

    Microsoft provides a number of tools for writing ActiveX controls, but none offer the balance of power and ease of use that Visual C++ and mfc do. Writing an ActiveX control from scratch can require weeks or even months of development time. You can write the same control with mfc in a matter of hours. In fact, you can write a simple control that works equally well in an application or a Web page in minutes with the tools that Visual C++ provides. One of those tools is the MFC ActiveX ControlWizard, which generates the initial source code for a control project. But make no mistake: it's MFC that's the belle of the ball, and without MFC or a similar class library to help out, writing an ActiveX control would be right up there on a list of fun things to do with having your fingernails pulled out.

    In this chapter, I'll begin with an overview of ActiveX controls and an explanation of how they work. I'll conclude with a few topics that aren't treated at length elsewhere in the chapter. In between, you'll learn how MFC supports ActiveX controls. Then you'll write your own control and an application that uses the control. You'll even test the control in a Web page. When you're done, I think you'll agree that even a subject as complex as ActiveX controls can be downright enjoyable when MFC is there to do the bulk of the work.

    ActiveX Control Basics

    ActiveX Control Basics

    So just what is an ActiveX control? Simply put, an ActiveX control is a COM object. But it's not just any COM object; it's a COM object that implements a certain set of interfaces that enable it to look and act like a control. There's some confusion over exactly what that set of interfaces is because technically, the only interface that's required is IUnknown. When I use the term ActiveX control in this chapter, I'm talking about a full-blown control: an ActiveX control that works equally well in an mfc dialog, a Visual Basic form, or a Web page.

    ActiveX controls leverage many of the technologies found elsewhere in COM. For example, most ActiveX controls expose methods and properties just as Automation servers do. They do it by implementing IDispatch. Most are also capable of being in-place activated, as OLE embedding servers are. They do that by implementing IOleObject, IOleInPlaceActiveObject, and other interfaces. ActiveX controls that expose properties generally provide a means for those properties to be stored persistently. They do that by implementing persistence interfaces such as IPersistStreamInit and IPersistPropertyBag. In short, ActiveX controls are complex objects that implement not just one COM interface, but many. In a moment, we'll examine those interfaces and the roles that they play in a control's operation.

    Methods, Properties, and Events

    Controls implement properties so that people using them can customize them to fit the needs of a particular application or Web page. For example, the calendar control that we'll build in this chapter exposes its background color as a property so that users can change its color. When you design an ActiveX control, try to anticipate all the things a user might want to change about its appearance or behavior and then make those characteristics of the control programmable by exposing them as properties.

    Controls implement methods so that they can be called to do useful work. A calculator control might support methods for computing square roots and medians. A clock control wouldn't be complete without a method for setting the time. Control methods are nothing more than Automation methods, and they're added to a control the same way methods are added to an Automation server. You already know how to add methods to an Automation server (refer to Chapter 20 if you need a refresher), so you know how to add methods to an ActiveX control, too.

    One feature that sets ActiveX controls apart from Automation servers is their ability to fire events. An event is a notification sent from a control to its container. A container is a window that hosts an ActiveX control. Windows controls send notifications to their owners by sending messages; ActiveX controls send notifications to their containers by firing events. Events are fired by calling Automation methods through interfaces—normally IDispatch interfaces—provided by control containers. A portion of the ActiveX control specification is devoted to the issue of how a control obtains a pointer to a container's IDispatch interface.

    When you design an ActiveX control, you should think about what kinds of things could happen inside the control that a container might be interested in and code them as ActiveX events. For instance, an ActiveX push button control should fire an event when it's clicked. Remember that it's better to fire too many events than too few because a container can ignore those in which it has no interest.

    Custom vs. Stock

    Another feature that differentiates ActiveX controls from Automation servers is the fact that a control's methods, properties, and events come in two varieties: custom and stock. Custom methods, properties, and events are ordinary Automation methods, properties, and events: ones for which you pick the names and dispatch IDs. Stock methods, properties, and events are "standard" methods, properties, and events that use names and dispatch IDs prescribed in the ActiveX control specification. The idea behind stock attributes is that if a control exposes, say, its background color as a property, using a standard name ( BackColor) and dispatch ID (-501) will promote uniformity among otherwise unrelated controls. If a Visual Basic user sees that your control has a property named BackColor, he or she will know exactly what that property does. If you call it something else, the meaning might be less obvious.

    The control specification contains a rather lengthy list of stock methods, properties, and events, complete with names and dispatch IDs. mfc contains built-in implementations of most of them, and ClassWizard makes adding stock methods, properties, and events to a control a piece of cake.

    Of course, you can forget about stock methods, properties, and events if you want to and make everything custom. But a savvy control designer will use them wherever applicable.

    Ambient Properties

    Another unique and interesting aspect of the ActiveX control architecture is that containers, too, can expose properties. Many times, a control needs to know something about the environment in which it's running before it can decide how to look or act. For example, if you want a control to blend in with its container, you might want to know the container's background color so that the control can paint its own background the same color. You can obtain these and other items of information by reading the container's ambient properties. An ambient property is an Automation property that's exposed through—you guessed it— IDispatch. The difference is that the container—not the control—implements the interface.

    Like stock control properties, ambient properties have well-known names and dispatch IDs. (You'll see a list of the ambient properties that a container can support in a subsequent section of this chapter.) The ambient property named BackColor, for example, exposes the container's background color. A control can read the ambient property named UserMode to find out whether it's running in a design-time environment (for example, in the Visual Basic forms editor or the Visual C++ dialog editor) or a "user" environment (for example, in a Web page or a running application). All it needs is an IDispatch interface pointer and a dispatch ID. The IDispatch interface pointer comes from the container; the dispatch ID comes from the control specification.

    Control States

    At any given time, a control can be in either of two states: active or inactive. These terms have roots in object linking and embedding, but they can be defined accurately enough for our purposes without resorting to the usual OLE technojargon.

    An active control is one that's alive and running in a container. The control's DLL is loaded (ActiveX controls are in-proc COM servers, so they live in DLLs), the control has a window of its own, and the control is able to paint to that window and respond to user input. I should say the control might have a window of its own, because one of the enhancements introduced in OCX 96 was the option to write windowless controls—controls that borrow real estate from their container and literally paint themselves into the container's window. A windowless control doesn't have a window even when it's active, but conceptually it's accurate to think of that control as having a window because both it and the container work very hard to foster that illusion.

    An inactive control, by contrast, doesn't have a window of its own. It therefore consumes fewer system resources and is dead to user input. When a container deactivates a control, it asks the control for a metafile image that it can use to represent the control in the container's window. Then it destroys the control's window and draws the metafile to make it appear that the control is still there. The control will typically remain inactive until it's clicked. OCX 96 defines a new COM interface named IPointerInactive that an inactive control can use to sense mouse movements or change the shape of the cursor, or to request that the container activate it as soon as the mouse enters the control rectangle. The net result is the illusion that the control is active and accepting input all the while; the user is usually none the wiser.

    Does it matter whether a control is active or inactive? It might, depending on what type of control you write. If your ActiveX control creates child window controls, for example, those child windows might render poorly into a metafile. Therefore, you might decide to do whatever you can to prevent the control from being deactivated. One of the options you have as a control designer is to tell the container you'd like the control to be active whenever it's visible. The container isn't absolutely required to honor that request, but most containers will.

    Another reason to be aware of activation states is that controls repaint when they transition from one state to the other. If the control looks the same whether it's active or inactive (most do), this repainting can produce an annoying flicker. The solution to this problem is yet another OCX 96 enhancement called flicker-free drawing. I'll have more to say about these and other ActiveX control options when we examine the MFC ActiveX ControlWizard.

    The ActiveX Control Architecture

    Because an ActiveX control is a COM object, it can be defined in terms of the interfaces that it supports. However, because no one set of interfaces makes an ActiveX control an ActiveX control, the best we can hope to do is to diagram a typical ActiveX control and use it to paint a broad picture of the ActiveX control architecture. Figure 21-2 contains one such diagram.

    Figure 21-2. A typical ActiveX control.

    The control depicted in Figure 21-2 is precisely what you get when you write an ActiveX control with MFC. The control object is housed in a win32 DLL that's commonly referred to as an OCX. The "OC" in OCX stands for OLE Control. An OCX can house one control or several controls. It usually has a .ocx file name extension, but that's left to the discretion of the control creator. Some OCXs have the extension .dll instead.

    Figure 21-2 is the perfect illustration of a full-blown ActiveX control, which can be more fully defined now as one that draws a visible manifestation of itself in a window; that supports methods, properties, and events; and that is equally at home in an application or on a Web page. Though technically none of these COM interfaces is required of an ActiveX control, as a practical matter, many of them are required if the control is to support the kinds of features normally associated with ActiveX controls. For example, a control must implement IConnectionPointContainer if it's to fire events. And it can't expose methods and properties without an IDispatch interface. In that sense, then, Figure 21-2 is a reasonable depiction of the objects that most people think of as ActiveX controls.

    So just what do all those interfaces do? One could easily write a book about each interface and the role that it plays in the operation of an ActiveX control, but that level of detail isn't necessary here. The following table briefly describes each interface.

    ActiveX Control Interfaces

    Interface Comments
    IConnectionPointContainer Exposes connection points for event interfaces
    IDataObject Makes presentation data available to the control container
    IDispatch Exposes the control's methods and properties
    IOleCache Controls the presentation data cache
    IOleControl Base interface for ActiveX controls
    IOleInPlaceActiveObject Base interface for embedded objects that support in-place activation
    IOleInPlaceObjectWindowless Allows the container to manage the activation and deactivation of both windowed and windowless controls
    IOleObject Base interface for embedded objects
    IQuickActivate Speeds control creation in containers that recognize this interface
    IPerPropertyBrowsing Allows containers to acquire information about control properties, such as each property's name
    IPersistMemory Allows the control to write property values to memory and read them back
    IPersistPropertyBag Allows the control to save property values in "property bag" objects provided by the container
    IPersistStorage Allows the control to save property values in storage objects
    IPersistStreamInit Allows the control to save property values in stream objects
    IProvideClassInfo2 Makes type information available to the control container
    ISpecifyPropertyPages Allows the control to add pages to property sheets displayed by the container
    IViewObjectExAllows the container to acquire images of inactive controls and paint windowless controls

    IConnectionPointContainer indirectly enables containers to provide IDispatch interface pointers to controls for event firing. You already know that to fire an event, a control calls an Automation method on its container's IDispatch interface. To find out what kinds of events the control is capable of firing (and by extension, what methods the container must implement in order to respond to control events), most containers read the control's type information. Type information is accessed by calling the control's IProvideClassInfo2::GetClassInfo method and calling ITypeInfo methods through the returned ITypeInfo pointer.

    If you were writing an ActiveX control from scratch (that is, without the aid of a class library), you'd have to understand the semantics of all these interfaces and others to the nth degree. But write a control with MFC and MFC will implement the interfaces for you. You don't even have to know that the interfaces are there; they just work.

    ActiveX Control Containers

    ActiveX control containers are complex COM objects in their own right. That's right: to host an ActiveX control, a container must implement COM interfaces, too. The exact set of interfaces required depends somewhat on the nature of the control that's being hosted, but control containers tend to be more uniform in the interfaces that they implement than controls are.

    Figure 21-3 shows a typical ActiveX control container. For each control that it hosts, the container implements a control site object. Apart from the individual site objects, it also implements COM's IOleContainer and IOleInPlaceFrame interfaces. As the diagram shows, most containers provide two separate implementations of IDispatch. One exposes the container's ambient properties, and the other is provided to the control for event firing. The following table provides brief descriptions of commonly used ActiveX control container interfaces.

    Figure 21-3. A typical ActiveX control container.

    ActiveX Control Container Interfaces

    InterfaceComments
    IOleContainer Base interface for embedding containers
    IOleInPlaceFrameBase interface for OLE containers that support in-place activation
    IOleClientSite Base interface for OLE containers
    IOleInPlaceSite Base interface for OLE containers that support in-place activation
    IOleControlSite Base interface for ActiveX control sites
    IDispatch Exposes the container's ambient properties
    IDispatchTraps events fired by a control
    IPropertyNotifySink Allows the control to notify the container about property changes and to ask permission before changing them

    Writing a control container involves more than just implementing the required COM interfaces; you have to wrestle with a protocol, too. For example, when a control is created, a conversation ensues between it and its container. Among other things, the two exchange interface pointers, the container plugs IDispatch and IPropertyNotifySink (and perhaps other) interface pointers into connection points implemented by the control, and the container usually activates the control by calling its IOleObject::DoVerb method. In some cases, the container reads type information from the control so it will know what to do when events are fired through its IDispatch interface. The conversations don't stop after the control is initialized; the control and its container are constantly responding to calls from the other by placing calls of their own. In other words, hosting an ActiveX control is a big job. That's all the more reason to build ActiveX control containers with MFC, which does an excellent job of hiding all these complexities under the hood and making it very easy to host ActiveX controls in any CWnd-derived object.

    MFC Support for ActiveX Controls

    MFC simplifies the writing of ActiveX controls and control containers by providing built-in implementations of the required COM interfaces and encapsulating the protocol that links ActiveX controls and control containers. For those COM methods that can't be implemented in a generic fashion, MFC provides virtual functions that you can override in a derived class to implement control-specific behavior. To a large extent, writing an ActiveX control with MFC is a matter of deriving from MFC base classes and overriding virtual functions here and there to add the logic that makes your control unique.

    Though by no means exhaustive, the following sections highlight the MFC classes that provide the foundation for MFC's ActiveX control support.

    COleControl

    Much of MFC's ActiveX control support is found in COleControl. The base class for all MFC ActiveX controls, COleControl is a large and complex class that implements most of the COM interfaces shown in Figure 21-2. It also includes handlers for dozens of Windows messages and provides built-in implementations of stock methods, properties, and events.

    When you derive a class from COleControl to represent an ActiveX control, you'll override some of its virtual functions, too. COleControl includes about sixty virtual functions, each of which is important in its own right. The two functions listed in the following table, however, stand out for their utter importance in the operation of a control:

    Key Virtual COleControl Functions

    Function Description
    OnDraw Called to paint the control. Override to add control-specific painting logic.
    DoPropExchange Called to save or load a control's persistent properties. Override to support persistent control properties.

    Both functions are overridden for you if you use the MFC ActiveX ControlWizard to create the control project. Issues that you should be aware of when implementing these functions in a derived class are covered later in this chapter.

    COleControl also includes a diverse assortment of nonvirtual functions that a control programmer should be aware of. One of those functions is InvalidateControl. It should be used in lieu of CWnd::Invalidate to repaint a control because unlike Invalidate, it works with both windowed and windowless controls. The following table lists some of the nonvirtual functions that are useful when you're writing ActiveX controls with MFC.

    Key Nonvirtual COleControl Functions

    Function Description
    Ambientxxx Retrieves an ambient property value from the container (for example, AmbientBackColor)
    Firexxx Fires a stock event (for example, FireClick)
    GetAmbientProperty Retrieves the values of an ambient property for which no Ambientxxx function is defined
    Getxxx Retrieves the value of a stock property (for example, GetBackColor)
    InitializeIIDs Makes the IDs of the control's event interface and IDispatch interface known to MFC; normally called from the class constructor
    InvalidateControl Repaints the control
    SerializeStockProps Serializes the control's stock properties
    SetModifiedFlag Marks the control as dirty or not dirty (A "dirty" control is one that contains unsaved property changes.)
    SetNotSupported Generates an error when a client attempts to write to a read-only property
    ThrowError Signals that an error occurred; used in method implementations and property accessor functions
    TranslateColor Translates an OLE_COLOR value into a COLORREF value

    You'll see some of these functions used in this chapter's sample control. Two of them— InitializeIIDs and SerializeStockProps—are automatically added to COleControl-derived classes by ControlWizard.

    COleControlModule

    Every MFC application includes a global instance of a CWinApp-derived class that represents the application itself. COleControlModule is to MFC ActiveX controls what CWinApp is to conventional MFC applications: it represents the server module (that is, the DLL) in which the control is housed.

    COleControlModule is a thin class that adds little to the functionality it inherits from its base class, CWinApp. Its primary contribution is an InitInstance function that calls AfxOleInitModule to enable COM support in an MFC DLL. It follows that if you override InitInstance in a COleControlModule-derived class, you should call the base class's InitInstance function before executing any code of your own. When ControlWizard creates an ActiveX control project, it adds the call for you.

    COlePropertyPage

    Most ActiveX controls expose their properties to developers by implementing property sheet pages that are displayed by the control container. In ActiveX land, property pages are COM objects, complete with CLSIDs. A property page object—sometimes referred to as an OLE property page—is one that implements COM's IPropertyPage or IPropertyPage2 interface.

    MFC's COlePropertyPage class makes creating OLE property pages a snap by implementing IPropertyPage2 for you. You simply derive from COlePropertyPage and add a bit of infrastructure; MFC does the rest. (Of course, ControlWizard and ClassWizard are happy to do the derivation for you and add the necessary infrastructure themselves.) Typically, you don't even have to override any virtual functions except for DoDataExchange, which links the controls in the property page to properties exposed by the ActiveX control. I'll describe exactly how this linkage is performed later in this chapter.

    CConnectionPoint and COleConnPtContainer

    ActiveX controls use COM's connectable object protocol to accept interface pointers from their containers for event firing. A connectable object is one that implements one or more connection points. Logically, a connection point is a receptacle that interfaces can be plugged into. Physically, a connection point is a COM object that implements the IConnectionPoint interface. To expose its connection points to clients, a connectable object implements COM's IConnectionPointContainer interface. Implementing IConnectionPoint and IConnectionPointContainer also means implementing a pair of enumerator interfaces named IEnumConnectionPoints and IEnumConnections. All this just so a control can fire events to its container.

    The details of connectable object interfaces are beyond the scope of this discussion, but suffice it to say that implementing them is no picnic. Enter MFC, which provides default implementations of all four in classes such as CConnectionPoint and COleConnPtContainer. The implementations are generic enough that they can be used even outside the ActiveX control architecture, but it is ActiveX controls that benefit the most from their existence. For the most part, you don't even know these classes are there because they're tucked away deep inside COleControl.

    COleControlContainer and COleControlSite

    The bulk of MFC's support for ActiveX control containers is found inside the classes COleControlContainer and COleControlSite. The former implements IOleContainer and IOleInPlaceFrame, and the latter contributes stock implementations of IOleClientSite, IOleControlSite, IOleInPlaceSite, and other per-control interfaces required of ActiveX control containers. When you build a control container with MFC, you get a container that looks very much like the one in Figure 21-3 with three additional interfaces thrown in:

    • IBoundObjectSite
    • INotifyDBEvents
    • IRowsetNotify

    These interfaces are used to bind ActiveX controls to external data sources—specifically, RDO (Remote Data Object) and OLE DB data sources.

    COleControlContainer and COleControlSite are complex classes, and they work in conjunction with a similarly complex (and undocumented) class named COccManager. Fortunately, it's rare to have to interact with any of these classes directly. As you'll see, simply checking a box in AppWizard or adding a statement to InitInstance is enough to endow any MFC application with the ability to host ActiveX controls. Five minutes with MFC can save you literally weeks of coding time.