Athena

Writing And Controlling Automation Servers In C++Builder 3

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

Automation (which is Microsoftís new term for what used to be known as 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 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. This side of OLE used to be known as compound document technology or OLE storage, but is now known as Active Documents.

There is a component in C++Builder, which 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, OLESDI.MAK and OLEMDI.MAK, for details of how to write an OLE in-place-editing client.

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 Automation server, and the one doing the controlling is called the Automation controller or Automation client. The client establishes the link between the two applications.

Like DDE, 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, 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 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 Automation - 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 either a Word.Basic OLE object (for controlling the old WordBasic language) or a Word.Application object (for controlling the newer Visual basic for Applications or VBA language from Word 97, aka Word 8, and onwards). 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 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.

What Is An Automation Server?

An Automation server is an application or DLL that implements a COM object, that is, an object that adheres to Microsoft's COM (Component Object Model) specification. COM objects implement various interfaces. Other applications or DLLs talk to the object through its interfaces. An interface is a well-defined collection of methods and properties that can be implemented by some object or objects. All COM objects implement the interface IUnknown, which defines the reference counting methods used to control the COM object's lifetime management. It also defines a method that allows other interfaces to be accessed in the COM object.

An Automation server is a COM object, which in addition to implementing IUnknown, implements IDispatch. IDispatch allows an arbitrary automation client to execute functionality in the COM object. The client does this by passing information about which method or property to access, and any additional parameters, to methods of IDispatch. At run-time the relevant IDispatch method, Invoke(), will endeavour to call the relevant routine whose detail were passed along. This is a form of late binding. Applications that access COM object functionality directly through its other various interfaces use early binding and so are more efficient.

You can manufacture Automation server objects in C++Builder 3 that can have their interface methods and properties accessed either through IDispatch, or directly through their other interfaces. An Automation object that supports accessing functionality through both these routes is referred to as supporting dual interfaces.

Interface methods that are accessible through a dispatch interface (in other words through IDispatch) must have a certain appearance. In their implementation, they must all return a HRESULT, a Windows type used to convey error information. When accessed through normal interfaces, you must check the HRESULT in case something went wrong. But when accessed through the dispatch interface, this is taken care of for you.

Controlling Automation Servers With Variants

C++Builder supports controlling Automation servers either through their dispatch interface, or directly through any other interface you can get hold of. We will firstly examine the support for dispatch interfaces, using variables of type Variant. A Variant variable can have values from a range of 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 Automation objects. In other words, a Variant can contain a reference to an IDispatch object.

To set one up, you can either #include comobj.hpp and call CreateOleObject(), assigning the result to a Variant or you can call a Variantís CreateObject() method and assign that to a Variant. A reference to server object will then be available until the user closes the server application, 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 reference to the Automation object will be released. If this is the last remaining reference to the Automation object, it will typically destroy itself.

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;
}

Depending upon which version of Word you have set up on your machine, these statements will do different things. If Word is not running, these statements should cause Word to be invoked (but not necessarily seen) and terminated respectively. Word 6 will be visible but later versions will start hidden.

A point worth bearing in mind is that recent versions of Microsoft Office do not seem to behave with regards to terminating when they should. Generally, all automation objects should destroy themselves when the last reference to them is released. If you have a recent copy of Microsoft Office, you might find that the code above fails to terminate Word. If this is the case you may need to use code like this.

void __fastcall TForm1::Button2Click(TObject *Sender)
{
  //Should be able to say:
  // MSWord = Unassigned
  //but MS Office apps that have VBA support
  //don't adhere to the lifetime management
  //aspects of the MS COM spec., so...
  MSWord.OleProcedure("FileExit", 2);
}

If an early version of Word is already running, this causes a connection to be made, and dropped, to that same Word instance. If a later copy of Word is already running, it makes no difference - a new instance will be started regardless. If Word is unavailable, CreateObject() will raise an EOleSysError exception.

By default, most 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 some form of documentation from the server vendor (such as a type library, which is explained later). 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. To call the Automation object methods we use either the OleFunction() or OleProcedure() methods of the Variant depending whether it returns a value or not. To read and write a property you can use OlePropertyGet() and OlePropertySet() respectively. Follow the call to CreateOleObject() with:

