Einführung in die OCX-Programmierung
GNOM SOFT Dipl.Kfm. Gerhard Paulus & Co. GmbH
OLE Custom Controls (OCX'es) use most of the software technology built into Object Linking and Embedding. OLE means about 120 API functions and about 80 interfaces that average 5 members functions each.
The concept behind the API functions, which are implemented in DLL's, is clear. They take parameters, return a value and that's it. But what are OLE "interfaces" ?
Basically OLE interfaces are groups of functions which you can only call after getting a pointer to the interface in question. Usually this OLE programming is accomplished with C or C++. In file Query.prg you find a Visual FoxPro (VFP) program which is a C++ -> VFP port of the "query" sample code of chapter 2 of "Inside OLE" (Microsoft Press). It shows coding OLE style, especially the software mechanism behind OLE interfaces.
In advance a word to the C++ -> VFP ports of the sample code of "Inside OLE". The ports are "true to the spirit" but they are not 1:1 ports. VFP does not support all concepts possible in C++ (eg. class attributes cannot be passed by reference) so I had to come up with some workarounds. And to keep things concise and simple there is next to none error checking in the VFP samples. To bring the VFP code more in line with the C++ original all functions (either normal ones or interface methods) should return result handles indicating success or failure of the respective operation. In OLE 32-Bit values are returned where the highest Bit is either set to 0 (success) or 1 (failure). The remaining 31 Bits of these return values specify error facility (category) and the actual error code.
Query.prg implements an OLE style object (Object1) with 3 interfaces (IUnknown, ISampleOne and ISampleTwo). In the usual OLE diagrams it would look similar to the following:
O IUnknown ¦ ¦---------------¦ ¦ Object1 ¦ ISampleOne O-¦ ¦ ¦ ¦ ISampleTwo O-¦ ¦ ¦---------------¦
In OLE-speak you have "server", "object" and "client". The OLE client is some code which calls functions in an OLE object which is implemented in an OLE server which is either a DLL or an EXE file. The samples here are a bit contrived as client, server and objects are all implemented in the same file (a VFP PRG).
The code of the OLE "client" of QUERY.PRG which instantiates and uses the OLE object is on the companion disk. It will get a pointer to the ISampleOne interface and call it's "GetMessage" function. And it will get a pointer to the ISampleTwo interface and call it's "GetString" function.
How does it work ?
If you want to use the functionality of an OLE style object (Object1 in this case) you first have to get this object loaded into memory. This is handled by procedure "CreateObject1". In OLE speak this is the "Class Factory". And you must have a pointer to this object. The pointer variable "pObject1" is passed to "CreateObject1" by reference and "CreateObject1" will make sure that this variable will point to the new object. This is accomplished by calling the "QueryInterface" function of this object.
Function "QueryInterface" is a bare necessity of life as far as OLE style objects are concerned. If you have a pointer to an OLE object you *must* be able to call "QueryInterface" with this pointer. Otherwise it simply is not an OLE style object.
"QueryInterface" expects 2 parameters: the first is the ID of the OLE interface in question and the other is a reference to (address of) the pointer variable which will later point to this interface. The "CreateObject1" function will pass the ID of the interface "IUnknown". In this simplified sample the ID of IUnknown is an integer (1). In real OLE programming interface ID's are 128 Bit structures known as GUIDs (globally unique ID's). Just think of it as a Bit sequence assured to be unique on this planet. Any OLE interface gets a unique number like this (there is a special program generating these numbers: GUIDGEN.EXE). As soon as an OLE interface changes (new functions etc.) it becomes a new interface with a new ID. These GUID's are stored in the system registry under HKEY_CLASSES_ROOT\Interface.
As far as the interface "IUnknown" is concerned the pointer variable will ultimately point to the OLE object itself. IUnknown is basically the run-time identity of the object. A shorter version (without the call to "CreateObject1") would have been:
pObject1 = CREATEOBJECT("Object1")
This way pObject will also point to the object. In OLE style programming the indirect route via "QueryInterface" is better because "QueryInterface" does also reference counting. This makes sure that as long as there is a reference (Pointer) to the OLE object this object won't get deleted. By convention "QueryInterface" will call the "AddRef" function of the object (another must-have). "AddRef" will increase the reference count by 1. If you don't need the pointer to the OLE object any longer you call the Object's "Release" function (another must-have for OLE objects). "Release" will decrease the reference count by 1. If the resulting reference count reaches zero, "Release" by convention makes sure that the object destroys itself and frees all memory involved. The mentioned 3 must-have functions (QueryInterface, AddRef, Release) together form the interface "IUnknown" which each and every OLE style object is required to implement.
But all this was just foreplay. How do you actually call the worker functions implemented in the OLE object ? You go the "QueryInterface" route. You call this function using the pointer to the object, pass the ID of an interface (eg. the ID of ISampleOne) and pass a reference to another pointer variable. If the pointer variable ultimately has a value other than .NULL. it will point to the implementation of the interface in question. Use this pointer to call the function you want and you are settled. Of course you have to call the "Release" function through this new pointer as soon as you don't need it any longer.
Ultimately this means that OLE is designed to be a perpetual building-site. There will always be OLE objects and OLE clients (code that uses OLE objects) leap-frogging each other. If an OLE client doesn't know the ID of an interface (and thus cannot use it's functions) the client is presumably out-dated. But the client still works as it can use the old interfaces the OLE object carries still around. If an OLE client knows more interfaces than a given OLE object supports the client will get NULL pointers from "QueryInterface" and can find alternatives. According to Microsoft OLE interfaces make "robust evolution of software over time" possible.
OLE is object-oriented technology. So how does inheritance work with OLE objects ? Answer: it doesn't work at all. OLE doesn't know the concept of implementation inheritance.
That certainly sounds strange but reusability of code in the world of OLE objects goes the interface route. There are 2 methods for a given OLE object to re-use the functions already implemented in a given other OLE object: Containment and Aggregation. VFP code which shows the concept of Containment is in Koala_C.prg. VFP code which shows the concept of Aggregation is in Koala_A.prg. Both programs are C++ -> VFP ports of the "Koala" (Reuse) sample in chapter 2 of "Inside OLE", Microsoft Press.
The standard way in VFP to do this stuff would be to inherit the Koala class from the Animal class:
DEFINE CLASS Animal AS Custom
? "Eat() in Animal called ..."
DEFINE CLASS Koala AS Animal
? "ClimbEucalyptusTrees() in Koala called ..."
The Containment sample (Koala_C.prg) will implement 2 OLE style objects: Animal and Koala. As soon as the Koala object (outer object) gets instantiated it will also load an instance of the Animal object (inner object) into memory and keeps a pointer to Animal. The Koala object effectively becomes a client of the Animal object and uses that object's functionality.
As far as interfaces are concerned the Animal object knows 2 interfaces: IUnknown and IAnimal (implementing the Eat() function). The Koala object knows 3 interfaces: IUnknown, IKoala (implementing the ClimbEucalypusTrees() function) and IAnimal. The latter is implemeted in such a way that calls to Koala's IAnimal interface are simply delegated to Animal's IAnimal interface.
In short: Koala's client has only direct access to Koala's interfaces and has no direct access to Animal's interfaces. Koala's client is unaware of the Animal object.
The Aggregation sample (Koala_A.prg) will implement 2 OLE style objects: Animal and Koala. As soon as the Koala object (outer object) gets instantiated it will also load an instance of the Animal object (inner object) into memory and keeps a pointer to Animal. But it will also pass a pointer to itself to the Animal object which stores this pointer as the so-called "Outer Unknown".
As far as interfaces are concerned the Animal object knows 2 interfaces: IUnknown and IAnimal (implementing the Eat() function). Animal makes sure that IAnimal also gets the pointer to the Outer Unknown (Koala object). The Koala object knows 2 interfaces: IUnknown and IKoala (implementing the ClimbEucalypusTrees() function). If Koala's client wants a pointer to the IAnimal interface, Koala will make sure that the pointer points to Animal's IAnimal interface directly. This is the reason why Koala doesn't have to implement a delegating IAnimal interface.
The reason why the Animal object and Animal's IAnimal interface must hold the Outer Unknown pointer to the Koala object is this: if Koala's client gets a pointer to the IAnimal interface a "QueryInterface" through this pointer for the IUnknown interface must return a pointer to the outer object. That's one of the "contracts" behind OLE which also makes sure that reference counting works correctly.
In short: Koala's client has direct access to Koala's interfaces and also to Animal's interfaces.
You will find the code files KOALA_C.PRG and KOALA_A.PRG on the companion disk. Both use the same 'client' portion.
OLE Custom Controls can fire events and send notifications to their respective container applications. It's clear that there must be a close communication between the OCX (an OLE object) and its container (an OLE client) and one way to achieve this tight coupling is the 'Connectable Object' approach.
The VFP project Connect.pjx (with Connect.prg and Connect.mnx as elements) demonstrates the basic software mechanism behind Connectable Objects. It's a C++ -> VFP port of the 'Connect' sample in chapter 4 of "Inside OLE" (Microsoft Press).
Up to now we've been dealing with OLE objects which offer 'incoming' interfaces. That is, the client application gets pointer(s) to the object's interface(s) and calls the respective functions/methods through this (these) pointer(s). The OLE object can send information back to the client via function return value or by storing values in out-parameters. But there is no way that the object on its own can call functions implemented by the client application.
For an OLE object to fire events it must support 'outgoing' interfaces as well, which are used by the OLE object but actually implemented by the client application. The following graph shows the concept.
¦------------¦ ¦ Client ¦in ¦---------------¦ ¦ ¦-------> O-¦ Object ¦ ¦ ¦--------¦ ¦ ¦ (source) ¦ ¦ ¦ sink ¦---O <-------¦ ¦ ¦ ¦--------¦ ¦ out ¦---------------¦ ¦ ¦ ¦------------¦
The client application (say, VFP.EXE) must create at least one OLE object with some kind of OLE interface. It then passes a pointer to this object's interface on to the OLE Connectable Object (say, an OCX). This Connectable Object can make sense of this pointer, stores it and uses it as soon as an event has to be fired. Basically this means: an event-firing OLE object becomes the client of another OLE object, which happens to be created by its own client. In OLE speak the Connectable Object is called the 'source' and the OLE object implementing the outgoing interface is called the 'sink' for events/notifications.
How does Connect.prg work ?
It will facilitate user intervention (just to show that this approach is quite flexible) by creating a menu with this structure:
Object Trigger -------------------------------------------- Create object Quack Release Object Flap Paddle
Sink1 --> Connect --> Disconnect Sink2 --> Connect --> Disconnect
Seriously, the going gets tough; we are firing duck events now ... <g>. Anyway, the user has options to create a Connectable Object and also to release it again. If the Connectable Object is created then user can connect/disconnect one or two sinks to this object. And if a duck event is triggered (by selecting one of the Trigger options) all connected sinks will receive this event. Of course, if none of the sinks is connected to the Connectable Object, choosing one of the Trigger options will basically produce no results at all.
This scenario is a bit contrived insofar as client code will actually start the event firing process targeting at itself. Usually events are started by something outside of the client application's control (say, a character arriving at a communication port or a user clicking on a visible OCX'es surface (the OCX'es Window window). But other than that the sample shows all techniques involved, including typical OLE enumeration objects. And it certainly shows how flexible and extensible these techniques are.
The three parties involved in this scenario are: OLE client code, sink objects and a Connectable Object.
Client code in Connect.prg looks like this:
# DEFINE IID_IUnknown 1
# DEFINE IID_IConnectionPointContainer 2
# DEFINE IID_IConnectionPoint 3
# DEFINE IID_IEnumConnectionPoints 4
# DEFINE IID_IEnumConnections 5
# DEFINE IID_IDuckEvents 9999
MAXCONNPOINTS = 1
MAXCONNECTIONS = 2
* pointer to Connectable Object
* create 'sink' objects
pSink= CREATEOBJECT("DuckEvents", 1)
pSink= CREATEOBJECT("DuckEvents", 2)
PUSH MENU _msysmenu
POP MENU _msysmenu
In addition, the Client portion of Connect.prg will also implement the following functions to create/release a Connectable object, to connect and disconnect sinks and actually to initiate the triggering mechanism:
In the array pSink[ ] pointers to 2 sinks are stored. Client code will create these sinks as instances of the VFP class "DuckEvents". These objects are independant OLE style objects with own reference counting etc. They have 2 interfaces: the ubiquitous IUnknown and a custom interface IDuckEvents (with arbitrary ID 9999). Here the definition of this class:
DEFINE CLASS DuckEvents AS Custom
m_dwCookie=0 && connection Key
PROCEDURE QueryInterface(IID, pInterface)
CASE IID = IID_IUnknown
pInterface = this
CASE IID = IID_IDuckEvents
pInterface = this
this.m_cRef = this.m_cRef + 1
this.m_cRef = this.m_cRef - 1
IF this.m_cRef = 0
? "Sink "+cSinkNo+" received 'Quack' event ..."
? "Sink "+cSinkNo+" received 'Flap' event ..."
? "Sink "+cSinkNo+" received 'Paddle' event ..."
Client code has a pointer variable (pObj) for the one and only Connectable Object this sample can create. This object in Connect.prg is an instance of VFP class 'Connect'. It's actually an implementation of the "IConnectionPointContainer" interface. Just to make sure, this is pretty avant-garde as far as OLE-style programming is concerned. The specification for this interface is quite new in OLE history and it was designed primarily with having OLE Custom Controls in mind.
Apart from the ubiquitous IUnknown-methods 2 methods/functions have to be implemented in the IConnectionPointContainer-interface:
In addition the "Connect" class has the "TriggerEvent" method to perform the event firing process (once the OLE client has started things).
Upon creation a "Connect" object will create another object of VFP class "ConnectionPoint" and hold a pointer to this object in an array-style attribute. To keep things simple this sample uses only one ConnectionPoint (a ConnectionPointContainer should be able to handle more).
The ConnectionPoint object is an OLE style object of its own. Apart from IUnknown this kind of object implements the interface IConnectionPoint with following methods:
The ConnectionPoint object does the actual connecting business. Each ConnectionPoint cares for one particular outgoing interface implemented by the client application. In the Connect.prg sample this outgoing interface is identified by IID_IDuckEvents (9999 that is) and the ConnectionPoint object saves this ID as an attribute (m_iid). If for some reason whatsoever the Connectable Object must also fire, say, FoxEvents then a new, additional ConnectionPoint object would be necessary dealing with the IFoxEvents outgoing interface. In this case the ConnectionPointContainer object (the Connectable Object itself in this sample) would hold pointers to 2 ConnectionPoint objects.
A ConnectionPoint object must handle a certain number (fixed or variable) of connections. In the 'Connect' sample the upper limit of connections for a given ConnectionPoint is 2 which is sufficient as the sample knows about only 2 possible sinks. 'Connection' in this context means that a sink object connects to a ConnectionPoint by passing a pointer to its own IUnknown interface.
Apart from IConnectionPointContainer and IConnectionPoint the Connect.prg sample also shows how to use the IEnumConnections interface. An object implementing this interface iterates through all connections a ConnectionPoint knows about and returns pointers to the respective sinks. In Connect.prg this enumerator interface is implemented as a VFP class of its own (EnumConnections) and offers these methods:
There is another enumeration interface of interest in this context: IEnumConnectionPoints. Such an interface iterates through all ConnectionPoints a ConnectionPointContainer knows about. But as the Connect-sample has just one ConnectionPoint implementing the IEnumConnectionPoints interface was not necessary.
So much for the framework, what about dynamics ?
To get things going at first an instance of the Connectable Object has to be created. Function "CreateConnectObject" in the client portion of Connect.prg does this. Then at least one of the sinks has to be connected to the connectable object. Function "ConnectSink" in client code will do this. It gets an interface pointer to the Connectable Object and then an interface pointer to the ConnectionPoint responsible for firing "Duck Events". Through this interface pointer the Advise-method is called passing the pointer to the sink on to the ConnectionPoint. In return (via out-parameter) the sink gets a 'Cookie' value. This is an incrementing number (ID) the ConnectionPoint object manages to uniquely identify the various connections. The sink will use this cookie later to disconnect from the ConnectionPoint.
With Connectable Object created and at least one sink connected, choosing one of the Trigger menu options will initiate the event firing mechanism. The 'StartTrigger' function in client code will call the 'TriggerEvent' method in the Connectable Object. 'TriggerEvent' will first use the pointer to the respective ConnectionPoint to create an enumeration object of class 'EnumConnections'. The Connectable Object itself does not know or care what connections are currently active. It delegates the job of iterating through the active connections to the EnumConnections object. In particular the 'Next' function will successively return (via out-parameter) the pointers to connected sinks. As this one and only ConnectionPoint specialized on IID_IDuckEvents the corresponding Duck interface functions (quack, flap, paddle) can be savely called. Which in this sample means: event fired, mission accomplished.
Connected sinks can be disconnected at any time. This will ultimately call the 'UnAdvise' method in the ConnectionPoint object, making the respective pointer slot free to accept new connections. As soon as a sink is disconnected any events fired will not touch it any longer. Of course such a disconnected sink can re-connect any time.
OLE Custom Controls
As far as OCX'es are concerned these are OLE objects offering quite a lot of interfaces. But also the OCX container application (eg, VFP) has to implement OLE interfaces so that OCX and container can talk to each other.
In particular these are the interfaces an OCX must implement:
IClassFactory: (implemeted by OCX server DLL)
IClassFactory2: (implemeted by OCX server DLL)
And here is a description of what theses interfaces are made for:
Has all functions necessary to embed a content object in an OLE compound document. A prominent function of this interface is "SetClientSite" which gets as argument a pointer to the IOleClientSite interface in the Container app.
They make sure an OCX can be activated in-place. This way the window of an OCX is tightly integrated within the window of eg. a VFP form.
Has functions to get and set data in the OCX and to query and enumerate data formats the OCX can offer. Also has functions to establish and terminate notifications if data changes happen or are about to happen. For these notifications to work the Container app (eg. VFP) has to implement the outgoing interface "IAdviseSink", a prominent method of which is "OnDataChange".
Makes the OCX viewable by drawing presentations of the OCX'es data directly to devices (screen or printer). This interface also has functions for notifications. As with IDataObject these notifications call methods in the outgoing interface "IAdviseSink" (primarily "OnViewChange").
Functions in these interfaces save/load the OCX'es data to/from a storage or stream. In this context OLE's structured storage model is used. In a nutshell this means that OLE creates a complete file system within a single DOS-level file (only one file handle from the operating system is used). In the structured storage model "storage" has the DOS-equivalent "directory and "stream" has the DOS-equivalent "file".
For caching IUnknown interface pointers in the "Running Object Table" maintained by the OLE API. "Register" places the IUnknown pointer of an OCX in the ROT and "GetObject" will retrieve it later via OLE monikers.
Has function "GetPages" which will return the GUID's which can be used for editing the OCX'es properties from within the Container app. This means that each property page must be registered as a different OLE object in the system registry.
Manages IConnectionPoint objects which enable the OCX to fire events. The Container must implent event sinks (outgoing interfaces) so that these events actually trigger something.
This interface is used for OLE automation. The OCX client (the Container app) can use this interface to call functions in the OCX indirectly (late binding). The work horse in this interface is method "Invoke", which also makes sure that parameters and return values are passed correctly between Container and OCX.
This gives acces to the OCX'es type information. Thus the Container app can programmatically analyze the OCX'es incomimg and outgoing interfaces at runtime.
This interface is used for handling keyboard accelerators associated with the OCX and notifications about ambient (Container-side) properties. Function "FreezeEvents" is used by the Container to tell the OCX that it should no longer fire events (or restart doing so). Ambient properties are eg. "name", position ("left", "top", right", bottom") etc.
This will instantiate the OCX object (load it into memory). IClassFactory2 supports licensing schemes which were designed especially for OCX'es.
These are the main interfaces to be implemented by the OCX developer. On the Container (client) side of things the following interfaces must be present to support OCX'es. In a nutshell: the Container must be a full OLE embedding container (linking support is not necessary) that also supports in-place activation.
For in-place activation of embeddable OLE object (eg OCX).
The OCX gets a pointer to this Container interface shortly after instantiation. This way the OCX can query at runtime the capabilities of the Container and also its environment.
This is the outgoing interface for notifications on data changes and view chages the OCX sends to the Container. On the OCX side the matching Interfaces are IDataObject and IViewObject.
For client <-> object communication sprcific to OCX'es. Eg. function "OnFocus" is called by the OCX when it gains or loses focus. "TransformCoords" is called by the OCX for translating OCX and Container coordinates.
Has to functions "OnChanged" and "OnRequestEdit" which are called by the OCX when properties change or are about to change.
This interface has 2 functions "PreMessagefilter" and "PostMessagefilter".
If the OCX is marked as "Simple Frame" it will pass all Windows messages it receives on to the Container by calling function "PreMessagefilter" in the interface "ISimpleFrameSite". If the Container does not return FALSE the OCX has a chance to process this Windows message, too. After that the OCX must call function "PostMessagefilter" giving the Container another cjance to react on the Windows message.
Interfaces IPropertyNotifySink and ISimpleFrameSite are optional according to the "OLE Controls and OLE Control Containers Guidelines 1.0". The interface IVBFormat is specific to Visual Basic and not mentioned in these Guidelines.
And finally: memory life-cycle of an OCX
OCX'es are coded as in-process OLE servers, which means as DLL's (Dynamic Link Libraries) and not EXE'es. This DLL exports the function "DllGetClassObject". The OCX Container app (eg. VFP) will load the DLL and call this function which will first instantiate the class factory of the OCX which will in turn instantiate the real OCX (load it into memory) and pass an interface pointer back to the Container app. From this point on the Container (eg. VFP) has access to the full functionality of the OCX by querying the OCX'es interfaces and calling the necessary interface functions.
The OCX in turn gets its first interface pointer of the Container (IOleClinetSite, to be specific) when the Container calls function "SetClientSite" in the OCX'es interface "IOleObject".
As soon as there are no more references to the OCX (the OCX'es internal reference count reaches zero) the OCX will delete itself in memory.