Athena

Interfacing C++Builder With The World Using OLE Automation

Brian Long (www.blong.com)

If you find this article useful then please consider making a donation. It will be appreciated however big or small it might be and will encourage Brian to continue researching and writing about interesting subjects in the future.


Introduction

OLE automation is one facet of OLE (Object Linking and Embedding) which was originally designed to take over from DDE in the area of information exchange. Most application users' exposure to OLE is similar to their exposure to DDE - a way of inserting information from one application into some document in another application. The idea with OLE is that the information is represented as an object and either some information describing a link to the document object is inserted, or the whole document object is embedded - hence Object Linking and Embedding.

There is a component in C++Builder, that allows you to build OLE clients that support this in-place editing, called TOLEContainer. You can refer to the two sample projects in C++Builder's EXAMPLES\DOC\OLECTNRS directory, OLESDI.MAK and OLEMDI.MAK, for details of how to write an OLE in-place editing client.

OLE Automation is a separate aspect of OLE dedicated to allowing one application to control or automate another application. The application being controlled is called an OLE automation server, and the one doing the controlling is called the OLE automation controller or OLE automation client. The client establishes the link between the two applications.

Like DDE, OLE automation allows information to go backwards and forward between the applications, and also allows the client to cause functionality to be executed in the server. Unlike DDE, where there is a distinction between conversation topics and items within each topic, OLE automation is managed by objects. The server supports one or more objects that have properties and methods available to external controllers. You can read and write properties, and call methods.

In order to invoke an OLE automation server, it must be registered - that is it must have information stored in the Windows registry, sufficient to describe and locate it. This is another difference between DDE and OLE - a DDE client must rely on the DDE server being on the path, or must know where it resides. An OLE client need not care - the OLE code in Windows will find where the server application resides by examining the registry. To start controlling an OLE server you ask the OLE support DLLs to create an appropriate object. In the case of Microsoft Word, you would create a Word.Basic OLE object. Once OLE has given you the object, you can control it.

Word.Basic is effectively the class that you are creating an instance of and it is sometimes referred to as the OLE class name or a class string, but is correctly termed (as far as OLE is concerned) as a ProgID.

It's worth noting at this point that the OLE automation server can be an application or a DLL. Because DLLs live in the process address space of the EXE that uses it, DLL servers are called in-process servers or in-proc servers. EXE servers are called out of process servers or out-of-proc servers. OCXs and ActiveXs are OLE in-proc servers with specific extra bits in to make them work as visual controls.

Controlling Automation Servers

The C++Builder support for controlling OLE servers revolves around variables of type Variant. A Variant variable can have values of a range different types assigned to it and read from it. Visual Basic and Delphi both support Variants as native types but C++Builder implements a class to represent them. For example the following code is valid.

Variant V = 5;
V = "Hello";
V = True;
V = 5.75;
String S = V; // S now has "5.75" in it

In addition to integers, reals, strings, Booleans, date-and-time values, and arrays of varying size and dimension with elements of any of these types (including Variants), a Variant is also used to represent OLE automation objects. An OLE Automation server is an application that implements a derivative of OLE 2's IDispatch interface, and a Variant can contain a reference to an IDispatch object.

To set one up, you need to call the CreateObject() member function of the Variant. The object will then be available until the server application is closed by the user, you explicitly terminate it programmatically or the Variant goes out of scope. The last point means that if you declare a Variant local to an event handler, or other routine, then when the routine ends the OLE object will be destroyed.

Let's test this out using Microsoft Word as the server. Make a new project and declare a Variant called MSWord as a private data field in the form class (remember that Ctrl+F6 switches between a unit implementation file and its associated header file). Put two buttons on the form with captions of Start Word and Stop Word. Give them functionality as follows:

void __fastcall TForm1::Button1Click(TObject *Sender)
{
  MSWord = MSWord.CreateObject("Word.Basic");
}
void __fastcall TForm1::Button2Click(TObject *Sender)
{
  MSWord = Unassigned;
}

