(ZT)Notes on Implementing an OLE Control Container

来源:互联网 发布:网络远程教育报考 编辑:程序博客网 时间:2024/05/04 18:04

original url from : https://msdn.microsoft.com/en-us/library/ms974281.aspx


Notes on Implementing an OLE Control Container

 

Kraig Brockschmidt
Microsoft Developer Relations

Preliminary Version
Created: September 21, 1994

Inasmuch as this is a preliminary version, mostly straight from my computer to yours, there will invariably be errors, omissions, inaccuracies, and other problems that a formal review would catch. Please forgive, but do report such problems to my Internet e-mail address: kraigb@microsoft.com. Please also understand that I do not have the resources to answer questions about this material personally. Questions should be handled through other means.

Abstract

Programming container applications that can interact and fully exploit OLE Controls is an important part of the developing component software market centered on OLE. This article, along with the accompanying sample code for CPatron, is intended to help you understand the architecture and technologies in OLE Controls more thoroughly and to provide detailed information on writing OLE Control Containers. The Control Development Kit with beta versions of Microsoft Visual C++™ 2.0 contains the necessary information about writing controls.

Overview

I do assume in this article that you are already conversant in OLE 2 as it exists today, as well as versed in the OLE Controls specification itself. This paper isnot a rehash of the specification, although I will extract some information from it to provide context as appropriate.

The first section, "Control Container Architecture," goes into detail about exactly what a control container looks like from the point of view of a control. While we're at it, we'll also look at how a control looks to a container. This information will draw from the "OLE Control Architecture" paper that is Chapter 1 of the current CDK, but oriented more specifically toward grouping the various interfaces in the OLE Controls specification into subtechnologies. As we'll see, most of what's in the spec is not part of OLE Controls proper, but rather lower-level technologies that can, like any other set of interfaces in OLE, be used outside the context of controls altogether. So this section will look at the groupings of interfaces in both control and container that relate to various subtechnologies. This will lay the groundwork for looking at code.

The next section, "The CPatron Sample Application," provides some notes about the sample application that is the basis for this document. Specifically, this section will describe how you can spot the differences between the control container and a regular in-place–capable compound document container. It also explains which parts of the consummate OLE Controls functionality were actually implemented and which were, well, punted for lack of time or suitable motivation.

The final section, "Implementing a Control Container Step by Step," looks like many of the chapters fromInside OLE 2. We're creatures of habit, I suppose, so this section is written to provide a structured sequence for adding various features to a container. At any point you should be able to stop, compile, and run through some of your code to see if it is working the way you expect. At least that's what I hope—because this is a preliminary paper and sample code, I have not had the luxury of time to really test the presented sequence as the best one. So despite all my endeavors to do things right, expect that this section will have the most inaccuracies and errors because the information I'm working from to write this document—and the sample—is sketchy in places.

So that's the disclaimer—on to the good stuff!

Control Container Architecture

Although the purpose of this article is to describe the implementation of an OLE Control Container application, it helps to know what it is we're trying to contain. That is, what exactly is an OLE Control? To answer that question, we first need to look at what a control is in general, and what mechanisms are required to make it work. From there we can look at the control container side and see what is required of it. Then we can get into the real meat of this section which is to describe the interfaces and mechanisms through which controls and containers function.

What Is a Control?

A control is mostly a collection of information and functionality that may also have a specific and dedicated user interface to allow an end user to manipulate it. Not only that, a control also allows itself to be manipulated programmatically so that another piece of code can use it to perform specific tasks. Controls are therefore self-contained units of information and functionality that are independent of where they actually reside. That is, a control of a specific type in one container behaves like another control of the same type in another container.

A control describes its "information" through properties—named characteristics or values such as color, text, font, and so on. Properties can include not only visual aspects but also behavioral ones. For example, a button might have a property that indicates whether the button is momentary or push-on/push-off. In any case, a control's properties define its "state," and some or all of these properties may be persistent. Although the control can change its own properties, it is also possible that the container holding the control might change a property, in response to which the control would change its state, user interface, and so on. Note that some controls may wish to support structured information by supplying the appropriate formats through theIDataObject interface. Properties are best for exchanging one piece of information at a time. Data structures are best handled throughIDataObject.

A control describes its "functionality" with two mechanisms: methods andevents. Methods are simply functions implemented in the control that can be called from the container to perform some action. For example, an edit or other text-oriented control supports methods to allow the control container to retrieve or modify the current text, perhaps performing such operations as copy and paste with that control. In many cases a control uses its own methods to implement other parts of itself—it just exposes those methods to any container of that control as well.

Events, on the other hand, are external actions that happen to a control that causes the control to call an event handler. The act of calling the event handler is termed "firing an event." Event handlers are nothing more than functions implemented by the container of the control which execute other actions in response to the event. In this way the control transforms mundane actions like keystrokes, mouse clicks, and elapsed timers into more meaningful programmatic events to which the container can attach user-defined code. The trick is writing a container that can handle an arbitrary event set for each contained control.

Of course, besides just defining information and functions, a control must be able to define how it appears when visible in a container, and a container must communicate to the control when its size and other visual aspects of the control have changed. In the design of OLE Controls, there are five mechanisms, each with its own combination of interfaces and protocols, that control use to fulfill all their responsibilities

  • Control Properties. A mechanism through which the container can retrieve and modify properties as well as call methods. The control must expose the names of properties as well as the names and parameters of its methods. This also includes control-supplied user interface to allow end users to manipulate properties.
  • Control Events. A mechanism through which the control notifies the control container of events that occur in the control.
  • Control Visuals. A mechanism through which the control draws itself but gives the container the responsibility to manage the control's position and dimensions.
  • Control Mnemonics. A mechanism through which the control can specify and process its keyboard mnemonics and accelerators, such as ALT+ key combinations and arrow keys. In addition, controls such as buttons and labels have special behavior requirements as far as accelerators are concerned.
  • Control Persistence. A mechanism through which the control container can ask the control to save its current information into a storage or stream object.

As we'll see shortly, most of these mechanisms are provided by such current OLE 2 technologies as compound documents and automation, and new technologies defined in the OLE Controls specification fill in the rest.

What Is a Control Container?

Simply said, a control container is an application that provides the necessary "sites" that can contain controls. Typically these sites all exist on the same document or "form." Having sites alone makes only a compound document container capable of embedding of linking objects into the sites. A control container goes further, however, to support the added control mechanisms.

First of all, a control container exposes some of its own form properties to all the controls in the form. These are known asambient properties because they define the ambiance of the control's surrounding environment. Using ambient properties, the container can specify the default colors, fonts, alignment, and behavioral suggestions. Controls may choose to retrieve these properties from the container at run time to integrate better into the form as a whole.

As mentioned in the previous discussion on controls themselves, a container must also implement entry points for the object's events. The container must provide such points for the control's entire event set regardless of whether or not the container will perform any actions for those events. Providing all the entry points means that a control never has to make any special case considerations about whether it should fire an event: It just fires away and lets the container ignore them.

The container, if it's interested, can also set up a property notification sink that the control calls whenever a property is about to change or has changed. This sink, in some circumstances, allows the container to veto the change of a property in the control.

Besides dealing with properties and events, the container is also responsible for providing all the other facilities for object layout, ordering, and keyboard processing, because only the container is aware of all the objects in a form and the relationships between objects. It must also facilitate saving the document or form to a file for later reloading. If it so chooses, the container can provide additional user interface features for the registration and creation of controls (such as a dialog to add controls to the registry and a toolbar populated with buttons that represent the registered controls).

It is also quite possible for the control container itself to be a control, but this article will not cover the additional considerations for such a component.

All of these requirements together yields a list of six necessary mechanisms to make the container side of controls succeed:

  • Container Layout. A mechanism to create, place, size, and order controls.
  • Container Form Persistence. A mechanism through which to save and retrieve the persistent state of controls and the mapping of their events to container actions.
  • Container Ambient Properties. A mechanism to expose ambient properties to all controls.
  • Container Event Handlers. A mechanism to provide and expose event entry points to each specific control.
  • Container Extended Controls. A mechanism for the container to wrap controls into an "extended control" that layers container-managed properties and methods on top of any other controls. This allows the container to treat all controls in a uniform fashion through those properties and methods.
  • Container Keyboard. A mechanism to inform controls of accelerator and other keyboard events, as well as to handle special-purpose buttons and labels.

We're now in position to see which OLE technologies fulfill these requirements and those of controls.

New Technologies in OLE Controls

For the most part, an OLE Control is an inside-out, in-place capable, in-process compound document object, with extra support for the additional control mechanisms. Yikes! If that noun stack in the previous sentence lost you, then let's try saying that a control has the following features:

  • Is a compound document object
  • Supports in-place activation
  • Is marked as an inside-out object (usually includes "activate-when-visible" as well)
  • Is usually implemented in a dynamic-link library (DLL) server (in-process servers and local servers are also possible, although the current Visual C++ Control Development Kit doesn’t support controls in local servers because there is no marshaling code for the new interfaces involved)
  • Implements additional interfaces for control requirements
  • Includes a type library to describe the control's properties, methods, and events

Likewise, a control container is also a compound document container—it is capable of working with compound document objects as well as controls. This includes not only all the embedding support and in-place activation support, but also support for events and properties.

In short, a good portion of implementing controls and control containers is implementing compound document objects and containers. As described in "The CPatron Sample Application" section below, the sample control container accompanying this document began life as a full compound document container.

You might think, then, that OLE Controls is a bunch of extensions to OLE Documents (the compound document) technology, and that all of OLE Controls builds on compound documents.Au contraire, my liege. The OLE Controls Specification defines a number of new technologies that are down on the COM and OLE Automation levels, as well as some that rest above compound documents. Table 1 describes the new technologies and OLE extensions included with the controls spec (along with the interfaces involved), and Figure 1 illustrates the relationships between these technologies and the core of OLE. You can see that some of the additional technologies, such as Connectable Objects and even Events, can be very useful without controls. It's just that these technologies were created for the purpose of controls, but like a good design for an OLE technology, the OLE Controls design created many more useful subtechnologies.

Table 1. New Technologies Specified in OLE Controls

TechnologyPurposeInterfaces, FunctionsCOM extensionsAdds self-registering objects, registry entries for toolbar images, versioning rules, and licensing support. (Versioning and licensing are deployment issues rather than mechanism issues, so they are not described further in this document. See the OLE Controls Specification.)DllRegisterServer, DllUnregisterServer, IClassFactory2Storage extensionsAdds a new persistence interface for initialized streams, rules for objects that serialize only into streams, and rules for saving persistent object data as straight text.IPersistStreamInitAutomation extensionsODL enhancements, standards for classifying and naming common properties and methods, and a mechanism to retrieve an object's type information.IProvideClassInfoConnectable ObjectsA generalized mechanism through which advisory connections are established between some source object and some sink object.IConnectionPoint, IConnectionPointContainerEventsStandards for defining the event set of an object and connecting an event sink to the object.(none)Property Change NotificationA standard property change notification interface.IPropertyNotifySinkProperty PagesMechanisms to define object-controlled user interface for end-user property manipulation.ISpecifyPropertyPages, IPropertyPage[2], OleCreatePropertyFrame-[Indirect], IPerProperty-BrowsingOLE Controls (Proper)Extensions to compound documents to fill out the control requirementsIOleControl, IOleControlSite

Figure 1. New technologies in OLE Controls extend OLE on all levels.

We can now match up the mechanisms necessary for controls and control containers to their respective OLE technologies, as shown in Table 2. All the requirements are fulfilled using some core OLE technologies, while others depend, of course, on the new technologies. Note that some of the new technologies are not listed here because they are oriented more towards the deployment of controls rather than run-time interaction.

Table 2. Technologies Used to Address Control and Container Requirements

RequirementOLE Technologies AppliedControl PropertiesAutomation, Property Notifications, Property Pages, Connectable ObjectsControl EventsEvents, Automation, Connectable ObjectsControl VisualsCompound DocumentsControl MnemonicsOLE ControlsControl PersistenceStructured Storage (plus extensions)Container LayoutCompound Documents, Drag & DropContainer Form PersistenceStructured StorageContainer Ambient PropertiesAutomation, Properties, OLE ControlsContainer Event HandlersEvents, AutomationContainer Extended ObjectsOLE Controls, Property Pages [?]Container KeyboardOLE Controls

Combining all these technologies, we get pictures for both controls and container sites as shown. A control, shown in Figure 2, implements a number of additional interfaces besidesIOleObject, IDataObject, IViewObject2, IOleCache,IRunnableObject, and IOleInPlaceObject (plus IOleInPlaceActiveObject), as is normal for a compound document object. A control adds eitherIPersistStorage, IPersistStream, or IPersistStreamInit; anIDispatch interface for methods and properties; IProvideClassInfo and IConnectionPointContainer, through which the container connects to events and property change notification;ISpecifyPropertyPages (optional); and IOleControl to fill out the set.That's a lot of interfaces, but we we'll see they all have their purposes in making controls work completely.

Figure 2. A control with new control-related interfaces

A control container, shown in Figure 3, implements interfaces above and beyond what is normal for a compound document container. For compound documents, the container must implementIOleInPlaceFrame on its frame window and IOleInPlaceUIWindow on its document or form windows. OLE Controls doesn't affect these objects much, however, because most of the changes affect site objects. For compound documents, the site implementsIAdviseSink, IOleClientSite, and IOleInPlaceSite. For controls the site adds anIDispatch interface for ambient properties, IPropertyNotifySink to detect changes in control properties, andIOleControlSite to complete the set. In addition, the site contains one or more conceptual "event sink" objects that each implement anIDispatch interface for a particular event set. However, as demonstrated in the CPatron sample, you can actually implement the eventsIDispatch interface on the site itself, provided you play a few games withQueryInterface.

Figure 3. A control container with new control-related interfaces

The following sections describe the mechanisms of the new technologies defined in OLE Controls and how these interfaces on both controls and control containers are put to use.

COM extensions

The enhancements to the COM level defined for OLE Controls is generally concerned with the deployment, registration, and creation of objects. Because this article focuses on what happens when we've already created an object (control), this section is intentionally kept minimal. To that end, the versioning and licensing enhancements such as theIClassFactory2 interface are not covered here—they are covered adequately in the OLE Controls Specification.

One enhancement that is important for control containers is Self-Registering Servers—that is, servers that are able to create their own registry entries on request and therefore not rely on a separate .REG file. This allows even the registry information to be self-contained in the object, and the object can dynamically update the path entries based on where the module (.DLL or .EXE) is found. Because the registry is critical to OLE, self-registration removes the external dependency of a .REG file, without which an object would be useless.

Did that last paragraph surprise you? Earlier I had mentioned that controls are considered in-process objects. For the most part that's true because the first release of OLE Controls (that is, the first release of the Control Development Kit) does not provide proxies and stubs (marshaling code) for the new control interfaces. So for the time being, a control has to be implemented in a .DLL. However, the COM enhancements for self-registering servers are not limited only to the scope of controls, but rather apply to any object servers whatsoever. Accordingly there are different rules for handling .DLLs and .EXEs, DLLs being the most straightforward.

The reason that self-registration is important to a control container is that users of the container (typically programmers) will copy OLE Control files (.OCX) to their machines and thus need to have a way to get those controls registered. The control container should provide some user interface (usually something like the File Open dialog with a different caption and a "Register" button) in which these users can locate .OCX files. Once located, the container uses these COM extensions to ask the control to register itself. Furthermore, a control can include information about where the container can find a toolbar/toolbox bitmap for the control.

Those readers who are familiar with Visual Basic Controls (.VBX) will see some parallels here, of course, because these COM extensions are meant to provide the same functionality while working off existing OLE technology (which means portability to 32-bits) rather than maintaining something separate.

Self-registering DLL servers

A self-registering DLL server indicates that it supports this capability using the following constructs:

  • A new value, "OleSelfRegister", in its version information
  • An exported function: HRESULT DllRegisterServer(void)
  • An exported function: HRESULT DllUnregisterServer(void)

The added value in the DLL's version information is necessary to allow other control containers to determine if a DLL is a self-registering server without actually loading the DLL (execution ofLibMain[32] can potentially have negative side effects). In addition, the version information can allow future operating systems to detect a self-registering component when a file is copied to the system. As described in the OLE Controls spec, the "OleSelfRegister" string is included under the "StringFileInfo" section of a DLL's VS_VERSION_INFO structure. This structure, if you are unfamiliar with versioning APIs in Win32®, is obtained by callingGetFileVersionInfoSize and GetFileVersionInfo. (These are also available under Microsoft Windows version 3.1.)

Note   This article does not currently include any information about navigating through the version information to locate this key. When available, that information will appear in the last section of the article.

Once the container has verified that the .DLL is, in fact, self-registering, it can go ahead and load that .DLL withCoLoadLibrary, get a pointer to DllRegisterServer using the Win32 APIGetProcAddress, and call the registration function through that pointer. If registration succeeds, all the objects supplied from the server will be available to containers.

If the container has a reason to unregister an object (for example, if the user decides the control is not worth using anymore), the container can useGetProcAddress to find and call DllUnregisterServer. The server, in response, will remove its registry entries created inDllRegisterServer. This is especially useful for developers to ensure that registry information is clean and consistent—they can unregister a control and re-register it to remove any potential glitches in the registry.

Self-registering EXE servers

Because you cannot just arbitrarily load an .EXE file and call GetProcAddressto find a function, the simple model for .DLLs doesn't work for .EXE servers. Instead, OLE Controls specifies two command-line flags: /REGSERVER and /UNREGSERVER. If an .EXE is launched with either of these flags it is instructed to perform the registration or unregistration and terminate.

A container can determine if an .EXE is self-registering using the same version information as .DLLs.

Registry entries of interest to containers