MSWord.OleProcedure("AppShow");

Note that in contrast to a C++Builder member function call, in order to call a member function of an Automation 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 Automation 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 Automation server relies on using a Variantís Exec() member function. Exec() takes an AutoCmd object as a parameter. There are four useful AutoCmd descendant classes: Function, Procedure, PropertyGet and PropertySet. 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", "Hello world at ");
  MSWord.OleProcedure("InsertDateTime", "dddd, dd MMMM yyyy");
  MSWord.OleProcedure("Insert", "\r");
  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");
  Procedure InsertDateTime("InsertDateTime");
  MSWord.Exec(Insert << "Hello world at ");
  MSWord.Exec(InsertDateTime << "dddd, dd MMMM yyyy");
  MSWord.Exec(Insert << "\r");
  MSWord.Exec(Insert << Memo1->Text);
}

You should find that 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 Automation 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 temporary Variants used to hold the additional Automation objects that Word returns. These help avoid excessive Automation calls to the IDispatch interface being made in the ComObj unit implementation.

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. The code above comes from the WordEg.Bpr project. Another project, WordEg2.Bpr does exactly the same things with Word but uses the VBA Automation object, Word.Application instead. The event handlers from that project look like this.

void __fastcall TForm1::Button1Click(TObject *Sender)
{
  MSWord = MSWord.CreateObject("Word.Application");
  MSWord.OlePropertySet("Visible", True);
}
void __fastcall TForm1::Button2Click(TObject *Sender)
{
  //Should be able to say:
  // MSWord = Unassigned
  //but MS Office apps that have VBA support
  //don't adhere to the lifetime management
  //aspects of the MS COM spec., so...
  MSWord.OleProcedure("Quit", False);
}
void __fastcall TForm1::Button3Click(TObject *Sender)
{
  MSWord.OleFunction("Documents").OleProcedure("Add");
}
void __fastcall TForm1::Button4Click(TObject *Sender)
{
  Variant Selection = MSWord.OleFunction("Selection");
  Selection.OleProcedure("TypeText", "Hello world at ");
  Selection.OleProcedure("InsertDateTime", "dddd, dd MMMM yyyy", False);
  Selection.OleProcedure("TypeParagraph");
  Selection.OleProcedure("TypeText", Memo1->Text);
}
void __fastcall TForm1::Button5Click(TObject *Sender)
{
  //Obtain Word's doc stats
  Variant DocStats = MSWord.OleFunction("ActiveDocument");
  DocStats = DocStats.OleFunction("BuiltInDocumentProperties");
  ListBox1->Items->Clear();
  ListBox1->Items->Add("Pages: " + DocStats.OlePropertyGet("Item", "Number of pages").OleFunction("Value"));
  ListBox1->Items->Add("Paragraphs: " + DocStats.OlePropertyGet("Item", 
  "Number of paragraphs").OleFunction("Value"));
  ListBox1->Items->Add("Lines: " + DocStats.OlePropertyGet("Item", "Number of lines").OleFunction("Value"));
  ListBox1->Items->Add("Words: " + DocStats.OlePropertyGet("Item", "Number of words").OleFunction("Value"));
  ListBox1->Items->Add("Characters: " + DocStats.OlePropertyGet("Item", "Number of characters").OleFunction("Value"));
}

In addition to these two projects, there is another one called OfficeAutomation.bpr that does the same as the others, but also talks to Microsoft Excel. It uses a #define to decide whether to use the Variant's Exec() method with AutoCmd descendants or use the more common way. These three projects can be found in the OfficeAutomation.bpg project group.

We will come back to the alternative way of controlling an Automation server later.

Writing An Automation Server

The process of writing an Automation server is very much automated itself, through the Automation Object Wizard. 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 a controller starts it the main form will not show up. So to remind us 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 (a finished project has been supplied in Server.Bpr).

Select File | New... and choose Automation Object from the ActiveX page of the dialog. This asks for a class name: enter MyOleServer and press OK.

Pressing OK manufactures three units and displays the type library editor. A type library is a binary file with a .TLB extension that gets linked into your program and allows various development systems to examine the capabilities of your exposed objects. The type library editor allows us to build up much of the structure of our Automation object without too much typing.

