The low road to OLE controls has its charms
Steve Apiki
OLE controls are the heirs apparent to VBXes (Visual Basic custom controls), arguably implementations of the most successful component software model to date. OLE controls, like VBXes, are reusable, binary software objects with well-defined properties and I/O interfaces. Like VBXes, OLE controls make possible the rapid construction of sophisticated applications through the wiring together of component objects atop hosts like Visual Basic. Unlike VBXes, however, OLE controls can be built with 32-bit code; are based on COM (Common Object Model), a well-supported model; and are potentially portable beyond Windows and Win32 to the Mac OS.
Building an OLE control
can be as simple as firing up Visual C++ 2.0, choosing Control Wizard from the menu, and instantly generating a new OLE control project. However, there's nothing magical about OLE control generation; in fact, if you've already built an OLE server that you want to convert to an OLE control, or if you don't want to be tied to Visual C++, you may want to consider implementing the control yourself.
The View from 10,000 Feet
Except for its intended use, an OLE control is nothing like a VBX (
see the figure
). At its core, an OLE control is an in-process OLE server that supports in-place activation (i.e., the ability to control the container's user interface elements). To this core, an OLE control adds layers of OLE Automation support and a few new interfaces to handle behaviors that are unique to controls.
The ``embedding'' part of OLE already provides most of the requirements for a drop-in custom control. It's the mechanism through which users can pull in
a control, activate it, and edit it using the host's menus and toolbars. Supporting OLE embedding means building a COM object that exposes the OLE interfaces shown in black in the figure. These interfaces let the container place, activate, and store and retrieve the object, as well as mediate communication between control and container required for display updates and data access. The container also uses standard OLE models to locate the DLL in which the control resides (using the system registry) and to create an instance of the control (a DLLGetClassObject entry point in the DLL).
But a control is more than an embedded server. Controls have user-editable properties and user-callable methods. A button control might expose a color property, for example, or a ``press'' method that makes the control appear to have been clicked by a mouse. The types of these properties and the parameters of these methods must be made available to the container; the container also needs to know the names of these items so
it may present them to the user. The IDispatch interface (the blue arrow in the figure) that forms the basis for OLE Automation accomplishes this.
IDispatch provides a way to obtain type information and access to properties and methods. Controls carry with them (usually bound in a resource) a binary object called a type library, which supplies a means to find the names, types, and parameters of the control's properties and methods. Containers find objects in the type library using the IDispatch interface.
OLE embedding or OLE automation interfaces don't address two key control behaviors: mnemonic accelerator support (e.g., Alt-key combinations in place of a mouse-click to activate a button) and a method for firing events. Mnemonics are handled through the new IOleControl interface (the red lines in the figure indicate new interfaces), which lets the container find accelerators associated with the control and call a handler when appropriate.
Event capability is the most radical OLE techno
logy introduced with OLE controls. Events are calls to functions within the container (e.g., OnClick) in response to an external action (a mouse-click on the control). For the container to know which functions it must implement to support these events, the control exposes a description for a second IDispatch interface inside its type library. The container uses this description to build its own dispatch table that implements the required functions. When the control needs to fire an event, it calls through the container's dispatch interface to the user's implementation. This is a tricky point; the container must essentially implement an interface that is only described to it when the control is loaded. Making sure events get hooked up correctly is the responsibility of IConnectionPoint and IConnectionPointContainer interfaces introduced for OLE controls.
There is considerably more to an OLE control, of course. It should present dialogs for editing properties (property pages), it must provide functions f
or self-registration, and it can support licensing features.
Almost from Scratch
Building an OLE control from scratch is too involved a process to review in this space, so we'll start from where an OLE control begins to deviate from an OLE server supporting in-place activation. If you're starting with an OLE 2.0 server that's part of an application, you'll have to move the applicable code to a DLL, which is the only acceptable venue for an OLE control. You can also cut out any custom interfaces you may have implemented and any interfaces not related to those listed in the figure, as the container will never see them.
The first real step is to define the properties, methods, and events that your control will support. You do this by writing a script in ODL (Object Description Language), which you'll compile using Microsoft's MkTypLib. The ODL script describes both the dispatch interface (incoming) and the event connection interface (outgoing). If you've built OLE automation
servers before, you'll notice that several ODL extensions have been added for controls, including specifying the events dispatch interface and new property attributes to support data binding. MkTypLib generates a type library that you'll bind to the application.
Although the type library describes two IDispatch interfaces, you'll only implement the incoming one. The container will handle the outgoing implementation once we add events. Most of the functions in the IDispatch interface can be relayed to default handlers inside the OLE DLLs, so building the incoming interface is mostly a matter of finding your type library and passing the information back to OLE. IDispatch::Invoke will relay calls from the container into functions inside the control.
We now have an OLE server that supports in-place activation and OLE automation and that contains custom properties and methods. To make it begin to look like an OLE control, we add an IOleControl interface. IOleControl primarily supplies a medium throug
h which the container can notify the control of mnemonic keystrokes and of changes in the container's ambient properties. Ambient properties are those that describe the container's environment around the control, such as fonts and colors.
OLE control DLLs must include functions that automatically register and unregister the DLL with the system registry. These allow users to browse for additional control DLLs (not unlike the Visual Basic ``Add File'' menu option). The registration functions add keys describing the classes to the registry using the access APIs the SHELL library provides.
With the registration functions added, we now have an object that we can legitimately call an OLE control--control containers will recognize it and be able to load it. However, until we add a way to fire events, our control is little more than a pretty picture. Adding events requires writing the IConnectionPointContainer and IConnectionPoint interfaces and coding up the calls to make to the container when events o
ccur.
Supporting IConnectionPointContainer is how a control demonstrates to the outside world that it has outgoing event sets. To establish event communication, a container first calls through IConnectionPointContainer to find the IConnectionPoint interfaces. Each connection point handles an entire event set, so your control will probably need just one or two connection points--one for your outgoing events and possibly one for data change notifications. Writing IConnectionPointContainer amounts to keeping track of these two interface pointers.
Once the container has found the control's connection point, it calls IConnectionPoint::Advise for each connection it wants to make to the control. The container hands this function a pointer to its implementation of the event dispatch interface that you described in your type library. Because the container can have multiple connections to a connection point, you need to maintain a list of these pointers and be prepared to enumerate them.
With the c
onnection interfaces in place, we can now get a list of dispatch pointers to use when firing events off to the control. The listing gives an example of how you might actually fire an event (a mouse-click). The first step is to put the function call arguments into a form that IDispatch can understand (makeDispParams). Then we find each dispatch interface in the list we maintain (those that came from IConnectionPoint::Advise) and call the Invoke method to send the function. The ID we use to identify the function (DID_CLICK) and the number and types of its parameters (the arguments to makeDispParams) are those that we used when we built the event dispatch description into the type library.
To finish our OLE control, we need to add support for property pages. Property pages are COM objects that support a dialog window displayed when end users wish to browse or edit the control's properties. Supporting these requires adding an ISpecifyPropertyPages interface to our control and building the property-page obj
ects. The interface contains a single function that hands back the class ID of our property-page object. The property page supports a single interface that allows the placing of the dialog within a property browsing frame window and updates properties that are changed.
The CDK Approach
Microsoft's CDK (Control Development Kit), bundled (in both 16-bit and 32-bit versions) with Visual C++ 2.0, uses MFC (Microsoft Foundation Classes) and a lot of built-in code to hide these implementation details. The steps above got us about as far as launching the Control Wizard from VC++ would have, so the CDK is obviously the way to go for most controls. However, if you're starting with a working OLE server, are curious about the wiring underneath MFC, or have other special requirements, a pure OLE approach can work just as well.
...
hr = ppl->pIConnectionPoint->EnumConnections(
&pEnum );
if SUCCEEDED(hr)
{ LPDISPATCH pSend;
makeDispParams( &pparams, 2, VT_XPOS_PIXELS, &x,
VT_YPOS_PIXELS, &y);
while( NOERROR == pEnum->Next(1, &cdata, NULL) )
{ hr = cdata.pUnk->QueryInterface( IID_IDispatch,
(LPLPVOID)&pSend);
if ( NOERROR == hr)
{ pSend->Release();
hr = pSend->Invoke( DID_CLICK, IID_NULL,
ppl->lcid, DISPATCH_METHOD,
pparams, NULL, &einfo, &badarg);
}
.}
pEnum->Release();
freeDispParams( &pparams);
}
...
Inside an OLE control
illustration_link (37 Kbytes)
An OLE control equals an OLE in-place server DLL plus OLE automation interfaces for access to properties and methods plus a few new interfaces for mnemonic keystroke and event handling.
Steve Apiki is a BYTE contributing editor and senior
developer at Appropriate Solutions, Inc., in Peterborough, NH. You can reach him on BIX at ``apiki'' or on the Internet at
``apiki@apsol.com
.''