For the most part, the registry entries for a control are like those of any other embeddable object: The control registers a ProgID key under which it stores its CLSID, and under the CLSID key it stores its "InProcServer[32]" entry along with verbs, MiscStatus, and so on. There are a few keys that are of specific interest to containers.

First is the "Insertable" key. This may be registered under the control's ProgID, as well as under its CLSID, but is not required for all controls. Existing compound document containers check for the Insertable key in either location in order to populate their Insert Object dialog boxes. If a control includes this key, it indicates that it can be inserted into non-control container applications. Some controls, such as timers (which are practically useless without their events), would not show themselves to regular container applications.

If a control only wants to appear to control containers, then it does not include the Insertable key, but instead includes a key named "Control" registered only under the CLSID key. Containers can use "Control" to populate an "Insert Control" dialog box that sits alongside "Insert Object." There are some cases for special-purpose controls that do not want to be exposed to any container other than those that have hard-coded the control's CLSID, and in such cases there may be neither Insertable nor Control in the registry. Be prepared for such.

The other interesting keys added for controls are called "ToolboxBitmap" (Win16) and "ToolboxBitmap32" (Win32), which each have a value of a path to a DLL and a resource identifier. This works on the same order as the "DefaultIcon" entry for compound documents. This new key allows a container to extract a 16-by-16 button face image (16 * 15 in Win16?) for each registered control so that the container can create a toolbar or toolbox populated with controls, such as that from Visual Basic shown in Figure 4. Usually the DLL registered for ToolboxBitmap will be the same as the control DLL itself, and the container need only callFindResource and others to obtain the bitmap.

Figure 4. A toolbox with buttons for each registered control. This toolbox, taken from Visual Basic 3.0, uses VBX controls instead of OLE Controls, but the UI for a toolbox in a control container is exactly the same.

Note   At this time, this article does not cover implementation details about registering controls or providing user interface to locate or register them. The CPatron sample simply leverages its Insert Object dialog for creating controls, meaning that tools like TSTCON and REGSVR in the CDK are necessary to register them. Please see the CDK for more information about TSTCON and REGSVR.

Storage extensions

Because controls are potentially very lightweight objects with few storage requirements, OLE Controls introduces additional features of persistent storage to handle both objects that persist to streams, as well as the ability for a container to save control information in a straight text stream.

Objects that persist to streams

In the normal scheme of compound documents, an object implements the IPersistStorage interface for persistent storage purposes, and the object is given an entireIStorage into which it can write its information. Although this works well for heavyweight compound document objects, it is potentially an overkill for simple controls such as labels, which use very little storage space. For this reason OLE Controls allows an object to support persistence to an IStream as well, by implementingIPersistStreamInit instead of IPersistStorage. The object gets to choose which persistence model it would like to use—it is then up to the container to handle all the cases appropriately.

Copy
interface IPersistStreamInit : public IPersistStream    {    HRESULT  InitNew(void);    }

IPersistStreamInit is defined in OLE Controls because although IPersistStoragehas an initialization function, IPersistStream does not. IPersistStreamInit is thus a simple extension ofIPersistStream to accommodate initialization. If a control implements both interfaces (which it should, to be the most flexible), anInitNew call in one is equivalent to an InitNew call in the other.

Again, a container should be prepared to handle objects that have IPersistStreamInit as well as those withIPersistStorage. More on this in a moment. First, however, we need a brief look at initialization functions.IPersistStorage::InitNew, IPersistStorage::Load, IPersistStreamInit::InitNew, andIPersistStreamInit::Load are considered "initialization" functions—that is, they must be the first functions called after object creation in order to properly initialize the object.

For OLE Controls, there is a new MiscStatus bit called OLEMISC_SETCLIENTSITEFIRST. This bit applies only to control (and compound document objects as well) inasmuch as it relates to theIOleObject::SetClientSite function. With this bit set, the object indicates that it would like to useSetClientSite as its initialization function even before IPersist[Storage | StreamInit]::[InitNew | Load] is called. The reason is that a control may want to retrieve some of its container's ambient properties before loading, and the control needs the container's IOleClientSite pointer through which to QueryInterface for the ambient propertiesIDispatch.

However, this part of the OLE Controls spec is somewhat unclear and is problematic, so do not consider it final at this time. Given what I just said, a container generally will handle new object creation in this manner:

Copy
ObjectCreationFunction    {    Get CLSID    Create the object of the CLSID and ask for IUnknown        if (OLEMISC_SETCLIENTSITEFIRST)        {        QueryInterface(IOleObject)        IOleObject::SetClientSite        }    if (QueryInterface(IPersistStorage))        IPersistStorage::InitNew    else        {        Create an IStream in the object storage        if (QueryInterface(IPersistStreamInit)            IPersistStreamInit::InitNew        }    }SaveObject    {    if (QueryInterface(IPersistStorage))        {        IPersistStorage::Save        IPersistStorage::SaveCompleted        }    else        {        if (QueryInterface(IPersistStream))            {            Create stream in the object storage if needed            IPersistStream::Save            IPersistStream::SaveCompleted            }        }    }LoadObject    {    Create object uninitialized    if (OLEMISC_SETCLIENTSITEFIRST)        {        QueryInterface(IOleObject)        IOleObject::SetClientSite        }        if (QueryInterface(IPersistStorage))        IPersistStorage::Load    else        {        if (QueryInterface(IPersistStream))            {            Open object stream            IPersistStream::Load            }        }    }

On the surface this looks good enough: We use the right interfaces for saving and loading the object, taking it upon ourselves as the container to create and manage a stream for the object if it only supportsIPersistStream[Init]. However, there are some problems that are not yet resolved as of the writing of this article. A few things, however, can be said. If you have an object that is insertable into non-control containers, then you have to supportIPersistStorage anyway, and it's a waste to support IPersistStream (and in fact, you shouldn't support the latter interface). If you use the cache in any way (so your control doesn't end up looking like a gray box when your DLL isn't available), the cache exposes IPersistStorage and the object must support it too. If the control is only insertable into control containers and doesn't use any caching, the control can choose to implement onlyIPersistStream[Init]. Some of the remaining issues surround the use of OLEMISC_SETCLIENTSITEFIRST, inasmuch as functions such asOleCreate and OleLoad are not sensitive to this flag, and passing anIOleClientSite to them is optional anyway. The internals of these functions are not documented either, so it's difficult for a container to replace itsOleCreate and OleLoad calls with CoCreateInstance. A little more work is obviously needed in this area.

Save As Text

Some control containers may want to save their form description files as straight text, including all the container's positional and state information as well as the object's persistent state. Visual Basic, for example, is just such a container—a .FRM file from VB looks something like the following:

Copy
VERSION 2.00Begin Form ControlDialog   Caption     =  "Automation Controller"   ClientHeight  =  1665   ClientLeft   =  7035   ClientTop    =  1710   ClientWidth   =  3795   Height     =  2040   Left      =  6990   LinkTopic    =  "Form1"   ScaleHeight   =  1665   ScaleWidth   =  3795   Top       =  1380   Width      =  3885   Begin CommandButton Animate      Caption     =  "&Animate..."      Height     =  495      Left      =  240      TabIndex    =  4      Top       =  960      Width      =  1095   End...

For this reason, OLE Controls defines how a control should prepare its persistent data for serialization as text. In short, the control provides, through itsIDataObject::GetData function, a property set that the container can convert to text. To reload the control, the container reads the text, parses it into a property set, and callsIDataObject::SetData. Therefore it is the responsibility of the container to determine the actual text representation—the control simply says what data it wants to store.

Remember that a property set in the context of persistent storage is not the same as properties of the control as handled through automation nor does it have anything to do with property pages. A property set is basically a flexible data structure and is fully documented in Appendix B of the OLE 2.0 Programmer's Reference. There is also an article titled “OLE Property Sets Exposed" that is worth reading. The OLE Controls specification has the gory details about what values a control might put into this property set (and there are still some open question in the specs I'm looking at, so this feature is obviously incomplete at this time). The point, though, is that if a container is using a flat text file for its storage instead of a compound file, it needs to be able to handle property sets.

Because this article is incomplete at this time, we will not discuss it further, nor does the CPatron sample do anything with Save As Text.

Automation extensions

The extensions to OLE Automation found in OLE controls come under two headings: technological and societal. On the technical side, OLE Controls adds one new interface,IProvideClassInfo, through which a client can access an entire object's type information. OLE Controls also adds a few extensions to type libraries themselves, in the form of new Object Definition Language (ODL) attributes. On the social side, OLE Controls defines a number of additional standard methods and properties for objects along with classifications for such items. These additions apply mostly to controls but may be generally applicable to other automation objects—therefore they amend the existing property and method name standards as described in Volume 2 of the OLE 2.0 Programmer's Reference.

The IProvideClassInfo interface

Normally if a client of an object wishes to retrieve the type information for an object, it has to retrieve the type library from the object's resource. This type information for the entire object describes thecoclass of the object as opposed to the dispinterface information available fromIDispatch::GetTypeInfo. In other words, if a client has an object's IDispatch pointer it can, at best, retrieve the type information for the dispinterface attached to thatIDispatch interface. Through IDispatch there really isn't a way to get at the coclass information for the object as a whole, through which the client could look at all the dispinterfaces implemented by that object.

To make a client's life easier, because a control container needs the entire object's type information for various purposes, OLE Controls defines the newIProvideClassInfo interface:

Copy
interface IProvideClassInfo : public IUnknown    {    HRESULT  GetClassInfo(ITypeInfo **);    }

When a client calls GetClassInfo it obtains an ITypeInfo pointer for the whole object, as opposed to anITypeInfo for a single dispinterface. Using this pointer, the client can navigate through the entire type library, looking for specific dispinterfaces with specific attributes (such as the newSource and Default). This may not seem important now, but it is critical in the mechanism through which a container connects to a control's event set, as described in the "Connectable Objects" section.

You might wonder why the object itself doesn't directly implement ITypeInfo. The reason is that the object will typically have a type library already and theITypeInfo interface that is needed here is readily available from that library. Second, the type information is conceptually part of a type library, not another interface on an object, so there must be a separation with regard toQueryInterface. Finally, implementing ITypeInfo is nontrivial, so asking objects to do this would make not a few people quite angry.

ODL extensions

For the purposes of event and property handling, OLE Controls defines a few extensions to the type library technology in OLE Automation. This really doesn't affect automation functionally, but extends the Object Definition Language (ODL) used to create an object's type library. A control uses a type library to describe its properties, methods, and events to willing containers.

OLE Controls adds a number of attributes to ODL as described in the spec. These areBindable, RequestEdit, DefaultBind,DisplayBind, Licensed, Source,Restricted, and Default. The first four attributes apply to specific properties; the latter four apply to coclass entries in the type library instead, that is, they apply to dispinterfaces as a whole rather than specific portions of those dispinterfaces.

Some of these new attributes build a necessary base for the Properties technology described below.Bindable means that the object that implements a property with this attribute will send an "OnChanged" notification (seeIPropertyNotifySink) when it changes. RequestEdit means that the object will send a "RequestEdit" notification (again, seeIPropertyNotifySink) before changing the property, allowing the property sink to cancel the change.DefaultBind and DisplayBind are important for what are called "data-bound" controls that are tied to some section of a database as a source for their data, and these attributes specify certain characteristics for handling these properties.