The type library defines interfaces (amongst other things). A type library says which interfaces exist in an application, but gives no information on implementation details.

The Automation Object Wizard manufactured three units along with the type library. The unit that is open in the editor in an unsaved state defines a class (in its header file) that implements the interface that will be set up in the type library editor. The class is called TMyOleServerImpl and the interface is called IMyOleServer. The syntax that implies this interface implementation looks like this:

class ATL_NO_VTABLE TMyOleServerImpl:
AUTOOBJECT_IMPL(TMyOleServerImpl, MyOleServer, IMyOleServer)
{
public:
...
BEGIN_COM_MAP(TMyOleServerImpl)
  AUTOOBJECT_COM_INTERFACE_ENTRIES(IMyOleServer)
END_COM_MAP()
...
DECLARE_TYPED_COMSERVER_REGISTRY("Server.MyOleServer")
protected:
};

This defines a class TMyOleServer, inherited from a class TAutoObject and which implements the IMyOleServer interface. The sample project has this file saved as ServerOLEClassUnit.cpp.

One of the other units manufactured has the same name as your project but with a _ATL suffix. If your project is called Server.Bpr, this unit will be called Server_ATL.Cpp. This unit is for Microsoftís ATL (ActiveX Template Library) support, and we can safely ignore it for our purposes.

The other unit that was manufactured has not been opened automatically. If your project is called Server.Bpr, this other unit will be called Server_TLB.Cpp. This is a C++ representation of what is in your type library and is referred to as a type library import unit. It gets regenerated upon demand and so should not be edited directly. Just like the F12 key can toggle between a form and its corresponding form unit, F12 will also toggle between the type library editor and the type library import unit.

This unit (or its header) contains the definition of the IMyOleServer interface, which looks something like this at the moment:

DEFINE_GUID(IID_IMyOleServer, 0x262A30A2, 0x9D87, 
0x11D1, 0x96, 0xE5, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00);
//***********************************************************//
// Declaration of CoClasses defined in Type Library
// (NOTE: Here we map each CoClass to it's Default Interface
// **********************************************************//
typedef IMyOleServer MyOleServer;
// **********************************************************//
// Interface: IMyOleServer
// Flags: (4416) Dual OleAutomation Dispatchable
// GUID: {262A30A2-9D87-11D1-96E5-444553540000}
// **********************************************************//
interface IMyOleServer : public IDispatch
{
public:
};

The long numeric-like string in the interface definition (and also visible in the type library editor if IMyOleServer is selected) is a GUID (Globally Unique IDentifier). Specifically, this is an IID (Interface IDentifier) designed to uniquely represent this interface. There is also another GUID, called a Class ID, whose value will be very similar. The Class ID is designed to uniquely represent your COM object and can be seen by selecting the CoClass, MyOleServer, in the type library editor. Your Automation objectís ProgID will be Server.MyOleServer assuming you are using the same file and object names as have been suggested. All these details will eventually be stored in the registry.

Letís proceed and define a read-only property in our interface that will return the current time, and then implement it in the class. To add something to an interface, we use the type library editor. If you cannot find the right window, choose View | Type Library.

Select the IMyOleServer interface in the type library editor and press the down arrow next to the Property speedbutton. Choose a Read Only property.

This adds a new property to the interface. Set the name of the property to be CurrentTime. Now drop down the Type: combobox and choose DATE.

To get all the source code generated, press the Refresh button. This updates the interface (which, remember, has no inherent implementation) to look like this:

interface IMyOleServer : public IDispatch
{
public:
  virtual HRESULT STDMETHODCALLTYPE get_CurrentTime(
    DATE* Value/*[out,retval]*/) = 0; // [1]
};

and updates the implementing class to look like this in the ServerOleClassUnit.h header:

class ATL_NO_VTABLE TMyOleServerImpl:
AUTOOBJECT_IMPL(TMyOleServerImpl, MyOleServer, IMyOleServer)
{
public:
...
BEGIN_COM_MAP(TMyOleServerImpl)
  AUTOOBJECT_COM_INTERFACE_ENTRIES(IMyOleServer)
END_COM_MAP()
...
DECLARE_TYPED_COMSERVER_REGISTRY("Server.MyOleServer")
protected:
  STDMETHOD(get_CurrentTime(DATE* Value));
};

