Книга: Windows API Tutorials

OLE Automation

OLE Automation

I just wanted to be able to syphoon information from a running copy of Microsoft Developers Studio. It should have been a simple task--DevStudio, like many other MS apps, exposes its interfaces through OLE Automation. Not so simple! You see, Microsoft assumed that clients of VC++ automation interfaces will either be using Visual Basic, or DevStudio's own clever wizards. I, on the other hand, like programming in C++. (Don't you think that Microsoft's Visual C++ should be renamed Microsoft Visual MFC wizard? It relegated C++ to the role of a script language for MFC.)

Anyway, once I've found out how things should be done, it turned out not to be too difficult. You just have to figure out where the heck all the information is kept in the registry. In particular, the IIDs of all the interfaces. Tip: Use OLE-COM Object Viewer that comes with VC++ to look at type libraries. It would be common courtesy if Microsoft provided a source file or an obj file with the definitions of interface ids. As it is, I had to copy and paste them from the Object Viewer. Here's an example.

static const IID IID_IApplication = { 0xEC1D73A1, 0x8CC4, 0x11CF, { 0x9B, 0xE9, 0x00, 0xA0, 0xC9, 0x0A, 0x63, 0x2C } };

So how does one get hold of a running copy of DevStudio? First, you have to create an OLE object. For that, you'll need a class id of this object. You can get the class id from the system (it's stored in the registry) if you know the program id. Program id is supposed to be a human readable name. Of course, every human being knows that the Developer Studio goes by the name "MSDEV.APPLICATION". So that's easy.

With the class id in hand we can create our SObject. We're passing true as the value of the parameter running, because we would like to connect to the running copy of MSDEV.APPLICATION, if possible. Getting an interface from SObject is as simple as instantiating the SObjFace template with the appropriate arguments. That will be our starting point, the interface to the application.

CLSID idMsDev;
HRESULT hr = ::CLSIDFromProgID (L"MSDEV.APPLICATION", &idMsDev);
if (FAILED (hr)) throw HEx (hr, "Couldn't convert prog id to class id");
SObject obj (idMsDev, true);
SObjFace<IApplication, &IID_IApplication> app (obj);

Notice that the string you pass to CLSIDFromProgID must be Unicode (wide character). Putting L in front of the string literal takes care of that.

I hope you can appreciate the simplicity of this code. It's almost as simple as its VB equivalent.

Dim app as Application
Set app = GetObject (, "MSDEV.APPLICATION")
if (app = NULL) Set app = CreateObject ("MSDEV.APPLICATION")

Now let's do something with this interface. It so happens that IApplication has a member Visible that you can put or get. When you set Visible to true, the application window becomes visible. Here's the syntax for "putting" a member. Notice that in OLE you have to use things called VARIANT_BOOL and VARIANT_TRUE instead of bool and true. That's because of compatibility with Basic (makes Bill happy).

VARIANT_BOOL b = VARIANT_TRUE;
app->put_Visible (b);

How did I know that IApplication has a member Visible? Good question! There is a subdirectory objmodel in VC++ include directory where you can find such files as Appauto.h that contain lines like the ones below. You can sort of learn to interpret these files. Their crypticity is caused by the (silly!) requirement that they be includable in both C and C++ code. Microsoft didn't want to maintain two sets of header files, so here you go.

STDMETHOD(get_Visible)(THIS_ VARIANT_BOOL FAR* Visible) PURE;
STDMETHOD(put_Visible)(THIS_ VARIANT_BOOL Visible) PURE;

So what do we do, now that we have the application in our hands? How about finding out what document is currently active. Using the IApplication interface as our source, we can create another OLE object that will represent the active document. This particular OLE object, SActiveDocument, can be used as a source of a few interfaces, one of them being IGenericDocument. We grab this interface in a standard way  — by creating an object from the SObjFace template. SActiveDocument, like all our OLE/COM objects, inherits from CoObject, the source of interfaces.

IGenericDocument has a member FullName that can be gotten by calling the method get_FullName. Unfortunately, Basic compatibility strikes again — the result is passed in the form of a BSTR, or Basic string. I have created two helper classes BString and CString to take care of this weirdness. In particular BString makes sure that the string is deallocated using the API SysFreeString.

SActiveDocument docObj(app);

if (docObj) {
 SObjFace<IGenericDocument, &IID_IGenericDocument> doc (docObj);
 BString bPath;
 doc->get_FullName (bPath.GetPointer ());
 CString path (bPath);
 canvas.Text (20, y, "Active Document:");
 canvas.Text (200, y, path);
}

This a typical situation in OLE Automation. Using a method of one interface, app, in this case, you get hold of another object, docObj, with its own interfaces. For each such object-returning method, we'll construct a new smart pointer class, whose only distinguishing feature is its constructor. For instance, here's the class SActiveDocument.

class SActiveDocument: public DispObject {
public:
 SActiveDocument (SObjFace<IApplication, &IID_IApplication> & app) {
  HRESULT hr = app->get_ActiveDocument (&_iDisp);
  if (FAILED (hr)) throw HEx (hr, "get_ActiveDocument failed");
 }
};

Notice that the base class of SActiveDocument is not SObject — it's a new class DispObject. It is almost exactly like SObject with one distinction — instead of internally using a pointer to IUnknown it uses a pointer to IDispatch. Does it matter? Not really, I could have used SObject and everything would've work the same. Except that IDispatch can be used for something more than just querying other interfaces. It can be used for dynamic dispatching of calls. Since our program is written in C++ and it knows all the interfaces up front, we don't really need to use dynamic dispatching. But there are situations in which you need to let the user decide what object to load and which method to call, real time. Script interpreters have to be able to do it. In particular, Visual Basic scripting tools require such functionality.