An alternative to the Variant's CreateObject() method is to #include the VCL\OLEAuto.HPP header right at the top of the unit. Placing this #include before the already present VCL\VCL.H seems to make the pre-compiled header system work better. Then an OLE automation session with Word can be invoked using:

MSWord = CreateOleObject("Word.Basic");

If Word is not running, but is set up on your machine, these statements cause Word to be invoked and terminated respectively. If you have Word 95 or Word 97 then Word will remain hidden when it is invoked. Word 6 makes its presence know by being visible. If Word is already running, this code causes a connection to be made, and dropped, to that Word instance. If Word is unavailable, CreateObject() will raise an EOLEError exception.

By default, most OLE automation servers will remain hidden when invoked by a controller. If your version of Word starts hidden and you want to see it, you will need to call one of the Word.Basic methods to do the job. In order to find out what methods, properties and objects exist relies on documentation from the server vendor. In the case of Word, the automation interface matches very closely the entirety of the Word Basic language. Since Word Basic has an AppShow command to make Word visible, the Word.Basic object has an AppShow method. Follow the call to CreateObject() or CreateOleObject() with:

MSWord.OleProcedure("AppShow");

Note that in contrast to a C++Builder member function call, in order to call a routine in an OLE server object we must pass its name and any parameters along to either the OleProcedure() or OleFunction() member functions of the Variant. OleFunction() should be used if the OLE member function returns a value. If the specified member function is invalid you will get an exception at run-time rather than a compile-time error.

An alternative way of calling routines in an OLE server relies on using a Variant's Exec() member function. Exec() takes an AutoCmd object as a parameter. There are two useful AutoCmd descendant classes: Function and Procedure. They can be used like this:

Procedure AppShow("AppShow");
MSWord.Exec(AppShow);

Now add two more buttons (with captions of New file and Insert text) and a memo to the form. Either give this code to the buttons:

void __fastcall TForm1::Button3Click(TObject *Sender)
{
  MSWord.OleProcedure("FileNew");
}
void __fastcall TForm1::Button4Click(TObject *Sender)
{
  MSWord.OleProcedure("Insert", Memo1->Text);
}

or if you prefer, use this code:

