Program examples compiled using Visual C++ 6.0 compiler on Windows XP Pro machine with Service Pack 2. The Excel version is Excel 2003/Office 11. Topics and sub topics for this tutorial are listed below. Don’t forget to read Tenouk’s small disclaimer. The supplementary notes for this tutorial are Demo.xls, mymfc29A.xls, mymfc29B.xls, automation., variant and Colevariant class.
After reading Module 9, you should know what an interface is; you've already seen two standard COM interfaces, IUnknown and IClassFactory. Now you're ready for "applied" COM, or at least one aspect of it, Automation (formerly known as OLE Automation). You'll learn about the COM IDispatch interface, which enables C++ programs to communicate with Microsoft Visual Basic for Applications (VBA) programs and with programs written in other scripting languages. In addition, IDispatch is the key to getting your COM object onto a Web page. You'll use the MFC library implementation of IDispatch to write C++ Automation component and client programs. Both out-of-process components and in-process components are explored. But before jumping into C++ Automation programming, you need to know how the rest of the world writes programs. In this module, you'll get some exposure to VBA as it is implemented in Microsoft Excel. You'll run your C++ components from Excel, and you'll run Excel from a C++ client program.
Connecting C++ with Visual Basic for Applications (VBA)
Not all programmers for Microsoft Windows-based applications are going to be C++ programmers, especially if they have to learn the intricacies of COM theory. If you've been paying attention over the last few years, you've probably noticed a trend in which C++ programmers produce reusable modules. Programmers using higher-level languages (Visual Basic, VBA, and Web scripting languages, for example) consume those modules by integrating them into applications. You can participate in this programming model by learning how to make your software Script-friendly. Automation is one tool available now that is supported by the Microsoft Foundation Class library. ActiveX Controls are another tool for C++/VBA integration and are very much a superset of Automation because both tools use the IDispatch interface. Using ActiveX Controls, however, might be overkill in many situations. Many applications, including Microsoft Excel 97, can support both Automation components and ActiveX controls. You'll be able to apply all that you learn about Automation when you write and use ActiveX controls.
Two factors are responsible for Automation's success. First, VBA (or VB Script) is now the programming standard in most Microsoft applications, including Microsoft Word, Microsoft Access, and Excel, not to mention Microsoft Visual Basic itself. All these applications support Automation, which means they can be linked to other Automation-compatible components, including those written in C++ and VBA. For example, you can write a C++ program that uses the text-processing capability of Word, or you can write a C++ matrix inversion component that can be called from a VBA macro in an Excel worksheet. The second factor connected to Automation's success is that dozens of software companies provide Automation programming interfaces for their applications, mostly for the benefit of VBA programmers. With a little effort, you can run these applications from C++. You can, for example, write an MFC program that controls Visio drawing program. Automation isn't just for C++ and VBA programmers. Software-tool companies are already announcing Automation-compatible, Basic-like languages that you can license for your own programmable applications. One version of Smalltalk even supports Automation.
Automation Clients and Automation Components
A clearly defined "master-slave" relationship is always present in an Automation communication dialog. The master is the Automation client and the slave is the Automation component (server). The client initiates the interaction by constructing a component object (it might have to load the component program) or by attaching to an existing object in a component program that is already running. The client then calls interface functions in the component and releases those interfaces when it's finished. Here are some interaction scenarios:
A C++ Automation client uses a Microsoft or third-party application as a component. The interaction could trigger the execution of VBA code in the component application.
A C++ Automation component is used from inside a Microsoft application (or a Visual Basic application), which acts as the Automation client. Thus, VBA code can construct and use C++ objects.
A C++ Automation client uses a C++ Automation component.
A Visual Basic program uses an Automation-aware application such as Excel. In this case, Visual Basic is the client and Excel is the component.
Microsoft Excel: A Better Visual Basic than Visual Basic
At the time that the first three editions of this book were written, Visual Basic worked as an Automation client, but you couldn't use it to create an Automation component. Since version 5.0, Visual Basic lets you write components too, even ActiveX controls. We originally used Excel instead of VB because Excel was the first Microsoft application to support VBA syntax and it could serve as both a client and a component. We decided to stick with Excel because C++ programmers who look down their noses at Visual Basic might be inclined to buy Excel (if only to track their software royalties).
We strongly recommend that you get a copy of Excel 97 (or a later version - 2000, 2003). This is a true 32-bit application and a part of the Microsoft Office suite. With this version of Excel, you can write VBA code in a separate location that accesses worksheet cells in an object-oriented manner. Adding visual programming elements, such as pushbuttons, is easy. Forget all you ever knew about the old spreadsheet programs that forced you to wedge macro code inside cells.
This module isn't meant to be an Excel tutorial, but we've included a simple Excel workbook. (A workbook is a file that can contain multiple worksheets plus separate VBA code.) This workbook demonstrates a VBA macro that executes from a pushbutton. You can use Excel to load Demo.xls or you can key in the example from scratch. Figure 1 shows the actual spreadsheet with the button and sample data.
In this spreadsheet, you highlight cells A4 through A9 and click the Process Col button. A VBA program iterates down the column and draws a hatched pattern on cells with numeric values greater than 10. Figure 2 shows the macro code itself, which is "behind" the worksheet. In Excel 97/2000/2003 (this Tutorial uses Excel 2003), choose Macro from the Tools menu, and then choose Visual Basic Editor. Alt-F11 is the shortcut. As you can see, you're working in the standard VBA 5.0 environment at this point.
Figure 1: An Excel spreadsheet that uses VBA code.
Figure 2: The VBA code for the Excel spreadsheet.
If you want to create the example yourself, follow these steps:
Start Excel with a new workbook, press Alt-F11 to invoke the Visual Basic Editor.
Figure 3: Excel workbook.
Then double-click Sheet1 in the top left window.
Figure 4: visual Basic editor.
Type in the macro code shown below.
Do Until ActiveCell.Value = ""
If ActiveCell.Value > 10 Then
Selection.Interior.Pattern = xlCrissCross
Selection.Interior.Pattern = xlNone
Figure 5: Typing the macro code.
Return to the Excel window by choosing Close And Return To Microsoft Excel from the File menu as shown in Figure 6. Choose Toolbars from the View menu. Check Forms to display the Forms toolbar as shown in Figure 7. You can also access the list of toolbars by right-clicking on any existing toolbar.
Figure 6: Closing the Visual Basic Editor and back to Excel.
Figure 7: Excel forms toolbar.
Click the Button control, and then create the pushbutton by dragging the mouse in the upper-left corner of the worksheet. Assign the button to the Sheet1.ProcessColumn macro.
Figure 8: Attaching macro to button in Excel.
Size the pushbutton as needed, and type the caption Process Col, as shown in Figure 9.
Figure 9: Button in Excel.
Type some numbers in the column starting at cell A4. Select the cells containing these numbers, and then click the Process Col button to test the program.
Figure 10: VBA in action.
Pretty easy, isn't it? Let's analyze an Excel VBA statement from the macro above:
The first element, Selection, is a property of an implied object, the Excel application. The Selection property in this case is assumed to be a Range object that represents a rectangular array of cells. The second element, Offset, is a property of the Range object that returns another Range object based on the two parameters. In this case, the returned Range object is the one-cell range that begins one row down from the original range. The third element, Range, is a property of the Range object that returns yet another range. This time it's the upper-left cell in the second range. Finally, the Select method causes Excel to highlight the selected cell and makes it the new Selection property of the application.
As the program iterates through the loop, the preceding statement moves the selected cell down the worksheet one row at a time. This style of programming takes some getting used to, but you can't afford to ignore it. The real value here is that you now have all the capabilities of the Excel spreadsheet and graphics engine available to you in a seamless programming environment.
Properties, Methods and Collections
The distinction between a property and a method is somewhat artificial. Basically, a property is a value that can be both set and retrieved. You can, for example, set and get the Selection property for an Excel application. Another example is Excel's Width property, which applies to many object types. Some Excel properties are read-only; most are read/write.
Properties don't officially have parameters, but some properties are indexed. The property index acts a lot like a parameter. It doesn't have to be an integer, and it can have more than one element (row and column, for example). You'll find many indexed properties in Excel's object model, and Excel VBA can handle indexed properties in Automation components.
Methods are more flexible than properties. They can have zero or many parameters, and they can either set or retrieve object data. Most frequently they perform some action, such as showing a window. Excel's Select method is an example of an action method.
The Excel object model supports collection objects. If you use the Worksheets property of the Application object, you get back a Sheets collection object, which represents all the worksheets in the active workbook. You can use the Item property (with an integer index) to get a specific Worksheet object from a Sheets collection or you can use an integer index directly on the collection.
The Problem That Automation Solves
You've already learned that a COM interface is the ideal way for Windows programs to communicate with one another, but you've also learned that designing your own COM interfaces is mostly impractical. Automation's general-purpose interface, IDispatch, serves the needs of both C++ and VBA programmers. As you might guess from your glimpse of Excel VBA, this interface involves objects, methods, and properties.
You can write COM interfaces that include functions with any parameter types and return values you specify. IMotion and IVisual, created in Module 23, are some examples. If you're going to let VBA programmers in, however, you can't be fast and loose anymore. You can solve the communication problem with one interface that has a member function smart enough to accommodate methods and properties as defined by VBA. Needless to say, IDispatch has such a function: Invoke(). You use IDispatch::Invoke for COM objects that can be constructed and used in either C++ or VBA programs.
Now you're beginning to see what Automation does. It funnels all inter module communication through the IDispatch::Invoke function. How does a client first connect to its component? Because IDispatch is merely another COM interface, all the registration logic supported by COM comes into play. Automation components can be DLLs or EXEs, and they can be accessed over a network using distributed COM (DCOM).
The IDispatch Interface
IDispatch is the heart of Automation. It's fully supported by COM marshaling (that is, Microsoft has already marshaled it for you), as are all the other standard COM interfaces, and it's supported well by the MFC library. At the component end, you need a COM class with an IDispatch interface (plus the prerequisite class factory, of course). At the client end, you use standard COM techniques to obtain an IDispatch pointer. As you'll see, the MFC library and the wizards take care of a lot of these details for you.
Remember that Invoke() is the principal member function of IDispatch. If you looked up IDispatch::Invoke in the Visual C++ online documentation, you'd see a really ugly set of parameters. Don't worry about those now. The MFC library steps in on both sides of the Invoke() call, using a data-driven scheme to call component functions based on dispatch map parameters that you define with macros.
Invoke() isn't the only IDispatch member function. Another function your controller might call is GetIDsOfNames(). From the VBA programmer's point of view, properties and methods have symbolic names, but C++ programmers prefer more efficient integer indexes. Invoke uses integers to specify properties and methods, so GetIDsOfNames() is useful at the start of a program for converting each name to a number if you don't know the index numbers at compile time. You've already seen that IDispatch supports symbolic names for methods. In addition, the interface supports symbolic names for a method's parameters. The GetIDsOfNames() function returns those parameter names along with the method name. Unfortunately, the MFC IDispatch implementation doesn't support named parameters.
Automation Programming Choices
Suppose you're writing an Automation component in C++. You've got some choices to make. Do you want an in-process component or an out-of-process component? What kind of user interface do you want? Does the component need a user interface at all? Can users run your EXE component as a stand-alone application? If the component is an EXE, will it be SDI or MDI? Can the user shut down the component program directly?
If your component is a DLL, COM linkage will be more efficient than it would be with an EXE component because no marshaling is required. Most of the time, your in-process Automation components won't have their own user interfaces, except for modal dialog boxes. If you need a component that manages its own child window, you should use an ActiveX control, and if you want to use a main frame window, use an out-of-process component. As with any 32-bit DLL, an Automation DLL is mapped into the client's process memory. If two client programs happen to request the same DLL, Windows loads and links the DLL twice. Each client is unaware that the other is using the same component.
With an EXE component, however, you must be careful to distinguish between a component program and a component object. When a client calls IClassFactory::CreateInstance to construct a component object, the component's class factory constructs the object, but COM might or might not need to start the component program.
Here are some scenarios:
The component's COM-creatable class is programmed to require a new process for each object constructed. In this case, COM starts a new process in response to the second and subsequent CreateInstance() calls, each of which returns an IDispatch pointer.
Here's a special case of scenario 1 above, specific to MFC applications. The component class is an MFC document class in an SDI application. Each time a client calls CreateInstance(), a new component process starts, complete with a document object, a view object, and an SDI main frame window.
The component class is programmed to allow multiple objects in a single process. Each time a client calls CreateInstance(), a new component object is constructed. There is only one component process, however.
Here's a special case of scenario 3 above, specific to MFC applications. The component class is an MFC document class in an MDI application. There is a single component process with one MDI main frame window. Each time a client calls CreateInstance(), a new document object is constructed, along with a view object and an MDI child frame window.
There's one more interesting case. Suppose a component EXE is running before the client needs it, and then the client decides to access a component object that already exists. You'll see this case with Excel. The user might have Excel running but minimized on the desktop, and the client needs access to Excel's one and only Application object. Here the client calls the COM function GetActiveObject(), which provides an interface pointer for an existing component object. If the call fails, the client can create the object with CoCreateInstance().
For component object deletion, normal COM rules apply. Automation objects have reference counts, and they delete themselves when the client calls Release() and the reference count goes to 0. In an MDI component, if the Automation object is an MFC document, its destruction causes the corresponding MDI child window to close. In an SDI component, the destruction of the document object causes the component process to exit. The client is responsible for calling Release() for each IDispatch interface before the client exits. For EXE components, COM will intervene if the client exits without releasing an interface, thus allowing the component process to exit. You can't always depend on this intervention, however, so be sure that your client cleans up its interfaces!
With generic COM, a client application often obtains multiple interface pointers for a single component object. Look back at the spaceship example in Module 23, in which the simulated COM component class had both an IMotion pointer and an IVisual pointer. With Automation, however, there's usually only a single (IDispatch) pointer per object. As in all COM programming, you must be careful to release all your interface pointers. In Excel, for example, many properties return an IDispatch pointer to new or existing objects. If you fail to release a pointer to an in-process COM component, the Debug version of the MFC library alerts you with a memory-leak dump when the client program exits.
The MFC IDispatch Implementation
The component program can implement its IDispatch interface in several ways. The most common of these pass off much of the work to the Windows COM DLLs by calling the COM function CreateStdDispatch() or by delegating the Invoke() call to the ITypeInfo interface, which involves the component's type library. A type library is a table, locatable through the Registry, which allows a client to query the component for the symbolic names of objects, methods, and properties. A client could, for example, contain a browser that allows the user to explore the component's capabilities.
The MFC library supports type libraries, but it doesn't use them in its implementation of IDispatch, which is instead driven by a dispatch map. MFC programs don't call CreateStdDispatch() at all, nor do they use a type library to implement IDispatch::GetIDsOfNames. This means that you can't use the MFC library if you implement a multilingual Automation component, one that supports English and German property and method names, for example. CreateStdDispatch() doesn't support multilingual components either.
Later in this module you'll learn how a client can use a type library, and you'll see how AppWizard and ClassWizard create and maintain type libraries for you. Once your component has a type library, a client can use it for browsing, independent of the IDispatch implementation.
An MFC Automation Component
Let's look at what happens in an MFC Automation component, in this case, a simplified version of the MYMFC29C alarm clock program that is discussed later in this module. In the MFC library, the IDispatch implementation is part of the CCmdTarget base class, so you don't need INTERFACE_MAP macros. You write an Automation component class, CClock, for example, derived from CCmdTarget. This class's CPP file contains DISPATCH_MAP macros:
DISP_PROPERTY(CClock, "Time", m_time, VT_DATE)
DISP_PROPERTY_PARAM(CClock, "Figure", GetFigure, SetFigure, VT_VARIANT, VTS_I2)
DISP_FUNCTION(CClock, "RefreshWin", Refresh, VT_EMPTY, VTS_NONE)
DISP_FUNCTION(CClock, "ShowWin", ShowWin, VT_BOOL, VTS_I2)
Looks a little like an MFC message map, doesn't it? The CClock class header file contains related code, shown here:
afx_msg VARIANT GetFigure(short n);
afx_msg void SetFigure(short n, const VARIANT& vaNew);
afx_msg void Refresh();
afx_msg BOOL ShowWin(short n);
What does all this stuff mean? It means that the CClock class has the following properties and methods.
Linked directly to class data member m_time.
Indexed property, accessed through member functions GetFigure() and SetFigure(): first parameter is the index; second (for SetFigure()) is the string value. The figures are the "XII," "III," "VI," and "IX" that appear on the clock face.
Linked to class member function Refresh()- no parameters or return value.
Linked to class member function ShowWin()- short integer parameter, Boolean return value.
How does the MFC dispatch map relate to IDispatch and the Invoke() member function? The dispatch-map macros generate static data tables that the MFC library's Invoke() implementation can read. A controller gets an IDispatch pointer for CClock (connected through the CCmdTarget base class), and it calls Invoke() with an array of pointers as a parameter. The MFC library's implementation of Invoke(), buried somewhere inside CCmdTarget, uses the CClock dispatch map to decode the supplied pointers and either calls one of your member functions or accesses m_time directly.
As you'll see in the examples, ClassWizard can generate the Automation component class for you and help you code the dispatch map.
An MFC Automation Client Program
Let's move on to the client's end of the Automation conversation. How does an MFC Automation client program call Invoke()? The MFC library provides a base class COleDispatchDriver for this purpose. This class has a data member, m_lpDispatch, which contains the corresponding component's IDispatch pointer. To shield you from the complexities of the Invoke() parameter sequence, COleDispatchDriver has several member functions, including InvokeHelper(), GetProperty() and SetProperty(). These three functions call Invoke() for an IDispatch pointer that links to the component. The COleDispatchDriver() object incorporates the IDispatch pointer.
Let's suppose our client program has a class CClockDriver, derived from COleDispatchDriver(), that drives CClock objects in an Automation component. The functions that get and set the Time property are shown here.
GetProperty(1, VT_DATE, (void*)&result);
void CClockDriver::SetTime(DATE propVal)
SetProperty(1, VT_DATE, propVal);
Here are the functions for the indexed Figure property:
VARIANT CClockDriver::GetFigure(short i)
static BYTE parms = VTS_I2;
InvokeHelper(2, DISPATCH_PROPERTYGET, VT_VARIANT, (void*)&result, parms, i);
void CClockDriver::SetFigure(short i, const VARIANT& propVal)
static BYTE parms = VTS_I2 VTS_VARIANT;
InvokeHelper(2, DISPATCH_PROPERTYPUT, VT_EMPTY, NULL, parms, i, &propVal);
And finally, here are the functions that access the component's methods:
InvokeHelper(3, DISPATCH_METHOD, VT_EMPTY, NULL, NULL);
BOOL CClockDriver::ShowWin(short i)
static BYTE parms = VTS_I2;
InvokeHelper(4, DISPATCH_METHOD, VT_BOOL, (void*)&result, parms, i);
The function parameters identify the property or method, its return value, and its parameters. You'll learn about dispatch function parameters later, but for now take special note of the first parameter for the InvokeHelper(), GetProperty() and SetProperty() functions. This is the unique integer index, or dispatch ID (DISPID), for the property or method. Because you're using compiled C++, you can establish these IDs at compile time. If you're using an MFC Automation component with a dispatch map, the indexes are determined by the map sequence, beginning with 1. If you don't know a component's dispatch indexes, you can call the IDispatch member function GetIDsOfNames() to convert the symbolic property or method names to integers. The following illustration shows the interactions between the client (or controller) and the component.
Figure 11: Interaction between client and component.
The solid lines show the actual connections through the MFC base classes and the Invoke() function. The dotted lines represent the resulting logical connections between client class members and component class members.
Most Automation components have a binary type library file with a TLB extension. ClassWizard can access this type library file to generate a class derived from COleDispatchDriver. This generated controller class contains member functions for all the component's methods and properties with hard-coded dispatch IDs. Sometimes you need to do some surgery on this generated code, but that's better than writing the functions from scratch.
After you have generated your driver class, you embed an object of this class in your client application's view class (or in another class) like this:
Then you ask COM to create a clock component object with this statement:
Now you're ready to call the dispatch driver functions:
When the m_clock object goes out of scope, its destructor releases the IDispatch pointer.
An Automation Client Program Using the Compiler's #import Directive
Now there's an entirely new way of writing Automation client programs. Instead of using ClassWizard to generate a class derived from COleDispatchDriver, you use the compiler to generate header and implementation files directly from a component's type library. For the clock component, your client program contains the following statement:
#import "..\mymfc29C\debug\mymfc29C.tlb" rename_namespace("ClockDriv") using namespace ClockDriv;
The compiler then generates (and processes) two files, mymfc29C.tlh and mymfc29C.tli, in the project's Debug or Release subdirectory. The TLH file contains the IMymfc29C clock driver class declaration plus this smart pointer declaration:
The _COM_SMARTPTR_TYPEDEF macro generates the IMymfc29CPtr pointer type, which encapsulates the component's IDispatch pointer. The TLI file contains inline implementations of member functions, some of which are shown in the following code:
inline HRESULT IMymfc29C::RefreshWin()
return _com_dispatch_method(this, 0x4, DISPATCH_METHOD, VT_EMPTY, NULL, NULL);
inline DATE IMymfc29C::GetTime()
_com_dispatch_propget(this, 0x1, VT_DATE, (void*)&_result);
inline void IMymfc29C::PutTime(DATE _val)
_com_dispatch_propput(this, 0x1, VT_DATE, _val);
Note the similarity between these functions and the COleDispatchDriver() member functions you've already seen. The functions _com_dispatch_method(), _com_dispatch_propget() and _com_dispatch_propput() are in the runtime library. In your Automation client program, you declare an embedded smart pointer member in your view class (or in another class) like this:
Then you create a clock component object with this statement:
Now you're ready to use the IMymfc29CPtr class's overloaded → operator to call the member functions defined in the TLI file:
When the m_clock smart pointer object goes out of scope, its destructor calls the COM Release() function.
The #import directive is the future of COM programming. With each new version of Visual C++, you'll see COM features moving into the compiler, along with the document-view architecture itself.
Further reading and digging:
Win32 process, thread and synchronization story can be found starting from Module R.
MSDN What's New (MFC Feature Pack) - feature pack.
DCOM at MSDN.
COM+ at MSDN.
COM at MSDN.