Athena

Writing And Controlling Automation Servers In C++Builder 4

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 directory, 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 Automation client need not care - the COM/Automation code in Windows will find where the server application resides by examining the registry. To start controlling an Automation server you ask the COM/Automation support DLLs to create an appropriate object. In the case of Microsoft Word, you would create either a Word.Basic Automation 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 Windows 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 a DLL lives 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 in-proc Automation 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 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 (where its Type() method with return varDispatch).

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 a new 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.

CreateOleObject() and CreateObject() are used to connect to a new instance of the server's object (possibly in a new instance of the executable). To connect to an existing instance, you can call GetActiveOleObject() from comobj.hpp or call the Variant's GetActiveObject() method, assuming the object has registered itself in the Windows Running Object Table (ROT).

An Automation (by Variant) example using Microsoft Word

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

Before trying this out, you must remember to #include utilcls.h, as the Variant support header will not compile in version 4 otherwise. This header must be included wherever you are planning to call Automation methods or properties through a Variant where you will be passing parameters, otherwise various errors will be produced.

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 (version 6 or 7) 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 (e.g. version 8), 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.

Variant Automation-support methods

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.

Automation-support through AutoCmd

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

One primary advantage of these AutoCmd descendants is that they support passing values for optional, named arguments. Automation servers such as Microsoft Word make great use of optional arguments, and it is often helpful to pass values for only those arguments you are interested in. If these arguments are not the first arguments taken by the routine, you need to specify the argument name.

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");
  Insert.ClearArgs();
  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.OlePropertyGet("CurValues");
  Variant DocStats = CurValues.OlePropertyGet("FileSummaryInfo");
  ListBox1->Items->Clear();
  ListBox1->Items->Add(String(DocStats.OlePropertyGet("NumPages")) + " pages");
  ListBox1->Items->Add(String(DocStats.OlePropertyGet("NumParas")) + " paragraphs");
  ListBox1->Items->Add(String(DocStats.OlePropertyGet("NumLines")) + " lines");
  ListBox1->Items->Add(String(DocStats.OlePropertyGet("NumWords")) + " words");
  ListBox1->Items->Add(String(DocStats.OlePropertyGet("NumChars")) + " characters");
}

So calling or automating an OLE server is easy enough as long as you know its ProgID and what methods and properties it exposes. 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.OlePropertyGet("Documents").OleProcedure("Add");
}
void __fastcall TForm1::Button4Click(TObject *Sender)
{
  Variant Selection = MSWord.OlePropertyGet("Selection");
  //The typecast to WideString is to ensure no stray non-alpha
  //characters are given to Word, as they are otherwise
  Selection.OleProcedure("TypeText", WideString("Hello world at "));
  Selection.OleProcedure("InsertDateTime", "dddd, dd MMMM yyyy", False);
  Selection.OleProcedure("TypeParagraph");
  Selection.OleProcedure("TypeText", WideString(Memo1->Text));
}
void __fastcall TForm1::Button5Click(TObject *Sender)
{
  //Obtain Word's doc stats
  Variant DocStats = MSWord.OlePropertyGet("ActiveDocument").OlePropertyGet("BuiltInDocumentProperties");
  ListBox1->Items->Clear();
  ListBox1->Items->Add("Pages: " + DocStats.OlePropertyGet("Item", "Number of pages"));
  ListBox1->Items->Add("Paragraphs: " + DocStats.OlePropertyGet("Item", "Number of paragraphs"));
  ListBox1->Items->Add("Lines: " + DocStats.OlePropertyGet("Item", "Number of lines"));
  ListBox1->Items->Add("Words: " + DocStats.OlePropertyGet("Item", "Number of words"));
  ListBox1->Items->Add("Characters: " + DocStats.OlePropertyGet("Item", "Number of characters"));
}

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.

One snippet from this project looks like this:

  Selection.Exec(procTypeText << "Hello world at ");
  Selection.Exec(procInsertDateTime << "dddd, dd MMMM yyyy" << False);

If you wished to make use of named parameters, to be sure that the right values were going to the right parameters, you could change it to use NamedParm objects like this:

  Selection.Exec(procTypeText << NamedParm("Text", "Hello world at "));
  Selection.Exec(procInsertDateTime
    << NamedParm("DateTimeFormat", "dddd, dd MMMM yyyy")
    << NamedParm("InsertAsField", False));

In this way, you can pass values for any arguments you like, ignoring any parameters that you wish to get the default value.

These three Office Automation projects can be found in the OfficeAutomation.bpg project group.