Of more interest to control containers is the Source attribute. Any dispinterface marked as such in an object's type library means that the control doesn't implement this interface itself—rather, it is marking this interface as one that it would like to call in some other sink object. This is called an "outgoing" interface. In other words, the object uses theSource attribute to define interfaces it would like to call when, say, events occur. While an object can specify any number of outgoing interfaces it can only mark one asDefault as well. The Default Source dispinterface in an object's type information is defined as the object's event set (we'll see this in "Events" below).

Finally, Restricted is an attribute that a control can assign to an interface to hide it from macro programming languages. That is, restricted interfaces cannot be called from arbitrary automation client code, because a client tool does not expose this interface to the programmer. Sometimes an interface is only to be used internally by clients that know more about the automation object than a generic programming client would, so this attribute allows the object to protect itself.

Additional property and method standards

First of all, OLE Controls defines a categorization scheme for methods, properties, and even events (event standards are briefly mentioned in the "Event Standards" section, as you might expect). Categorization is done using two qualities: location and variety.

  • Location is nothing more than a fancy way of describing where a property or method is implemented. There are three locations: control, extended, and ambient.
    • Control properties are, as you might have guessed, implemented on the control.
    • Extended properties are those that a container implements on its own "extended controls" that augment the properties from an actual control.
    • Ambient properties are those that the container implements on its own site objects, providing an environmental description to the controls in those sites.
  • Variety is a way to describe how strictly an implementor of a property must follow the contract defined for that property.
    • Standard properties are those that, if you implement them, you absolutely must guarantee a standard behavior. These properties are identified by negative DISPIDs, not by name.
    • Common properties are more lax than standard ones—they have a suggested, but not a strict, behavioral contract.
    • Everything else falls under the other properties category, meaning that they are specific to the implementation of the control or container.

The OLE Controls specification lists a number of standard and common properties for controls, standard extended control properties, and standard ambient properties. It does not define any common extended control or ambient properties, nor does it define any "other" properties whatsoever. Microsoft does reserve the right to define new standard and common properties in the future, but you are free to define any "other" properties you want.

This article will not repeat the tables of the OLE Controls-defined standards, which are already in the spec.

Connectable Objects

"While OLE 2 COM defines a general mechanism (IUnknown) for objects to implement and expose functionality in interfaces, it does not define a general method for objects to incorporate external interfaces. That is, COM defines how incoming pointers to objects (pointers to that object's interfaces) are handled, but it does not have an explicit model foroutgoing interfaces (pointers the object holds to other objects' interfaces). Instead, ad hoc solutions are invented where needed."

So begins the section on "Connectable Objects" in the OLE Controls specification. The problem is that the only existing notification mechanisms in the core OLE technologies (referred to as the "ad hoc solutions") are very limited in scope. The only sink interfaces in the core OLE are IAdviseSink and IAdviseSink2,whose members are only applicable to IDataObject, IOleObject, andIViewObject[2]. This is not an extensible architecture; hence the need for Connectable Objects.

Connection points

A connectable object is one that knows all of the possible outgoing interfaces it would like to expose what is called a "connection point" for each of those outgoing interfaces. Each connection point is conceptually another object with one interface,IConnectionPoint as illustrated in Figure 5. I say “conceptually” because the main object itself doesn’t have to explicitly create separate sub-objects for each connection point. It can easily implementIConnectionPoint once on itself that can handle any outgoing interfaces. Objects that do so, however, must not returnIConnectionPoint from its main QueryInterface, and the QueryInterface function inIConnectionPoint should not return any pointers other than IUnknown andIConnectionPoint, thereby making the connection point interface look like a different object. This is something on the same order as handlingIOleInPlaceActiveObject.

Copy
interface IConnectionPoint : public IUnknown    {    HRESULT  GetConnectionInterface(IID *);    HRESULT  GetConnectionPointContainer(IConnectionPointContainer *);    HRESULT  Advise(IUnknown *, DWORD *);    HRESULT  Unadvise(DWORD);    HRESULT  EnumConnections(IEnumConnections *);    }

Figure 5. A connection point object

So, for example, if some object wanted to expose two outgoing interfaces, IOutOne and IOutTwo, it would conceptually contain two separate connection points for those interfaces where each connection point is tied to a single outgoing IID—the one returned fromIConnectionPoint::GetConnectionInterface.

When a client wants to connect to an object's outgoing interface, that client instantiates an object called the "sink," which implementsthe outgoing interface. The client then locates the appropriate connection point for that interface and callsIConnectionPoint::Advise, passing to it the sink's interface pointer. The connection point holds onto this sink pointer (returning a connection "key" in the DWORD * parameter), and when the right events occur, it calls the appropriate member of that outgoing interface on the sink. Thus the client receives the function call. This relationship is illustrated in Figure 6.

Figure 6. A connection point connected to a sink object

Note that a single connection point is capable of "multicasting"— that is, capable of connecting to multiple sinks, as shown in Figure 7. Whenever any function in one sink is called, the same function in every other sink is called as well.

Figure 7. A connection point connected to multiple sink objects must "multicast" outgoing calls.

When the client is no longer interested in receiving these calls from the object, it callsIConnectionPoint::Unadvise (with the DWORD connection key from Advise), which terminates the connection. A caller can also learn of all the current connections to the same connection point throughEnumConnections, which returns an enumerator that enumerates IUnknown pointers that are each tied to implementations of the outgoing interface.

The other function, GetConnectionPointContainer, returns a pointer to an interface calledIConnectionPointContainer that is implemented on what is called (you guessed it) a "connection point container." This allows navigation from a connection point to the full object that manages it and any other connection points on that same object.

The "Connection Point Container"

The previous discussion about connection points is all well and good, except for one minor detail that I intentionally glossed over. I said that the client "locates the appropriate connection point for [the outgoing] interface," which begs the question, "How does a client locate the connection point?"

The answer is the IConnectionPointContainer interface, which an object (also called the "source" in this context) implements alongside all its other interfaces (meaning it is available throughQueryInterface from the object’s main IUnknown).

Copy
interface IConnectionPointContainer : public IUnknown    {    HRESULT  EnumConnectionPoints(IEnumConnectionPoints *);    HRESULT  FindConnectionPoint (REFIID, IConnectionPoint **);    }

Figure 8 illustrates the relationship between the IConnectionPointContainer interface on an object and its conceptual connection points.

Figure 8. A connection point container internally maintains all the individual connection points that are available through theIConnectionPointContainer member functions.

With this interface, a caller can either try to locate a specific connection point for a known outgoing interface IID usingFindConnectionPoint, or can enumerate all the possible outgoing interfaces for the object usingEnumConnectionPoints. This latter function supplies an enumerator with the interfaceIEnumConnectionPoints which the caller can use to retrieve all the IConnectionPoint interfaces for the object (that is, all the connection points themselves). With eachIConnectionPoint, the caller can then use GetConnectionInterface to learn the IID of that connection point.

You might wonder why there is a conceptual separation of the connection point and the connection point container. It would have been possible to combine the two to eliminate the individual connection points and just put theAdvise members in the connection point container. However, this does not allow for extensibility of the connection point itself, which may, in the future, want to implement additional interfaces. OLE is meant to be fully extensible, and this is just another manifestation of that principle.

The sink interface passed to IConnectionPoint::Advise must have a QueryInterface that responds to the IID of the connection point. This is especially important for connection points that handle dispinterfaces, asQueryInterface must respond not only to IUnknown and IDispatch but also to the connection point IID.

Note   The specifications mention some considerations surrounding aggregate objects andIConnectionPointContainer. This information is buried in the "Implementing Events with Connections" section, but is important mostly to the Connectable Objects, and belongs there.

This is all well and good again, but begs another question: "How does a client know the IID it wants to connect to?" Excellent question! There are two possible answers:

  • The client assumes that the object supports a connection point for an interface known at compile time. For example, control containers assume that controls support theIPropertyNotifySink interface as an outgoing interface, and can pass IID_IPropertyNotifySink directly to FindConnectionPoint (as we'll see in the "Properties" section).
  • The client uses the object's type information to find the IID of an outgoing dispinterface. A control provides the IID of its event set in this fashion.

The first answer is easy to work with—it's like passing an IID to QueryInterface. The second is a little trickier, because as a client you need a mechanism to look for the IID you want, without having to load the object's type library from its DLL or EXE. That mechanism is exactly what the IProvideClassInfo interface was created for, as described earlier under "Automation Extensions."The ITypeInfo interface returned from IProvideClassInfo::GetClassInfo is the interface on the object's entire type library. With this pointer you can callITypeInfo::GetTypeAttr to look for an interface with the Source attributes and callGetRefTypeOfImplType and GetRefTypeInfo to retrieve the ITypeInfo interface for that particular interface. Outgoing interfaces can be standard COM type interfaces or dispinterfaces. Only one, however, can be marked with theDefault attribute that makes it the primary “event set.”

Confusing? Well, we'll see how this actually works when we look at the control container's implementation later on for connection to an object's event set. The important point here is that once you have anITypeInfo for a dispinterface, you can call ITypeInfo::GetTypeAttr, which returns a pointer to a TYPEATTR structure, and in that structure is a field calledguid, which is the IID of that dispinterface. This is the IID you want to pass toIConnectionPointContainer::FindConnectionPoint to start the process of connecting your sink interface to an object's event set. In fact, we're now in position to fill out our knowledge of the events mechanism.

Events

If you've read the previous sections you already know two key points about events:

  • An object's "primary event set" is defined as the interface in the object's type library that is marked with bothDefault and Source attributes. In the later section on implementing a control container, we'll see exactly how to find this particular interface.
  • A sink connects its implementation of an event set by using connection points, and retrieves the IID of event sets by navigating throughITypeInfo interfaces looking for those marked Source (andDefault). Again, in the implementation section we'll see how to do this.

There are some additional points we have not yet seen:

  • An object can actually have any number of other event sets or outgoing interfaces. These can be dispinterfaces (where each is marked with theSource attribute in the type library) or other predefined vtable-based interfaces with a known IID and known function signatures. Only one event set, however, can be the default, and thus only one is the primary event set.
  • Outgoing interfaces are not necessarily event sets just because they are outgoing.IPropertyNotifySink is an example of an interface that relates to the Properties technology instead of events. Usually outgoing dispinterfaces are event sets, but other vtable-based interfaces are not.
  • The "sink" object implementing the event set is conceptually a stand-alone object contained inside the sink. For convenience, however, the events dispinterface is readily implemented on the site object itself, but this requires thatQueryInterface must be carefully implemented on a sink. For example, a site object in a control container will have anIDispatch for ambient properties; this is the IDispatch pointer that is returned fromQueryInterface through, say, IOleClientSite. When the events IDispatch is part of the site object as well, there seems to be a conflict, because there can be only oneIDispatch seen through QueryInterface. By definition, the site'sQueryInterface handling of IDispatch returns the ambient propertiesIDispatch. A QueryInterface on the events IDispatch responds only toIID_IUnknown, the events IID_IDispatch, and the event set IID, isolating it from the rest of the site.

Note also that a sophisticated control container should locate all event sets—that is, all "source" interfaces for a control—and allow the end user to attach code to any event in any event set. This allows controls to separate their events into logical groupings instead of forcing them into a single interface. CPatron does not demonstrate this, but you should be able to readily expand the idea of looking for the "default source" interface to all "source" interfaces.

It is highly convenient for event sets to be dispinterfaces because an arbitrary sink can provide all the necessaryIDispatch entry points at compile time. This means that a sink doesn't have to figure out how to dynamically create function entry points in memory with the proper calling conventions and stack handling of parameters in order to use events. ImplementingIDispatch is easy, on the other hand, because all the function signatures are known at compile time. So just the presence of the interface allows a source of events to fire those events to the sink, and the sink will receive theIDispatch::Invoke calls. It is here that the interesting work takes place.

If the sink doesn't want to do anything with events, it can simply ignore all Invoke calls but return NOERROR from Invoke, meaning that it "handled" the event (it just didn't do anything with it). The source is happy because it was able to fire the event, and it really doesn't care what happens with that event—it was "handled." So the sink then has the freedom to pick and choose what events it will assign to some other actions.

In the realm of controls, the control container (which implements the sink) can present the end-user (a programmer) with the list of events that will come from the control. The user can then write code to attach to those events, where that code can do anything within the scope of the container. CPatron, for example, allows the user to assign a different system beep to each event—mundane, yes, but it illustrates the technique. Whenever CPatron sees a DISPID in its eventsIDispatch, it looks for an assigned system beep for that ID and generates the sound if one exists. More sophisticated containers will do pretty much the same thing, except that instead of mapping event IDs to beeps, they map the IDs to user-defined code. If code exists, the container executes it. If no code exists, nothing happens. So the whole process is one of the control calling the sink'sIDispatch, which then executes user-defined code that is provided through some type of language or other programming facility. This process is what the OLE Controls spec calls "Language Integration."

Event persistence

As far as a control is concerned, its events have no persistent state, they just exist to be fired when a sink is connected to the events connection point. The only persistent information surrounding events relates to the actions assigned to events by a control container. It is therefore the container's responsibility to save, as part of the form or document, the mappings from event ID to user-defined actions, and the container can do this in any manner it chooses—OLE Controls defines no standards for event mapping serialization.

One consideration in keeping persistent event information is that a control's event set might change from instantiation to instantiation. This is perfectly legal, and a container must be robust enough to handle such cases. This means that when an event no longer exists for a control it will never be fired, and the container should do something intelligent with whatever actions were assigned to that event. CPatron, for example, just discards the event mapping when the document is next saved. Containers that attach user-defined code to events should take that code and store it as a global function instead of as an event handler, rather than just junking the code. Programmers get very angry if code they spent a long time writing disappears unexpectedly and without explanation. Alternately, a container may warn the user that the event set has changed. In any case, a container must be robust in all circumstances, both when new events appear and when existing ones disappear. (Note that technically a control should not change its event set without changing the interface ID of that outgoing interface. However, a container may not save the IID persistently, especially if it always looks for the “default source” interface. The IID therefore becomes somewhat irrelevant, and the container must handle changes robustly.)

Event types and standards

The OLE Controls specification contains sections describing types of events—Request,Before, Do, and After—as well as standards for common events that should be the same in all controls that might generate them ("Click" is an example). The specification describes in detail the meaning of the event types and standard events, as well as the mechanism through which a container can control whether or not a control performs default handling of aDo event. In addition, there are some notes about event sequences—situations may arise in which a control ignores changes to properties or calls to methods if they are done in response to an event that is nested in a sequence of related events. A container must be robust in the event (pun only vaguely intended) of failures.

At this time, this article will not examine these issues any further. I encourage you to study the specs if you are interested.

Property change notification

Although OLE Automation handles properties very well, the one thing it doesn't provide for is a method through which a client of an object is notified of a change in an object's "bindable" properties. This is important for a control container in case it needs to update its own state or some other data when such changes occur—for example, if a "data-bound" control (one whose data is linked to a database query or some such thing) is displaying records from a database and allows the user to change the data in the control. Such a change should trigger a notification to the container so that the container can make an update to the real underlying database.

An object that includes properties marked with the Bindable attribute in its type library will be capable of generating a change notification for that property. To facilitate this, the object exposes a connection point for the new interface defined in OLE Controls called IPropertyNotifySink:

Copy
interface IPropertyNotifySink : public IUnknown    {    HRESULT  OnChanged(DISPID);    HRESULT  OnRequestEdit(DISPID);    }

This interface has two kinds of notification. The first happens with OnChanged, which informs the sink that the property with the given DISPID has already changed, so that if the object was asked for that property within the implementation ofOnChanged, the object would return the new value. As far as the sink is concerned, it doesn't care what caused the change, only that the change occurred—the object should callOnChanged regardless of the reason for the change.

The second kind of notification handles property changes that the sink (or client, or container) is allowed to cancel before the property is actually changed. When a property is about to change, the object callsOnRequestEdit with the DISPID of the property, to which the sink responds with S_TRUE or S_FALSE (or any other error code). Returning S_TRUE (or S_OK, or NOERROR) means that the sink will allow the change to occur. The change then takes place, which generates an OnChanged call. If S_FALSE or any other error is returned, the sink willnot allow the change, and the object must prevent the change from happening altogether. If an object is to implement cancelable property changes, it must guarantee the contract andOnRequestEdit must be called before the change happens. It is not a strict requirement, mind you, that properties be cancelable—if they are not, for whatever reason, the object can just change them and sendOnChanged.

A special parameter to both OnChanged and OnRequestEdit is DISPID_UNKNOWN. This means that a large number of properties may be changing at once, in response to which the sink should iterate through all the properties of interest to it and check for changes.

Note also that changes are not sent from objects that are being created, initialized, or deserialized—notifications are sent only for changes that occur to fully constructed and initialized objects.

A note about data validation: You might think that OnRequestEdit could be used for validation in certain controls like a text box or combo box. In other words, it seems that a container might useOnRequestEdit to determine when validation should take place, so that if validation fails, the container can return S_FALSE fromOnRequestEdit. Not so! The catch is that when OnRequestEdit is called, the container has no access to the new value; only the old. Therefore this function cannot be used for validation. If a control wants to support validation at all (only a few types will), that control will support relevant events such as "Validate" or "BeforeLoseFocus." The container would allow the user to place validation code in the handlers for these events.

Property pages

[This subject deferred because I have not yet worked on property pages. Please see the OLE Controls spec for details about property pages.]

OLE Controls

The glue that binds together all the OLE technologies, extensions to them, and the new technologies described above, is OLE Controls. As I mentioned before, "OLE Controls" in a strict sense is the set of extensions above compound documents for dealing with control-type objects. "OLE Controls"—the specification is simply where all this new stuff is defined. My purpose in saying this is to emphasize the fact that you do not need a proper control object to use events, connection points, property pages, and so on. Only when you add the final touches of a few extra interfaces do you enter the realm of OLE Controls proper.

This section will first look briefly at the control-specific interfaces IOleControland IOleControlSite. After that, we'll take a look at how specific members of these interfaces provide the needed mechanisms, such as keyboard handling, to make control integration complete. To be perfectly honest, there's not a tremendous amount of content here, no more than some of the other technology sections above. Much of what makes a control involves the COM, Automation, and Properties and Events. Here we wrap it all together into a meaningful unit, leaving some small considerations, such as Z-order of in-place windows, for the implementation section.

The IOleControl and IOleControlSite interfaces

These two new interfaces extend the communication that normally happens between a container and an object in standard compound document scenarios. A control will implementIOleControl alongside IOleObject and other interfaces, while a container will implementIOleControlSite alongside IOleClientSite and friends:

Copy
interface IOleControl : public IUnknown    {    HRESULT  GetControlInfo(CONTROLINFO *);    HRESULT  OnMnemonic(LPMSG);    HRESULT  OnAmbientPropertyChange(DISPID);    HRESULT  FreezeEvents(BOOL);    }interface IOleControlSite : public IUnknown    {    HRESULT  OnControlInfoChanged(void);    HRESULT  LockInPlaceActive(BOOL);    HRESULT  GetExtendedControl(IDispatch **);    HRESULT  TransformCoords(POINTL *, POINTF *, DWORD);    HRESULT  TranslateAccelerator(LPMSG, DWORD);    HRESULT  OnFocus(BOOL);    HRESULT  ShowPropertyFrame(void);    }

The keyboard-related GetControlInfo and OnMnemonic methods ofIOleControl, and the OnControlInfoChanged, TranslateAccelerator,and OnFocus methods of IOleControlSite, are described in the "Keyboard and Mnemonic Handling" section. TheGetExtendedControl method of IOleControlSite is described in the "Extended Controls" section.

That leaves only a few other methods, which we can describe here shortly, inasmuch as the mechanisms in which they are involved are simple. First isIOleControl::OnAmbientPropertyChange, which a container calls whenever it changes an ambient property. DISPID_UNKNOWN is a valid parameter to this function, meaning that an unspecified set of ambient properties has changed. In response, a control will retrieve the new property (or properties) and update its own visual and behavioral state, as appropriate.

You may wonder why this mechanism is used to notify a control of ambient property changes when there already exists a more complicated mechanism, involvingIPropertyNotifySink and connection points, for control properties. The reason is that whereas the properties available on the controls in a form can and will vary widely (especially with "other properties,” which will not be uncommon), the set of ambient properties will not generally change much from container to container. In addition, whereas a container needs notifications from many objects, a control only needs notifications from a single container. Use of the generic property notification scheme is an overkill. This was a conscious decision on the part of the OLE Controls designers for the sake of efficiency and simplicity.This doesn’t in any way preclude a control from implementing IPropertyNotifySink nor does it preclude a container from implementing connection points. There simply has to be a minimal entry point for control support, and instead of using the more costly and complex mechanism, this simple one is provided to minimize the cost of entry for controls.

The other IOleControl member, FreezeEvents, allows the container to turn off (freeze) and turn on (thaw) a control's events. When frozen, the control will not fire its events—it might queue them or discard them, but it won't fire them. Which ones a control saves in a queue (to be fired upon thawing) and which ones it discards depends on the design of the control and the event, but is entirely up to the control.

The other two members of IOleControlSite (LockInPlaceActive andTransformCoords) generally have to do with events as well. LockInPlaceActive(TRUE)tells the container that a control would like to stay in the active state untilLockInPlaceActive(FALSE) is called, primarily to prevent crashing problems when an in-place window is destroyed and messages are still coming to that window. A control will often callLockInPlaceActive before and after an event is fired if destroying the in-place window would cause problems.

TransformCoords handles the problem of presenting a uniform coordinate system to the container through all events, methods, and properties, while allowing controls to choose whatever coordinate system they want. For example, if a "MouseDown" event is fired, the container would like to receive the coordinates in a system meaningful to it instead of whatever system the control is using. Programmers for Microsoft Windows have always taken this for granted—any message or event coming from Windows had coordinates expressed in client-area units. But inasmuch as events are no longer coming from one consistent source with OLE Controls, there needs to be a mechanism through which the container still receives coordinates in a uniform coordinate system. For this reason,TransformCoords is exposed to controls for their use in converting between HIMETRIC and whatever system the container is using. Whenever the control receives coordinates from the container, it can convert them to HIMETRIC. Whenever the control wants to send some of its own coordinates to the container, it can convert them to the container's coordinate system first.

Again, the other functions of these interfaces are described in sections below.

IOleClientSite::RequestNewObjectLayout

Before moving on I want to mention this function briefly. RequestNewObjectLayoutis part of the standard IOleClientSite interface in the core OLE Documents technology. It is not used by compound documents proper, but OLE Controls uses it to let the control tell the container that it would like to change its size. This is a signal for the container to retrieve the control's desired size (IOleObject::GetExtent) and pass it back, when convenient, toIOleInPlaceObject::SetObjectRects.

Container modes and ambient properties

Container applications such as Microsoft Access, Visual Basic, and even the CPatron sample, differentiate between various "modes" of the container. These applications have a "Design Mode" in which the user, called the "designer" (who is in some cases a programmer), can lay out inactive controls on a form or document and assign actions to control events. They also have a "Run Mode" in which the controls are all active and events are functioning. To express the modality of the container, OLE Controls defines two standard ambient properties that specify how the control should behave in such modes, as opposed to specifying the “mode” itself, which is ambiguous, whereas expressing the desired effect is not.

  • UserMode. When TRUE, the container is in a mode in which the end user (not necessarily a designer) is interacting with controls; when FALSE, the user (usually the designer) is laying controls on a form. A control uses this flag to modify its behavior to hide things that may not be appropriate for an end user but are needed by a designer. The OLE Controls spec indicates other uses.
  • UIDead. When TRUE, indicates that controls should be dead as far as user input is concerned. That is, the control should ignore mouse clicks and keystrokes, and not change the mouse cursor even when the mouse is over the control window. A container such as Visual Basic would set this flag to TRUE when the programmer stops the program during execution—the container is not in design mode, yet not in run mode either; it simply wants the controls to be inoperative. When this flag is FALSE, controls act normally.

On a related note are the two ambient properties ShowGrabHandles andShowHatching. In normal in-place activation, a container is responsible for drawing hatching and handles when an object is inactive; the object is responsible when it is either in-place active or UI active. If you think about it, however, grab handles and hatched borders around controls would look ridiculous in a form with many controls—as you press this or that button or work with this or that control, an annoying hatch border moves around and sizing handles appear where they are really inoperative. In other words, it would violate the way we understand user interface to operate. When a container is in run mode, it really doesn't want the control to be able to resize itself.

Therefore the container has control over a control's behavior with these two ambient properties. If they are FALSE, the control suppresses its grab handles and hatching when UI active, regardless of all other ambient properties. If they are TRUE, the control is responsible for showing these elements when UI active. Controls check this property whenever they transition to UI active. CPatron, as an example, sets these two to FALSE whenever it is in run mode, and inasmuch as controls are never UI active outside of run mode, this effectively means that the controls never draw either element. In design mode, with inactive objects, CPatron draws its own handles. Other containers that keep objects UI active or in-place active even in design mode may handle these properties differently.

Miscellaneous status bits, new and used

OLE Controls adds a number of new "MiscStatus" bits to indicate additional behavioral characteristics about controls. Two important flags that exist in the core OLE that are also important for controls are OLEMISC_INSIDEOUT and OLEMISC_ACTIVATEWHENVISIBLE. A control container should honor these flags well even for non-controls, but note that controls are normally marked with both. "Inside-out" simply means that the control can be in-place active without being UI active, which is critical for controls. "Activate-when-visible" means that a control should always be in-place active when it is visible, except in containers where "design mode" means that all controls become inactive. Normal compound document containers always honor activate-when-visible; with their different modes, control containers have conditions under which the flag can be ignored.

Like OLEMISC_ACTIVATEWHENVISIBLE is the new addition OLEMISC_ALWAYSRUN. This basically tells the container to always callOleRun on the object regardless of its visibility, but doesn't mean the object is active in any way. Having it running, however, means that control code can be loaded to handle events or other necessary actions. Another new related flag is OLEMISC_INVISIBLEATRUNTIME, which means that when a container is in "run time" this control has no visuals whatsoever. For example, a timer control is visible during design-time, but disappears in run time. A container should be sure to not draw anything for such controls.

The new OLEMISC_NOUIACTIVATE flag goes along with a control marked OLEMISC_INSIDEOUT to indicate that it can only ever become in-place active and doesn't care about UI active. A control that doesn't need menu space or toolbar space is a good example, as it is still quite usable through its own in-place window and its properties and methods.

Finally there are OLEMISC_ACTSLIKEBUTTON and OLEMISC_ACTSLIKELABEL, which specify a slightly different behavior for keyboard processing with these controls. To explain them accurately we need to look at keyboard handling.

Others flags include the following:

  • OLEMISC_ALIGNABLE—indicates that a control can be aligned on some side of the container, but I'm unclear as to what this means.
  • OLEMISC_IMEMODE—I have no idea what IME mode is, so I won't try to say anything about it.
  • OLEMISC_SIMPLEFRAME—means the control has ISimpleFrameSite but I can't find documentation for this interface, so I won't try to make it up.
  • OLEMISC_SETCLIENTSITEFIRST we've already seen.

Keyboard and mnemonic handling

Note   The information here is based on marginal guesswork and no real experience in implementing code (this part is not implemented in CPatron at this time), so there are bound to be assumptions or statements that are completely wrong. In addition, the controls spec has some discussion of an improvedOleTranslateAccelerator that is used by EXE-based controls so that the container can receive wildcard keystrokes like an ALT+ key combination. However, this discussion applies only to in-process objects whereOleTranslateAccelerator is irrelevant.

The keyboard support included with normal in-place activation only provides for handling keystrokes in the currently UI active object. Although this works great for compound documents, where there's usually only onein-place active object at one time (which is also UI active), it doesn't work so well for controls where you have many in-place active objects in the same form or document that would all like certain keystrokes to affect them.

With normal in-place activation, the container will forward any keyboard message to the currently UI active object viaIOleInPlaceActiveObject::TranslateAccelerator. Normally the object will handle whatever accelerators it wants, returning to the container a code that indicates whether processing took place. If the object didn't handle the message, the container gets a shot.

Modifications to IOleInPlaceActiveObject::TranslateAccelerators

This is where the first modification with OLE Controls enters the scene. Instead of the normal implementation ofTranslateAccelerator, the control first processes any keystroke that it wants to override or eat before giving it to either its immediate container (the implementor ofIOleControlSite) or the top container frame (the one calling this TranslateAccelerator). If it doesn't handle the keystroke, it then callsIOleControlSite::TranslateAccelerator instead of returning immediately. If the site doesn't handle the keystroke then the control can again choose to handle it (meaning it gives the first shot for some keystrokes to its immediate container) or the control can return with S_FALSE which gives the keystroke to the outer container.

This mechanism allows controls to give different priorities to different keys. An edit control might always override a CTRL+C (Edit/Copy) to copy text, overriding its immediate container. The immediate container itself could be an edit control which would take the same Edit/Copy command and perhaps copy the nested object. In other cases, the nested edit control might first let its immediate container process the TAB key, and process the key itself only if that container does not. Allowing a fine granularity of prioritization for specific keystrokes allows the control to do what makes the most sense in all circumstances.

Mnemonic Information for Controls

With normal compound documents the container would not process its own accelerators. With OLE Controls we need to be able to have ALT+ key combinations essentially trigger the primary event for controls, and we need the RETURN and ESC keys to trigger the "default" and "cancel" buttons appropriately. In addition, we also want to allow controls like text editors to "eat" RETURN and ESC if they so choose.

Objects that have keyboard needs outside of what they get when they're UI active must first implement the functionIOleControl::GetControlInfo. A container will call this function to retrieve a CONTROLINFO structure:

Copy
struct CONTROLINFO:    {    ULONG  cb;        //Structure size    HACCEL  hAccel;   //Mnemonics table for the control    USHORT  cAccel;   //Number of mnemonics    DWORD  dwFlags;   //CTRLINFO_ flags    };#define CTRLINFO_EATS_RETURN    1    //Control processes RETURN key#define CTRLINFO_EATS_ESCAPE    2    //Control processes ESC key

The hAccel member of this structure is a global memory handle to an array of ACCEL structures (see the Windows SDK). Each ACCEL structure contains a virtual key or ASCII key code, some flags (for SHIFT, CONTROL, ALT, and so on), and the WM_COMMAND ID associated with the accelerator (this is ignored for OLE Controls). With this table the container knows what specific keystrokes are of interest to the control. For example, a button control might have the text "Press Here" showing, in which case the mnemonic ALT-P would press the button. An ASCII "P" along with a flag for the ALT- key would then appear in the CONTROLINFO for the button.

Because a control's mnemonics can change dynamically—as with a button when the user changes the text or placement of the ampersand in the text—a container must know when to reload the CONTROLINFO structure. This is the purpose ofIOleControlSite::OnControlInfoChanged. A container will normally load the CONTROLINFO when creating or reloading a control, and when the control calls this function the container must reload the structure over the old one.

At run time, then, the container must check all the accelerator tables for each control if the UI active object doesn't handle the keystroke. This is essentially the process of looping through the controls and comparing the keystroke message from the container's message loop to the entries in the CONTROLINFO structure for each control. If a match is found, then the container calls that control'sIOleControl::OnMnemonic function, and the control does its thing. Note that the container can choose to only support certain mnemonic keys, for instance only ALT- keys, instead of arbitrary ones such as CTRL-ENTER.

A container that supports this functionality must expose the ambient property SupportsMnemonics with the value of TRUE. If this ambient property is FALSE, a control may choose to not display underline characters.

The two flags CTRLINFO_EATS_[RETURN | ESCAPE] are important in handling special buttons, as described in the next section.

Default and Cancel Buttons

In Windows dialog boxes there is the concept of the "Default" button and the "Cancel" button. The "Default" button is the button activated when the RETURN key is pressed, regardless of what control actually has the focus at the time. Normally this is whatever button has the focus at the time, or if no button has the focus it is whatever control is marked as the "Default" (in Windows dialog this is the DEFPUSHBUTTON in a dialog script). The "Cancel" button is the same sort of thing but works off the ESC key instead, and is usually just one specific control—that is, it doesn't shift with the focus.

OLE Controls provides mechanisms to handle these types of controls. First of all, controls that act like buttons (that is, those that may understand "defaultness" and "cancelness") mark themselves with the OLEMISC_ACTSLIKEBUTTON bit . In design mode, a control container then provides the programmer with some menu commands or such to mark one such button as the "default" and one such button as "cancel," if desired. These commands should only be enabled if the selected control is marked with OLEMISC_ACTSLIKEBUTTON. Assigning a default and cancel button is like marking one button as a DEFPUSHBUTTON and giving another the identifier IDCANCEL in a typical Windows dialog.

What the container now does with this information depends on a number of factors. Let's take the easy one first: the ESC key. When this key is pressed, it first goes to the UI active control. If that control is marked CTRLINFO_EATS_ESCAPE, we can be assured that it will process that keystroke. Note, however, that the button marked "cancel" in the container doesn't know that it's the "cancel" button, so it will not process this keystroke even if it is UI active. Anyway, if the UI active control does not eat the ESC key, the container checks if it has a button marked as "cancel" and, if so, calls that control'sIOleControl::OnMnemonic. Because the control knows it's a button (it's marked as such), it understands any mnemonic to mean "press me" and will fire its primary event, even if the mnemonic isn't in its CONTROLINFO. That's just part of being a button.

The "default" button is a little more complicated. First of all, there is an added UI element—a thick frame around the button that indicates that it is the default. Second, the "default" button isn't always the one that the programmer marked. Open a normal Windows dialog box, such as File Open, and the initial default button will be the DEFPUSHBUTTON in the template, in this case the OK button. Notice that the OK button doesn't have the focus, but is still the default because it is marked as such. Now hit the TAB key until the Cancel button has the focus. Notice that it is now the "default" button because it has the thick frame, so if you press RETURN you cancel the dialog. Well, don't cancel the dialog; instead, take the mouse and click on one of the listboxes. Notice that the focus has gone to the listbox and the default button has reverted to OK.

This is the behavior OLE Controls allows you to duplicate, and it involves the ambient propertyDisplayAsDefault. Only one site in any given form or document should have this flag set to TRUE at any one time, for when it is set, and the control in that site is a pushbutton, the control will draw itself with a thick border. Therefore:

  1. First, you have to change this ambient property in your sites as focus changes between buttons—you detect focus changes withIOleControl::OnFocus.
  2. If the control that receives the focus (TRUE passes to OnFocus) is marked with OLEMISC_ACTSLIKEBUTTON, set DisplayAsDefault to TRUE and notify the control withIOleControl::OnAmbientPropertyChange.
  3. If the control with the focus is not a button, set DisplayAsDefault to FALSE and notify the control,then set DisplayAsDefault to TRUE for the button that the programmer marked as "Default," again notifying the control.
  4. Finally, whenever any button control that is not the default loses focus, be sure to set the sitesDisplayAsDefault to FALSE and notify the control.

Handling OnFocus and the DisplayAsDefault ambient in this fashion handles the UI element almost correctly[I hope—I haven't tried this in code!]. What still remains is how we handle the RETURN key and non-button controls that eat it. As with ESC, if the UI active control wants to eat the RETURN key, it will eat it. It marks itself with this behavior using CTRLINFO_EATS_RETURN. What a container must do is check to see if the currently UI active control will eat the keystroke—if it does, then we have to turnDisplayAsDefault completely off for all buttons, even the one marked "Default," because we will never see the RETURN key. If the UI active control does not eat the RETURN key, we call theOnMnemonic function of whatever button has DisplayAsDefault currently set (not the one marked as "Default"). The button will, again by virtue of being a button, understand theOnMnemonic call to mean "press me" and so will fire its primary event.

In summary, we handle the RETURN key using three pieces of information: (1) which button is marked as "Default", (2) which button hasDisplayAsDefault set, and (3) whether or not the UI active button has CTRLINFO_EATS_RETURN set. Using this information we handle any button as follows:

  1. If a button has the focus (IOleControlSite::OnFocus(TRUE) is called), set itsDisplayAsDefault to TRUE, otherwise FALSE except in case 3 below. Notify the control withIOleControl::OnAmbientPropertyChange.
  2. If a button is not marked "Default" and loses the focus (IOleControlSite::OnFocus(FALSE)) is called), set itsDisplayAsDefault to FALSE and notify the control.
  3. If a button is marked "Default" and does not have the focus, AND the control with the focus does not have CTRLINFO_EATS_RETURN, set itsDisplayAsDefault to TRUE, otherwise FALSE, and notify the control.
  4. Whenever RETURN is pressed and is detected in the container, call the OnMnemonic function of the control withDisplayAsDefault set. If no control has it set, ignore the keystroke.

Steps 1, 2, and 3 handle the thick border of the default control; step 4 handles the keystroke correctly. You can see now why ESC key handling was so much easier!

Labels

Certain controls may be marked with OLEMISC_ACTSLIKELABEL, and these require special keyboard handling. The user interface for a label specifies that a label never has the focus itself, and that pressing a label's mnemonic key sets the focus to whatever control is "next to" or "attached" to the label. Labels are normally used on listboxes because a listbox doesn't have any caption in which to have a mnemonic; thus pressing the mnemonic in the label sets the focus to the listbox.

A control container must then make a check for OLEMISC_ACTSLIKELABEL before it firesOnMnemonic to a control. If this flag is set, the container should not callOnMnemonic but should instead UI activate (effectively SetFocus) the control associated with the label.

The TAB Key

This isn't too hard to figure out: When the container sees a TAB keystroke, it should move the focus to (UI activate) the next control in the order determined by the layout of the form. Do note that the container should skip any control marked as OLEMISC_ACTSLIKELABEL, OLEMISC_SIMPLEFRAME, OLEMISC_INVISIBLEATRUNTIME, and OLEMISC_NOUIACTIVATE. The latter flag allows the container to determine if a control doesn't understand the idea of UI activation, which is equivalent to focus in OLE Controls. Icons and pictures are good examples of visible, non-label, non-UI active controls.

Exclusive buttons

Only a brief mention here: A control container should handle groups of what are known as "exclusive buttons," where only one button in the group can be "checked" at any given time—that is, radio buttons. The container is responsible for determining where the group starts and stops, and is responsible for making sure that only one button of the group is checked. The container determines if a control is an exclusive button by looking at the type of the object's "Value" property, DISPID_VALUE. (DISPID_VALUE is defined by OLE Automation because it is not specific to controls.) If the type is OLE_OPTEXCLUSIVE, this control is an exclusive button.

The explanation of how a container actually checks for the type is rather lengthy. (The OLE Controls spec was designed to use as much of the existing type information as possible. This usually places the burden on the container instead of the control.) Every type in a type library has its own GUID. The type of a property also has a GUID, so you have to extract the GUID for the type for the DISPID_VALUE property from each control and compare it to GUID_OPTIONVALUEEXCLUSIVE (OLECTLID.H). Those controls that match types are exclusive buttons. (The exact sequence of ITypeInfo calls to retrieve these types and perform the comparison is not covered in this article.)

The container must have UI for assigning the boundaries of the group of exclusive buttons. It then watches for a change in DISPID zero throughIPropertyNotifySink in each of these. When one control is checked, it forcibly unchecks whatever control was checked by changing that control's property.

Extended objects

[This subject deferred for later work. Please see the OLE Controls spec for details about extended objects.]

The CPatron Sample Application

The CPatron sample container is a modification of the Patron example from Chapter 15 ofInside OLE 2. The Chapter 15 sample is an in-place–capable OLE Container application that also supports just about every other feature in OLE, such as Compound Files, Drag & Drop, as well as the various types of compound document objects (embedded, linked), along with conversion/emulation support. Note that there is a lot more source code in this sample than is necessarily pertinent to controls—an OLE Control only requires the container to support embedding and in-place activation as a minimum. Table 3 lists all the important source files in the sample (.CPP, .H, and resource files), their function, and if and how they relate to the control modifications.

Table 3. CPatron Source Files and Their Contents


Source File (* = added for controls)Modified for controls?

Purposeambients.cpp*YesImplementation of the IDispatch interface for the control container's ambient properties.client.cppNoClient-area handling.connect.cpp*YesHelper functions to work with IConnectionPointContainer andIConnectionPoint interfaces as well as functions to locate type information for a control's event set.document.cppYesDocument window handling; contains a few modifications to pass-through control-related commands from the frame window.dragdrop.cppNoDrag and drop helper functions.events.cpp*YesEvents dialog procedure, implementation of a class CEventMap for working with events, and the implementation of the container'sIDispatch interface for handling events.events.dlg*YesTemplate for a dialog that CPatron uses to assign actions to events.iadvsink.cppNoIAdviseSink interface implementation, used for normal compound document features.iclassf.cppNoClass factory implementation for linking-to-embedding support, unrelated to controls.iclisite.cppYesIOleClientSite interface implementation, modified slightly for controls, specifically inIOleClientSite::RequestNewObjectLayout.iconsite.cpp*YesIOleControlSite interface implementation.idropsrc.cppNoIDropSource interface implementation.idroptgt.cppNoIDropTarget interface implementation.iipsite.cppYesIOleInPlaceSite interface implementation.iipuiwin.cppYesIOleInPlaceUIWindow interface implementation for document windows.iolecont.cppNoIOleItemContainer interface implementation used specifically for linking to embeddings.iperfile.cppNoIPersistFile interface implementation used specifically for linking to embeddings.ipropnot.cpp*YesIPropertyNotifySink interface implementation.iuilink.cppNoIOleUILinkContainer interface implementation, used for linking only.page.cppYesCode to handle pages within a document. This file contains some modifications for handling controls, such as displaying the Events dialog.pagemous.cppYesMouse handling code for pages within a document, modified slightly to add additional menu items to right-button popup menus.pages.cppYesCode to handle pages within a document, modified slightly to route some control-related commands from the frame window.pages.hYesHeader file for page-related structures and classes.pagewin.cppNoPage window handling.patron.cppYesFrame window handling code, modified slightly to route control-related commands from the menu to the current page or object.patron.hYesFrame and document window structures and classes.patron.rcYesIncludes events dialog and additional control-related menu items.precomp.cppNoPrecompiled header file.resource.hYesAdditional symbols for events dialog and control-related menu items.tenant.cppYesImplementation of container sites. Many additions and modifications to handle control creation, initialization, and so forth.tenant.hYesDefinitions for added control interfaces and structures.

Besides supporting In-Place Activation, this starting version of Patron also contains support for inside-out objects, which is beneficial, inasmuch as OLE Controls are generally inside-out objects. I do expect that the reader of this document is already familiar with the implementation of compound document containers as well as in-place activation, because that is 70–80 percent of the work involved in making a control container. If you have not looked much into inside-out objects, you can look at the code in the TENANT.CPP source file for the OLEMISC_INSIDEOUT and OLEMISC_ACTIVATEWHENVISIBLE flags. The extra code executed when these flags are set for an object is the code that handles inside-out differences.

The specific modifications made to Patron to turn it into CPatron are marked in the code with//CONTROLMOD and //End CONTROLMOD. All of the files in Table 3 marked with "Yes" in the "Modified for Controls" column will have some comments like this. I'm pretty sure I have all control-specific modifications marked.

There are a few things that CPatron doesn't do at all and a few things it doesn't do well. As far as control support is concerned, CPatron does not implement "extended controls," does not handle buttons/labels/exclusive buttons/default buttons/cancel buttons at all, does not provide for registering controls, and does not include any keyboard handling (RETURN/ESC/TAB/CONTROLINFO) over normal in-place activation use. For example, the TAB key is ignored because CPatron doesn't currently keep an order of the controls on the form other than Z-order, and Z-order changes as you switch the focus. I simply have not added the code to handle this. I also have to say that I haven't really tested this thing thoroughly, especially with the more specific control features, generally from the lack of having controls that use these features.

CPatron implements a "design mode" and a "run mode" but it is different from what an application like Visual Basic does. The only difference between the two modes is whether or not controls are allowed to in-place activate and UI activate. In design mode, in-place activation is turned off, giving the container full control over layout, grab handles, and so on. In run mode, all the controls are activated and work like controls, and CPatron turns off theShowGrabHandles and ShowHatching ambient properties to suppress those UI elements. What is different from an application like Visual Basic is that you can still create new controls in run mode, a feature that you would normally disable in a real forms-based development environment. It would not be hard to disable the UI, but heck, I didn't bother.

In addition, CPatron has some minor glitches, bugs, or whatever you would like to call them that are more annoying than detrimental to functionality. One such bug regards object extents: calls toIOleObject::SetExtent require HIMETRIC units (not MM_HIMETRIC mapping mode—the scale but not the axis orientation), but calls throughIOleInPlaceSite::OnPosRectChange and IOleInPlaceObject::SetObjectRects require pixels in client coordinates. CPatron, furthermore, works internally on MM_LOMETRIC. Somewhere in all the calls to these functions that start inCTenant::RectSet (TENANT.CPP) the coordinates get jumbled and confused to such a degree that a control may not appear in the proper rectangle. This is one of those bugs that you keep looking at and never quite figure out. If you see what’s going wrong and point it out to me, I promise to be really nice to you if I ever meet you in a dark alley somewhere. This happens in the call toCTenant::Select made earlier in CPage::TenantCreate. Select will UI activate the selected object. The reason we want to skip the Show part here is that when we’re in design mode, we disable in-place activation temporarily so a Show verb would pop the control up in a little window! It’s an ugly user interface, so we skip the Show here to suppress it.

CPatron is also somewhat limited as to what it can actually do with a control's events. As we'll see, it can only perform a fixed set of actions when an event occurs and has no language/programming structure in which you might use an event from one control to call a method or manipulate a property in another. Nevertheless I will attempt to describe something about containers that might work with these things in a more sophisticated manner in the next section.

Other than that, CPatron does demonstrate most of the responsibilities of a control container, and fragments of its code will be shown in the next section to demonstrate how a particular feature of OLE Controls might be implemented.

External files need to compile and run CPatron

The .ZIP file that comes with this article should be unzipped with the -d switch. It will create INC, LIB, BUILD, and CONTROL directories in which various files are stored. The CPatron source files will be in CONTROL\CPATRON. The other files you'll have are as follows:

  • Include files in INC: BOOKGUID.H, DBGOUT.H, BOOK1632.H, BTTNCUR.H, GIZMOBAR.H, STASTRIP.H, CLASSLIB.H, and CLASSRES.H. These are updates from those found in theInside OLE 2 samples. You'll need to put these in your INCLUDE environment path.
  • Libraries in LIB: CLASSMDI.LIB, BTTNCUR.LIB, GIMZMOBAR.LIB, STASTRIP.LIB, BOOKUI.LIB, and BOOKUI32.LIB. Put these in your LIB environment path.
  • DLLs in BUILD: BTTNCUR.DLL, GIZMOBAR.DLL, STASTRIP.DLL, DATATRAN.DLL, LNKASSIS.DLL, BOOKUI.DLL (Win16 UI Library), and BOOKUI32.DLL (Win32® Unicode® UI Library).
  • Registry information (root): CPATRON.REG. This registers the necessary information for CPatron along with that needed for DATATRAN.DLL and LNKASSIS.DLL. The paths in this files are hard-codes to assume the directory c:\control\build. Change them to where you place the DLLs and your compile CPatron.
  • Other files in root directory: INOLE2A.MAK, INOLE2B.MAK, and build notes in BUILD.TXT. The .MAK files are generic makefiles used from CPatron's MAKEFILE. Note that INOLE2A.MAK under Win32 refers to the library "crtdll.lib" for linking. This is the Win32 SDK C run-time import library. If you are using Visual C++™, you'll need to change this file to use "msvcrt.lib" instead.

You'll also need the include file OLECTL.H from the CDK besides having the rest of OLE 2 on your system. In addition, depending on your compiler setup, you may need the OLE2UI.H and OLESTD.H files that are also in the INC directory. For example, the files of the same name that come with Visual C++ 2.0 have removed some of the structures and functions in these files, which will cause compilation errors. The include files from the Win32 SDK, however, contain the same information as the copies included with CPatron.

Implementing a Control Container Step by Step

We're now in a position to take a look at some actual code for a control container. The code here is taken from the CPatron sample, so what is not present in the sample will not be shown here unless I have another source for code fragments (I'll indicate such cases).

The steps presented here are ordered in such a way that I believe you can stop after each step, compile, and test your new code. I find this incremental approach to development much more enjoyable than attempting to implement everything at once. I hope it helps you too.

I am assuming that readers are starting with an in-place capable container application already, andInside OLE 2 has the information you need to do that work (do note that some of the material in Chapter 15 on in-place containers is sketchy, especially concerning inside-out objects; double-check with theOLE 2.0 Programmer's Reference and the additional notes on in-place activation provided with the SDK).

Note   There is one change: controls may not support changing the container's caption bar when activated in place. In particular, you need to make sure your container is prepared for a NULL in the last parameter to IOleInPlaceUIWindow::SetActiveObject (the pszObj string to use in the caption bar) and IOleInPlaceFrame::SetActiveObject. If this parameter is NULL, don't modify the caption bar at all.

That said, here are the steps we'll discuss:

  1. Add stubs for the site interfaces of IOleControlSite, IDispatch,IDispatch (Events), and IPropertyNotifySink, and add site variables.
  2. Implement control-specific creation cases along with control registration.
  3. Initialize controls by obtaining control interfaces, retrieving the object's CONTROLINFO, connecting to the primary event set, and connecting to the property change notification sink.
  4. Implement a "Design-Mode" feature.
  5. Handle Z-order and MiscStatus bits.
  6. Implement the ambient properties IDispatch.
  7. Implement IPropertyNotifySink.
  8. Implement events UI and the events IDispatch.
  9. Save and load controls and their event mappings.
  10. Implement IOleControlSite and IOleClientSite::RequestNewObjectLayout.
  11. Implement keyboard mnemonics.
  12. Handle buttons and labels.
  13. Implement Extended Controls.

This article only contains material for steps 1 through 10; the last three are not implemented in CPatron. What I know about these I've already included previously (and as you know, my knowledge of Extended Controls is really minimal right now!). I figure that some of these steps might come earlier in the list of steps above, but I have no way to judge well without knowing more about them. Keyboard handling does need to come after you've filled inIOleControlSite because it depends on some of its member functions.

Add Site Interfaces and Variables

The first step in implementing a control container is to extend the container's existing site object with stub implementations of the interfaces needed for controls and with the variables needed for maintain control states. The interfaces areIOleControlSite, IPropertyNotifySink, and two implementations ofIDispatch, with the latter interfaces making a special requirement on implementations ofQueryInterface.

CTenant variables

Before looking at IUnknown implementations in these interfaces, Table 4 contains the list of variables I added to CPatron's site object class, CTenant, in TENANT.H.

Table 4. List of Variables Added to CPatron's Site Object Class, CTenant

Variable Name CommentPIMPIOLECONTROLSITEm_pIOleControlSite;ImplementedPIMPIDISPATCHm_pIDispatch;ImplementedPIMPIDISPATCHEVENTSm_pIDispatchEvents;ImplementedDWORDm_dwConnEvents;Events connection keyIIDm_iidEvents;Event set IIDPCEventMapm_pEventMap;Event mappingPIMPIPROPERTYNOTIFYSINKm_pIPropNoteSink;ImplementedDWORDm_dwConnProp;Prop Notify connection keyControl interfaces  LPOLECONTROLm_pIOleControl;UsedLPDISPATCHm_pIDispatchControl;UsedAmbient properties  OLE_COLORm_clrBack;BackColorOLE_COLORm_clrFore;ForeColorHFONTm_hFont;Default fontLCIDm_lcid;Locale IDBOOLm_fDesignMode;Design mode?Other state information  BOOLm_fHaveControlInfo;Did we load CONTROLINFOCONTROLINFOm_ctrlInfo;Actual CONTROLINFOULONGm_cLockInPlace;LockInPlaceActive countBOOLm_fPendingDeactivate;Deactivate prevented by a lock?

Those variables of type PIMPxx are CPatron's "interface implementation" classes, defined earlier in TENANT.H, with one implementation class per interface that singly inherits from that interface. Each class maintains three variables: a reference count, a pointer to the CTenant object in which it is instantiated, and an IUnknown * to which it normally delegates allIUnknown functions (the IUnknown function always delegates to theIUnknown implementation in CTenant).

The CEventMap class to which CTenant maintains a pointer is a class defined in TENANT.H specifically for handling the mapping between events and actions. (We'll see how this is used later.) The pointer itself is initialized to NULL unless we find an object with an event set in this site. As for initialization of everything else, all the pointers, connection keys, and counts are initialized to NULL and zero in the CTenant constructor,m_iidEvents gets GUID_NULL, and only the ambient properties are initialized with anything interesting:

Copy
//0x80000000 in OLE_COLOR indicates low byte is color index.m_clrBack=0x80000000+COLOR_WINDOW;m_clrFore=0x80000000+COLOR_WINDOWTEXT;m_hFont=m_pPG->m_hFont;m_lcid=LOCALE_USER_DEFAULT;m_fDesignMode=m_pPG->m_fDesignMode;

The m_pPG->m_fDesignMode variable is kept in the CPages class (PAGES.H), a pointer to which is given to the CTenant constructor. The flag here indicates whether we are initially in design mode or run mode when creating the site, and this information affects how the site initializes a newly created object (such as whether it honors OLEMISC_ACTIVATEWHENVISIBLE).

The variables m_pEventMap, m_pIOleControlSite, m_pIDispatch, andm_pIDispatchEvents are deleted in the CTenant destructor (along with interface implementations that existed before control modifications). The variables that maintain object interface pointers,m_pIOleControl and m_pIDispatchControl, are released (that is, Release is called through them) in CTenant::Close, where all other object pointers are released as well. Them_hFont variable used for the ambient font property is maintained by CPages, so the tenant doesn't need to worry about cleanup.

For a more complete container implementation that handles those additional features that CPatron does not, you will obviously have more site variables. For example, you'll need to maintain the following:

  • The DisplayAsDefault ambient property (an extra BOOL)
  • Flags that say if the control here is the "Default" or "Cancel" button
  • Flags that say whether or not the control is an exclusive button and what group it belongs to
  • Flags that say if the control is a label
  • Flags that say whether the control is a tab stop or can have the focus
  • Other ambient properties your container finds important (CPatron uses m_fDesignMode to supply things likeShowGrabHandles as described later under "Ambient properties")
  • Anything necessary for extended controls

Interfaces and IUnknown implementations

I have found it helpful early on to add stub interface implementations that generally don't contain any functional code exceptIUnknown members. Then as you add features to the application you can fill in the interface functions as required. Having the interfaces present and instantiated, however, allows an object to obtain those interfaces and attempt to call the members.

The CTenant class instantiates most of its interface implementations in CTenant::FOpen, which is essentially a small initialization function. As mentioned before, these interfaces are deleted in the destructor.

Anyway, having interfaces around at this point requires that we fill in their implementations at least enough to return an error code if need be. Table 5 describes the default return code you should put into each interface function right now. In many cases we won't need to change these at all, as they are sufficient for working with controls. (In the "Will Modify" column, those functions marked No are not modified in CPatron because it doesn't yet implement certain features, such as keyboard handling and extended controls. More complete containers would eventually implement these functions to do more than return E_NOTIMPL. Those marked with "No*" are not implemented but NULL any out-parameters sent to the function.)

Table 5. Initial Implementations of Site Interfaces

InterfaceMember FunctionDefault ImplementationWill Modify?IPropertyNotifySinkOnChangedNOERRORNo OnRequestEditNOERRORNoIDispatchGetTypeInfoCountE_NOTIMPLNo*(Both events andGetTypeInfoE_NOTIMPLNo*ambient properties)GetIDOfNamesE_NOTIMPLNo* InvokeE_NOTIMPLYesIOleControlSiteOnControlInfoChangedE_NOTIMPLYes LockInPlaceActiveE_NOTIMPLYes GetExtendedControlE_NOTIMPLNo* TransformCoordsE_NOTIMPLYes TranslateAcceleratorE_NOTIMPLNo OnFocusNOERRORNo Show PropertyFrameE_NOTIMPLNo

Note that CPatron, as mentioned before, implements the events IDispatch on the site object alongside all these others. Accordingly we play some games withQueryInterface, as we'll see in a bit.

We also need to implement the IUnknown behavior of all these interfaces at this time. For all of them,AddRef and Release behave as usual: increment or decrement the site's overall reference count. All of CPatron's interface implementations maintain a reference count for the interface (for debugging) and then callAddRef or Release in CTenant (through m_pUnkOuter, which is sent to the interfaces' constructors)to handle the total object count:

Copy
STDMETHODIMP_(ULONG) CImpIOleControlSite::AddRef(void)   {   ++m_cRef;   return m_pUnkOuter->AddRef();   }STDMETHODIMP_(ULONG) CImpIOleControlSite::Release(void)   {   --m_cRef;   return m_pUnkOuter->Release();   }//Same for IPropertyNotifySink, IDispath (ambients), and IDispatch (events)

For all the interfaces except the events IDispatch, QueryInterface is implemented in the same delegatory fashion, andCTenant::QueryInterface is augmented to handle the new interfaces:

Copy
STDMETHODIMP CImpIOleControlSite::QueryInterface(REFIID riid   , LPVOID *ppv)   {   return m_pUnkOuter->QueryInterface(riid, ppv);   }//Same for IPropertyNotifySink and IDispatch (ambients)...STDMETHODIMP CTenant::QueryInterface(REFIID riid, PPVOID ppv)   {   *ppv=NULL;   if (IID_IUnknown==riid)      *ppv=this;   if (IID_IOleClientSite==riid)      *ppv=m_pIOleClientSite;   if (IID_IAdviseSink2==riid || IID_IAdviseSink==riid)      *ppv=m_pIAdviseSink;   if (IID_IOleWindow==riid || IID_IOleInPlaceSite==riid)      *ppv=m_pIOleIPSite;   //CONTROLMOD   if (IID_IOleControlSite==riid)      *ppv=m_pIOleControlSite;   if (IID_IPropertyNotifySink==riid)      *ppv=m_pIPropNoteSink;   //Queries for IDispatch return the ambient properties interface   if (IID_IDispatch==riid)      *ppv=m_pIDispatch;   //End CONTROLMOD   if (NULL!=*ppv)      {      ((LPUNKNOWN)*ppv)->AddRef();      return NOERROR;      }   return ResultFromScode(E_NOINTERFACE);   }

CTenant::QueryInterface never returns a pointer to the events IDispatch; instead, any requests forIDispatch return the one for ambient properties. The site still maintains the eventsIDispatch, but that interface has its own version of QueryInterface to support the idea that it's conceptually contained within and separate from the site:

Copy
STDMETHODIMP CImpIDispatchEvents::QueryInterface(REFIID riid, PPVOID ppv)   {   *ppv=NULL;   /*      * This interface stands alone. Note that it is IUnknown,      * IDispatch, AND the events IID of the control, so we must      * respond to all three.      */   if (IID_IUnknown==riid || IID_IDispatch==riid      || m_pTen->m_iidEvents==riid)      *ppv=this;   if (NULL!=*ppv)      {      ((LPUNKNOWN)*ppv)->AddRef();      return NOERROR;      }   return ResultFromScode(E_NOINTERFACE);   }

Besides returning its own pointer for IUnknown and IDispatch requests, the eventsIDispatch must also return its IDispatch pointer when asked for the IID of the control's event setdispinterface. This is very important because the control will, as part of its connection point implementation, query the eventsIDispatch for the events IID. The CTenant::m_iidEvents variable is set to the events IID during control initialization, as we'll see later. For now, it's good to have this code in place so that when we do set the IID, this code will begin working properly.

Now that we have variables and interfaces in place, we can look at control creation.

Implement Control-Specific Creation Cases

At this point we're ready to create controls, but a control container will need a little bit of user interface to facilitate creation. A full control container will generally include the following, although I have not seen any strict requirements for these UI elements:

  • A dialog in which the user can add or remove controls from the system. This is typically a dialog that shows the currently registered controls. Pressing an "Add" button opens a file browsing dialog in which the user locates the .OCX file, and when located, the container checks the version information for "OleSelfRegister" and calls DllRegisterServer. A "Remove" button in this dialog would call DllUnregisterServer to remove the control from the registry.
  • The ability to create a toolbox with buttons for each registered control where the button image is taken from the "ToolboxBitmap" entry under the control's CLSID. The container should be robust in case it cannot obtain a bitmap for some reason.
  • The ability to add another option to the standard Insert Object dialog alongside "Create New" and "Create From File" that reads "Create Control." When selected, the listbox in the dialog should show only registered controls (those with the "Control" key in the registry) and not show any other compound document objects. The "Create New" list should show only those controls with the "Insertable" key as is normal for compound documents.

I have to admit that CPatron skimps a great deal on this UI—basically it doesn't implement any of it at this time. In order to do self-registration/unregistration of controls you will have to use the TSTCON or REGSVR apps that come with the controls development kit (VC++ 2.0). CPatron doesn't create any sort of toolbox, so registered bitmaps are ignored. Furthermore, CPatron just uses its normal Insert Object dialog for creating controls—that is, it doesn't look for controls specifically. This means that when you run CPatron to try it out, the Insert Object dialog will not normally have all registered controls showing, because some controls do not include the "Insertable" key. CPatron, however, should handle all the registered controls just fine,so you will need to manually add the "Insertable" key under the ProgID for each control. Otherwise you're going to be confused (as I was for a long time) as to why these controls are not showing up in the list. Do note that marking these controls as Insertable will allow you to stick them into other compound document containers in which bad things can happen—doing this was a hack just because I didn't get around to implementing a control-specific dialog before writing this article.

In any case, one way or another the container ends up with a CLSID from Insert Object or whatever dialog allows the user to choose a control to create. With this CLSID, CPatron simply goes through its normal creation process (CTenant::UCreate) for compound document objects: call OleCreate, then initialize the object (CTenant::FObjectInitialize)through such interfaces as IPersistStorage and IOleObject. CPatron doesn't make any provision for working with controls marked OLEMISC_SETCLIENTSITEFIRST, nor does it handle objects that useIPersistStream as opposed to IPersistStorage, basically because of the uncertainties surrounding these things as described in the first section of this document. So for now, CPatron simply treats controls as if they were normal embedded objects.

Although the next section describes extra control initialization, there is one small modification made for controls that's found in theCPage::TenantCreate (PAGE.CPP) function:

Copy
if (!m_pTenantCur->HasControl())   {   m_pTenantCur->Activate(OLEIVERB_SHOW, NULL);   m_pTenantCur->Update();   }

This code is executed as the last step in the control creation process. Normally what it does is activate the object immediately, showing it for editing. This is what we want to happen for compound document objects, but "showing" a control, especially in another window, is not necessary and is, in fact, quite pointless. So here CPatron callsCTenant::HasControl (which returns TRUE or FALSE depending on whether the tenant's initialization process foundIOleControl), and if we do have a control we skip the activation. Most controls only make sense to activate in place, and if a control was created we've already activated it in place by this time.

Initialize Controls

As mentioned in the previous section, all objects in CPatron are initialized withCTenant::FObjectInitialize. To handle controls, this initialization function also callsCTenant::FControlInitialize, which (as you might guess) handles control-specific steps. Following are the steps executed inFControlInitialize:

  1. Query the object for its IOleControl pointer. If this fails, the object is not a control. CPatron uses this pointer (which is initially NULL) as a flag to determine if the site has a control (as withCTenant::HasControl).
  2. Retrieve the control's CONTROLINFO structure for keyboard handling. CPatron retrieves this and saves it, although it doesn't do anything with it. If you successfully retrieve this information, set a flag so you later know whether to use this information when handling keystrokes.
  3. Query the object for its IDispatch interface, through which you can access control properties and methods. CPatron retrieves this pointer but never uses it; other containers obviously will.
  4. Connect your IPropertyNotifySink to the control using connection points.
  5. Connect your events IDispatch to the control and initialize your event mapping structures. This is a rather involved process, which is spelled out in more detail in the "Connect and Initialize Events" section.

We can see these steps implemented in code:

Copy
BOOL CTenant::FControlInitialize(void)   {   HRESULT     hr;   BOOL        fEvents;   if (NULL==m_pObj)       return FALSE;   //1. Check if the object is a control   hr=m_pObj->QueryInterface(IID_IOleControl, (PPVOID)&m_pIOleControl);if (FAILED(hr))      return FALSE;   //2. Get the keyboard information   m_fHaveControlInfo=SUCCEEDED(m_pIOleControl->GetControlInfo(&m_ctrlInfo));   //3. Get the IDispatch for properties and methods   m_pObj->QueryInterface(IID_IDispatch, (PPVOID)&m_pIDispatchControl);   //4. Connect to property notifications   m_pIPropNoteSink=new CImpIPropertyNotifySink(this, this);   InterfaceConnect(m_pObj, IID_IPropertyNotifySink, m_pIPropNoteSink, &m_dwConnProp);   //5. Connect to the control's events   ObjectEventsIID(m_pObj, &m_iidEvents);   m_pIDispatchEvents=new CImpIDispatchEvents(this, this);   fEvents=InterfaceConnect(m_pObj, m_iidEvents, m_pIDispatchEvents, &m_dwConnEvents);   //Initialize the event map (or load it if saved previously)   if (fEvents)      {      LPTYPEINFO   pITypeInfo;      LPSTREAM    pIStream;      //Get the ITypeInfo specifically for events (connect.cpp)      fEvents=ObjectTypeInfoEvents(m_pObj, &pITypeInfo);      if (fEvents)        {         fEvents=FALSE;         //CEventMap implemented in events.cpp         m_pEventMap=new CEventMap(pITypeInfo);         if (NULL!=m_pEventMap)            {            fEvents=m_pEventMap->Init();            //Check if there's mappings already and load them.            if (fEvents)               {               if (SUCCEEDED(m_pIStorage->OpenStream(SZEVENTSSTREAM, NULL, STGM_DIRECT                  | STGM_READWRITE | STGM_SHARE_EXCLUSIVE, 0, &pIStream)))                  {                  m_pEventMap->Deserialize(pIStream);                  pIStream->Release();                  }               }            }         pITypeInfo->Release();         }      if (!fEvents)         {         InterfaceDisconnect(m_pObj, m_iidEvents, &m_dwConnEvents);         ReleaseInterface((IUnknown **)&m_pIDispatchEvents);         delete m_pEventMap;         m_pEventMap=NULL;         }      }return TRUE;   }
Note   This code doesn't match line-for-line with the code in the sample source—for brevity some comments are removed, others shortened, and some of the lines that word-wrap in 80 columns are unwrapped here. It is the same code, however.

Steps 1, 2, and 3 are trivial: Retrieve the pointers and information you'll need for later use. Step 4 involves the use of connection points, and step 5 is even more complex, so they warrant separate sections.

Connect property sink notifications

The whole point of what we're about to do is connect our IPropertyNotifySinkinterface to the control so that the control can call us as described earlier in this document. To accomplish this we need to execute the following steps:

  1. Query the object for its IConnectionPointContainer.
  2. Call IConnectionPointContainer::FindConnectionPoint, passing to it IID_IPropertyNotifySink. This will return an IConnectionPoint interface for property notifications.
  3. Call IConnectionPoint::Advise passing our IPropertyNotifySink pointer (which is cast to anIUnknown for Advise). On return we are given a DWORD key that we later use to disconnect the notifications (see below).
  4. Call IConnectionPoint::Release and IConnectionPointContainer::Releaseto clean up. (Some containers may wish to cache these pointers so as to not have to query for them later to disconnect, which is fine. CPatron does not to reduce the overall number of site variables).

CPatron implements these steps in a generic connection function, InterfaceConnect, that is found in CONNECT.CPP. This function is intended to be generic enough so that you can cut and paste it, unmodified, into your own container code. As shown in the code above, the initialization procedure calls this function with a pointer to the object'sIUnknown (IID_IPropertyNotifySink), the site's IPropertyNotifySink pointer (as anIUnknown), and the address of CTenant::m_dwConnProp in which is stored the connection key:

Copy
BOOL InterfaceConnect(LPUNKNOWN pObj, REFIID riid   , LPUNKNOWN pIUnknownSink, LPDWORD pdwConn)   {   HRESULT           hr;   LPCONNECTIONPOINTCONTAINER pCPC;   LPCONNECTIONPOINT      pCP;   if (NULL==pObj || NULL==pIUnknownSink || NULL==pdwConn)      return FALSE;   hr=pObj->QueryInterface(IID_IConnectionPointContainer      , (PPVOID)&pCPC);   if (FAILED(hr))      return FALSE;   hr=pCPC->FindConnectionPoint(riid, &pCP);   if (SUCCEEDED(hr))      {      hr=pCP->Advise(pIUnknownSink, pdwConn);      pCP->Release();      }   pCPC->Release();   return SUCCEEDED(hr);   }

You can see that there's nothing fancy going on here, just the straight sequence of using connection points to hand the control an outgoing interface pointer. Once this code is complete (and successful), the control will have a pointer through which to notify us when properties change.

When we delete the object or otherwise destroy the site, we need to perform similar steps to disconnect theIPropertyNotifySink from the object. Disconnection happens in CTenant::Close:

Copy
if (0!=m_dwConnProp)   {   InterfaceDisconnect(m_pObj      , IID_IPropertyNotifySink, &m_dwConnProp);   }

The generic function InterfaceDisconnect, also in CONNECT.CPP and also made to be pastable into your own code, performs the same sequence of calls asInterfaceConnect, except that it calls IConnectionPoint::Unadvise instead ofAdvise with the DWORD key returned from Advise:

Copy
BOOL InterfaceDisconnect(LPUNKNOWN pObj, REFIID riid   , LPDWORD pdwConn)   {   HRESULT                hr;   LPCONNECTIONPOINTCONTAINER pCPC;   LPCONNECTIONPOINT         pCP;   if (NULL==pObj || NULL==pdwConn)      return FALSE;   if (0==*pdwConn)      return FALSE;   hr=pObj->QueryInterface(IID_IConnectionPointContainer      , (PPVOID)&pCPC);   if (FAILED(hr))      return FALSE;   hr=pCPC->FindConnectionPoint(riid, &pCP);   if (SUCCEEDED(hr))      {      hr=pCP->Unadvise(*pdwConn);      if (SUCCEEDED(hr))         *pdwConn=0L;      pCP->Release();      }   pCPC->Release();   return SUCCEEDED(hr);   }

You'll notice that instead of passing the DWORD key to this function, we pass the address of the key. This is soInterfaceDisconnect can zero the key when it successfully disconnects, allowing us to use the key as a flag that indicates whether or not we're connected.

Connect and initialize events

For the most part, connecting our events IDispatch to the control happens in exactly the same way as withIPropertyNotifySink—the same InterfaceConnect and InterfaceDisconnect functions apply perfectly. However, the main trick in connecting to the control's events is finding the IID for the connection point. As shown in the initialization code above, CPatron calls a function ObjectEventsIID to retrieve the dispinterface IID for events, saving that IID inCTenant::m_iidEvents (which, again, we use in the QueryInterface function for the eventsIDispatch):

Copy
ObjectEventsIID(m_pObj, &m_iidEvents);

Let's first see how this magic function works, then we'll look at initializing our event map.

Finding the events IID

ObjectEventsIID is another generic cut-and-paste function found in CONNECT.CPP that plays a few games with OLE Automation interfaces to find the IID we want:

Copy
BOOL ObjectEventsIID(LPUNKNOWN pObj, IID *piid)   {   HRESULT          hr;   LPTYPEINFO       pITypeInfo;   LPTYPEATTR       pTA;   *piid=CLSID_NULL;   if (!ObjectTypeInfoEvents(pObj, &pITypeInfo))      return FALSE;   hr=pITypeInfo->GetTypeAttr(&pTA);   if (SUCCEEDED(hr))      {      *piid=pTA->guid;      pITypeInfo->ReleaseTypeAttr(pTA);      }   pITypeInfo->Release();   return SUCCEEDED(hr);   }

This function first locates the ITypeInfo interface for the events dispinterface by calling another function in CONNECT.CPP calledObjectTypeInfoEvents. With this pointer we only need to call ITypeInfo::GetTypeAttr to retrieve the TYPEATTR structure in the dispinterface's IID resides (in theguid field). That done, we release the TYPEATTR structure and the ITypeInfo interface, and we're done. Easy enough?

Well, we still need to know how we get the right ITypeInfo pointer, which is the purpose of theObjectTypeInfoEvents function. As described earlier, we have to look through the object's entire (coclass) type info to find the dispinterface markedDefault and Source. This first requires us to obtain the coclass type info using the newIProvideClassInfo interface. This step is implemented in the ObjectTypeInfo function in CONNECT.CPP:

Copy
BOOL ObjectTypeInfo(LPUNKNOWN pObj, LPTYPEINFO *ppITypeInfo)   {   HRESULT          hr;   LPPROVIDECLASSINFO pIProvideClassInfo;   if (NULL==pObj || NULL==ppITypeInfo)      return FALSE;   *ppITypeInfo=NULL;   hr=pObj->QueryInterface(IID_IProvideClassInfo      , (PPVOID)&pIProvideClassInfo);   if (FAILED(hr))      return FALSE;   hr=pIProvideClassInfo->GetClassInfo(ppITypeInfo);   pIProvideClassInfo->Release();   return SUCCEEDED(hr);   }

With this ITypeInfo pointer we can now look at the process of finding the right dispinterface:

Copy
BOOL ObjectTypeInfoEvents(LPUNKNOWN pObj, LPTYPEINFO *ppITypeInfo)   {   HRESULT          hr;   LPTYPEINFO       pITypeInfoAll;   LPTYPEATTR       pTA;   if (NULL==pObj || NULL==ppITypeInfo)      return FALSE;   if (!ObjectTypeInfo(pObj, &pITypeInfoAll))      return FALSE;   *ppITypeInfo=NULL;   if (SUCCEEDED(pITypeInfoAll->GetTypeAttr(&pTA)))      {      UINT      i;      int       iFlags;      for (i=0; i < pTA->cImplTypes; i++)         {         //Get the implementation type for this interface         hr=pITypeInfoAll->GetImplTypeFlags(i, &iFlags);         if (FAILED(hr))            continue;         if ((iFlags & IMPLTYPEFLAG_FDEFAULT)            && (iFlags & IMPLTYPEFLAG_FSOURCE))            {            HREFTYPE   hRefType=NULL;            /*             * This is the interface we want. Get a handle to             * the type description from which we can then get             * the ITypeInfo.             */            pITypeInfoAll->GetRefTypeOfImplType(i, &hRefType);            hr=pITypeInfoAll->GetRefTypeInfo(hRefType               , ppITypeInfo);            break;            }         }      pITypeInfoAll->ReleaseTypeAttr(pTA);      }   pITypeInfoAll->Release();   return (NULL!=*ppITypeInfo);   }

In this code we start with pITypeInfoAll pointing to the coclass type info. Calling itsGetTypeAttr function gives us the TYPEATTR structure that contains the number of "types" or dispinterfaces in the class, in thecImplTypes field. We then iterate over all those types, for each callingGetImplTypeFlags, which will return us the attributes for each dispinterface. We're looking for the default source dispinterface, which will have the IMPTYPEFLAG_FDEFAULT and IMPLTYPEFLAG_FSOURCE flags. If both these flags are found, then we have discovered which dispinterface is the one we want. To finish up, then, we need to get the ITypeInfo pointer for that dispinterface with GetRegTypeOfImplType andGetRefTypeInfo. That done, we release the TYPEATTR we retrieved and release the coclassITypeInfo, and we're done.

If you have any questions about why this code works as it does, you'll need to read about theITypeInfo interface in Volume 2 of the OLE Programmer's Reference. All this code is strictly OLE Automation, except for the default and source flags, which are extensions defined in OLE Controls.

Initializing an event map

Once CPatron has obtained the events IID, it calls InterfaceConnect to link its eventsIDispatch to the control. What is now left to do is to initialize the structures we use to map events to user-defined actions in the container.

CPatron uses another class, CEventMap (TENANT.H and EVENTS.CPP), to maintain a mapping from event DISPIDs to actions:

Copy
//Event actionstypedef enum   {   ACTION_NONE=-1,   ACTION_BEEPDEFAULT=MB_OK,   ACTION_BEEPASTERISK=MB_ICONASTERISK,   ACTION_BEEPEXCLAMATION=MB_ICONEXCLAMATION,   ACTION_BEEPHAND=MB_ICONHAND,   ACTION_BEEPQUESTION=MB_ICONQUESTION,   ACTION_TAILING=-2   } EVENTACTION;typedef struct tagEVENTMAP   {   DISPID           id;           //Event ID   EVENTACTION      iAction;      //Action to take   BSTR             bstrName;     //Event name (function only)   } EVENTMAP, *PEVENTMAP;class CEventMap   {   public:      UINT           m_cEvents;      LPTYPEINFO     m_pITypeInfo;      PEVENTMAP      m_pEventMap;   public:      CEventMap(LPTYPEINFO);      ~CEventMap(void);      BOOL         Init(void);      BOOL         Set(DISPID, EVENTACTION);      EVENTACTION  Get(DISPID);      void         Serialize(LPSTREAM);      void         Deserialize(LPSTREAM);   };typedef CEventMap *PCEventMap;//Events stream in the object storage#define SZEVENTSSTREAM TEXT("\003Event Mappings")

This structure is only instantiated and initialized if the site successfully connected to the control's events. What is of interest to us at this point is how we learn the names and IDs of the events so we can use such information in the event UI that we'll implement later. So here I want to look at the construction and initialization of a CEventMap object, leaving the details about mapping actions and serialization of the mappings to later sections.

A note about design: CPatron doesn't do much with events and handles them in a very simplistic manner—all it does is map a DISPID to a system beep. It doesn't handle event parameters that are meaningful to more sophisticated containers. Such containers will need a much more complex data structure to maintain event mapping information to make use of event parameters. However, the general idea of maintaining a map of some sort, and how we work with that map in the eventsIDispatch, is pretty much the same regardless of the complexity of event mapping, so that is what CPatron demonstrates.

After successfully connecting the control's events, we construct a CEventMap object, passing to it theITypeInfo of the events dispinterface that we get from ObjectEventsTypeInfo:

Copy
m_pEventMap=new CEventMap(pITypeInfo);

This does little more than hold onto the ITypeInfo pointer:

Copy
CEventMap::CEventMap(LPTYPEINFO pITypeInfo)   {   m_cEvents=0;   m_pITypeInfo=pITypeInfo;   if (NULL!=m_pITypeInfo)      m_pITypeInfo->AddRef();   m_pEventMap=NULL;   return;   }

If this creation succeeds, we tell the CEventMap to initialize. Initialization allocates an array of EVENTMAP structures, one for each event in the dispinterface. The EVENTMAP structure saves the DISPID, the text name, and the action (aMessageBeep parameter) associated with the event. The interesting part of initialization is retrieving the name and ID of each:

Copy
BOOL CEventMap::Init(void)   {   LPTYPEATTR    pTA;   UINT         i;   if (NULL==m_pITypeInfo)      return FALSE;   if (FAILED(m_pITypeInfo->GetTypeAttr(&pTA)))      return FALSE;   m_cEvents=pTA->cFuncs;   m_pITypeInfo->ReleaseTypeAttr(pTA);   m_pEventMap=new EVENTMAP[m_cEvents];   if (NULL==m_pEventMap)      {      m_cEvents=0;      return FALSE;      }   for (i=0; i < m_cEvents; i++)      {      LPFUNCDESC    pFD;      m_pEventMap[i].id=0;      m_pEventMap[i].bstrName=NULL;      m_pEventMap[i].iAction=ACTION_NONE;      if (SUCCEEDED(m_pITypeInfo->GetFuncDesc(i, &pFD)))         {         UINT      cNames;         HRESULT    hr;         m_pEventMap[i].id=pFD->memid;         hr=m_pITypeInfo->GetNames(pFD->memid            , &m_pEventMap[i].bstrName, 1, &cNames);         m_pITypeInfo->ReleaseFuncDesc(pFD);         }      }   return TRUE;   }

All of this code is straight OLE Automation again. We use ITypeInfo::GetTypeAttragain because the TYPEATTR field cFuncs is the number of functions or members in the dispinterface—that is, the number of events. We use this information to know how many EVENTMAPs to allocate. Once allocated, we loop through each member to retrieve whatever information we're interested in keeping. In CPatron it's the DISPID and name; other containers will want more information about the parameters as well. In any case, you must first retrieve the FUNCDESC structure for the member by callingITypeInfo::GetFuncDesc passing the index of the member you're looping on at the moment.

The FUNCDESC structure tells you all sorts of things, such as the number of parameters and so forth. What we're interested in here is thememid (member ID) field, which is the event DISPID. We can then call ITypeInfo::GetNames with this DISPID to retrieve the first name in the member, which is the event name itself. The parameter "1" toGetNames says we're only interested in getting back one name, a pointer to which we store in thebstrName field of our EVENTMAP. If you want parameters' names (and other info) as well, you need to use theFUNCDESC::cParams value, which tells you how many parameters there are to the function. Then allocate an array of BSTRs of sizecParams+1 (+1 to include the function name itself) and pass cParams+1 toGetNames.

Note   I'm not sure how you retrieve the specific parameter types, I'm not that far into Automation yet. . . .

After all of this, we'll have a CEventMap that has in it all the names of the events and their DISPIDs. We'll use this in creating an events dialog later on, as well as for serialization and deserialization to the "\003Event Mappings" stream in which CPatron saves this information. But those are topics for later sections; the only other thing to mention here is that the BSTR pointers returned byITypeInfo::GetNames must be freed using SysFreeString (an Automation API). We can see how this works in the CEventMap destructor:

Copy
CEventMap::~CEventMap(void)   {   if (NULL!=m_pITypeInfo)      m_pITypeInfo->Release();   if (NULL!=m_pEventMap)      {      UINT      i;      //Be sure to clean up allocated BSTRs      for (i=0; i < m_cEvents; i++)         SysFreeString(m_pEventMap[i].bstrName);      delete [] m_pEventMap;      }   return;   }

Of course, this also releases the ITypeInfo interface and deletes the EVENTMAP structure.

Implement "Design-Mode"

Once you've completed the creation and initialization steps of your container, you can start creating controls and placing them on your forms or documents. If you started this implementation with a container that handles inside-out objects, you'll be able to create a control and play with it. If you run your application in a debugger you should also see calls made to yourIOleControlSite interface, your ambient properties IDispatch, and even your eventsIDispatch! Unfortunately, we don't do anything with these interfaces yet, and we'll get to that in a moment.

More pressing, however, is something that I noticed about my container after I started plopping controls onto a document: When a control is in-place active (or UI active), the container's grab handles were not visible. Therefore I had no way to move or resize controls! This is bad—because my container always creates a new object in the upper-left corner of a page (I'm lazy—I didn't put in any logic to place them in empty places on the page), creating multiple controls would basically pile them up in the same place. Not all that useful.

To remedy this situation I basically needed a way to put the controls into a mode in which I could move and size them. I therefore implemented a "Design Mode" feature. This meant adding a top-level menu item (to the Page menu, see PATRON.RC) that is enabled whenever there's a document available. This command is routed from the frame window (CPatronFrame::OnCommandin PATRON.CPP) to the document (CPatronDoc::FToggleOrQueryDesignMode in DOCUMENT.CPP) to the page manager (CPages::ToggleDesignModein PAGES.CPP) to the page (CPage::ToggleDesignMode in PAGE.CPP), which loops through all the sites in the page to callCTenant::ToggleDesignMode (TENANT.CPP). I know, going through this many functions for commands like this is pretty ugly; sooner or later I’ll improve this setup, but as you can probably understand, I’ve just never had the time! This function activates or deactivates objects when we change modes:

Copy
void CTenant::ToggleDesignMode(BOOL fDesign)   {   BOOL   fChange=!(m_fDesignMode & fDesign);   if (fDesign==m_fDesignMode)      return;   m_fDesignMode=fDesign;   /*    * Inform the control of ambient property changed. A change    * in design mode changes UserMode, ShowGrabHandles, and    * ShowHatching (see AMBIENTS.CPP)    */   AmbientChange(DISPID_AMBIENT_USERMODE);   AmbientChange(DISPID_AMBIENT_SHOWGRABHANDLES);   AmbientChange(DISPID_AMBIENT_SHOWHATCHING);   if (m_fDesignMode)      {      //This even deactivates inside-out objects      DeactivateInPlaceObject(TRUE);      Invalidate();      }   else      {      //First hide whatever windows might be open      if (TENANTSTATE_OPEN & m_dwState)         Activate(OLEIVERB_HIDE, NULL);      //Activate all tenants, UI activate the selected one      Activate(OLEIVERB_INPLACEACTIVATE, NULL);      if (TENANTSTATE_SELECTED & m_dwState)         Activate(OLEIVERB_UIACTIVATE, NULL);      }   return;   }

If we enter design mode, we call IOleInPlaceObject::InPlaceDeactivate on every object (followed by a repainting of the site). This removes the object's in-place window and any other UI elements, making the object look like a normal, inactive, embedded object that CPatron can move and size as desired (using mouse tracking and drag-and-drop code in other parts of the application). If we reenter design mode, we basically in-place activate all objects again and UI activate the currently selected tenant, which is the one that has the focus. In this code we also hide any object that happens to be open, just in case—if for some reason an object is showing in a different window (this is known to the site fromIOleClientSite::ShowWindow) we want to close that window. Sending the hide verb accomplished just that.

Switching to and from design mode has another potential side effect: changing ambient properties. In our case, theUserMode, ShowGrabHandles, and ShowHatching ambient properties depend on theCTenant::m_fDesignMode flag (we'll see this when we implement ambient properties). So when the design mode flag changes, these ambient properties will change, and we must notify the control by callingIOleControl::OnAmbientPropertyChange. This is done through CTenant::AmbientChange (which just simplifies other code in CTenant so it doesn't always have to check for a NULLm_pIOleControl pointer):

Copy
void CTenant::AmbientChange(DISPID dispID)   {   if (NULL!=m_pIOleControl)      m_pIOleControl->OnAmbientPropertyChange(dispID);   return;   }

There are now a few other small considerations and modifications to make concerning design mode:

  • First, be sure to tell any new sites you create about the current design mode setting (CTenant copies theCPages::m_fDesignMode flag in its constructor) or else things will start working in very strange ways and you won't know why (can you tell this happened to me?).
  • Second, ignore the OLEMISC_ACTIVATEWHENVISIBLE flag—the purpose of design mode is to deactivate everything so this flag is irrelevant.
  • Third, I added a fragment of code to suppress drawing of handles on objects if they are marked OLEMISC_INVISIBLEATRUNTIME, but I have to add the design mode condition as well.
  • Fourth, my sites have a Select function that normally changes the UI activation of objects as selection changes; activation doesn't occur in design mode.
  • Finally, the IOleInPlaceSite::OnInPlaceActivate function should return S_FALSE if you are in design mode. This will ensure that no object tries to activate in place if for some reason it ends up here (and this is the same reason why you want to send the Hide verb when you go into run mode, in case the control made any popup windows because of non-in-place activation).

Another way that might work to do design mode—which I have not tried, so I cannot guarantee this working—is to set the ambient properties forShowGrabHandles and ShowHatching to TRUE in design mode and FALSE in run mode, which might allow object movement and resizing. With these ambient properties set to TRUE, a control should show these elements; and if you move or size the control it should call IOleInPlaceSite::OnPosRectChange, in which case you update your site's position in the page. Potential problems are: (1) some controls may not support these UI elements (so you're stuck without them), and (2) a right-mouse click on the control will not generate a popup menu of the container's choosing, because the message will go to the control. To make things consistent, it is best to just deactivate everything.

Handle Z-Order and MiscStatus Bits

Before we get into implementing interfaces and other major features, there are a few snippets of code you should add to work well with controls.

First of all, you'll need to make sure that the Z-order of the controls matches the layering of controls that overlap each other. Your container may already handle this in some respects. When I started working on CPatron, I had already implemented Z-ordering of my sites so I could determine the order in which to draw them (the first one in the order was drawn last so as to appear on top). With multiple in-place active objects in your container at one time, you will have multiple child windows for those objects, the Z-order of which must match what your container understands to be the ordering.

In doing an in-place activation implementation, I have a call to SetWindowPosin CPage::SwitchActiveTenant, which places the newly-UI-activated object at the top of the Windows Z-order, so I didn't need any changes to handle controls. I mention this here so you can check that your container handles windowing in the correct way as well. Note also that the order in which you reactivate controls coming out of design mode will determine the Windows Z-order: The first activated control will create its window first, and will therefore be first in the Z-order. CPatron activates controls according to its internal ordering, so this is done automatically as a virtue.

Now is a good time to think about keeping a separate order list for implementing TAB key functionality (CPatron does not do this, mind you). The TAB order is different from the Z-order, inasmuch as it does not change as objects are activated. By default the TAB order is generally the order in which controls were created, and containers that support this functionality will typically have a dialog box in which the order can be sorted (Microsoft Access does this, for example).

The other set of considerations that introduce little bits of code are the new OLEMISC_* bits. I've already mentioned ignoring OLEMISC_ACTIVATEWHENVISIBLE if design mode is on. The other two of relevance here are OLEMISC_ALWAYSRUN and OLEMISC_INVISIBLEATRUNTIME. If OLEMISC_ALWAYSRUN is set, you should always call OleRun as soon as you load or create any object (CPatron does this inCTenant::FObjectInitialize). When OLEMISC_INVISIBLEATRUNTIME is set, you should suppress any code that can potentially draw anything for the object in your document or form when run time is active. Invisible controls will generally not even in-place activate, and before I added checks on this flag CPatron's handling of such objects was to draw the object's images from the cache and draw grab handles if it was selected. If this flag is set, CPatron now prevents any calls toOleDraw (IViewObject[2]::Draw) and other functions to draw hatching or grab handles.

Implement the Ambient Properties IDispatch

It's time now to fill in the IDispatch implementation for ambient properties, which is not at all complicated. If you are implementing only standard ambient properties, all the functions in this interface exceptInvoke can be left unimplemented (except small pieces of code to NULL out-parameters, which you must always do, even if you return E_NOTIMPL). Furthermore, you do not need any sort of type library or type information for standard properties because controls make compile-time assumptions about their types.

If, however, you provide "other" ambient properties specific to your container, you will want to be able to provide the type information for those properties, so you will need to implement the other functions inIDispatch. Other properties are beyond the scope of this article, but filling out the implementation ofIDispatch is just standard OLE Automation, so there's plenty of information elsewhere on doing it.

That leaves us with the implementation of Invoke for ambient properties. In order to implement this function you will need to know which ambient properties you are going to expose and where the values of those properties are stored. In CPatron's case, it maintains a number of variables in the CTenant class for the ambient properties as listed in Table 6.

Table 6. CPatron's Supported Ambient Properties and Implementing Variables

Property IDVariable (CTenant unless otherwise stated)DISPID_AMBIENT_BACKCOLORm_clrFore (COLOR_WINDOW)DISPID_AMBIENT_FORECOLORm_clrBack (COLOR_WINDOWTEXT)DISPID_AMBIENT_FONTm_pPG->m_hFont (CPages::m_hFont)DISPID_AMBIENT_LOCALEIDm_lcid (always LOCALE_USER_DEFAULT)DISPID_AMBIENT_USERMODE!m_fDesignModeDISPID_AMBIENT_UIDEADFALSEDISPID_AMBIENT_SUPPORTSMNEMONICSTRUEDISPID_AMBIENT_SHOWGRABHANDLES!m_fDesignModeDISPID_AMBIENT_SHOWHATCHING!m_fDesignMode

Invoke ends up being little more than a big switch statement to put these values into the result parameter:

Copy
STDMETHODIMP CImpIDispatch::Invoke(DISPID dispIDMember, REFIID riid   , LCID lcid, unsigned short wFlags, DISPPARAMS * pDispParams   , VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)   {   HRESULT    hr;   VARIANT    varResult;   if (IID_NULL!=riid)      return ResultFromScode(E_INVALIDARG);   if(NULL==pVarResult)      pVarResult=&varResult;   VariantInit(pVarResult);   //The most common case is Boolean; use as an initial type.   V_VT(pVarResult)=VT_BOOL;   //Anything but a property get is invalid.   if (!(DISPATCH_PROPERTYGET & wFlags))      return ResultFromScode(DISP_E_MEMBERNOTFOUND);   hr=NOERROR;   switch (dispIDMember)      {      case DISPID_AMBIENT_BACKCOLOR:         V_I4(pVarResult)=m_pTen->m_clrBack;         V_VT(pVarResult)=VT_I4;         break;      case DISPID_AMBIENT_FORECOLOR:         V_I4(pVarResult)=m_pTen->m_clrFore;         V_VT(pVarResult)=VT_I4;         break;      case DISPID_AMBIENT_FONT:         V_I4(pVarResult)=(LONG)(UINT)m_pTen->m_hFont;         V_VT(pVarResult)=VT_I4;         break;      case DISPID_AMBIENT_LOCALEID:         V_I4(pVarResult)=m_pTen->m_lcid;         V_VT(pVarResult)=VT_I4;         break;      case DISPID_AMBIENT_USERMODE:         V_BOOL(pVarResult)=!m_pTen->m_fDesignMode;         break;      case DISPID_AMBIENT_UIDEAD:         V_BOOL(pVarResult)=FALSE;         break;      case DISPID_AMBIENT_SUPPORTSMNEMONICS:         V_BOOL(pVarResult)=TRUE;         break;      case DISPID_AMBIENT_SHOWGRABHANDLES:         V_BOOL(pVarResult)=m_pTen->m_fDesignMode;         break;      case DISPID_AMBIENT_SHOWHATCHING:         V_BOOL(pVarResult)=m_pTen->m_fDesignMode;         break;      default:         hr=ResultFromScode(DISP_E_MEMBERNOTFOUND);         break;      }   return hr;   }

Two things to notice about this code: First, this doesn't pay any attention to the locale ID. Second, CPatron considers ambient properties to be read-only, so anything other than DISPATCH_PROPERTYGET inwFlags is an error and extra parameters to the invoked member are ignored. The only suitable return code right now is DISP_E_MEMBERNOTFOUND, which really isn't appropriate. In the future there will probably be a DISP_E_ACCESSDENIED or something like that. Do note that ambient properties are not always necessarily read-only—if you want to allow modification, feel free to implement it.

One other point: CPatron doesn't bother watching for WM_WININICHANGE for changes to the foreground and background colors. It really should, and when they change, the ambient properties change, so CPatron should be callingIOleControl::OnAmbientPropertyChange in all controls for both colors.

Implement IPropertyNotifySink

If implementing IDispatch for ambient properties seemed easy, implementingIPropertyNotifySink is generally easier. Here's what CPatron has for it in IPROPNOT.CPP:

Copy
STDMETHODIMP CImpIPropertyNotifySink::OnChanged(DISPID dispID)   {   return NOERROR;   }STDMETHODIMP CImpIPropertyNotifySink::OnRequestEdit(DISPID dispID)   {   return NOERROR;   }

Now this is not, of course, fully appropriate for a more sophisticated container.OnChanged is really an event like any other, but a very standard event. A container can allow the user to write code to attach to specific property changes; that is, the container can get the entire list of all control properties, present that list to the programmer, then allow the programmer to pick any property and attach code to theOnChanged notification. This is especially useful for controls whose Value property is attached to some dynamic data source—users of that control will typically want something else to happen when the value changes, andOnChanged can be used to detect the change instead of depending on the control firing an event (which it might do, but that's not guaranteed).

OnRequestEdit is a little less commonly used. Microsoft Access uses it for lazy locking of database records, locking those records only when someone starts to change a value (in which case it returns NOERROR anyway). A container might also want a snapshot copy of the old value before it changed, perhaps to compare it to the new value—a container might generate detect events that the control might not otherwise send, such as when a change in a stock price exceeds a certain limit. Another use forOnRequestEdit would be for a container to implement some external read-only feature for object properties, checking the flag in this function to determine whether to allow the change.

The bottom line is that at least you should return NOERROR from both functions. Whatever else you do with them is really specific to the container and depends upon the end-user features you provide.

Implement Event UI and the Events IDispatch

Now for the fun part: event handling. Events are really what makes a control interesting, for most everything else is either OLE Automation (properties and methods) or mundane but necessary things like Insert Object dialogs and keyboard mnemonics. Implementing event handling in your container can be done in two steps:

  1. Implement UI in which the programmer/end user can assign actions to events.
  2. Implement the events IDispatch to respond to those events.

The following sections cover these topics. Before that, however, I want to remind you that now would be a good time to evaluate your possible uses ofIOleControl::FreezeEvents. Think about times in your application where you would want to prevent excess calls into your interfaces or otherwise prevent event-handling code to really trash some other operation that's going on in the container. To be truly safe, you'll want to wrap any risky sections of code with FreezeEvents(TRUE)and FreezeEvents(FALSE).

Events UI

Depending on your container, the UI in which a user programs events can be anywhere from simple to mind-bogglingly complex. CPatron is a simple example: Because it only maps event DISPIDs to one of seven system beeps (none, machine speaker, default, exclamation, hand, question, asterisk) the UI that allows the user to program events is quite simple.

Other applications like Visual Basic are basically structured around event handling. Most of what Visual Basic does is create a form, plop controls on the form, and assign code to control events. So Visual Basic has a lot of functionality for the purposes of writing code, using event parameters, calling methods in controls, manipulating properties, and so on. I expect that most people reading this article will be working on applications on the same order, but inasmuch as this is your livelihood, I'll leave it to you to work out all the details. CPatron's purpose is to illustrate how a container implements a general event handler for an arbitrary event set.

We've already seen how CPatron creates an event map using the type information from an event dispinterface (see "Initializing an Event Map," above). By default, CPatron assigns no actions to any events. To allow the user to assign different actions, CPatron supplies the Events dialog shown in Figure 9.

Figure 9. CPatron's Events dialog, in which the user can assign actions to events

This dialog is invoked from an "Events. . ." menu item that CPatron adds to both the Edit menu and the right-mouse popup menu for objects. This menu item is only enabled when the currently selected control is activated (whenever you right-mouse click on a control, CPatron selects it). In order to do that, we need a code path from the frame window level (that sees WM_INITMENUPOPUP) down to the site level in order to ask the site if it has not just a control, but a control with events. The path starts inCPatronFrame::UpdateMenus (PATRON.CPP), which calls CPatronDoc:: FQueryEnableEvents, which calls the same named function in CPages, which calls the same in CPage. CPage in turn callsCTenant::HasEvents, which returns TRUE only if the site has instantiated anIDispatch for events. If you look back at the initialization code earlier in this document, you'll see that this pointer is NULL unless we successfully connected an eventsIDispatch to the control.

So the command will be enabled for event-capable controls only, and when that command is selected from the menu, CPatron goes through a similar path to invoke the dialog, using theAssignEvents function in CPatronDoc, CPages, and CPage. CPage::AssignEvents does nothing more than invoke the dialog box shown in Figure 9, but passes the current site's CEventMap object to the dialog. The dialog box procedure in EVENTS.CPP then handles selecting an event and an action and storing them in the CEventMap object. When the user closes the dialog, any modifications made in the dialog will be current in the site'sm_pEventMap.

Events IDispatch

With an initialized and populated event map structure, such as CPatron's CEventMap, we can now implement theIDispatch interface to handle the calls from the control. If you've created a stub implementation already, the only work to do here will be to implementInvoke—besides storing NULL in output parameters, none of the other functions (IUnknown excluded) need to be implemented at all. A control should never need to ask a container for the type information for its own event set!

Implementing Invoke is interesting because there is no predefined set of DISPIDs to which this function responds, and having some dynamic mapping is the big trick of implementing events. CPatron implementsInvoke by scanning the event map of the appropriate site for DISPID matching the one passed toInvoke. If a match is found, we look up the action associated with the event and execute it; otherwise we return an error:

Copy
STDMETHODIMP CImpIDispatchEvents::Invoke(DISPID dispIDMember, REFIID riid   , LCID lcid, unsigned short wFlags, DISPPARAMS * pDispParams   , VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)   {   HRESULT    hr;   VARIANT    varResult;   EVENTACTION iAction;   UINT      i;   PEVENTMAP pEM;   if (IID_NULL!=riid)      return ResultFromScode(E_INVALIDARG);   if(NULL==pVarResult)    pVarResult=&varResult;   VariantInit(pVarResult);   V_VT(pVarResult)=VT_EMPTY;   //Only method calls are valid.   if (!(DISPATCH_METHOD & wFlags))      return ResultFromScode(DISP_E_MEMBERNOTFOUND);   iAction=ACTION_NONE;   pEM=m_pTen->m_pEventMap->m_pEventMap;   for (i=0; i < m_pTen->m_pEventMap->m_cEvents; i++)      {      if (dispIDMember==pEM[i].id)         {         iAction=pEM[i].iAction;         break;         }      }   if (ACTION_NONE==iAction)      hr=ResultFromScode(DISP_E_MEMBERNOTFOUND);   else      {      MessageBeep((UINT)iAction);      hr=NOERROR;      }   return hr;   }

I suspect that most event-handling Invoke code will look a lot like this, except that what you do after you find a matching DISPID and how you then execute the action will be a lot more complicated than this. But this code illustrates the simple technique of working with DISPID mappings.

A few other notes to make here: This code doesn't handle any event parameters—a real container will generally need to because these in turn become parameters to the user-defined code attached to the event. So you'll have more work to do to parse thepDispParams argument. You will also want to handle events that want return values, which might be the case (check the type information for a non-void return type). With a complete implementation you'll basically allow a user to do anything in response to an event, which is much more interesting (and marketable) than a container that doesn't do anything more than beep, tweak, ding, and frazzle when you twiddle controls.

Save and Load Control and Event Mappings

Once the end user has gone to all the trouble of creating a form with controls and assigning actions to events, setting properties, and so forth, you better make sure you can save all this information persistently and reload it when you load the object.

Saving controls

The first concern is saving the internal state of controls, which includes their properties. As mentioned before, the OLE Controls spec allows controls to implementIPersistStream for this purpose; besides the creation/loading problems for stream-based controls, saving them is easy but requires the container to create a stream for the control before saving. For the purposes of this document we can assume that a control implements IPersistStorage and thus saving its state requires nothing special: Use your existing compound document code, which usually callsOleSave and IPersistStorage::SaveCompleted.

The mapping between events and actions is persistent information of the container that is always attached to a particular control, but the control itself will never be aware of the assignments, nor will it handle persistence of this information for you. Therefore you must implement code that saves the assignments. CPatron handles this through theCEventMap::Serialize (EVENTS.CPP) function in which the event map object will write its mappings to anIStream:

Copy
void CEventMap::Serialize(LPSTREAM pIStream)   {   EVENTMAP      emTemp;   ULONG         cbWrite=sizeof(DISPID)+sizeof(EVENTACTION);   if (NULL==pIStream)      return;   if (NULL!=m_pEventMap)      {      UINT      i;      for (i=0; i < m_cEvents; i++)         pIStream->Write(&m_pEventMap[i], cbWrite, NULL);      }   //Write terminating entry   emTemp.id=0;   emTemp.iAction=ACTION_TAILING;   pIStream->Write(&emTemp, cbWrite, NULL);   return;   }

This function loops through the existing events saving the DISPID and the action assigned to it. The name of the event is not saved because it is just another property of the DISPID that we can retrieve when the object is reloaded.Serialize also uses a terminating structure in which a special code is stored for the action. This is so the deserialization code doesn't have to depend on any set number of events. It would be just as effective to write the number of saved events in a short header in the stream.

The stream into which this information is saved is created under the IStorage given to the control. Because the container owns and manages this stream, we have to prefix the name with an ASCII 3. Therefore the name used for the stream is "\003Event Mappings", which is a constant (defined in TENANT.H) called SZEVENTSTREAM. This stream is created in theCTenant::Update function before CEventMap::Serialize is called, and the STGM_CREATE flag means that we will overwrite any already existing stream:

Copy
if (HasEvents())   {   LPSTREAM   pIStream;   HRESULT    hr;   hr=m_pIStorage->CreateStream(SZEVENTSSTREAM      , STGM_CREATE | STGM_DIRECT | STGM_READWRITE      | STGM_SHARE_EXCLUSIVE, 0, 0, &pIStream);   if (SUCCEEDED(hr));      {      m_pEventMap->Serialize(pIStream);      pIStream->Release();      }   }

This code is, of course, only executed if the object in the site is a control with events.

It is very important in designing your events serialization that the IID of the event set and the functions in that set are not considered persistent. This is because the control may change these the next time you load it. The next section deals with this in more detail.

Loading controls

As with saving controls, there are general concerns right now about controls that serialize intoIPersistStream, so again we'll just assume that controls are like other embedded objects in that they work throughIPersistStorage. Therefore, loading a control is like loading any other embedded object: CallOleLoad and initialize the object as you normally would. As part of normal in-place activation support you'll have a check in your loading procedure for OLEMISC_ACTIVATEWHENVISIBLE, and if it's set you'll send the verb OLEIVERB_INPLACEACTIVATE right away. As a control container, skip this activation if you're in design mode.

During the initialization of a control you will need to load any existing event mappings. CPatron does this way down inCTenant::FControlInitialize, as we've seen (but didn't discuss) before:

Copy
if (SUCCEEDED(m_pIStorage->OpenStream   (SZEVENTSSTREAM, NULL, STGM_DIRECT   | STGM_READWRITE | STGM_SHARE_EXCLUSIVE   , 0, &pIStream)))   {   m_pEventMap->Deserialize(pIStream);   pIStream->Release();   }

Deserialization of CPatron's event mappings is just the process of loading the DISPID/Action pairs from the stream, checking if the DISPID still exists in the map, and assigning the action to it if it does exist:

Copy
void CEventMap::Deserialize(LPSTREAM pIStream)   {   if (NULL==pIStream)      return;   if (NULL==m_pEventMap)      return;   while (TRUE)      {      ULONG      cbRead=sizeof(DISPID)+sizeof(EVENTACTION);      UINT      i;      HRESULT    hr;      EVENTMAP   em;      hr=pIStream->Read(&em, cbRead, NULL);      if (FAILED(hr))         break;      //If we hit the tail, we're done.      if (ACTION_TAILING==em.iAction)         break;      //Assign the action to the ID, if it exists.      Set(em.id, em.iAction);      }   return;   }...BOOL CEventMap::Set(DISPID id, EVENTACTION iAction)   {   BOOL      fRet=FALSE;   if (NULL!=m_pEventMap)      {      UINT      i;      for (i=0; i < m_cEvents; i++)         {         if (m_pEventMap[i].id==id)            {            m_pEventMap[i].iAction=iAction;            fRet=TRUE;            }         }      }   return fRet;   }

There is one major concern with deserialization of an event set: Do not assume that the control still has the same event set as it did before. A control has the right to modify its event set between instantiations, meaning it can add new events and delete old ones. This mostly happens on version changes, but is something your container has to expect every time the control is loaded. Therefore, when you load a single event mapping, you need to see if the DISPID for that event still exists. If it does, reinstate the action mapping. If not, then you'll have to do something else that will not annoy the user. In CPatron's case, the user did virtually no work to assign actions—we just discard the assignment. However, consider the user who wrote code for an event that is no longer available. The container should do something with the code and definitelynot discard it. I would be really annoyed if a container up and deleted code I spent hours writing. What an environment like Visual Basic does in such cases, for example, is to copy the code to a global function—it can't keep it attached to a control because the control is invalid, so it makes it global. I expect most sophisticated containers will do something similar.

Implement IOleControlSite and IOleClientSite::RequestNewObjectLayout

The final topic for this article is filling out a few more IOleControlSite functions as well as making one enhancement toIOleClientSite. Let's deal with the latter item first.

The IOleClientSite interface has always had a function called RequestNewObjectLayout, but nothing in compound document technology uses it. Controls do, however, whenever they would like to get updated positional information. This generally means asking the object for its current extents and calling IOleInPlaceObject::SetObjectRects again (the latter happens inCTenant::UpdateInPlaceObjectRects):

Copy
STDMETHODIMP CImpIOleClientSite::RequestNewObjectLayout(void)   {   RECT   rc, rcT;   SIZEL   szl;   HRESULT hr;   //Get the size from the control   if (NULL!=m_pTen->m_pIViewObject2)      {      hr=m_pTen->m_pIViewObject2->GetExtent(m_pTen->m_fe.dwAspect         , -1, NULL, &szl);      }   if (FAILED(hr))      return hr;   //Add these extents to the existing tenant position.   SetRect(&rcT, 0, 0, szl.cx*10, -szl.cy*10);   RectConvertMappings(&rcT, NULL, TRUE);   rc=m_pTen->m_rcPos;   rc.right=rc.left+rcT.right;   rc.bottom=rc.top+rcT.bottom;   m_pTen->UpdateInPlaceObjectRects(&rc, FALSE);   return NOERROR;   }

The next function, IOleControlSite::OnControlInfoChanged, instructs the container to reload the CONTROLINFO structure, which is trivial. Be sure to update your "have control info" flag here, if you maintain one.

Copy
STDMETHODIMP CImpIOleControlSite::OnControlInfoChanged(void)   {   //We also update our "have info" flag here.   m_pTen->m_fHaveControlInfo=SUCCEEDED(m_pTen->m_pIOleControl      ->GetControlInfo(&m_pTen->m_ctrlInfo));   return NOERROR;   }

Next, IOleControlSite::TransformCoords needs an implementation so that coordinates appear to the container in a uniform fashion. CPatron internally uses MM_LOMETRIC, so converting to HIMETRIC means multiplying by 10 and negating they coordinate. Converting from HIMETRIC means dividing by 10 instead:

Copy
STDMETHODIMP CImpIOleControlSite::TransformCoords(POINTL *pptlHiMet   , POINTF *pptlCont, DWORD dwFlags)   {   if (NULL==pptlHiMet || NULL==pptlCont)      return ResultFromScode(E_POINTER);   if (XFORMCOORDS_HIMETRICTOCONTAINER & dwFlags)      {      pptlCont->x=(float)(pptlHiMet->x/10);      pptlCont->y=(float)-(pptlHiMet->y/10);      }   else      {      pptlHiMet->x=(long)(pptlCont->x*10);      pptlHiMet->y=(long)-(pptlCont->y*10);      }   return NOERROR;   }

With IOleControlSite::LockInPlaceActive you either increment or decrement a lock count for the object. CPatron maintains the count in theCTenant::m_cLockInPlace variable. If this lock count is positive and a deactivation attempt is made, you can mark a deactivation as pending, as is done inCTenant::DeactivateInPlaceObject:

Copy
void CTenant::DeactivateInPlaceObject(BOOL fFull)   {   if (NULL!=m_pIOleIPObject)      {      if ((OLEMISC_INSIDEOUT & m_grfMisc) && !fFull)         m_pIOleIPObject->UIDeactivate();      else         {         //CONTROLMOD         if (0==m_cLockInPlace)            m_pIOleIPObject->InPlaceDeactivate();         else            m_fPendingDeactivate=TRUE;         //End CONTROLMOD         }      }   return;   }

The CTenant::m_fPendingDeactivate flag must be cleared back to its initial FALSE state whenever the object is reactivated—either by the container sending an in-place activate or UI activate verb (seeCTenant::Activate), or when either IOleInPlaceSite::OnInPlaceActivate orIOleInPlaceSite::OnUIActivate is called. In any case, this flag is used in our implementation ofLockInPlaceActive and the lock count is then decremented to zero:

Copy
STDMETHODIMP CImpIOleControlSite::LockInPlaceActive(BOOL fLock)   {   if (fLock)      m_pTen->m_cLockInPlace++;   else      {      if (0==--m_pTen->m_cLockInPlace)         {         //If there's a pending deactivate, do it now.         if (m_pTen->m_fPendingDeactivate)            m_pTen->DeactivateInPlaceObject();         }      }   return NOERROR;   }

Finally, the other functions in IOleControlSite are not implemented in CPatron because it doesn't do the features related to those functions.

Other Steps

Because CPatron does not have keyboard mnemonics, button/label handling, or extended controls implemented, there's not a lot I can say about these features from an implementation standpoint. Pending additional work on the subjects, there's no more material to add here.

0 0
原创粉丝点击