Automation Examples
The remainder of this chapter presents five sample programs. The first three programs are Automation components—an EXE component with no user interface, a DLL component, and a multi-instance SDI EXE component. Each of these component programs comes with an Excel driver workbook file. The fourth sample program is an MFC Automation client program that drives the three components and also runs Excel using the COleDispatchDriver class. The last sample is a client program that uses the C++ #import directive instead of the MFC COleDispatchDriver class.
The Ex23a Example: An Automation Component EXE with No User Interface
The Ex23a example represents a typical use of Automation. It is similar to the Visual C++ .NET Autoclik example, which is an MDI framework application with the document object as the Automation component. (To find the Autoclik example, look in the MFC Library Reference and search for AutoClik.) However, unlike the Autoclik example, the Ex23a example has no user interface. There is one Automation-aware class, and in the first version of the program, a single process supports the construction of multiple Automation component objects. In the second version, a new process starts up each time an Automation client creates an object.In the Ex23a example, a C++ component implements financial transactions. VBA programmers can write user-interface–intensive applications that rely on the audit rules imposed by the Automation component. A production component program would probably use a database, but Ex23a is simpler. It implements a bank account with two methods, Deposit and Withdrawal, and one read-only property, Balance. Obviously, Withdrawal can't permit withdrawals that make the balance negative. You can use Excel to control the component, as shown in Figure 23-3.
Figure 23-3: An Excel workbook controlling the Ex23a component.
Here are the steps for creating the program from scratch:
Run the MFC Application Wizard to create the Ex23a project. Select the Dialog Based option on the Application Type page. Deselect all options on the User Interface Features and the Advanced Features pages except the Automation check box on the Advanced Features page. This is the simplest application the MFC Application Wizard can generate.
Eliminate the dialog class from the project. Using Windows Explorer, delete the files Ex23aDlg.cpp, Ex23aDlg.h, DlgProxy.cpp, and DlgProxy.h. Remove Ex23aDlg.cpp, Ex23aDlg.h, DlgProxy.cpp, and DlgProxy.h from the project by deleting them from Solution Explorer. Edit Ex23a.cpp to remove the dialog #include, and remove all dialog-related code from the InitInstance function. In Resource View, delete the IDD_EX23A_DIALOG dialog resource template.
Add code to enable Automation. Selecting the Automation check box added this line in StdAfx.h :#include <afxdisp.h>
The InitInstance function (in Ex23a.cpp ) now has COM initialization code in it. Be sure to add the return TRUE statement that's shown in boldface:BOOL CEx23aApp::InitInstance()
{
CWinApp::InitInstance();
// Initialize OLE libraries
if (!AfxOleInit())
{
AfxMessageBox(IDP_OLE_INIT_FAILED);
return FALSE;
}
// Parse command line for automation or reg/unreg switches.
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);
// App was launched with /Embedding or /Automation switch.
// Run app as automation server.
if (cmdInfo.m_bRunEmbedded || cmdInfo.m_bRunAutomated)
{
// Register class factories via CoRegisterClassObject().
COleTemplateServer::RegisterAll();
return TRUE;
}
// App was launched with /Unregserver or /Unregister switch.
// Remove entries from the registry.
else if (cmdInfo.m_nShellCommand ==
CCommandLineInfo::AppUnregister)
{
COleObjectFactory::UpdateRegistryAll(FALSE);
AfxOleUnregisterTypeLib(_tlid, _wVerMajor, _wVerMinor);
return FALSE;
}
// App was launched standalone or with other switches
// (e.g. /Register or /Regserver). Update registry entries,
// including typelibrary.
else
{
COleObjectFactory::UpdateRegistryAll();
AfxOleRegisterTypeLib(AfxGetInstanceHandle(), _tlid);
if (cmdInfo.m_nShellCommand ==
CCommandLineInfo::AppRegister)
return FALSE;
}
return FALSE;
}
Use the Add Class Wizard to add a new class, CBank, as shown here:
Be sure to select the Creatable By Type ID option.
Use the Add Method Wizard and the Add Property Wizard to add two methods and a property. To get to these wizards, open Class View, select the library node to expand the library information, and right-click on the IBank node. You'll see two commands: Add Method and Add Property. First, add a Withdrawal method, as shown here:
The dAmount parameter is the amount to be withdrawn, and the return value is the actual amount withdrawn. If you try to withdraw $100 from an account that contains $60, the amount withdrawn will be $60.Add a similar Deposit method that returns void, and then add the Balance property, as shown here:
We could have selected direct access to a component data member, but then we wouldn't have read-only access. We selected Get/Set Methods so we can code the SetBalance function to do nothing.
Add a public m_dBalance data member of type double to the CBank class. Because we selected the Get/Set Methods option for the Balance property, the Add Property Wizard won't generate a data member. You should declare m_dBalance in the Bank.h file and initialize m_dBalance to 0.0 in the CBank constructor located in the Bank.cpp file.
Edit the generated method and property functions. Add the following boldface code:DOUBLE CBank::Withdrawal(DOUBLE dAmount)
{
AFX_MANAGE_STATE(AfxGetAppModuleState());
if (dAmount < 0.0) {
return 0.0;
}
if (dAmount <= m_dBalance) {
m_dBalance -= dAmount;
return dAmount;
}
double dTemp = m_dBalance;
m_dBalance = 0.0;
return dTemp;
}
void CBank::Deposit(DOUBLE dAmount)
{
AFX_MANAGE_STATE(AfxGetAppModuleState());
if (dAmount < 0.0) {
return;
}
m_dBalance += dAmount;
}
DOUBLE CBank::GetBalance(void)
{
AFX_MANAGE_STATE(AfxGetAppModuleState());
return m_dBalance;
}
void CBank::SetBalance(DOUBLE newVal)
{
AFX_MANAGE_STATE(AfxGetAppModuleState());
TRACE("Sorry, Dave, I can't do that!\n");
}
Build the Ex23a program, and run it once to register the component.
Set up five Excel macros in a new workbook file, Ex23a.xls . Add the following code:Dim Bank As Object
Sub LoadBank()
Set Bank = CreateObject("Ex23a.Bank")
End Sub
Sub UnloadBank()
Set Bank = Nothing
End Sub
Sub DoDeposit()
Range("D4").Select
Bank.Deposit (ActiveCell.Value)
End Sub
Sub DoWithdrawal()
Range("E4").Select
Dim Amt
Amt = Bank.Withdrawal(ActiveCell.Value)
Range("E5").Select
ActiveCell.Value = Amt
End Sub
Sub DoInquiry()
Dim Amt
Amt = Bank.Balance()
Range("G4").Select
ActiveCell.Value = Amt
End Sub
Arrange an Excel worksheet as shown in Figure 23-3. Attach the macros to the buttons (by right-clicking on the buttons).
Test the Ex23a bank component. Click the Load Bank Program button, enter a deposit value in cell D4, and click the Deposit button. Click the Balance Inquiry button, and watch the balance appear in cell G4. Enter a withdrawal value in cell E4, and click the Withdrawal button. To see the balance, click the Balance Inquiry button.
What's happening in this program? Look closely at the CEx23aApp::InitInstance function. When you run the program directly from Windows, it displays a message box and then quits, but not before it updates the Registry. The COleObjectFactory::UpdateRegistryAll function hunts for global class factory objects, and the CBank class's IMPLEMENT_OLECREATE macro invocation defines such an object. (The IMPLEMENT_OLECREATE_FLAGS line was generated because we selected the Createable By Type ID check box when we added the CBank class.) The unique class ID and the program ID, Ex23a.Bank, are added to the Registry.
When Excel then calls CreateObject, COM loads the Ex23a program, which contains the global factory for CBank objects. COM then calls the factory object's CreateInstance function to construct the CBank object and return an IDispatch pointer. Here's the CBank class declaration that the Add Class Wizard generated in the Bank.h file, with unnecessary detail (and the method and property functions you've already seen) omitted:
#pragma once
// CBank command target
class CBank : public CCmdTarget
{
DECLARE_DYNCREATE(CBank)
public:
CBank();
virtual ~CBank();
virtual void OnFinalRelease();
DOUBLE m_dBalance;
protected:
DECLARE_MESSAGE_MAP()
DECLARE_OLECREATE(CBank)
DECLARE_DISPATCH_MAP()
DECLARE_INTERFACE_MAP()
DOUBLE Withdrawal(DOUBLE dAmount);
enum
{
dispidBalance = 3, dispidDeposit = 2L, dispidWithdrawal = 1L
};
void Deposit(DOUBLE dAmount);
DOUBLE GetBalance(void);
void SetBalance(DOUBLE newVal);
};
Here's the code that was automatically generated by the Add Class Wizard in Bank.cpp :
// Bank.cpp : implementation file
//
#include "stdafx.h"
#include "Ex23a.h"
#include "Bank.h"
// CBank
IMPLEMENT_DYNCREATE(CBank, CCmdTarget)
CBank::CBank()
{
EnableAutomation();
// To keep the application running as long as an OLE automation
// object is active, the constructor calls AfxOleLockApp.
AfxOleLockApp();
m_dBalance = 0.0;
}
CBank::~CBank()
{
// To terminate the application when all objects created with
// with OLE automation, the destructor calls AfxOleUnlockApp.
AfxOleUnlockApp();
}
void CBank::OnFinalRelease()
{
// When the last reference for an automation object is released
// OnFinalRelease is called. The base class will automatically
// delete the object. Add additional cleanup required for your
// object before calling the base class.
CCmdTarget::OnFinalRelease();
}
BEGIN_MESSAGE_MAP(CBank, CCmdTarget)
END_MESSAGE_MAP()
BEGIN_DISPATCH_MAP(CBank, CCmdTarget)
DISP_FUNCTION_ID(CBank, "Withdrawal", dispidWithdrawal,
Withdrawal, VT_R8, VTS_R8)
DISP_FUNCTION_ID(CBank, "Deposit", dispidDeposit,
Deposit, VT_EMPTY, VTS_R8)
DISP_PROPERTY_EX_ID(CBank, "Balance", dispidBalance,
GetBalance, SetBalance, VT_R8)
END_DISPATCH_MAP()
// Note: we add support for IID_IBank to support typesafe binding
// from VBA. This IID must match the GUID that is attached to the
// dispinterface in the .IDL file.
// {8BAD2B0C-62CC-4952-811C-C736DA06858E}
static const IID IID_IBank =
{ 0x8BAD2B0C, 0x62CC, 0x4952,
{ 0x81, 0x1C, 0xC7, 0x36, 0xDA, 0x6, 0x85, 0x8E } };
BEGIN_INTERFACE_MAP(CBank, CCmdTarget)
INTERFACE_PART(CBank, IID_IBank, Dispatch)
END_INTERFACE_MAP()
// {3EC6FA59-9F9F-4619-9F62-BA5FE37176F0}
IMPLEMENT_OLECREATE_FLAGS(CBank, "Ex23a.Bank",
afxRegApartmentThreading, 0x3ec6fa59, 0x9f9f,
0x4619, 0x9f, 0x62, 0xba, 0x5f, 0xe3, 0x71,
0x76, 0xf0)
// CBank message handlers
This first version of the Ex23a program runs in single-process mode, as does the Autoclik program. If a second Automation client asks for a new CBank object, COM will call the class factory CreateInstance function again and the existing process will construct another CBank object on the heap. You can verify this by making a copy of the Ex23a.xls workbook (under a different name) and loading both the original and the copy. Click the Load Bank Program button in each workbook, and watch the Debug window. InitInstance should be called only once.
When an Automation client launches an EXE component program, it sets the /Embedding command-line parameter. If you want to debug your component, you must do the same. Right-click on the project in Solution Explorer. Choose Properties and then click Debugging in the Property Pages dialog box. Enter /Automation (or /Embedding) in the Command Arguments box, as shown here:
When you choose Start from the Debug menu or press F5, your program will start and then wait for a client to activate it. At this point, you should start the client program from Windows (if it is not already running) and then use it to create a component object. Your component program in the debugger should then construct its object. It might be a good idea to include a TRACE statement in the component object's constructor.Remember that your component program must be registered before the client can find it. That means you have to run it once without the /Automation (or the /Embedding) flag. Many clients don't synchronize with Registry changes. If your client is running when you register the component, you might have to restart the client.
The Ex23b Example: An Automation Component DLL
You could easily convert Ex23a from an EXE to a DLL. The CBank class would be exactly the same, and the Excel driver would be similar. It's more interesting, though, to write a new application—this time with a minimal user interface. We'll use a modal dialog box because it's the most complex user interface we can conveniently use in an Automation DLL.The Ex23b program is fairly simple. An Automation component class, identified by the registered name Ex23b.Auto, has the following properties and method:
Name | Description |
---|---|
LongData | Long integer property |
TextData | VARIANT property |
DisplayDialog | Method—no parameters, BOOL return |
DisplayDialog displays the Ex23b data-gathering dialog box shown in Figure 23-4. An Excel macro passes two cell values to the DLL and then updates the same cells with the updated values.
Figure 23-4: The Ex23b DLL dialog box in action.
So far, you've seen VBA parameters passed by value. VBA has pretty strange rules for calling methods. If the method has one parameter, you can use parentheses; if it has more than one, you can't (unless you're using the function's return value, in which case you must use parentheses). Here's some sample VBA code that passes the string parameter by value:
Object.Method1 parm1, "text"
Object.Method2("text")
Dim s as String
s = "text"
Object.Method2(s)
Sometimes, though, VBA passes the address of a parameter (a reference). In this example, the string is passed by reference:
Dim s as String
s = "text"
Object.Method1 parm1, s
You can override VBA's default behavior by prefixing a parameter with ByVal or ByRef. Your component can't predict if it's getting a value or a reference—it must prepare for both. The trick is to test vt to see whether its VT_BYREF bit is set. Here's a sample method implementation that accepts a string (in a VARIANT) passed either by reference or value:
void CMyComponent::Method(long nParm1, const VARIANT& vaParm2)
{
CString str;
if ((vaParm2.vt & 0x7f) == VT_BSTR) {
if ((vaParm2.vt & VT_BYREF) != 0)
str = *(vaParm2.pbstrVal); // byref
else
str = vaParm2.bstrVal; // byval
}
AfxMessageBox(str);
}
If you declare a BSTR parameter, the MFC library will do the conversion for you. Suppose your client program passes a BSTR reference to an out-of-process component and the component program changes the value. Because the component can't access the memory of the client process, COM must copy the string to the component and then copy it back to the client after the function returns. So, before you declare reference parameters, remember that passing references through IDispatch is not like passing references in C++.
The example was first generated as a normal MFC DLL using the MFC DLL Wizard with the Regular DLL Using Shared MFC DLL option and the Automation option selected. Here are the steps for building and testing the Ex23b component DLL from the code installed from the companion CD:
In Visual Studio .NET, open the \vcppnet\Ex23b\ Ex23b.sln solution. Build the project.
Register the DLL. You can use the RegComp program in the \vcppnet\REGCOMP\Release directory on the companion CD; a file dialog box makes it easy to select the DLL file. Or you can use Regsvr32.exe.
Start Excel, and then load the \vcppnet\Ex23b\ Ex23b.xls workbook file. Type an integer in cell C3, and then type some text in cell D3, as shown here:
Click the Load DLL button, and then click the Gather Data button. Edit the data, click OK, and watch the new values appear in the spreadsheet.
Click the Unload DLL button. If you've started the DLL (and Excel) from the debugger, you can watch the Debug window to be sure the DLL's ExitInstance function is called.
Now let's look at the Ex23b code. Like an MFC EXE, an MFC regular DLL has an application class (derived from CWinApp) and a global application object. The overridden InitInstance member function in Ex23b.cpp looks like this:
BOOL CEx23bApp::InitInstance()
{
TRACE("CEx23bApp::InitInstance\n");
CWinApp::InitInstance();
// Register all OLE server (factories) as running. This enables the
// OLE libraries to create objects from other applications.
COleObjectFactory::RegisterAll();
return TRUE;
}
To debug a DLL, you must tell the debugger which EXE file to load. Right-click on the project name in Solution Explorer and choose Properties. Click Debugging in the Property Pages dialog box and enter the controller's full pathname (including the EXE extension) in the Command box, as shown here:
When you press F5, your controller will start. When you activate the component from the controller, the DLL will load.It might be a good idea to include a TRACE statement in the component object's constructor. Don't forget that your DLL must be registered before the client can load it.Here's another option: If you have the source code for the client program, you can start the client program in the debugger. When the client loads the component DLL, you can see the output from the component program's TRACE statements.
There's also the following code for the three standard COM DLL exported functions:
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
return AfxDllGetClassObject(rclsid, riid, ppv);
}
// DllCanUnloadNow - Allows COM to unload DLL
STDAPI DllCanUnloadNow(void)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
return AfxDllCanUnloadNow();
}
// DllRegisterServer - Adds entries to the system registry
STDAPI DllRegisterServer(void)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
if (!AfxOleRegisterTypeLib(AfxGetInstanceHandle(), _tlid))
return SELFREG_E_TYPELIB;
if (!COleObjectFactory::UpdateRegistryAll())
return SELFREG_E_CLASS;
return S_OK;
}
// DllUnregisterServer - Removes entries from the system registry
STDAPI DllUnregisterServer(void)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
if (!AfxOleUnregisterTypeLib(_tlid, _wVerMajor, _wVerMinor))
return SELFREG_E_TYPELIB;
if (!COleObjectFactory::UpdateRegistryAll(FALSE))
return SELFREG_E_CLASS;
return S_OK;
}
The PromptDlg.cpp file contains code for the CPromptDlg class, but that class is a standard class derived from CDialog. The file PromptDlg.h contains the CPromptDlg class header.The CEx23bAuto class—the Automation component class initially generated by the Add Class Wizard (with the Createable By Type ID option)—is more interesting. This class is exposed to COM under the program ID Ex23b.Ex23bAuto. The following listing shows the header file Ex23bAuto.h :
Ex23bAuto.h
#pragma once
// CEx23bAuto command target
class CEx23bAuto : public CCmdTarget
{
DECLARE_DYNCREATE(CEx23bAuto)
public:
CEx23bAuto();
virtual ~CEx23bAuto();
virtual void OnFinalRelease();
protected:
DECLARE_MESSAGE_MAP()
DECLARE_OLECREATE(CEx23bAuto)
DECLARE_DISPATCH_MAP()
DECLARE_INTERFACE_MAP()
void OnLongDataChanged(void);
LONG m_lData;
enum
{
dispidDisplayDialog = 3L,
dispidTextData = 2,
dispidLongData = 1
};
void OnTextDataChanged(void);
VARIANT m_vaTextData;
VARIANT_BOOL DisplayDialog(void);
};
The following listing shows the implementation file Ex23bAuto.cpp :
Ex23bAuto.cpp
// Ex23bAuto.cpp : implementation file
//
#include "stdafx.h"
#include "Ex23b.h"
#include "Ex23bAuto.h"
#include "Promptdlg.h"
// CEx23bAuto
IMPLEMENT_DYNCREATE(CEx23bAuto, CCmdTarget)
CEx23bAuto::CEx23bAuto()
{
EnableAutomation();
// To keep the application running as long as an OLE automation
// object is active, the constructor calls AfxOleLockApp.
::VariantInit(&m_vaTextData); // necessary initialization
m_lData = 0;
AfxOleLockApp();
}
CEx23bAuto::~CEx23bAuto()
{
// To terminate the application when all objects created with
// with OLE automation, the destructor calls AfxOleUnlockApp.
AfxOleUnlockApp();
}
void CEx23bAuto::OnFinalRelease()
{
// When the last reference for an automation object is released
// OnFinalRelease is called. The base class will automatically
// delete the object. Add additional cleanup required for your
// object before calling the base class.
CCmdTarget::OnFinalRelease();
}
BEGIN_MESSAGE_MAP(CEx23bAuto, CCmdTarget)
END_MESSAGE_MAP()
BEGIN_DISPATCH_MAP(CEx23bAuto, CCmdTarget)
DISP_PROPERTY_NOTIFY_ID(CEx23bAuto, "LongData", dispidLongData, m_lData,
OnLongDataChanged, VT_I4)
DISP_PROPERTY_NOTIFY_ID(CEx23bAuto, "TextData", dispidTextData,
m_vaTextData, OnTextDataChanged, VT_VARIANT)
DISP_FUNCTION_ID(CEx23bAuto, "DisplayDialog", dispidDisplayDialog,
DisplayDialog, VT_BOOL, VTS_NONE)
END_DISPATCH_MAP()
// Note: we add support for IID_IEx23bAuto to support typesafe binding
// from VBA. This IID must match the GUID that is attached to the
// dispinterface in the .IDL file.
// {125FECB2-734D-49FD-95C7-FE44B77FDE2C}
static const IID IID_IEx23bAuto =
{ 0x125FECB2, 0x734D, 0x49FD, { 0x95, 0xC7, 0xFE, 0x44, 0xB7,
0x7F, 0xDE, 0x2C } };
BEGIN_INTERFACE_MAP(CEx23bAuto, CCmdTarget)
INTERFACE_PART(CEx23bAuto, IID_IEx23bAuto, Dispatch)
END_INTERFACE_MAP()
// {BAF3D9ED-4518-43CA-B017-2EBA332CB618}
IMPLEMENT_OLECREATE_FLAGS(CEx23bAuto, "Ex23b.Ex23bAuto",
afxRegApartmentThreading, 0xbaf3d9ed, 0x4518, 0x43ca,
0xb0, 0x17, 0x2e, 0xba, 0x33, 0x2c, 0xb6, 0x18)
// CEx23bAuto message handlers
void CEx23bAuto::OnLongDataChanged(void)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
TRACE("CEx23bAuto::OnLongDataChanged\n");
}
void CEx23bAuto::OnTextDataChanged(void)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
TRACE("CEx23bAuto::OnTextDataChanged\n");
}
VARIANT_BOOL CEx23bAuto::DisplayDialog(void)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
VARIANT_BOOL bRet;
TRACE("Entering CEx23bAuto::DisplayDialog %p\n", this);
bRet = TRUE;
AfxLockTempMaps(); // See MFC Tech Note #3
CWnd* pTopWnd = CWnd::FromHandle(::GetTopWindow(NULL));
try {
CPromptDlg dlg /*(pTopWnd)*/;
if (m_vaTextData.vt == VT_BSTR){
dlg.m_strData = m_vaTextData.bstrVal; // converts
// double-byte
// character to
// single-byte
// character
}
dlg.m_lData = m_lData;
if (dlg.DoModal() == IDOK) {
m_vaTextData = COleVariant(dlg.m_strData).Detach();
m_lData = dlg.m_lData;
bRet = TRUE;
}
else {
bRet = FALSE;
}
}
catch (CException* pe) {
TRACE("Exception: failure to display dialog\n");
bRet = FALSE;
pe->Delete();
}
AfxUnlockTempMaps();
return bRet;
}
The two properties, LongData and TextData, are represented by the class data members m_lData and m_vaTextData, which are both initialized in the constructor. When the LongData property was added in the Add Property Wizard, a notification function, OnLongDataChanged, was specified. This function is called whenever the controller changes the property value. Notification functions apply only to properties that are represented by data members. Don't confuse this notification with the notifications that ActiveX controls give their container when a bound property changes.
The DisplayDialog member function, which is the DisplayDialog method, is ordinary except that the AfxLockTempMaps and AfxUnlockTempMaps functions are necessary for cleaning up temporary object pointers that would normally be deleted in an EXE program's idle loop.What about the Excel VBA code? Here are the three macros and the global declarations:
Dim Dllcomp As Object
Private Declare Sub CoFreeUnusedLibraries Lib "OLE32" ()
Sub LoadDllComp()
Set Dllcomp = CreateObject("Ex23b.Ex23bAuto")
Range("C3").Select
Dllcomp.LongData = Selection.Value
Range("D3").Select
Dllcomp.TextData = Selection.Value
End Sub
Sub RefreshDllComp() 'Gather Data button
Range("C3").Select
Dllcomp.LongData = Selection.Value
Range("D3").Select
Dllcomp.TextData = Selection.Value
Dllcomp.DisplayDialog
Range("C3").Select
Selection.Value = Dllcomp.LongData
Range("D3").Select
Selection.Value = Dllcomp.TextData
End Sub
Sub UnloadDllComp()
Set Dllcomp = Nothing
Call CoFreeUnusedLibraries
End Sub
The first line in LoadDllComp creates a component object as identified by the registered name Ex23b.Ex23bAuto. The RefreshDllComp macro accesses the component object's LongData and TextData properties. The first time you run LoadDllComp, it loads the DLL and constructs an Ex23b.Auto object. The second time you run it, something curious happens: A second object is constructed, and the original object is destroyed. If you run LoadDllComp from another copy of the workbook, you get two separate Ex23b.Auto objects. Of course, there's only one mapping of Ex23b.dll in memory at any time unless you're running more than one Excel process.
Look closely at the UnloadDllComp macro. When the Set Dllcomp = Nothing statement is executed, the DLL is disconnected, but it's not unmapped from Excel's address space, which means the component's ExitInstance function is not called. The CoFreeUnusedLibraries function calls the exported DllCanUnloadNow function for each component DLL and, if that function returns TRUE, CoFreeUnusedLibraries frees the DLL. MFC programs call CoFreeUnusedLibraries in the idle loop (after a one-minute delay), but Excel doesn't. That's why UnloadDllComp must call CoFreeUnusedLibraries after disconnecting the component.
The Ex23c Example: An SDI Automation Component EXE with User Interface
This Automation component example illustrates the use of a document component class in an SDI application in which a new process is started for each object. This component program demonstrates an indexed property plus a method that constructs a new COM object.The first Automation component example, Ex23a, doesn't have a user interface. The global class factory constructs a CBank object that does the component's work. But what if you want your EXE component to have a window? If you've bought into the MFC document-view architecture, you'll want the document, view, and frame, with all the benefits they provide.Suppose you create a regular MFC application and then add a COM-creatable class such as CBank. How do you attach the CBank object to the document and view? From a CBank class member function, you could navigate through the application object and main frame to the current document or view, but you'd have a tough time in an MDI application if you encountered several component objects and several documents. There's a better way: You make the document class the creatable class, and you have the full support of the MFC Application Wizard for this task. This is true for both MDI and SDI applications.The MDI Autoclik example demonstrates how COM triggers the construction of new document, view, and child frame objects each time an Automation client creates a new component object. Because the Ex23c example is an SDI program, Windows starts a new process each time the client creates an object. Immediately after the program starts, COM, with the help of the MFC application framework, constructs not only the Automation-aware document but also the view and the main frame window.Now is a good time to experiment with the Ex23c application, which was first generated by the MFC DLL Wizard with the Automation option selected. It's a Windows-based alarm clock program that's designed to be manipulated from an Automation client such as Excel. Ex23c has the following properties and methods:
Name | Description |
---|---|
Time | DATE property that holds a COM DATE (m_vaTime) |
Figure | Indexed VARIANT property for the four figures on the clock face (m_strFigure[]) |
RefreshWin | Method that invalidates the view window and brings the main frame window to the top (Refresh) |
ShowWin | Method that displays the application's main window (ShowWin) |
CreateAlarm | Method that creates a CAlarm object and returns its IDispatch pointer (CreateAlarm) |
Here are the steps for building and running Ex23c from the companion CD:
In Visual Studio .NET, open the solution \vcppnet\Ex23c\ Ex23c.exe file in the project's Debug subdirectory.
Run the program once to register it. The program is designed to be executed either as a standalone application or as an Automation component. When you run it from Windows or from Visual Studio .NET, it updates the Registry and displays the face of a clock with the characters XII, III, VI, and IX at the 12, 3, 6, and 9 o'clock positions. Exit the program.
Load the Excel workbook file \vcppnet\Ex23c\ Ex23c.xls . The worksheet should look like the one shown here:
Click the Load Clock button, and then double-click the Set Alarm button. (There might be a long delay after you click the Load Clock button, depending on your system.) The clock should appear as shown here, with the letter A indicating the alarm setting:
If you've started the component program from the debugger, you can watch the Debug window to see when InitInstance is called and when the document object is constructed.If you're wondering why there's no menu, it's because of the following statement in the CMainFrame::PreCreateWindow function:cs.hMenu = NULL;
Close the Clock program and then click the Unload Clock button. Or you can just click the Unload Clock button. The clock will go away.
The MFC Application Wizard did most of the work of setting up the document as an Automation component. In the derived application class CEx23cApp, it generated a data member for the component, as shown here:
public:
COleTemplateServer m_server;
The MFC COleTemplateServer class is derived from COleObjectFactory. It is designed to create a COM document object when a client calls IClassFactory::CreateInstance. The class ID comes from the global clsid variable defined in Ex23c.cpp . The human-readable program ID (Ex23c.Document) comes from the IDR_MAINFRAME string resource.In the InitInstance function (in Ex23c.cpp ), the MFC Application Wizard generated the following code, which connects the component object (the document) to the application's document template:
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CEx23cDoc),
RUNTIME_CLASS(CMainFrame), // main SDI frame window
RUNTIME_CLASS(CEx23cView));
AddDocTemplate(pDocTemplate);
m_server.ConnectTemplate(clsid, pDocTemplate, TRUE);
Now all the plumbing is in place for COM and the framework to construct the document, together with the view and frame. When the objects are constructed, however, the main window is not made visible. That's your job. You must write a method that shows the window.The following UpdateRegistry call from the InitInstance function updates the Windows Registry with the contents of the project's IDR_MAINFRAME string resource:
m_server.UpdateRegistry(OAT_DISPATCH_OBJECT);
The following dispatch map in the Ex23cDoc.cpp file shows the properties and methods of the CEx23cDoc class. Note that the Figure property is an indexed property that the Add Property Wizard can generate if you specify a parameter. Later, you'll see the code you have to write for the GetFigure and SetFigure functions.
BEGIN_DISPATCH_MAP(CEx23cDoc, CDocument)
DISP_PROPERTY_NOTIFY_ID(CEx23cDoc, "Time",
dispidTime, m_time, OnTimeChanged, VT_DATE)
DISP_FUNCTION_ID(CEx23cDoc, "ShowWin",
dispidShowWin, ShowWin, VT_EMPTY, VTS_NONE)
DISP_FUNCTION_ID(CEx23cDoc, "CreateAlarm",
dispidCreateAlarm, CreateAlarm, VT_DISPATCH, VTS_DATE)
DISP_FUNCTION_ID(CEx23cDoc, "RefreshWin",
dispidRefreshWin, RefreshWin, VT_EMPTY, VTS_NONE)
DISP_PROPERTY_PARAM_ID(CEx23cDoc, "Figure",
dispidFigure, GetFigure, SetFigure, VT_VARIANT, VTS_I2)
END_DISPATCH_MAP()
The ShowWin and RefreshWin member functions aren't very interesting, but the CreateAlarm method is worth a close look. Here's the corresponding CreateAlarm member function:
IDispatch* CEx23cDoc::CreateAlarm(DATE time)
{
AFX_MANAGE_STATE(AfxGetAppModuleState());
TRACE("Entering CEx23cDoc::CreateAlarm, time = %f\n", time);
// OLE deletes any prior CAlarm object
m_pAlarm = new CAlarm(time);
return m_pAlarm->GetIDispatch(FALSE); // no AddRef here
}
We've chosen to have the component create an alarm object when a controller calls CreateAlarm. CAlarm is an Automation component class that we've generated with the Add Class Wizard. It is not COM-creatable, which means there's no IMPLEMENT_OLECREATE macro and no class factory. The CreateAlarm function constructs a CAlarm object and returns an IDispatch pointer. (The FALSE parameter for CCmdTarget::GetIDispatch means that the reference count is not incremented; the CAlarm object already has a reference count of 1 when it is constructed.)The CAlarm class is declared in Alarm.h as follows:
#pragma once
// CAlarm command target
class CAlarm : public CCmdTarget
{
DECLARE_DYNAMIC(CAlarm)
public:
CAlarm(DATE time);
virtual ~CAlarm();
virtual void OnFinalRelease();
DATE m_time;
protected:
DECLARE_MESSAGE_MAP()
DECLARE_DISPATCH_MAP()
DECLARE_INTERFACE_MAP()
void OnTimeChanged(void);
enum
{
dispidTime = 1
};
};
Notice the absence of the DECLARE_DYNCREATE macro. Alarm.cpp contains a dispatch map, as follows:
BEGIN_DISPATCH_MAP(CAlarm, CCmdTarget)
DISP_PROPERTY_NOTIFY_ID(CAlarm, "Time",
dispidTime, m_time, OnTimeChanged, VT_DATE)
END_DISPATCH_MAP()
Why do we have a CAlarm class? We could have added an AlarmTime property in the CEx23cDoc class instead, but then we would have needed another property or method to turn the alarm on and off. By using the CAlarm class, what we're really doing is setting ourselves up to support multiple alarms—a collection of alarms.
To implement an Automation collection, we can write another class, CAlarms, that contains the methods Add, Remove, and Item. Add and Remove are self-explanatory; Item returns an IDispatch pointer for a collection element identified by an index, numeric, or some other key. We can also implement a read-only Count property that returns the number of elements. The document class (which owns the collection) will have an Alarms method with an optional VARIANT parameter. If the parameter is omitted, the method will return the IDispatch pointer for the collection. If the parameter specifies an index, the method will return an IDispatch pointer for the selected alarm.
Note | If we want our collection to support the VBA "For Each" syntax, we'll have some more work to do. We'll have to add an IEnum VARIANT interface to the CAlarms class to enumerate the collection of variants and implement the Next member function of this interface to step through the collection. Then we'll have to add a CAlarms method named _NewEnum that returns an IEnumVARIANT interface pointer. If we want the collection to be general, we must allow separate enumerator objects (with an IEnum VARIANT interface) and then implement the other IEnumVARIANT functions—Skip, Reset, and Clone. |
The Figure property is an indexed property, which makes it interesting. The Figure property represents the four figures on the clock face—XII, III, VI, and IX. It's a CString array, so we can use Roman numerals. Here's the declaration in Ex23cDoc.h :
public:
CString m_strFigure[4];
And here are the GetFigure and SetFigure functions in Ex23cDoc.cpp :
VARIANT CEx23cDoc::GetFigure(SHORT n)
{
AFX_MANAGE_STATE(AfxGetAppModuleState());
TRACE("Entering CEx23cDoc::GetFigure -- n = %d m_strFigure[n] = %s\n",
n, m_strFigure[n]);
return COleVariant(m_strFigure[n]).Detach();
}
void CEx23cDoc::SetFigure(SHORT n, VARIANT FAR& newVal)
{
AFX_MANAGE_STATE(AfxGetAppModuleState());
TRACE("Entering CEx23cDoc::SetFigure -- n = %d, vt = %d\n", n,
newVal.vt);
COleVariant vaTemp;
vaTemp.ChangeType(VT_BSTR, (COleVariant*) &newVal);
m_strFigure[n] = vaTemp.bstrVal; // converts double-to-single
SetModifiedFlag();
}
These functions tie back to the DISP_PROPERTY_PARAM macro in the CEx23cDoc dispatch map. The first parameter is the index number, specified as a short integer by the last macro parameter. Property indexes don't have to be integers, and the index can have several components (row and column numbers, for example). The ChangeType call in SetFigure is necessary because the controller might otherwise pass numbers instead of strings.You've just seen collection properties and indexed properties. What's the difference? A controller can't add or delete elements of an indexed property, but it can add elements to a collection and it can delete elements from a collection.What draws the clock face? As you might expect, it's the OnDraw member function of the view class. This function uses GetDocument to get a pointer to the document object, and then it accesses the document's property data members and method member functions.The Excel macro code is shown here:
Dim Clock As Object
Dim Alarm As Object
Sub LoadClock()
Set Clock = CreateObject("Ex23c.Document")
Range("A3").Select
n = 0
Do Until n = 4
Clock.figure(n) = Selection.Value
Selection.Offset(0, 1).Range("A1").Select
n = n + 1
Loop
RefreshClock
Clock.ShowWin
End Sub
Sub RefreshClock()
Clock.Time = Now()
Clock.RefreshWin
End Sub
Sub CreateAlarm()
Range("E3").Select
Set Alarm = Clock.CreateAlarm(Selection.Value)
RefreshClock
End Sub
Sub UnloadClock()
Set Clock = Nothing
End Sub
Notice the Set Alarm statement in the CreateAlarm macro. It calls the CreateAlarm method to return an IDispatch pointer, which is stored in an object variable. If the macro is run a second time, a new alarm is created, but the original one is destroyed because its reference count goes to 0.
Warning | You've seen a modal dialog box in a DLL (Ex23b), and you've seen a main frame window in an EXE (Ex23c). Be careful with modal dialog boxes in EXEs. It's fine to have an About dialog box that's invoked directly by the component program, but it isn't a good idea to invoke a modal dialog box in an out-of-process component method function. The problem is that once the modal dialog box is on the screen, the user can switch back to the client program. MFC clients handle this situation with a special "Server Busy" message box, which appears right away. Excel does something similar, but it waits 30 seconds, and this can confuse the user. |
The Ex23d Example: An Automation Client
So far, you've seen C++ Automation component programs. Now you'll see a C++ Automation client program that runs all the previous components and also controls Excel. The Ex23d program was originally generated by the MFC Application Wizard, but without any COM options. It was easier to add the COM code than it would have been to rip out the component-specific code. If you use the MFC Application Wizard to build such an Automation controller, add the following line at the end of StdAfx.h :
#include <afxdisp.h>
Then add this call at the beginning of the application's InitInstance function:
AfxOleInit();
To prepare Ex23d, open the \vcppnet\Ex23d\ Ex23d.sln solution and do the build. Run the application, and you'll see a standard SDI application with a menu structure similar to that shown in Figure 23-5.
Figure 23-5: A sample menu structure for a standard SDI application.
If you've built and registered all the components, you can test them from Ex23d. Notice that the DLL doesn't have to be copied to the \Winnt\System32 directory because Windows finds it through the Registry. For some components, you'll have to watch the Debug window to verify that the test results are correct. The program is reasonably modular. Menu commands and update command user interface events are mapped to the view class. Each component object has its own C++ controller class and an embedded data member in Ex23dView.h . We'll look at each part separately after we delve into type libraries.
Type Libraries and IDL Files
I've told you that type libraries aren't necessary for the MFC IDispatch implementation, but Visual C++ .NET has been quietly generating and updating type libraries for all your components. What good are these type libraries? VBA can use a type library to browse your component's methods and properties, and it can use the type library for improved access to properties and methods—a process called early binding (described later in this chapter). But we're building a C++ client program here, not a VBA program. It so happens that the Add Class Wizard can read a component's type library and use the information to generate C++ code for the client to use to "drive" an Automation component.
Note | The MFC Application Wizard initializes a project's Interface Definition Language (IDL) file when you first create it. The Add Property Wizard and the Add Method Wizard edit this file each time you generate a new Automation component class or add properties and methods to an existing class. |
When you added properties and methods to your component classes, the Add Method Wizard and the Add Property Wizard updated the project's IDL file. This file is a text file that describes the component in IDL. (Your GUID will be different if you used the MFC Application Wizard to generate this project.) Here's the IDL file for the bank component:
// Ex23a.idl : type library source for Ex23a.exe
// This file will be processed by the MIDL compiler to produce the
// type library (Ex23a.tlb).
#include "olectl.h"
[ uuid(60BCA7D2-14D1-4832-A278-50670CD9975E), version(1.0) ]
library Ex23a
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
// Primary dispatch interface for CEx23aDoc
[ uuid(1F013122-EA3D-414F-B58F-5A31A64EA5D5) ]
dispinterface IEx23a
{
properties:
methods:
};
// Class information for CEx23aDoc
[ uuid(5EE5C98C-5CCF-46F4-9E95-17BC06237D8B) ]
coclass Ex23a
{
[default] dispinterface IEx23a;
};
// Primary dispatch interface for Bank
[ uuid(8BAD2B0C-62CC-4952-811C-C736DA06858E) ]
dispinterface IBank
{
properties:
[id(3), helpstring("property Balance")] DOUBLE Balance;
methods:
[id(1), helpstring("method Withdrawal")]
DOUBLE Withdrawal(DOUBLE dAmount);
[id(2), helpstring("method Deposit")]
void Deposit(DOUBLE dAmount);
};
// Class information for Bank
[ uuid(3EC6FA59-9F9F-4619-9F62-BA5FE37176F0) ]
coclass Bank
{
[default] dispinterface IBank;
};
};
The IDL file has a unique GUID type library identifier, 60BCA7D2-14D1-4832-A278-50670CD9975E, that completely describes the bank component's properties and methods under a dispinterface named IBank. In addition, it specifies the dispinterface GUID, 8BAD2B0C-62CC-4952-811C-C736DA06858E, which is the same GUID that's in the interface map of the CBank class listed earlier. You'll see the significance of this GUID later in this chapter. The CLSID, 3EC6FA59-9F9F-4619-9F62-BA5FE37176F0, is what a VBA browser can actually use to load your component.Anyway, when you build your component project, Visual Studio .NET invokes the MIDL utility, which reads the IDL file and generates a binary TLB file in your project's debug or release subdirectory. By default, the type information is also included as part of the binary. When you develop a C++ client program, you can ask the Add Class Wizard to generate a driver class from the component project's TLB file.To actually do this, you choose Add Class from the Project menu and select the MFC Class From TypeLib template. You navigate to the component project's TLB file, and then the Add Class Wizard shows you a dialog box similar to the one shown here:
IBank is the dispinterface specified in the IDL file. You can keep this name for the class if you want, and you can specify the H filename. If a type library contains several interfaces, you can make multiple selections. You'll see the generated controller classes in the sections that follow.
The Controller Class for Ex23a.exe
The Add Class From Typelib Wizard generated the IBank class (derived from COleDispatchDriver) shown in the following listing. Look closely at the member function implementations. Note the first parameters of the GetProperty, SetProperty, and InvokeHelper function calls. These are hard-coded DISPIDs for the component's properties and methods, as determined by the component's dispatch map sequence.
BankDriver.h
class CBank : public COleDispatchDriver
{
public:
CBank(){} // Calls COleDispatchDriver default constructor
CBank(LPDISPATCH pDispatch) : COleDispatchDriver(pDispatch) {}
CBank(const CBank& dispatchSrc) : COleDispatchDriver(dispatchSrc) {}
// Attributes
public:
// Operations
public:
// IBank methods
public:
double Withdrawal(double dAmount)
{
double result;
static BYTE parms[] = VTS_R8 ;
InvokeHelper(0x1, DISPATCH_METHOD, VT_R8, (void*)&result,
parms, dAmount);
return result;
}
void Deposit(double dAmount)
{
static BYTE parms[] = VTS_R8 ;
InvokeHelper(0x2, DISPATCH_METHOD, VT_EMPTY, NULL,
parms, dAmount);
}
// IBank properties
public:
double GetBalance()
{
double result;
GetProperty(0x3, VT_R8, (void*)&result);
return result;
}
void SetBalance(double propVal)
{
SetProperty(0x3, VT_R8, propVal);
}
};
The CEx23dView class has a data member m_bank of class IBank. The CEx23dView member functions for the Ex23a.Bank component are listed below. They are hooked up to options on the Ex23d main menu. Of particular interest is the OnBankoleLoad function. The COleDispatchDriver::CreateDispatch function loads the component program (by calling CoGetClassObject and IClassFactory::CreateInstance) and then calls QueryInterface to get an IDispatch pointer, which it stores in the object's m_lpDispatch data member. The COleDispatchDriver::ReleaseDispatch function, called in OnBankoleUnload, calls Release on the pointer.
void CEx23dView::OnBankoleLoad()
{
if(!m_bank.CreateDispatch("Ex23a.Bank")) {
AfxMessageBox("Ex23a.Bank component not found");
return;
}
}
void CEx23dView::OnUpdateBankoleLoad(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_bank.m_lpDispatch == NULL);
}
void CEx23dView::OnBankoleTest()
{
m_bank.Deposit(20.0);
m_bank.Withdrawal(15.0);
TRACE("new balance = %f\n", m_bank.GetBalance());
}
void CEx23dView::OnUpdateBankoleTest(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_bank.m_lpDispatch != NULL);
}
void CEx23dView::OnBankoleUnload()
{
m_bank.ReleaseDispatch();
}
void CEx23dView::OnUpdateBankoleUnload(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_bank.m_lpDispatch != NULL);
}
The Controller Class for Ex23b.dll
The following listing shows the class header file generated by the Add Class From Typelib Wizard:AutoDriver.h
// Machine generated IDispatch wrapper class(es) created with
// Add Class from Typelib Wizard
// CEx23bAuto wrapper class
class CEx23bAuto : public COleDispatchDriver
{
public:
CEx23bAuto(){} // Calls COleDispatchDriver default constructor
CEx23bAuto(LPDISPATCH pDispatch) : COleDispatchDriver(pDispatch) {}
CEx23bAuto(const CEx23bAuto& dispatchSrc) :
COleDispatchDriver(dispatchSrc) {}
// Attributes
public:
// Operations
public:
// IEx23bAuto methods
public:
BOOL DisplayDialog()
{
BOOL result;
InvokeHelper(0x3, DISPATCH_METHOD, VT_BOOL,
(void*)&result, NULL);
return result;
}
// IEx23bAuto properties
public:
long GetLongData()
{
long result;
GetProperty(0x1, VT_I4, (void*)&result);
return result;
}
void SetLongData(long propVal)
{
SetProperty(0x1, VT_I4, propVal);
}
VARIANT GetTextData()
{
VARIANT result;
GetProperty(0x2, VT_VARIANT, (void*)&result);
return result;
}
void SetTextData(const VARIANT& propVal)
{
SetProperty(0x2, VT_VARIANT, &propVal);
}
};
Notice that each property requires separate Get and Set functions in the client class, even though a data member in the component represents the property.The view class header has a data member m_auto of class CEx23bAuto. Here are some DLL-related command handler member functions from Ex23dView.cpp :
void CEx23dView::OnDlloleGetdata()
{
m_auto.DisplayDialog();
COleVariant vaData = m_auto.GetTextData();
ASSERT(vaData.vt == VT_BSTR);
CString strTextData(vaData.bstrVal);
long lData = m_auto.GetLongData();
TRACE("CEx23dView::OnDlloleGetdata -- long = %ld, text = %s\n",
lData, strTextData);
}
void CEx23dView::OnUpdateDlloleGetdata(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_auto.m_lpDispatch != NULL);
}
void CEx23dView::OnDlloleLoad()
{
if(!m_auto.CreateDispatch("Ex23b.Ex23bAuto")) {
AfxMessageBox("Ex23b.Ex23bAuto component not found");
return;
}
COleVariant va("test");
m_auto.SetTextData(va); // testing
m_auto.SetLongData(79); // testing
// verify dispatch interface
// {125FECB2-734D-49FD-95C7-FE44B77FDE2C}
static const IID IID_IEx23bAuto =
{ 0x125FECB2, 0x734D, 0x49FD, { 0x95, 0xC7, 0xFE,
0x44, 0xB7, 0x7F, 0xDE, 0x2C } };
LPDISPATCH p;
HRESULT hr = m_auto.m_lpDispatch->QueryInterface(IID_IEx23bAuto,
(void**) &p);
TRACE("OnDlloleLoad -- QueryInterface result = %x\n", hr);
p->Release();
}
void CEx23dView::OnUpdateDlloleLoad(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_auto.m_lpDispatch == NULL);
}
void CEx23dView::OnDlloleUnload()
{
m_auto.ReleaseDispatch();
}
void CEx23dView::OnUpdateDlloleUnload(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_auto.m_lpDispatch != NULL);
}
The Controller Class for Ex23c.exe
The following code shows the headers for the CEx23c and CAlarm classes, which drive the Ex23c Automation component:ClockDriver.h
// Machine generated IDispatch wrapper class(es) created with
// Add Class from Typelib Wizard
// CEx23c wrapper class
class CEx23c : public COleDispatchDriver
{
public:
CEx23c(){} // Calls COleDispatchDriver default constructor
CEx23c(LPDISPATCH pDispatch) : COleDispatchDriver(pDispatch) {}
CEx23c(const CEx23c& dispatchSrc) :
COleDispatchDriver(dispatchSrc) {}
// Attributes
public:
// Operations
public:
// IEx23c methods
public:
void ShowWin()
{
InvokeHelper(0x2, DISPATCH_METHOD, VT_EMPTY, NULL, NULL);
}
LPDISPATCH CreateAlarm(DATE Time)
{
LPDISPATCH result;
static BYTE parms[] = VTS_DATE ;
InvokeHelper(0x3, DISPATCH_METHOD, VT_DISPATCH,
(void*)&result, parms, Time);
return result;
}
void RefreshWin()
{
InvokeHelper(0x4, DISPATCH_METHOD, VT_EMPTY, NULL, NULL);
}
VARIANT get_Figure(short n)
{
VARIANT result;
static BYTE parms[] = VTS_I2 ;
InvokeHelper(0x5, DISPATCH_PROPERTYGET, VT_VARIANT,
(void*)&result, parms, n);
return result;
}
void put_Figure(short n, VARIANT newValue)
{
static BYTE parms[] = VTS_I2 VTS_VARIANT ;
InvokeHelper(0x5, DISPATCH_PROPERTYPUT, VT_EMPTY,
NULL, parms, n, &newValue);
}
// IEx23c properties
public:
DATE GetTime()
{
DATE result;
GetProperty(0x1, VT_DATE, (void*)&result);
return result;
}
void SetTime(DATE propVal)
{
SetProperty(0x1, VT_DATE, propVal);
}
};
CAlarm.h
class CAlarm : public COleDispatchDriver
{
public:
CAlarm(){} // Calls COleDispatchDriver default constructor
CAlarm(LPDISPATCH pDispatch) : COleDispatchDriver(pDispatch) {}
CAlarm(const CAlarm& dispatchSrc) :
COleDispatchDriver(dispatchSrc) {}
// Attributes
public:
// Operations
public:
// IAlarm methods
public:
// IAlarm properties
public:
DATE GetTime()
{
DATE result;
GetProperty(0x1, VT_DATE, (void*)&result);
return result;
}
void SetTime(DATE propVal)
{
SetProperty(0x1, VT_DATE, propVal);
}
};
Of particular interest is the CEx23c::CreateAlarm member function in ClockDriver.h . This function can be called only after the clock object (document) has been constructed. It causes the Ex23c component to construct an alarm object and return an IDispatch pointer with a reference count of 1. The COleDispatchDriver::AttachDispatch function connects that pointer to the client's m_alarm object, but if that object already has a dispatch pointer, the old pointer is released. That's why, if you watch the Debug window, you'll see that the old Ex23c instance exits immediately after you ask for a new instance. You'll have to test this behavior with the Excel driver because Ex23d disables the Load menu command when the clock is running.The view class has the data members m_clock and m_alarm. Here are the view class command handlers:
void CEx23dView::OnClockoleCreatealarm()
{
CAlarmDialog dlg;
if (dlg.DoModal() == IDOK) {
COleDateTime dt(2002, 12, 23, dlg.m_nHours, dlg.m_nMinutes,
dlg.m_nSeconds);
LPDISPATCH pAlarm = m_clock.CreateAlarm(dt);
m_alarm.AttachDispatch(pAlarm); // releases prior object!
m_clock.RefreshWin();
}
}
void CEx23dView::OnUpdateClockoleCreatealarm(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_clock.m_lpDispatch != NULL);
}
void CEx23dView::OnClockoleLoad()
{
if(!m_clock.CreateDispatch("Ex23c.Document")) {
AfxMessageBox("Ex23c.Document component not found");
return;
}
m_clock.put_Figure(0, COleVariant("XII"));
m_clock.put_Figure(1, COleVariant("III"));
m_clock.put_Figure(2, COleVariant("VI"));
m_clock.put_Figure(3, COleVariant("IX"));
OnClockoleRefreshtime();
m_clock.ShowWin();
}
void CEx23dView::OnUpdateClockoleLoad(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_clock.m_lpDispatch == NULL);
}
void CEx23dView::OnClockoleRefreshtime()
{
COleDateTime now = COleDateTime::GetCurrentTime();
m_clock.SetTime(now);
m_clock.RefreshWin();
}
void CEx23dView::OnUpdateClockoleRefreshtime(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_clock.m_lpDispatch != NULL);
}
void CEx23dView::OnClockoleUnload()
{
m_clock.ReleaseDispatch();
}
void CEx23dView::OnUpdateClockoleUnload(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_clock.m_lpDispatch != NULL);
}
Controlling Excel
The Ex23d program contains code that loads Excel, creates a workbook, and reads from and writes to cells from the active worksheet. Controlling Excel is exactly like controlling an MFC Automation component, but you need to know about a few Excel peculiarities.If you study Excel VBA, you'll notice that you can use more than 100 "objects" in your programs. All of these objects are accessible through Automation, but if you write an MFC Automation client program, you'll need to know about the objects' properties and methods. Ideally, you want a C++ class for each object, with member functions coded to the proper dispatch IDs.Excel has its own type library that is registered in the Registry. The Add Class From Typelib Wizard can read the type library after looking it up in the Registry. The wizard can create C++ driver classes for individual Excel objects. It makes sense to select the objects you need and then combine the classes into single files, as shown in Figure 23-6.
Figure 23-6: The Add Class From Typelib Wizard can create C++?? classes for the Excel objects listed in Excel's type library.
You might need to edit the generated code to suit your needs. Let's look at an example. If you use the Add Class From Typelib Wizard to generate a driver class for the Worksheet object, you get a get_Range member function, as shown here:
LPDISPATCH get_Range(VARIANT Cell1, VARIANT Cell2)
{
LPDISPATCH result;
static BYTE parms[] = VTS_VARIANT VTS_VARIANT ;
InvokeHelper(0xc5, DISPATCH_PROPERTYGET, VT_DISPATCH,
(void*)&result, parms, &Cell1, &Cell2);
return result;
}
You know (from the Excel VBA documentation) that you can call the method with either a single cell (one parameter) or a rectangular area specified by two cells (two parameters). Remember that you can omit optional parameters in a call to InvokeHelper. Now it makes sense to add a second overloaded get_Range function with a single cell parameter, like this:
LPDISPATCH get_Range( VARIANT Cell1) // added
{
LPDISPATCH result;
static BYTE parms[] = VTS_VARIANT;
InvokeHelper(0xc5, DISPATCH_PROPERTYGET, VT_DISPATCH,
(void*)&result, parms, &Cell1);
return result;
}
How do you know which functions to fix up? They're the functions you decide to use in your program. You'll have to read the Excel VBA reference manual to figure out the required parameters and return values. Perhaps someday soon someone will write a set of C++ Excel controller classes.
The Ex23d program uses the Excel objects and contains the corresponding classes shown in the following table. The code for these objects is contained in the files CApplication.h , CRange.h , CWorksheet.h , CWorksheets.h , and CWorkbooks.h .
Class | View Class Data Member |
---|---|
CApplication | m_app |
CRange | m_range[5] |
CWorksheet | m_worksheet |
CWorkbooks | m_workbooks |
CWorksheets | m_worksheets |
The following view member function, OnExceloleLoad, handles the Excel Comp Load menu command. This function must work if the user already has Excel running on the desktop. The COM GetActiveObject function tries to return an IUnknown pointer for Excel. GetActiveObject requires a class ID, so we must first call CLSIDFromProgID. If GetActiveObject is successful, we call QueryInterface to get an IDispatch pointer and we attach it to the view's m_app controller object of class CApplication. If GetActiveObject is unsuccessful, we call COleDispatchDriver::CreateDispatch, as we did for the other components.
void CEx23dView::OnExceloleLoad()
{ // if Excel is already running, attach to it, otherwise start it
LPDISPATCH pDisp;
LPUNKNOWN pUnk;
CLSID clsid;
TRACE("Entering CEx23dView::OnExcelLoad\n");
BeginWaitCursor();
// Use Excel.Application.9 for Office 2000
// Use Excel.Application.10 for Office XP
::CLSIDFromProgID(L"Excel.Application.10", &clsid); // from registry
if(::GetActiveObject(clsid, NULL, &pUnk) == S_OK) {
VERIFY(pUnk->QueryInterface(IID_IDispatch,
(void**) &pDisp) == S_OK);
m_app.AttachDispatch(pDisp);
pUnk->Release();
TRACE(" attach complete\n");
}
else {
if(!m_app.CreateDispatch("Excel.Application.10")) {
AfxMessageBox("Microsoft Excel program not found");
}
TRACE(" create complete\n");
}
EndWaitCursor();
}
OnExceloleExecute is the command handler for the Execute command on the Excel Comp menu. Its first task is to find the Excel main window and bring it to the top. We must write some Windows code here because a method for this purpose couldn't be found. We must also create a workbook if no workbook is currently open.We have to watch our method return values closely. The Workbooks Add method, for example, returns an IDispatch pointer for a Workbook object and, of course, increments the reference count. If we generated a class for Workbook, we could call COleDispatchDriver::AttachDispatch so that Release would be called when the Workbook object was destroyed. We don't need a Workbook class, so we'll simply release the pointer at the end of the function. If we don't properly clean up our pointers, we might get memory-leak messages from the Debug version of MFC.The rest of the OnExceloleExecute function accesses the cells in the worksheet. It's easy to get and set numbers, dates, strings, and formulas. The C++ code is similar to the VBA code you would write to do the same job:
void CEx23dView::OnExceloleExecute()
{
LPDISPATCH pRange, pWorkbooks;
CWnd* pWnd = CWnd::FindWindow("XLMAIN", NULL);
if (pWnd != NULL) {
TRACE("Excel window found\n");
pWnd->ShowWindow(SW_SHOWNORMAL);
pWnd->UpdateWindow();
pWnd->BringWindowToTop();
}
m_app.put_SheetsInNewWorkbook(1);
VERIFY(pWorkbooks = m_app.get_Workbooks());
m_workbooks.AttachDispatch(pWorkbooks);
LPDISPATCH pWorkbook = NULL;
if (m_workbooks.get_Count() == 0) {
// Add returns a Workbook pointer, but we
// don't have a Workbook class
pWorkbook = m_workbooks.Add(COleVariant((short) 0)); // Save the
// pointer for later release
}
LPDISPATCH pWorksheets = m_app.get_Worksheets();
ASSERT(pWorksheets != NULL);
m_worksheets.AttachDispatch(pWorksheets);
LPDISPATCH pWorksheet = m_worksheets.get_Item(COleVariant((short) 1));
m_worksheet.AttachDispatch(pWorksheet);
m_worksheet.Select(COleVariant((short) 0));
VERIFY(pRange = m_worksheet.get_Range(COleVariant("A1"),
COleVariant("A1")));
m_range[0].AttachDispatch(pRange);
VERIFY(pRange = m_worksheet.get_Range(COleVariant("A2"),
COleVariant("A2")));
m_range[1].AttachDispatch(pRange);
VERIFY(pRange = m_worksheet.get_Range(COleVariant("A3"),
COleVariant("A3")));
m_range[2].AttachDispatch(pRange);
VERIFY(pRange = m_worksheet.get_Range(COleVariant("A3"),
COleVariant("C5")));
m_range[3].AttachDispatch(pRange);
VERIFY(pRange = m_worksheet.get_Range(COleVariant("A6"),
COleVariant("A6")));
m_range[4].AttachDispatch(pRange);
m_range[4].put_Value(COleVariant(COleDateTime(2002, 4, 24,
15, 47, 8)));
// retrieve the stored date and print it as a string
COleVariant vaTimeDate = m_range[4].get_Value();
TRACE("returned date type = %d\n", vaTimeDate.vt);
COleVariant vaTemp;
vaTemp.ChangeType(VT_BSTR, &vaTimeDate);
CString str(vaTemp.bstrVal);
TRACE("date = %s\n", (const char*) str);
m_range[0].put_Value(COleVariant("test string"));
COleVariant vaResult0 = m_range[0].get_Value();
if (vaResult0.vt == VT_BSTR) {
CString str(vaResult0.bstrVal);
TRACE("vaResult0 = %s\n", (const char*) str);
}
m_range[1].put_Value(COleVariant(3.14159));
COleVariant vaResult1 = m_range[1].get_Value();
if (vaResult1.vt == VT_R8) {
TRACE("vaResult1 = %f\n", vaResult1.dblVal);
}
m_range[2].put_Formula(COleVariant("=$A2*2.0"));
COleVariant vaResult2 = m_range[2].get_Value();
if (vaResult2.vt == VT_R8) {
TRACE("vaResult2 = %f\n", vaResult2.dblVal);
}
COleVariant vaResult2a = m_range[2].get_Formula();
if (vaResult2a.vt == VT_BSTR) {
CString str(vaResult2a.bstrVal);
TRACE("vaResult2a = %s\n", (const char*) str);
}
m_range[3].FillRight();
m_range[3].FillDown();
// cleanup
if (pWorkbook != NULL) {
pWorkbook->Release();
}
}
The Ex23e Example: An Automation Client
This program uses the #import directive to generate smart pointers. It behaves just like Ex23d except that it doesn't run Excel. The #import statements are in the StdAfx.h file to minimize the number of times the compiler has to generate the driver classes. Here is the added code:
#include <afxdisp.h>
#import "..\Ex23a\Debug\Ex23a.tlb" rename_namespace("BankDriv")
using namespace BankDriv;
#import "..\Ex23b\Debug\Ex23b.tlb" rename_namespace("Ex23bDriv")
using namespace Ex23bDriv;
#import "..\Ex23c\Debug\Ex23c.tlb" rename_namespace("ClockDriv")
using namespace ClockDriv;
If you have ActiveX controls turned on when you generate the code, the MFC Application Wizard will insert a call to AfxOleInit in your application class InitInstance member function. (Otherwise, you must add it by hand.)The view class header contains embedded smart pointers, as shown here:
IEx23bAutoPtr m_auto;
IBankPtr m_bank;
IEx23cPtr m_clock;
IAlarmPtr m_alarm;
Here's the code for the view class menu command handlers:
void CEx23eView::OnBankoleLoad()
{
if(m_bank.CreateInstance(__uuidof(Bank)) != S_OK) {
AfxMessageBox("Bank component not found");
return;
}
}
void CEx23eView::OnUpdateBankoleLoad(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_bank.GetInterfacePtr() == NULL);
}
void CEx23eView::OnBankoleTest()
{
try {
m_bank->Deposit(20.0);
m_bank->Withdrawal(15.0);
TRACE("new balance = %f\n", m_bank->GetBalance());
} catch(_com_error& e) {
AfxMessageBox(e.ErrorMessage());
}
}
void CEx23eView::OnUpdateBankoleTest(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_bank.GetInterfacePtr() != NULL);
}
void CEx23eView::OnBankoleUnload()
{
m_bank.Release();
}
void CEx23eView::OnUpdateBankoleUnload(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_bank.GetInterfacePtr() != NULL);
}
void CEx23eView::OnClockoleCreatealarm()
{
CAlarmDlg dlg;
try {
if (dlg.DoModal() == IDOK) {
COleDateTime dt(2001, 12, 23, dlg.m_nHours,
dlg.m_nMinutes, dlg.m_nSeconds);
LPDISPATCH pAlarm = m_clock->CreateAlarm(dt);
m_alarm.Attach((IAlarm*) pAlarm); // releases prior object!
m_clock->RefreshWin();
}
} catch(_com_error& e) {
AfxMessageBox(e.ErrorMessage());
}
}
void CEx23eView::OnUpdateClockoleCreatealarm(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_clock.GetInterfacePtr() != NULL);
}
void CEx23eView::OnClockoleLoad()
{
if(m_clock.CreateInstance(__uuidof(CEx23cDoc)) != S_OK) {
AfxMessageBox("Clock component not found");
return;
}
try {
m_clock->PutFigure(0, COleVariant("XII"));
m_clock->PutFigure(1, COleVariant("III"));
m_clock->PutFigure(2, COleVariant("VI"));
m_clock->PutFigure(3, COleVariant("IX"));
OnClockoleRefreshtime();
m_clock->ShowWin();
} catch(_com_error& e) {
AfxMessageBox(e.ErrorMessage());
}
}
void CEx23eView::OnUpdateClockoleLoad(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_clock.GetInterfacePtr() == NULL);
}
void CEx23eView::OnClockoleRefreshtime()
{
COleDateTime now = COleDateTime::GetCurrentTime();
try {
m_clock->PutTime(now);
m_clock->RefreshWin();
} catch(_com_error& e) {
AfxMessageBox(e.ErrorMessage());
}
}
void CEx23eView::OnUpdateClockoleRefreshtime(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_clock.GetInterfacePtr() != NULL);
}
void CEx23eView::OnClockoleUnload()
{
m_clock.Release();
}
void CEx23eView::OnUpdateClockoleUnload(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_clock.GetInterfacePtr() != NULL);
}
void CEx23eView::OnDlloleGetdata()
{
try {
m_auto->DisplayDialog();
COleVariant vaData = m_auto->GetTextData();
ASSERT(vaData.vt == VT_BSTR);
CString strTextData(vaData.bstrVal);
long lData = m_auto->GetLongData();
TRACE("CEx23dView::OnDlloleGetdata—long = %ld, text = %s\n",
lData, strTextData);
} catch(_com_error& e) {
AfxMessageBox(e.ErrorMessage());
}
}
void CEx23eView::OnUpdateDlloleGetdata(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_auto.GetInterfacePtr() != NULL);
}
void CEx23eView::OnDlloleLoad()
{
if(m_auto.CreateInstance(__uuidof(Ex23bAuto)) != S_OK) {
AfxMessageBox("Ex23bAuto component not found");
return;
}
IEx23bAuto* pEx23bAuto = 0;
m_auto.QueryInterface(__uuidof(IEx23bAuto), (void**)&pEx23bAuto);
if(pEx23bAuto) {
pEx23bAuto->PutLongData(42);
pEx23bAuto->Release();
}
}
void CEx23eView::OnUpdateDlloleLoad(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_auto.GetInterfacePtr() == NULL);
}
void CEx23eView::OnDlloleUnload()
{
m_auto.Release();
}
void CEx23eView::OnUpdateDlloleUnload(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_auto.GetInterfacePtr() != NULL);
}
Note the use of the try/catch blocks in the functions that manipulate the components. These blocks are particularly necessary for processing errors that occur when a component program stops running. In the previous example, Ex23d, the MFC COleDispatchDriver class took care of this detail.