void __fastcall TForm1::Button3Click(TObject *Sender)
{
  Procedure FileNew("FileNew");
  MSWord.Exec(FileNew);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button4Click(TObject *Sender)
{
  Procedure Insert("Insert");
  MSWord.Exec(Insert << Memo1->Text);
}

You should find you can connect to Word, get a new file created, copy the memo's text to the Word document and disconnect from Word.

Depending how advanced the server is (Word is advanced) the various properties available may return back more OLE objects that have their own methods and properties. Place one more button (with a caption of Stats) on the form and a listbox. Use the following code for the button and notice the nested levels of calls that are made. Also, for efficiency, the additional OLE objects returned by the various calls are stored in other Variants to avoid excessive OLE calls being made in the OLEAuto unit.

void __fastcall TForm1::Button5Click(TObject *Sender)
{
  //Update Word's doc stats
  MSWord.OleProcedure("DocumentStatistics");
  //Obtain doc stats using 2 variants
  Variant CurValues = MSWord.OleFunction("CurValues");
  Variant DocStats = CurValues.OleFunction("FileSummaryInfo");
  ListBox1-Items-Clear();
  ListBox1-Items-Add(
    String(DocStats.OleFunction("NumPages")) + " pages");
  ListBox1-Items-Add(
    String(DocStats.OleFunction("NumParas")) + " paragraphs");
  ListBox1-Items-Add(
    String(DocStats.OleFunction("NumLines")) + " lines");
  ListBox1-Items-Add(
    String(DocStats.OleFunction("NumWords")) + " words");
  ListBox1-Items-Add(
    String(DocStats.OleFunction("NumChars")) + " characters");
}

So calling or automating an OLE server is easy enough as long as you know what methods and properties it exposes and what its ProgID is. This test project is supplied with this paper as OLEWORD.MAK.

Writing An Automation Server

The process of writing an OLE automation server is very much automated itself, through an OLE Automation expert. The idea at the end of the day is to write an appropriate class in an EXE or DLL with certain properties and methods marked as available to automation controllers, and then to register the server.

Let's start by making a new application. By the time we finish this we will have the server acting rather like Word in that if it is started by a controller, the main form will not show up. So to remind ourselves of this fact, place a large label on the form with a caption indicating that the server has been started normally. Now save the project (one is supplied with this paper called SERVER.MAK).

Select File | New... and choose Automation Object from the Object Repository. This asks for a C++Builder class name (choose TMyOleServer), an OLE class name (there is already one made for you, Server.MyOleServer), a description (for example "This is my first OLE server") and an instancing option. This is needed to tell Windows what to do if a second application tries to create an object from your server. Multiple instancing means that a second object from your server must come from a separate instance - not the same one. Single instancing means that multiple objects can come from one instance of the server. Lastly, Internal means that the object will not be registered in the system registry. DLL servers are always marked Multiple. We will leave our server marked as multiple instancing as well.

Pressing OK manufactures a unit with your class laid out, and also various registration details present in the unit. You should not modify any of these registration details except (if needed) the description and instancing option. The ProgID is the OLE class name that automation controllers will use when creating one of your objects to control, and the class ID is a GUID (Globally Unique IDentifier) that is tied to your ProgID. Both these details will ultimately be stored in the registry.

Notice in your unit header that your class has an __automated section - it is here that you place properties and methods that you want to be available to OLE controllers. Let's proceed and implement a simple OLE automation server class that surfaces a property called TheTime. As the name suggests, this does nothing more interesting than return the current time.

A couple of important points to remember are: member functions that will be invoked by automation controllers either directly, or via properties, must not be implemented in the class declaration. In other words they must not be implemented as inline routines, otherwise you will see big failures. Member functions that are declared or referred to in the __automated section must be declared with the __fastcall calling convention modifier.

class TMyOleServer : public TAutoObject
{
private:
  TDateTime __fastcall GetTheTime(void);
public:
	__fastcall TMyOleServer();
__automated:
  __property TDateTime TheTime = { read=GetTheTime };
};

Having modified the class declaration in the unit header as shown above, the implementation of the GetTheTime() member function must be placed in the unit itself:

TDateTime __fastcall TMyOleServer::GetTheTime(void)
{
  return Time();
}

Some development systems that support writing OLE automation controllers allow you to call upon a server's default property or member function by not specifying any specific property or function. Whilst it seems that C++Builder does not cater for calling upon a server's default bits and pieces (unlike Delphi and Visual Basic) it does allow you to specify a default member function in a server you write.

If a member function is declared in the __automated section of an OLE class, then employing the __dispid directive allows you to hard code a dispatch identifier for the function. To make a member function the default, use a dispatch identifier with a value of DISPID_VALUE as shown in the following example declaration:

class TMyOleServer : public TAutoObject
{
private:
public:
	__fastcall TMyOleServer();
__automated:
  void __fastcall DefaultMemberFunction(void) __dispid(DISPID_VALUE);
};

Before we get onto registering the server, there was that little matter of hiding the main form if we are started under automation control. Make an OnCreate handler for the main form and put this statement in (you will again need to use the VCL\OLEAuto.HPP header at the top of the main form unit):

if (Automation-StartMode == smAutomation)
{
  //Don't show main form
  Application-ShowMainForm = False;
  //Also don't show task bar button for server
  ShowWindow(Application-Handle, SW_HIDE);
}

Registering An OLE Server

In order to get the relevant information stored in the registry we need to run our application. That alone is enough to get the server to store all the appropriate OLE information in the registry, however the application is left running for no real reason. Another possibility is to run the application with a command-line switch of /RegServer. To set up command-line parameters, choose Run | Parameters... When the parameter is set, run the application. You should find it runs and immediately stops. All it did was add enough information into the registry as is needed and then terminated. Now you can remove the parameter, again using Run | Parameters... If, at some later point you need to un-register the server, use the parameter /UnregServer.

The mechanism used to deal with these command-line parameters is all to do with the Application->Initialize() statement in the project source file. This allows the OLEAuto unit to hook in and execute some code after all the unit initialisation sections and start-up functions have executed, but before the program has properly begun to handle any events. As far as the VCL is concerned, that is the only purpose for the call to Initialize() so if you are not writing an OLE server application you can safely remove the call.

What happens when a server gets registered? Well, several things are added into the Windows registration database. Run REGEDIT.EXE to see the registry - if you are running Windows NT you will need to be logged on with supervisor rights to see the whole contents. If you expand HKEY_CLASSES_ROOT you find many keys. Scroll down until you see Server.MyOleServer (our server's ProgID). Click on it and you can see its value matches your server description: My first OLE server. If you expand the key, you can see a CLSID key. The class ID should match what you saw in the registration information in the C++Builder class unit. When OLE is told to make a Server.MyOleServer object it will be able to find the class ID, but what then?

It then does a bit of cross-referencing. Scroll back up through HKEY_CLASSES_ROOT until you find the CLSID key and expand it. You will find many GUIDs listed. Scroll down until you see your GUID and then select it. The value is again your server class's description. If you expand the key, you can see a ProgID key whose value will be Server.MyOleServer. Additionally there is a key marked LocalServer32 which gives the command-line necessary to launch this 32-bit local machine hosted OLE server.

Testing The OLE Server

To test out this new server object, we do much the same as we did with Microsoft Word. Make a new project and declare a Variant object in the private section of the form class. Remember the Ctrl+F6 shortcut to get to the class definition and call the Variant Server. Next #include the VCL\OLEAuto.HPP header in the main part of the form unit. Add a timer component, from the System page of the component palette, to the form and set its Interval to 500 (so the OnTimer event triggers every half a second). Make an OnCreate handler for the form and an OnTimer event handler for the timer and set them up like this.

__fastcall TForm1::TForm1(TComponent* Owner)
	: TForm(Owner)
{
  //Create the server
  Server = CreateOleObject("Server.MyOleServer");
  //Make the timer tick immediately
  Timer1-OnTimer(Timer1);
}
void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
  try
  {
    //Put time on form caption
    Caption = Server.OleFunction("TheTime");
  }
  catch (EOleError& E)
  {
    //if there was an OLE problem
    Caption = E.Message;
  }
  //Make the application icon match the form caption
  Application-Title = Caption;
}

The exception handling block helps cater for such problem as the server not being registered, or the property not being available. This test harness program is supplied along with this paper and is called CLIENT.MAK.

Your Server Versus The World

Because the OLE server application conforms to the OLE automation requirements it can be made use of by any other language that supports writing OLE automation controllers. For example, a VB test application can be written in exactly the same way. The following steps are approximate (I don't own Visual Basic so cannot verify them):

Run the app and it works just the same

Summary

So we can see that both automating an OLE server and writing one is pretty easy in Borland C++Builder. Admittedly, certain things are not given to you on a plate such as type library support but that will come in time. Delphi 3 has just added these features to its rather full complement and we should see the next version of C++Builder adding them as well. For more information on Borland C++Builder you could try Borland's home page, or the Borland C++Builder home page.

About Brian Long

Brian Long used to work at Borland UK, performing a number of duties including Technical Support on all the programming tools. Since leaving in 1995, Brian has spent the intervening years as a trainer, trouble-shooter and mentor focusing on the use of the C#, Delphi and C++ languages, and of the Win32 and .NET platforms. In his spare time Brian actively researches and employs strategies for the convenient identification, isolation and removal of malware. If you need training in these areas or need solutions to problems you have with them, please get in touch or visit Brian's Web site.

Brian authored a Borland Pascal problem-solving book in 1994 and occasionally acts as a Technical Editor for Wiley (previously Sybex); he was the Technical Editor for Mastering Delphi 7 and Mastering Delphi 2005 and also contributed a chapter to Delphi for .NET Developer Guide. Brian is a regular columnist in The Delphi Magazine and has had numerous articles published in Developer's Review, Computing, Delphi Developer's Journal and EXE Magazine. He was nominated for the Spirit of Delphi award in 2000.