We will come back to the alternative, interface-driven 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).

There are differences between the Automation support in version 3 and 4 of C++Builder and, since this paper mentions version 4 in its title, I will ignore version 3 specifics from this point on. However, in many cases these differences are very minor.

Select File | New... and choose Automation Object from the ActiveX page of the dialog. This asks for a CoClass name: enter MyOleServer. It also asks for various other pieces of information, for example a description. Enter My first Automation Server 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 also allows us to build up much of the structure of our Automation object without too much typing.

C++Builder's Type Library Editor

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 (MyOleServerImpl.cpp) 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 :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<TMyOleServerImpl, &CLSID_MyOleServer>,
  public IDispatchImpl<IMyOleServer, &IID_IMyOleServer, &LIBID_Project1>
{
public:
...
  // Data used when registering Object 
DECLARE_THREADING_MODEL(otApartment);
  DECLARE_PROGID("Server.MyOleServer");
  DECLARE_DESCRIPTION("My first Automation Server");
...
BEGIN_COM_MAP(TMyOleServerImpl)
  COM_INTERFACE_ENTRY(IMyOleServer)
  COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()

// IMyOleServer
public:
};

This defines a class TMyOleServerImpl, which implements the IMyOleServer interface.

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, 0x625A4606, 0xE840, 0x11D2, 0x96, 0xEC, 0x00, 0x60, 0x97, 0x8E, 0x13, 0x59);
//***********************************************************//
// 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:      {625A4606-E840-11D2-96EC-0060978E1359}
// *********************************************************************//
interface IMyOleServer : public IDispatch
{
public:
};

The long string of numbers used in the DEFINE_GUID macro (and also visible as one long string 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 are also other GUIDs, a Class ID and a Lib ID, whose values 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 (the ProgID can be seen referenced in the class definition shown earlier). 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.

Adding a property to an interface in the Type Library Editor

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]

#if !defined(__TLB_NO_INTERFACE_WRAPPERS)
  DATE __fastcall get_CurrentTime(void)
  {
    DATE Value;
    OLECHECK(this->get_CurrentTime(&Value));
    return Value;
  }
__property DATE CurrentTime = {read = get_CurrentTime};
#endif //   __TLB_NO_INTERFACE_WRAPPERS
};

You can see that the property reading function has been defined in the interface. 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. However, in addition, the type library import unit adds in a property into the interface which has appropriate support code added to allow the DATE value to be directly returned. This will make things much easier to call from client applications that are directly using the interface.

The press of the Refresh button also updates the implementing class to look like this in the ServerOleClassUnit.h header:

class ATL_NO_VTABLE TMyOleServerImpl :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<TMyOleServerImpl, &CLSID_MyOleServer>,
  public IDispatchImpl<IMyOleServer, &IID_IMyOleServer, &LIBID_Project1>
{
public:
...
  // Data used when registering Object 
DECLARE_THREADING_MODEL(otApartment);
  DECLARE_PROGID("Server.MyOleServer");
  DECLARE_DESCRIPTION("My first Automation Server");
...
BEGIN_COM_MAP(TMyOleServerImpl)
  COM_INTERFACE_ENTRY(IMyOleServer)
  COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()

// IMyOleServer
public:
  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;
};

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 manufacture separate Automation objects in the same, 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.

C++Builder's Project Manager

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.

Your Server Versus The World

Because the Automation server object conforms to the Automation requirements it can be made use of by any language that supports writing Automation controllers, such as Borland Delphi or Microsoft Visual Basic.

Testing The Automation Server From Visual Basic

For example, a VB test application can be written in much the same way. The following steps should suffice:

Testing The Automation Server From Borland Delphi

To create a test harness application in Borland Delphi, try the following steps:

Controlling Automation Servers Using Interfaces

Having seen (twice now) 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 COM 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, or indeed access the properties (remember that property support code was added to the type library import unit). Additionally, you get to take advantage of the Code Completion and Code Parameters facilities in the editor, just like when talking to native C++Builder objects. Finally, you will be told of any reference to invalid properties during the compilation, instead of at run-time, when using interfaces.

The timer's OnTimer event handler might now look like this:

#include <comobj.hpp>
...
//---------------------------------------------------------------------------
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;
}

DateTimeToStr() is used to translate the floating point value returned through the property into a string depicting a date and time.

This is similar to Client.Bpr, but now we talk directly to the property instead of using specialised methods of a Variant object. 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 once again access the properties and methods of the Automation server in exactly the same way as when using the normal COM interface.