Below is a skeleton implementation of a DispObject that demonstrates this point. It also explains why we were talking about "members" like Visible or FullName when discussing interfaces. In VB they actually appear as data members, or properties, of objects. Here, I have implemented a dispatch method GetProperty that can be used to load the value of any property based on its DISPID. And you can get a DISPID of any property or method as long as you know its name. The method GetDispId will do that for you. In a similar way, you should be able to implement PutProperty and, if you need it, Invoke, that can be used to invoke any member function by its DISPID. I leave this as an exercise for the reader.

class DispObject: public CoObject {
public:
 DispObject (CLSID const & classId) :_iDisp (0) {
  HRESULT hr = ::CoCreateInstance ( classId, 0, CLSCTX_ALL, IID_IDispatch, (void**)&_iDisp);
  if (FAILED (hr)) {
   if (hr == E_NOINTERFACE) throw "No IDispatch interface";
   else throw HEx (hr, "Couldn't create DispObject");
  }
 }
 ~DispObject () {
  if (_iDisp) _iDisp->Release ();
 }
 operator bool () const {
  return _iDisp != 0;
 }
 bool operator ! () const {
  return _iDisp == 0;
 }
 DISPID GetDispId (WCHAR * funName) {
  DISPID dispid;
  HRESULT hr = _iDisp->GetIDsOfNames ( IID_NULL, &funName, 1, GetUserDefaultLCID (), &dispid);
  return dispid;
 }
 void GetProperty (DISPID propId, VARIANT & result) {
  // In parameters DISPPARAMS args = { 0, 0, 0, 0 };
  EXCEPINFO except;
  UINT argErr;
  HRESULT hr = _iDisp->Invoke (propId, IID_NULL, GetUserDefaultLCID (), DISPATCH_PROPERTYGET, &args, &result, &except, &argErr);
  if (FAILED (hr)) throw HEx (hr, "Couldn't get property");
 }
 void * AcquireInterface (IID const & iid) {
  void * p = 0;
  HRESULT hr = _iDisp->QueryInterface (iid, &p);
  if (FAILED (hr)) {
   if (hr == E_NOINTERFACE) throw "No such interface";
   else throw HEx (hr, "Couldn't query interface");
  }
  return p;
 }
protected:
 DispObject (IDispatch * iDisp) : _iDisp (iDisp) {}
 DispObject () : _iDisp (0) {}
protected:
 IDispatch * _iDisp;
};

Below is a small illustration of dynamic dispatching. Of course, the same result could have been obtained directly by calling the method get_Name of the IGenericDocument interface. We'll use this direct (vtable) method in a moment to obtain the full path of the document.

// Use docObj as a dispatch interface
DISPID pid = docObj.GetDispId (L"Name");
VARIANT varResult ;
::VariantInit(& varResult) ;
docObj.GetProperty (pid, varResult);
BString bName (varResult);
CString cName (bName);
canvas.Text (20, y, "Name:");
canvas.Text (200, y, cName);

And this is how you obtain the path, the vtable way.

SObjFace<IGenericDocument, &IID_IGenericDocument> doc (docObj);
BString bPath;
doc->get_FullName (bPath.GetPointer ());

Now you shouldn't have any problems understanding the code that retrieves the line number of the line where the user positioned the cursor.

BString bType;
doc->get_Type (bType.GetPointer ());
if (type.IsEqual ("Text")) {
 SObjFace<ITextDocument, &IID_ITextDocument> text (docObj);
 SSelection selObj (text);
 SObjFace<ITextSelection, &IID_ITextSelection> sel (selObj);
 long line;
 sel->get_CurrentLine (&line);
 canvas.Text (20, y, "CurrentLine:");
 char buf [10];
 wsprintf (buf, "%ld", line);
 canvas.Text (200, y, buf);
}

SSelection is a DispObject that can be obtained by calling a method get_Selection of the text document interface.

class SSelection: public DispObject {
public:
 SSelection (SObjFace<ITextDocument, &IID_ITextDocument> & doc) {
  HRESULT hr = doc->get_Selection (& _iDisp);
  if (FAILED (hr)) throw HEx (hr, "get_Selection failed");
 }
};

You might be a little confused if this is your first contact with OLE (an understatement!). So here is a summary of what the various actions are that have to be done in order to accomplish a simple Automation task. Notice, this is the client side of the equation. If you want your application to be an Automation server, things are a bit more complicated. Fortunately, there is more literature about it.

So here's what you have to do.

The research. Search your registry (using RegEdt32 or OLE/COM object viewer) to find the ProgID of the application you want to get hold of. HKEY_CLASSES_ROOT is the starting point. You'll see there such keys as Word.Application, Excel.Application and many others. Find the Type Libraries using OLE/COM object viewer. They will have the class ids and interface ids that you have to copy and paste into your code. Find the header files for these interfaces. In your program: Convert ProgID to class id. To connect to a running app or to activate a new one, create SObject using the ClassID. Obtain the IApplication interface from the object (use the IObjFace template). Use this interface to get hold of other objects internal to the application. For each such object: Declare a class derived from DispObject In its constructor use the appropriate get_* method to get access to the internal object. Create an object of this class, passing it the interface of the parent object. Obtain the appropriate interface from this object by instantiating the template IObjFace. Call appropriate methods of this interface.

So what was this big deal with reference counting in OLE? Beats me! It must have disappeared, once we started using the correct encapsulation. Below is a diagram of class dependencies. OLE objects on the left, OLE interfaces on the right.


At runtime, you start with the SObject that represents the program you're connecting to and then keep going from object to interface and from interface to DispObject. You use objects as sources of interfaces and interfaces for calling specific methods and obtaining other objects.

Оглавление книги


Генерация: 1.483. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз