Introduction
This article is the second in a series describing the VOLE C++/COM Automation driver library. (The first one is here.) It illustrates the use of VOLE to rewrite (and dramatically simplify) Microsoft's own code example for how to drive Word's automation object model from C++.
Background
A new user of VOLE recently emailed me to ask whether VOLE could handle driving Microsoft Word, to achieve the functionality involved in Microsoft's own code example. He wrote:
"I have just come across your library while looking for a reasonably sane way to add a bit of Excel automation in my C++ program. Not being familiar with COM I found this example http://support.microsoft.com/kb/238393 too scary. Then I found your wrapper that promises to make COM easier to use."
He went on to say:
"The fact that I still have to deal with code like this
SAFEARRAYBOUND sab[2];
sab[0].lLbound = 1;
sab[0].cElements = 15;
sab[1].lLbound = 1;
sab[1].cElements = 15;
arr.parray = SafeArrayCreate(VT_VARIANT, 2, sab);
is not comforting. I haven't played that much with the code I downloaded from your site but is it possible that you library can actually hide this stuff behind a modern c++ interface?"
The simple answer is: Yes.
In this article I will rewrite the Microsoft example with VOLE, illustrating how to drive COM Automation servers that have sophisticated object models.
The Scenario
The Microsoft example does the following actions:
- Look up the CLSID for "Word.Application"
- Start Word and get an
IDispatch
pointer for the application object
- Make Word visible
- Get the
Documents
collection
- Call
Documents.Open()
to open "C:\Doc1.doc"
- Get the
BuiltinDocumentProperties
collection
- Get the "
Subject
" property from the BuiltInDocumentProperties
collection
- Get the
Value
of the "Subject
" property
- Set the
Value
of the "Subject
" DocumentProperty
- Get
CustomDocumentProperties
collection
- Add a new property named "
CurrentYear
"
- Get the custom property "
CurrentYear
" and delete it
- Close the document without saving changes
- Quit Word
The VOLE Version
You can see the full horror of the manual method of achieving this in C++ in the Microsoft example. Now let's look at how it's achieved using VOLE.
object word = object::create("Word.Application", CLSCTX_LOCAL_SERVER);
word.put_property(L"Visible", true);
collection documents = word.get_property<collection>(L"Documents");
object document = documents.invoke_method<object>(L"Open", L"C:\\Doc1.doc");
collection builtInProps = document.get_property<collection>(
L"BuiltinDocumentProperties");
object propSubject = builtInProps.get_property<object>(L"Item", L"Subject");
std::string subject = propSubject.get_property<std::string>(L"Value");
std::cout << "Subject property: \"" << subject << '"' << std::endl;
propSubject.put_property(L"Value", "This is my subject");
collection customProps = document.get_property<collection>(
L"CustomDocumentProperties");
customProps.invoke_method<void>(L"Add", L"CurrentYear", false, 1, 1999);
object propCurrYear = customProps.get_property<object>(
L"Item", L"CurrentYear");
propCurrYear.invoke_method<void>(L"Delete");
document.invoke_method<void>(L"Close", false);
word.invoke_method<void>(L"Quit");
The full program is included in the download; here I'll just show the salient parts. For clarity here, we'll assume that using declarations have been employed for the VOLE classes.
Step 1. Look up the CLSID for "Word.Application"
This step is easy: we can omit it entirely.
Because the (static) vole::create()
methods allow the user to specify the class to be create via CLSID ({000209FF-0000-0000-C000-000000000046}
), Programmatic Id ("Word.Application"
), or string form of the CLSID ("{000209FF-0000-0000-C000-000000000046}"
), we can just pass the Programmatic Id.
Step 2. Start Word and Get an IDispatch Pointer for the Application Object
To start the Word automation server, we pass "Word.Application"
to the vole::object::create()
method. This method returns an instance of vole::object
that owns the underlying COM interface associated with the server.
object word = object::create("Word.Application", CLSCTX_LOCAL_SERVER);
Note that, just as in the Microsoft example, we must specify CLSCTX_LOCAL_SERVER
to invoke Word as a local server.
Step 3. Make Word Visible
To make the Word application object visible, we set its Visible property to true
. This is done by using the vole::object::put_property()
method, as in:
word.put_property(L"Visible", true);
Step 4. Get the Documents collection
For this we need to elicit the application object's Documents property, in an instance of vole::collection
, via the vole::object::get_property()
method, as follows:
collection documents = word.get_property<collection>(L"Documents");
Note the explicit specialisation of the member function template vole::object::get_property()
. All modern compilers are able to handle this modest sophistication, but some (in particular VC++ 6) are not. I'll show the alternate code, where required, for each step at the end of this article.
Step 5. Call Documents.Open() to Open "C:\Doc1.doc"
To invoke an Automation server's methods, we use the vole::object::invoke_method()
:
object document = documents.invoke_method<object>(L"Open", L"C:\\Doc1.doc");
This returns an instance of vole::object
that holds the corresponding document object.
Note: just as with the Microsoft example, you need to make sure that the file C:\Doc1.doc exists, otherwise an exception will be thrown.
Step 6. Get the BuiltinDocumentProperties collection
We elicit the document's BuiltinDocumentProperties property into a collection in the same way as shown in Step 4.
collection builtInProps = document.get_property<collection>(
L"BuiltinDocumentProperties");
Step 7. Get the "Subject" Property from the BuiltInDocumentProperties Collection
And here also:
object propSubject = builtInProps.get_property<object>(L"Item", L"Subject");
Step 8. Get the Value of the "Subject" Property
This time we elicit a property value that is a string, rather than an object. To that end, we request is as an instance of std::string
.
std::string subject = propSubject.get_property<std::string>(L"Value");
Note that VOLE equally well supports elicitation of string values as instances of std::wstring
, so we could have written it as:
std::wstring subject = propSubject.get_property<std::wstring>(L"Value");
Step 9. Set the Value of the "Subject" DocumentProperty
This is achieved via the vole::object::put_property()
method:
propSubject.put_property(L"Value", "This is my subject");
Note that VOLE handles conversion between ANSI/multibyte and wide/Unicode strings automatically for method/property parameters. Hence, we could just as readily have written:
propSubject.put_property(L"Value", L"This is my subject");
The only difference between them is that the former is slightly less efficient, because the string "This is my subject"
has to be converted to wide form.
Note: This flexibility does not apply to the names of the methods/properties themselves - these must be specified either as a DISPID (aka a long) or as a wide-string.
Step 10. Get CustomDocumentProperties collection
This is essentially the same as Step 6.
collection customProps = document.get_property<collection>(
L"CustomDocumentProperties");
Step 11. Add a New Property Named "CurrentYear"
We invoke the new property's Add()
method using vole::object::invoke_method
, as follows:
customProps.invoke_method<void>(L"Add", L"CurrentYear", false, 1, 1999);
Because we do not return anything, we specialise as void
.
Step 12. Get the Custom Property "CurrentYear" and Delete It
To delete the property we've just created, we need to first retrieve it and then instruct the retrieved property object to delete itself, as follows:
object propCurrYear = customProps.get_property<object>(
L"Item", L"CurrentYear");
propCurrYear.invoke_method<void>(L"Delete");
Alternatively, we could have achieved this in one statement, as follows:
customProps.get_property<object>(
L"Item", L"CurrentYear").invoke_method<void>(L"Delete");
But that's less clear. Obviously, VOLE, although very expressive compared to raw C++ manipulation of COM Automation servers, still has a degree of verbosity to it. I tend, therefore, to try and keep each COM property/method invocation to a single C++ statement.
Step 13. Close the Document Without Saving Changes
Since we've modified the document object, trying to quit Word will fail because it will ask us whether we want to save our changes. So we first call the document method Close
, passing false
to indicate that we want to discard the changes.
document.invoke_method<void>(L"Close", false);
Step 14. Quit Word
Finally, we instruct Word to quit, which causes the Word application to shut down.
word.invoke_method<void>(L"Quit");
And that's all there is to it. Compared to the ~220 lines of code in the Microsoft example it's a huge win in expressiveness. Add in the flexibility in handling different fundamental types and character encodings, the robustness in that all server resources are handled in an exception-safe manner, and the fact that it's 100% header-only and compiler-independent, it's pretty clear that VOLE represents an optimal solution for driving COM Automation servers from C++. But I would say that, wouldn't I? So I invite you to try it out for yourselves.
Compatibility with Older Compilers
As described in the first article in this series, some old compilers (e.g. VC++ 6) have problems with VOLE's template syntax, so an alternate form is also supported. The alternate form is pertinent to the following steps:
collection documents = word.get_property(of_type<collection>(), L"Documents");
object document = documents.invoke_method(
of_type<object>(), L"Open", L"C:\\Doc1.doc");
collection builtInProps = document.get_property(
of_type<collection>(), L"BuiltinDocumentProperties");
object propSubject = builtInProps.get_property(of_type<object>(),
L"Item", L"Subject");
std::string subject = propSubject.get_property(of_type<std::string>(),
L"Value");
collection customProps = document.get_property(of_type<collection>(),
L"CustomDocumentProperties");
customProps.invoke_method_v(L"Add", L"CurrentYear", false, 1, 1999);
object propCurrYear = customProps.get_property(of_type<object>(),
L"Item", L"CurrentYear");
propCurrYear.invoke_method_v(L"Delete");
document.invoke_method_v(L"Close", false);
word.invoke_method_v(L"Quit");
More to Come ...
I plan to write another article about VOLE soon, illustrating the vole::collection
class's ability to provide STL iterators over a COM Collection's elements (via the IEnumXXXX
protocol).
Your comments/criticisms/feature requests for VOLE are welcome via the VOLE project home.
Your comments/criticisms/feature requests for STLSoft are welcome via the STLSoft newsgroup (here), which is kindly provided by Digital Mars, providers of free high-quality C/C++/D compilers.
History
5th August 2007: First version