Programming Windows with MFC, Second Edition
MFC vastly simplifies the writing of Automation servers, but what about Automation clients? Good news: with a little help from ClassWizard, it's almost as easy to write an Automation client with MFC as it is to write it with Visual Basic.
The key is a class named COleDispatchDriver, which puts a friendly face on IDispatch pointers exported by running Automation servers. The COleDispatchDriver helper functions InvokeHelper, SetProperty, and GetProperty simplify method and property accesses, but interacting with an Automation object using these functions is only slightly better than calling IDispatch::Invoke directly. The real value of COleDispatchDriver lies in creating type-safe classes whose member functions provide easy access to Automation methods and properties. After all, it's easier for a C++ programmer to call a class member function than to call IDispatch::Invoke.
To derive a class from COleDispatchDriver that's tailored to a specific Automation server, click ClassWizard's Add Class button, select From A Type Library, and point ClassWizard to the server's type library. ClassWizard will read the type library and generate the new class. Inside that class you'll find member functions for calling the server's methods and get and set functions for accessing its properties. For example, if the server supports a method named Add and a property named Pi, the ClassWizard-generated class will include a member function named Add and accessor functions named GetPi and SetPi. If the wrapper class were named CAutoMath and the object's ProgID were "Math.Object," the object could be instantiated and programmed using statements like these:
CAutoMath math; math.CreateDispatch (_T ("Math.Object")); int sum = math.Add (2, 2); double pi = math.GetPi (); |
CreateDispatch uses ::CoCreateInstance to create the Automation object. It caches the object's IDispatch pointer in a member variable named m_lpDispatch. Method calls and property accesses performed via CAutoMath member functions are translated into IDispatch calls to the object by InvokeHelper and other COleDispatchDriver functions.
The PieClient Application
Let's close out this chapter with an MFC Automation client. PieClient, a picture of which appears in Figure 20-16 and whose source code appears in Figure 20-17, is a dialog-based application whose main window features edit controls for entering and editing quarterly revenue values. Values entered in the controls are charted by AutoPie. PieClient drives AutoPie via Automation.
Figure 20-16. PieClient acting as an Automation client to AutoPie.
When started, PieClient calls CreateDispatch on a CAutoPie object named m_autoPie to start the Automation server:
BOOL bSuccess = m_autoPie.CreateDispatch (_T ("AutoPie.Application")); |
When its Set button is clicked, PieClient gathers the revenue values from the edit controls and transmits them to the server by writing them to the Chart object's Revenue property:
m_autoChart.SetRevenue (1, GetDlgItemInt (IDC_Q1)); m_autoChart.SetRevenue (2, GetDlgItemInt (IDC_Q2)); m_autoChart.SetRevenue (3, GetDlgItemInt (IDC_Q3)); m_autoChart.SetRevenue (4, GetDlgItemInt (IDC_Q4)); |
It then repaints the pie chart by calling the Window object's Refresh method:
m_autoWindow.Refresh (); |
Conversely, if the Get button is clicked, PieClient reads the property values from the Automation object and displays them in the edit controls.
m_autoChart and m_autoWindow are instances of CAutoChart and CAutoWindow. These classes and others—namely, CAutoPie and CAutoToolbar—are COleDispatchDriver derivatives that ClassWizard created from AutoPie's type library. CAutoPie represents the server's top-level Application object. The remaining classes represent the Chart, Window, and Toolbar subobjects. m_autoPie is initialized by CreateDispatch, but m_autoChart and m_autoWindow must be initialized separately because the corresponding subobjects are automatically created when the server is started. These initializations are performed by passing the IDispatch pointers returned by CAutoPie's GetChart and GetWindow functions to AttachDispatch:
m_autoChart.AttachDispatch (m_autoPie.GetChart ()); m_autoWindow.AttachDispatch (m_autoPie.GetWindow ()); |
Because m_autoPie, m_autoChart, and m_autoWindow are embedded data members, they're automatically destroyed when the dialog object is destroyed. And when a COleDispatchDriver-object is destroyed, the IDispatch pointer that it wraps is released by the class destructor. That's why AutoPie closes when PieClient is closed. When the last pointer to an MFC Automation server's dispinterface is released, the server obediently shuts itself down.
PieClient.h
// PieClient.h : main header file for the PIECLIENT application // #if !defined( AFX_PIECLIENT_H__3B5BA32A_3B72_11D2_AC82_006008A8274D__INCLUDED_) #define AFX_PIECLIENT_H__3B5BA32A_3B72_11D2_AC82_006008A8274D__INCLUDED_ |
Figure 20-17. The PieClient program.
#if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #ifndef __AFXWIN_H__ #error include `stdafx.h' before including this file for PCH #endif #include "resource.h" // main symbols /////////////////////////////////////////////////////////////////////////// // CPieClientApp: // See PieClient.cpp for the implementation of this class // class CPieClientApp : public CWinApp { public: CPieClientApp(); // Overrides // ClassWizard generated virtual function overrides //AFX_VIRTUAL // Implementation //AFX_MSG DECLARE_MESSAGE_MAP() }; /////////////////////////////////////////////////////////////////////////// // // Microsoft Visual C++ will insert additional declarations // immediately before the previous line. #endif // !defined( // AFX_PIECLIENT_H__3B5BA32A_3B72_11D2_AC82_006008A8274D__INCLUDED_) |
PieClient.cpp
// PieClient.cpp : Defines the class behaviors for the application. // #include "stdafx.h" #include "PieClient.h" #include "PieClientDlg.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif /////////////////////////////////////////////////////////////////////////// // CPieClientApp BEGIN_MESSAGE_MAP(CPieClientApp, CWinApp) //AFX_MSG ON_COMMAND(ID_HELP, CWinApp::OnHelp) END_MESSAGE_MAP() /////////////////////////////////////////////////////////////////////////// // CPieClientApp construction CPieClientApp::CPieClientApp() { // TODO: add construction code here, // Place all significant initialization in InitInstance } /////////////////////////////////////////////////////////////////////////// // The one and only CPieClientApp object CPieClientApp theApp; /////////////////////////////////////////////////////////////////////////// // CPieClientApp initialization BOOL CPieClientApp::InitInstance() { if (!AfxOleInit ()) { AfxMessageBox (_T ("AfxOleInit failed")); return FALSE; } // Standard initialization // If you are not using these features and wish to reduce the size // of your final executable, you should remove from the following // the specific initialization routines you do not need. CPieClientDlg dlg; m_pMainWnd = &dlg; int nResponse = dlg.DoModal(); if (nResponse == IDOK) { // TODO: Place code here to handle when the dialog is // dismissed with OK } else if (nResponse == IDCANCEL) { // TODO: Place code here to handle when the dialog is // dismissed with Cancel } // Since the dialog has been closed, return FALSE so that we exit the // application, rather than start the application's message pump. return FALSE; } |
PieClientDlg.h
// PieClientDlg.h : header file // #if !defined( AFX_PIECLIENTDLG_H__3B5BA32C_3B72_11D2_AC82_006008A8274D__INCLUDED_) #define AFX_PIECLIENTDLG_H__3B5BA32C_3B72_11D2_AC82_006008A8274D__INCLUDED_ #include "autopie.h" // Added by ClassView #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 /////////////////////////////////////////////////////////////////////////// // CPieClientDlg dialog class CPieClientDlg : public CDialog { // Construction public: CPieClientDlg(CWnd* pParent = NULL); // standard constructor // Dialog Data //{{AFX_DATA(CPieClientDlg) enum { IDD = IDD_PIECLIENT_DIALOG }; CButton m_wndSet; CButton m_wndGet; //}}AFX_DATA // ClassWizard generated virtual function overrides //AFX_VIRTUAL // Implementation protected: CAutoWindow m_autoWindow; CAutoChart m_autoChart; CAutoPie m_autoPie; HICON m_hIcon; // Generated message map functions //AFX_MSG DECLARE_MESSAGE_MAP() }; // // Microsoft Visual C++ will insert additional declarations // immediately before the previous line. #endif // !defined( // AFX_PIECLIENTDLG_H__3B5BA32C_3B72_11D2_AC82_006008A8274D__INCLUDED_) |
PieClientDlg.cpp
// PieClientDlg.cpp : implementation file // #include "stdafx.h" #include "PieClient.h" #include "PieClientDlg.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif /////////////////////////////////////////////////////////////////////////// // CPieClientDlg dialog CPieClientDlg::CPieClientDlg(CWnd* pParent /*=NULL*/) : CDialog(CPieClientDlg::IDD, pParent) { //AFX_DATA_INIT // Note that LoadIcon does not require a subsequent // DestroyIcon in Win32 m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME); } void CPieClientDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CPieClientDlg, CDialog) //AFX_MSG_MAP END_MESSAGE_MAP() /////////////////////////////////////////////////////////////////////////// // CPieClientDlg message handlers BOOL CPieClientDlg::OnInitDialog() { CDialog::OnInitDialog(); SetIcon(m_hIcon, TRUE); // Set big icon SetIcon(m_hIcon, FALSE); // Set small icon // // Start the Automation server. // BOOL bSuccess = m_autoPie.CreateDispatch (_T ("AutoPie.Application")); // // If CreateDispatch succeeded, initialize the m_autoChart and // m_autoWindow data members to represent the Chart and Window // subobjects, respectively. Then initialize the controls in // the dialog and make the server window visible. // if (bSuccess) { m_autoChart.AttachDispatch (m_autoPie.GetChart ()); ASSERT (m_autoChart.m_lpDispatch != NULL); m_autoWindow.AttachDispatch (m_autoPie.GetWindow ()); ASSERT (m_autoWindow.m_lpDispatch != NULL); OnGet (); m_autoWindow.SetVisible (TRUE); } // // If CreateDispatch failed, let the user know about it. // else { MessageBox (_T ("Error launching AutoPie. Run it once to " \ "register it on this system and then try again."), _T ("Error")); m_wndGet.EnableWindow (FALSE); m_wndSet.EnableWindow (FALSE); } return TRUE; // return TRUE unless you set the focus to a control } void CPieClientDlg::OnPaint() { if (IsIconic()) { CPaintDC dc(this); // device context for painting SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0); // Center icon in client rectangle. int cxIcon = GetSystemMetrics(SM_CXICON); int cyIcon = GetSystemMetrics(SM_CYICON); CRect rect; GetClientRect(&rect); int x = (rect.Width() - cxIcon + 1) / 2; int y = (rect.Height() - cyIcon + 1) / 2; // Draw the icon. dc.DrawIcon(x, y, m_hIcon); } else { CDialog::OnPaint(); } } HCURSOR CPieClientDlg::OnQueryDragIcon() { return (HCURSOR) m_hIcon; } void CPieClientDlg::OnGet() { // // Retrieve revenue values from the Automation server and display them. // SetDlgItemInt (IDC_Q1, m_autoChart.GetRevenue (1)); SetDlgItemInt (IDC_Q2, m_autoChart.GetRevenue (2)); SetDlgItemInt (IDC_Q3, m_autoChart.GetRevenue (3)); SetDlgItemInt (IDC_Q4, m_autoChart.GetRevenue (4)); } void CPieClientDlg::OnSet() { // // Retrieve the revenue values displayed in the edit controls // and provide them to the Automation server. // m_autoChart.SetRevenue (1, GetDlgItemInt (IDC_Q1)); m_autoChart.SetRevenue (2, GetDlgItemInt (IDC_Q2)); m_autoChart.SetRevenue (3, GetDlgItemInt (IDC_Q3)); m_autoChart.SetRevenue (4, GetDlgItemInt (IDC_Q4)); // // Repaint the pie chart. // m_autoWindow.Refresh (); } |
AutoPie.h
// Machine generated IDispatch wrapper class(es) created with ClassWizard /////////////////////////////////////////////////////////////////////////// // CAutoPie wrapper class class CAutoPie : public COleDispatchDriver { public: CAutoPie() {} // Calls COleDispatchDriver default constructor CAutoPie(LPDISPATCH pDispatch) : COleDispatchDriver(pDispatch) {} CAutoPie(const CAutoPie& dispatchSrc) : COleDispatchDriver(dispatchSrc) {} // Attributes public: LPDISPATCH GetChart(); void SetChart(LPDISPATCH); LPDISPATCH GetWindow(); void SetWindow(LPDISPATCH); LPDISPATCH GetToolbar(); void SetToolbar(LPDISPATCH); // Operations public: void Quit(); }; /////////////////////////////////////////////////////////////////////////// // CAutoChart wrapper class class CAutoChart : public COleDispatchDriver { public: CAutoChart() {} // Calls COleDispatchDriver default constructor CAutoChart(LPDISPATCH pDispatch) : COleDispatchDriver(pDispatch) {} CAutoChart(const CAutoChart& dispatchSrc) : COleDispatchDriver(dispatchSrc) {} // Attributes public: // Operations public: BOOL Save(LPCTSTR pszPath); long GetRevenue(short nQuarter); void SetRevenue(short nQuarter, long nNewValue); }; /////////////////////////////////////////////////////////////////////////// // CAutoWindow wrapper class class CAutoWindow : public COleDispatchDriver { public: CAutoWindow() {} // Calls COleDispatchDriver default constructor CAutoWindow(LPDISPATCH pDispatch) : COleDispatchDriver(pDispatch) {} CAutoWindow(const CAutoWindow& dispatchSrc) : COleDispatchDriver(dispatchSrc) {} // Attributes public: BOOL GetVisible(); void SetVisible(BOOL); // Operations public: void Refresh(); }; /////////////////////////////////////////////////////////////////////////// // CAutoToolbar wrapper class class CAutoToolbar : public COleDispatchDriver { public: CAutoToolbar() {} // Calls COleDispatchDriver default constructor CAutoToolbar(LPDISPATCH pDispatch) : COleDispatchDriver(pDispatch) {} CAutoToolbar(const CAutoToolbar& dispatchSrc) : COleDispatchDriver(dispatchSrc) {} // Attributes public: BOOL GetVisible(); void SetVisible(BOOL); // Operations public: }; |
AutoPie.cpp
// Machine generated IDispatch wrapper class(es) created with ClassWizard #include "stdafx.h" #include "autopie.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif /////////////////////////////////////////////////////////////////////////// // CAutoPie properties LPDISPATCH CAutoPie::GetChart() { LPDISPATCH result; GetProperty(0x1, VT_DISPATCH, (void*)&result); return result; } void CAutoPie::SetChart(LPDISPATCH propVal) { SetProperty(0x1, VT_DISPATCH, propVal); } LPDISPATCH CAutoPie::GetWindow() { LPDISPATCH result; GetProperty(0x2, VT_DISPATCH, (void*)&result); return result; } void CAutoPie::SetWindow(LPDISPATCH propVal) { SetProperty(0x2, VT_DISPATCH, propVal); } LPDISPATCH CAutoPie::GetToolbar() { LPDISPATCH result; GetProperty(0x3, VT_DISPATCH, (void*)&result); return result; } void CAutoPie::SetToolbar(LPDISPATCH propVal) { SetProperty(0x3, VT_DISPATCH, propVal); } /////////////////////////////////////////////////////////////////////////// // CAutoPie operations void CAutoPie::Quit() { InvokeHelper(0x4, DISPATCH_METHOD, VT_EMPTY, NULL, NULL); } /////////////////////////////////////////////////////////////////////////// // CAutoChart properties /////////////////////////////////////////////////////////////////////////// // CAutoChart operations BOOL CAutoChart::Save(LPCTSTR pszPath) { BOOL result; static BYTE parms[] = VTS_BSTR; InvokeHelper(0x1, DISPATCH_METHOD, VT_BOOL, (void*)&result, parms, pszPath); return result; } long CAutoChart::GetRevenue(short nQuarter) { long result; static BYTE parms[] = VTS_I2; InvokeHelper(0x2, DISPATCH_PROPERTYGET, VT_I4, (void*)&result, parms, nQuarter); return result; } void CAutoChart::SetRevenue(short nQuarter, long nNewValue) { static BYTE parms[] = VTS_I2 VTS_I4; InvokeHelper(0x2, DISPATCH_PROPERTYPUT, VT_EMPTY, NULL, parms, nQuarter, nNewValue); } /////////////////////////////////////////////////////////////////////////// // CAutoWindow properties BOOL CAutoWindow::GetVisible() { BOOL result; GetProperty(0x1, VT_BOOL, (void*)&result); return result; } void CAutoWindow::SetVisible(BOOL propVal) { SetProperty(0x1, VT_BOOL, propVal); } /////////////////////////////////////////////////////////////////////////// // CAutoWindow operations void CAutoWindow::Refresh() { InvokeHelper(0x2, DISPATCH_METHOD, VT_EMPTY, NULL, NULL); } /////////////////////////////////////////////////////////////////////////// // CAutoToolbar properties BOOL CAutoToolbar::GetVisible() { BOOL result; GetProperty(0x1, VT_BOOL, (void*)&result); return result; } void CAutoToolbar::SetVisible(BOOL propVal) { SetProperty(0x1, VT_BOOL, propVal); } /////////////////////////////////////////////////////////////////////////// // CAutoToolbar operations |
Stdafx.h
// stdafx.h : include file for standard system include files, // or project specific include files that are used frequently, but // are changed infrequently // #if !defined(AFX_STDAFX_H__3B5BA32E_3B72_11D2_AC82_006008A8274D__INCLUDED_) #define AFX_STDAFX_H__3B5BA32E_3B72_11D2_AC82_006008A8274D__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #define VC_EXTRALEAN // Exclude rarely-used stuff from Windows headers #include <afxwin.h> // MFC core and standard components #include <afxext.h> // MFC extensions #include <afxdtctl.h> // MFC support for Internet Explorer 4 // Common Controls #ifndef _AFX_NO_AFXCMN_SUPPORT #include <afxcmn.h> // MFC support for Windows Common Controls #endif // _AFX_NO_AFXCMN_SUPPORT #include <afxdisp.h> // // Microsoft Visual C++ will insert additional declarations // immediately before the previous line. #endif // !defined(AFX_STDAFX_H__3B5BA32E_3B72_11D2_AC82_006008A8274D__INCLUDED_) |
Keep in mind that checking the Automation box in AppWizard makes an application an Automation server, not an Automation client. With the Automation option unchecked, however, AppWizard will not add an AfxOleInit call to InitInstance, nor will it #include Afxdisp.h in Stdafx.h. Both are necessary for Automation clients, so I added them by hand to PieClient. Without these additions, the code will compile just fine, but CreateDispatch will fail every time.
Connecting to a Running Automation Server
Thanks to ClassWizard-generated wrapper classes and their member functions, accessing an Automation server's methods and properties from a C++ program is almost as easy as accessing them from Visual Basic. But what if you want to connect two or more instances of PieClient to one instance of AutoPie? As it stands now, that's not possible because each instance of PieClient calls CreateDispatch, which creates a brand new instance of the Automation object.
You can modify PieClient and AutoPie to support multiple simultaneous connections by adding a few lines of code to each. On the server side, AutoPie needs to call the API function ::RegisterActiveObject to register itself as an active object. Here's a modified version of CAutoPieDoc's constructor that demonstrates how.
CAutoPieDoc::CAutoPieDoc () { // Wizard-generated code EnableAutomation (); AfxOleLockApp (); // Additional code that registers the running object IUnknown* pUnknown; GetIDispatch (FALSE)-> QueryInterface (IID_IUnknown, (void**) &pUnknown); GetIDispatch (FALSE)->Release (); // Undo the AddRef // performed by QueryInterface. ::RegisterActiveObject (pUnknown, clsid, ACTIVEOBJECT_WEAK, &m_ulID); } |
In this example, m_ulID is an unsigned long member variable added to CAutoPieDoc. It receives a 32-bit value identifying the entry that ::RegisterActiveObject added to COM's running object table. clsid is the object's CLSID; it's declared in AutoPie.cpp and made visible in AutoPieDoc.cpp by adding the statement
extern CLSID clsid; |
to AutoPieDoc.cpp. For this extern statement to compile and link, you must remove the keywords static and const from the variable declaration in AutoPie.cpp.
So that clients won't attempt to connect to an Automation server that is no longer running, a version of AutoPie that registers itself in the running object table must unregister itself before it shuts down. The best way to do this is to override OnFinalRelease in CAutoPieDoc and call ::RevokeActiveObject, as shown here:
void CAutoPieDoc::OnFinalRelease() { ::RevokeActiveObject (m_ulID, NULL); CDocument::OnFinalRelease(); } |
::RevokeActiveObject does the opposite of ::RegisterActiveObject: Given a registration ID, it removes an object from the running object table. OnFinalRelease is a CCmdTarget function that's called just before an MFC COM object self-deletes.
The final modification needed to support multiple connections applies to the client, not the server. Before calling CreateDispatch to create the Automation object, PieClient should call ::GetActiveObject to find out whether the object is already running. The following code connects to an existing object instance if such an instance exists or creates a new instance if it doesn't:
BOOL bSuccess = FALSE; CLSID clsid; if (SUCCEEDED (CLSIDFromProgID (OLESTR ("AutoPie.Application"), &clsid))) { IUnknown* pUnknown; if (SUCCEEDED (::GetActiveObject (clsid, NULL, &pUnknown))) { IDispatch* pDispatch; if (SUCCEEDED (pUnknown->QueryInterface (IID_IDispatch, (void**) &pDispatch))) { pDispatch->Release (); // Undo the AddRef performed // by QueryInterface. m_autoPie.AttachDispatch (pDispatch); bSuccess = TRUE; } } } if (!bSuccess) bSuccess = m_autoPie.CreateDispatch (_T ("AutoPie.Application")); if (!bSuccess) { // Error: Unable to connect to an existing object instance or // launch a new one. } |
If you apply these modifications to AutoPie and PieClient, you'll find that no matter how many instances of PieClient you start, each will connect to the same Automation object. One drawback to the ::RegisterActiveObject/::GetActiveObject method is that it's powerless over a network, even though Automation itself works just fine between machines. Attaching multiple clients to an Automation server on another machine requires an altogether different approach to the problem. That, however, is a topic for another day.