The get_CurrentTime() property reader is given this implementation in ServerOLEClassUnit.cpp:

STDMETHODIMP TMyOleServerImpl::get_CurrentTime(DATE* Value)
{
  try
  {
  }
  catch(Exception &e)
  {
    return Error(e.Message.c_str(), IID_IMyOleServer);
  }
  return S_OK;
};

Notice that since this property reading method is part of a dual interface COM object, it returns a HRESULT, not the DATE value. The DATE is returned through one of the method parameters.

All that is left for us to do is to finish the implementation of the function, which simply needs to return the value of the Now() function. Change the function implementation to:

STDMETHODIMP TMyOleServerImpl::get_CurrentTime(DATE* Value)
{
  try
  {
    *Value = Now();
  }
  catch(Exception &e)
  {
    return Error(e.Message.c_str(), IID_IMyOleServer);
  }
  return S_OK;
};

Before we get onto registering the server, there was that little matter of hiding the main form if we are started under automation control. In the form's constructor, add this statement:

if (FindCmdLineSwitch(
     "AUTOMATION", 
     TSysCharSet() << '/' << '-', true))
  Application->ShowMainForm = false;

You can see that when an Automation server is launched for the purpose of being automated, it gets a /Automation command-line parameter.

When client applications get around to connecting to our server, we can control how things will work. Typically, you will either want one instance of your Automation object to service all client requests, or you will want multiple instances of your Automation object, each servicing one client. To choose, select Project | Options... and go to the ATL page. In the Instancing group box, Single Use means each Automation object will only talk to one client, Multiple Use means one Automation object will talk to all clients. There are also other options here that you might like to investigate.

It is this Instancing option that makes the different versions of Microsoft Word act in the way described earlier. Some versions serve Automation controllers with separate Automation object instances from separate EXE instances, and some reuse any existing Automation object instance in an already running EXE.

Registering An Automation 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 Automation 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 internal COM/OLE support code 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 Automation 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 it described as "MyOleServer Object". If you expand the ProgID key, you can see a CLSID key. The class ID should match what you see in the type library editor when the CoClass, MyOleServer, is selected in the type library editor. When the appropriate Windows DLLs are told to make a Server.MyOleServer object they will be able to find the class ID, but what then?

They then do 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 specific classID 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 Automation server.

Testing The Automation Server With A Variant

To test out this new server object, we do much the same as we did with Microsoft Word. We will make a new project that will control our Automation server, but we will use a project group to do it. This makes sense as the two projects are very much related right now.

Choose Project | Add New Project... and pick an Application. You now have two projects available in a currently unsaved project group. You can verify this by looking at the project manager (View | Project Manager). To save all the unsaved files, choose File | Save All. The sample files supplied are saved as Client.BPR, ClientMainFormUnit.CPP and the project group is AutomationClientAndServer.Bpg.

Remember that to switch the active project in a project group, select it in the project manager and press the Activate button.

Now declare a Variant object in the form class (this time, call it Server). 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.

#include <comobj.hpp>
...
void __fastcall TForm1::FormCreate(TObject *Sender)
{
  Server = CreateOleObject("Server.MyOleServer");
  //Execute timer event straight away
  Timer1->OnTimer(Timer1);
}
void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
  try
  {
    Caption = Server.OlePropertyGet("CurrentTime");
  }
  //if there is an Automation problem
  catch (EOleError &E)
  {
    //if there was an OLE problem
    Caption = E.Message;
    Timer1->Enabled = False;
  }
  //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 available as Client.Bpr.

Controlling Automation Servers Using Interfaces

Having seen how to control an automation server with a Variant, let us rebuild our client application and use the interfaces defined in the type library. These are defined in the type library import unit's header file, Server_TLB.h. Make a new application for our second client, with a timer on it as before, and #include this header into the source file. Also, since we will need to access some code generated in that header's corresponding source file, add Server_TLB.Cpp to the project (with Project | Add to Project...).

We can do this non-Variant approach in one of two ways, using the interface directly, or using the dispatch interface directly. The latter is a bit like using a Variant in that it goes via IDispatch, but takes less code than when using a Variant.

