กก | Win32 | |
Winnie | ||
Generic | ||
Controls | ||
Dialog-bsd App | ||
Generic Dialog | ||
Canvas | ||
Pens/Brushes | ||
Threads | ||
Folder Watcher | ||
Shell API | ||
OLE/COM | ||
Smart OLE | ||
OLE Automation | ||
Splitter Bar | ||
Bitmaps | ||
Direct Draw | ||
กก |
OLE AutomationI 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.
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.
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.
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).
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.
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.
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.
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.
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.
And this is how you obtain the path, the vtable way.
Now you shouldn't have any problems understanding the code that retrieves the line number of the line where the user positioned the cursor.
SSelection is a DispObject that can be obtained by calling a method get_Selection of the text document interface.
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.
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. Finally, you can download the source code that illustrates the application of these techniques. |