Programming Windows with MFC, Second Edition
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 |
IViewObjectEx | Allows 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
Interface | Comments |
---|---|
IOleContainer | Base interface for embedding containers |
IOleInPlaceFrame | Base 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 |
IDispatch | Traps 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.