Dual Interface And CoClass Approach

We will do both of them, firstly using the normal interface. In the form class, we need a symbol declared that represents the automation server. This is a data field of type TCOMIMyOleServer.

class TForm1 : public TForm
{
...
private:	// User declarations
  TCOMIMyOleServer Server;
}

This type is the dual interface class representing your interface. In the form's constructor event handler, the COM object is invoked by using the Create() method of a CoClass client proxy class:

#include "Server_TLB.h"
...
__fastcall TForm1::TForm1(TComponent* Owner)
	: TForm(Owner)
{
  //Create the server
  Server = CoMyOleServer::Create();
  //Make the timer tick immediately
  Timer1->OnTimer(Timer1);
}

Now you can call the methods of your interface directly, but not access properties - you need to talk to their method implementations instead. However it is more long-winded to call these methods. You need to take account yourself of the returned HRESULT, and deal with any values that were marked as being returned, which are now passed back as parameters. The timer's OnTimer event handler might now look like this:

void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
  //Put time on form caption
  DATE Time;
  if (SUCCEEDED(Server->get_CurrentTime(&Time)))
    Caption = DateTimeToStr(Time);
  else
  {
    //if there was an OLE problem
    Caption = "There was a problem";
    Timer1->Enabled = False;
  }
  //Make the application icon match the form caption
  Application->Title = Caption;
}

SUCCEEDED is a Windows macro that analyses a HRESULT and returns non-zero if there is no error indicated. There is a corresponding FAILED() macro as well. This version of the client application is stored as Client2.Bpr.

Dispatch Interface Approach

Now we will try with the dispatch interface. Using this we will again be able to talk to the properties in our interface.

Rebuild the client user interface again, placing the timer on the form. Also, add Server_TLB.Cpp to the project and #include Server_TLB.h in the form unit. Declare a data field in the form class, using the dispatch interface type. IMyOleServer is the interface, where IMyOleServerDisp is the dispatch interface (implemented as a proxy class).

class TForm1 : public TForm
{
...
private:	// User declarations
  IMyOleServerDisp Server;
}

In the form's constructor handler, call this object's BindDefault() method. Now you can access the properties and methods of the Automation server in a straightforward manner.

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

This final version of the client application is stored as Client3.Bpr. All three client applications, as well as the server can be accessed from the project group AutomationClientAndServer.Bpg.

Some Notes

Commercial Automation server applications, such as the Office 97 versions of Microsoft Word and Excel typically come with a type library. It is possible for the C++Builder environment to generate an import unit for these if you can find out where they reside. In many cases, type libraries get logged in the Windows registry and so the environment can find them easily, but sometimes you have to go hunting.

To make an import unit for Microsoft Excel's type library, choose Project | Import Type Library... and locate Microsoft Excel 8.0 Object Library. Specify an appropriate directory for the unit and press OK, and after a short delay, you will have a large unit. For Microsoft Word, you will need to press the Add... button as its type library is not registered. You will probably find the file stored as C:\Program Files\Microsoft Office\Office\MSWord8.Olb. Again, this will generate a very large unit.

With these files in hand, you can access all their interfaces directly, however it comes with a penalty. When talking to Automation objects using a Variant, any optional parameters that you are not interested in passing to the available methods can be simply omitted. When you are using specific interface types, you are obliged to pass values for all parameters, which can be tedious.

Your Server Versus The World

Because the Automation object application conforms to the Automation requirements it can be made use of by any other language that supports writing Automation controllers, such as Borland Delphi or Microsoft Visual Basic. For example, a VB test application can be written in exactly the same way. The following steps should suffice:

  1. Make a new project.
  2. In the global declarations section declare:
    Dim Server As Object
  3. Double click the form to make an OnLoad event handler and type:
    Set Server = CreateObject("Server.MyOleServer")
  4. Place a timer on the form and set its interval to 500 ms and set its Active property to True.
  5. Double-click the timer to make an OnTimer event handler and type:
    Form1.Caption = Server.CurrentTime
  6. Run the application and it should work just the same as our C++ program.

Summary

So we can see that both controlling an Automation server and writing one is pretty easy in C++Builder. 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.