#include <comobj.hpp>
...
__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 On Third Party Automation Servers

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.

An Automation (by Interface) example using Microsoft Word

To see how we automate a less academic example of an Automation server using interfaces, let's go back to Microsoft Word once again.

Make a new project with the same buttons and memo on the form as we had in the previous Word Automation projects and then import Microsoft Word's type library as per the steps above. C++Builder will, by default, save the type library import units in its IMPORTS subdirectory. Save the project (a ready-made project is supplied with this paper, called WordEg3.Bpr). However one of the files in the project is my own Word 97 type library unit in my particular C++Builder IMPORTS directory. To use this project you will need to remove Word_TLB.cpp from the project, and then add in your own version, so the project manager will know the right directory.

The interface implemented by Word that represents the VBA Application object is called _Application. You need to declare a field in your form class of type TCOM_Application to work with the interface. TCOM_Application is defined in the header file for the Word type library import unit. In the event handler for the Start Word button, we connect to the primary Word Automation object by using the client proxy class defined in the type library header file, CoApplication_:

void __fastcall TForm1::Button1Click(TObject *Sender)
{
  MSWord = CoApplication_::Create();
  MSWord.Visible = true;
}

Notice that when we use the interfaces, we can talk directly to the Automation object properties, in a more natural manner. The event handlers for the other buttons look much as expected (particularly if you have browsed through the type library import unit), but there are some noteworthy points about them.

void __fastcall TForm1::Button2Click(TObject *Sender)
{
  //Should be able to say:
  //  MSWord = NULL;
  //but MS Office apps that have VBA support don't adhere to
  //the lifetime management aspects of the MS COM spec., so...
  MSWord.Quit(&TVariant(False));
}
void __fastcall TForm1::Button3Click(TObject *Sender)
{
  MSWord.Documents->Add();
}
void __fastcall TForm1::Button4Click(TObject *Sender)
{
  Selection *Sel = MSWord.Selection;
  Sel->TypeText(WideString("Hello world at "));
  Sel->InsertDateTime(
    &TVariant("dddd, dd MMMM yyyy"),
    &TVariant(False));
  Sel->TypeParagraph();
  Sel->TypeText(WideString(Memo1->Text));
}

The fifth button's event handler uses a combination of pure interface access, and Variant access. This is because the required interface for accessing document properties is not defined in the type library. So we go back to an approach that doesn't use it.

void __fastcall TForm1::Button5Click(TObject *Sender)
{
  //Obtain Word's doc stats
  Variant DocStats = MSWord.ActiveDocument->BuiltInDocumentProperties;
  ListBox1->Items->Clear();
  ListBox1->Items->Add("Pages: " + DocStats.OlePropertyGet("Item", "Number of pages"));
  ListBox1->Items->Add("Paragraphs: " + DocStats.OlePropertyGet("Item", "Number of paragraphs"));
  ListBox1->Items->Add("Lines: " + DocStats.OlePropertyGet("Item", "Number of lines"));
  ListBox1->Items->Add("Words: " + DocStats.OlePropertyGet("Item", "Number of words"));
  ListBox1->Items->Add("Characters: " + DocStats.OlePropertyGet("Item", "Number of characters"));
}

Since these interfaces are defined using normal C++ syntax, there are some restrictions on parameters with default values. You cannot specify a parameter by name (as this goes against the C++ language). Parameters that support default values can be omitted, but only according to the rules of the language. In other words if a method takes 3 parameters, each of which is optional, then to specify a value for the third parameter, you will have to specify a value for the first and second parameters as well. If you really have no interest in some parameters that you have to give a value to, you can use TNoParam() instead.

C++Builder 4 does much more than version 3 when importing (or generating) type libraries. The support code for properties that is added to the interface is very helpful for starters, meaning you do not have to struggle calling the underlying methods and passing variables in to them. Also, the support for optional parameters is new. Additionally, the fact that you can access properties and methods of an interface directly with the . operator, rather than being forced to use the -> operator as before, is handy. This targeted support for easy interface-driven COM programming is sometimes referred to as EZ-COM.

Summary

This paper has investigated the subject of Automation. We saw how to automate an appropriate application either using a Variant class variable, or by using EZ-COM interfaces generated by importing the application's type library. Automation allows you to programmatically control another application, without user intervention.

We also looked at how C++Builder supports creating Automation servers which themselves can be automated by any other capable Windows application.

With this knowledge on board you should be able to tap into the existing Automation servers on the market, and write your own Automation servers to service your own application's needs and requirements.

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.