So far, all the code in this chapter has accessed COM interfaces and their functions directly. This is relatively easy from C++, since the vtable structure used by COM can be accessed from C++ code. However, other languages in particular, scripting languages like VBScript may not be able to access the vtable. This is because COM interface function calls would need to be resolved at run time, and the vtable does not contain information about the names of the functions, the arguments they take, or the order of the functions. For this reason, Automation was developed. Automation allows functionality in COM components to be called by creating and passing data structures detailing the nature of the call, rather than direct function calls. Automation is implemented by a single COM interface called IDispatch. ActiveX controls generally use Automation rather than direct COM interface calls, although many ActiveX controls support both Automation and COM interface calls by providing "dual interfaces." With dual interfaces, the client application can decide whether to use the IDispatch Automation interface or call functions directly through the vtable. IDispatch Automation interfaces are known as "dispinterfaces" or "dispatch interfaces" note that they are not COM interfaces although they are invoked through the IDispatch COM interface. Automation does not use standard C++ data types for parameters and return types. Instead, parameters are passed using the variant data type. While this limits to a certain extent the type of data that can be passed, it allows data coercion and data casting to be performed at run time. Some components do not support dual interfaces, so the dispinterface must be used. Therefore, you may need to call Invoke through IDispatch, passing the relevant parameter information. _bstr_t and _variant_t ClassesIn the same way that smart pointers make accessing COM interfaces easier, you can use the classes _bstr_t and _variant_t to make accessing BSTR and VARIANT variables easier and more reliable. These classes provide constructors that create BSTR directly from strings without having to use SysAllocString, and the BSTR variables are automatically de-allocated. These two classes are described in Chapter 16 (ADOCE). Automation DispInterfacesAn Automation DispInterface provides access to a COM component's functionality through the following:
Each method and property has a "dispid," or dispatch identifier, that is unique within the Automation object. A client application calls a method or accesses a property through the dispid rather than using the method or property's name directly. Note that dispids are not GUIDs but are simple integer numbers. Although properties are used like variables, they are actually accessed through method-like calls into the automation object. Information on the DispInterface is included in the type library information associated with the COM component. If the type library information is included in the COM component's DLL or EXE, the client application can interrogate this information at run time through the IDispatch interface. The IDispatch InterfaceAutomation is implemented using a single interface call, IDispatch, which has four interface functions:
Generally, Invoke and GetIDsOfNames are used to execute methods and properties, and GetTypeInfoCount and GetTypeInfo are used to obtain information about the methods and properties. You will find code examples showing how to call GetTypeInfoCount and GetTypeInfo at the end of Chapter 14.cpp in the Examples project on the CDROM. Obtaining an IDispatch Interface PointerAny COM component that provides an Automation interface must implement the IDispatch interface. The following function returns an IDispatch smart interface pointer for the Pocket Outlook Object Model: #include <dispex.h> // for IID_IDispatch definition _COM_SMARTPTR_TYPEDEF(IDispatch, IID_IDispatch); BOOL GetIDispatch(IDispatchPtr& pDispatchPtr) { HRESULT hr; hr = pDispatchPtr.CreateInstance(CLSID_Application); if (FAILED(hr)) { cout _T("Could not create object") endl; return FALSE; } return TRUE; } In previous uses of _COM_SMARTPTR_TYPEDEF, the __uuidof macro was used to obtain the interface identifier (IID) from the interface definition. This assumes that the interface is declared using the DECLSPEC_UUID attribute, as follows: interface DECLSPEC_UUID ("5B43F691-202C-11d2-8F18-0000F87A4335") IAppointment : public IDispatch { ... Many interface declarations do not use DECLSPEC_UUID, so you will need to use the IID directly (such as IID_IDispatch from the header file dispex.h). Obtaining Dispatch IdentifiersBefore calling a property or method, you must first obtain its dispatch identi- fier (DispId) using the IDispatch interface function GetIDsOfNames (Table 14.4). GetIDsOfNames can convert several property and method names in one call, which is more efficient than making single calls.
Potentially, an Automation interface could be localized so that method and property names are translated into different languages. For this reason, GetIDsOfNames requires that a locale be specified. Generally, the result of calling GetUserDefaultLCID (which obtains the locale of the current user) is passed to GetIDsOfNames. The following code shows how to get the DispID for the Version property in IPOutlookApp: LPTSTR names[] = {_T("Version")}; DISPID dispLogoff; HRESULT hr; // first get the dispatch ID for IPOutlookApp::Version hr = pDispatchPtr->GetIDsOfNames(IID_NULL, names, 1, GetUserDefaultLCID(), &dispVersion); if(FAILED(hr)) { cout _T("Could not get dispid for Version") endl; return FALSE; } The VARIANT Data TypeAll parameters and return values used when calling Automation methods and properties are passed using the VARIANT structure. This structure is also referred to a VARIANTARG. The vt member of this structure contains a constant that describes the type of data contained in the structure and an unnamed union with members for all the different data types that can be stored in a VARIANT. The first few members of this structure are the following: typedef struct tagVARIANT { VARTYPE vt; unsigned short wReserved1; unsigned short wReserved2; unsigned short wReserved3; union { unsigned char bVal; short iVal; long lVal; float fltVal; double dblVal; ... The basic data types that can be stored in a variant are shown in Table 14.5. The VARIANT structure can store IUnknown and IDispatch pointers; this allows Automation methods and properties to return COM pointers. This in turn allows COM components to create object hierarchies and object models like POOM.
The data types in Table 14.5 are used for passing data by value. When a property or method needs to return data, a "by reference" data type must be used. All the data types in Table 14.5 can be combined with the vt constant VT_BYREF. The union members for data passed by reference are all preceded by "p", so the VT_I2 member used when passing by reference is piVal. Two important VT values are used to signify that the VARIANT structure does not contain data. VT_EMPTY means that no data is stored in the VARIANT structure the value is 0 for numeric data, and an empty string for string data. VT_NULL signifies that the VARIANT structure intentionally does not contain data. The VariantInit function can be called to set the vt member to VT_EMPTY, and VariantClear sets it to VT_NULL. Each of these functions takes a pointer to a VARIANT structure and returns an HRESULT: VARIANT varg; HRESULT hr; hr = VariantInit(&varg); The VariantChangeTypeEx function can be used to coerce (or cast) a VARIANT structure from one data type to another. The function determines whether the coercion is legal and returns S_OK for success, or DISP_E_TYPEMISMATCH if the coercion failed. The function is passed the following:
The following code shows coercion for a VT_I2 to VT_R4 data type: VARIANT varI2, varR4; HRESULT hr; varI2.vt = VT_I2; varI2.iVal = VT_R4; hr = VariantChangeTypeEx(&varR4, &varI2, GetUserDefaultLCID(), 0, VT_R4); There are also functions used for explicitly converting from one data type to another (such as VarR4FromUI2); however, the advantage of using VariantChangeTypeEx is that you do not need to determine the current data type of a variant before attempting to coerce it to a different data type. Using an Automation PropertyAn Automation property is accessed through calling the IDispatch Invoke function, specifying whether the property value is to be retrieved or set. When a property value is to be set, the new value for the property is passed as a parameter to the invocation. A property value is retrieved through the return value of the invocation. In Listing 14.11a, the function DisplayVersion is passed a smart pointer to the POOM object model and first obtains the dispid for the Version property (which was described in the previous section, "Obtaining Dispatch Identifiers"). Listing 14.11a Accessing the Version propertyBOOL DisplayVersion(IDispatchPtr& pDispatchPtr) { DISPID dispVersion; HRESULT hr; VARIANTARG varResult; DISPPARAMS disparms = { NULL, NULL, 0, // zero arguments 0 // zero named arguments }; LPTSTR names[] = {_T("Version")}; // first get the dispatch ID for IPOutlookApp::Version hr = pDispatchPtr->GetIDsOfNames(IID_NULL, names, 1, GetUserDefaultLCID(), &dispVersion); if(FAILED(hr)) { cout _T("Could not get dispid for Version") endl; return FALSE; } hr = pDispatchPtr->Invoke(dispVersion, IID_NULL, GetUserDefaultLCID(), DISPATCH_PROPERTYGET, &disparms, &varResult, NULL, NULL); if(FAILED(hr)) { cout _T("Could not invoke Version") endl; return FALSE; } cout _T("Version invoked:") varResult.bstrVal endl; SysFreeString(varResult.bstrVal); return TRUE; } The Invoke function (Table 14.6) is called, passing the following parameters:
The Automation object will interpret the data passed to the Invoke and execute the necessary code to obtain the property's value and return the result. On return from Invoke the "varResult" variable contains the property's value, and the value is accessed through the statement varResult.bstrVal. Since this is a BSTR the function SysFreeString must be called to free the memory associated with the string. Calling Automation MethodsCalling Automation methods is very similar to accessing property values. However, you will need to initialize a DISPPARAMS structure to specify the parameters to be passed. The DISPPARAMS structure has four members:
Automation allows parameters to be passed by name rather than order. This allows default parameters, since the call to Invoke does not have to supply a VARIANT structure for each of the parameters, and the Automation object can default those not supplied. In the simplest case, a DISPPARAMS structure can be initialized to specify "no parameters", as is done in Listing 14.11a. In the following code VARIANTARG and DISPPARAMS structures are initialized for a call to the "Logon" method, and this method is passed a single HWND argument. A VARIANTARG variable is declared to hold the parameter value. Then, the DISPPARAMS structure is initialized to have one parameter, and this parameter will be specified in the first element of the varg array: VARIANTARG varg; DISPPARAMS disparms = { &varg, NULL, 1, // one parameter 0 // zero named parameters }; Next, calling VariantInit initializes the VARIANTARG structure, and the data type is set to VT_I4. The hWnd value is assigned to the lVal union member in VARIANT: VariantInit(&varg); varg.vt = VT_I4; varg.lVal = (LONG)hWnd; Parameters are passed in reverse order in the VARIANTARG array. For example, the following Automation method takes two arguments: AutomationFunction( VT_I4 argument1, VT_R8 argument2) The VARIANTARG array should be declared and initialized in the following way: VARIANTARG varg[2]; DISPPARAMS disparms = { &varg, NULL, 2, // two parameters 0 // zero named parameters }; VariantInit(&varg[0]); varg[0].vt = VT_R8; varg[0].lVal = 1.20; // initialize VT_R8 VariantInit(&varg[1]); varg.vt[1] = VT_I4; varg.lVal[1] = 42; // initialize VT_I4 The entire code for calling "Logon" is shown in Listing 14.11b. There is obviously much more code in calling Logon using the Automation interface and IDispatch::Invoke. C++ programmers should generally call COM interfaces directly rather than using IDispatch::Invoke, since it is easier, generates less code, and provides faster execution. However, some components do not provide a dual interface, leaving no option but to use Automation. MFC, described later in the chapter, provide helper classes for making the calling of Automation objects easier. Listing 14.11b Passing parameters to a methodBOOL Logon(IDispatchPtr& pDispatchPtr, HWND hWnd) { DISPID dispLogon; HRESULT hr; VARIANTARG varg; DISPPARAMS disparms = { &varg, NULL, 1, // one parameter 0 // zero named parameters }; LPTSTR names[] = {_T("Logon")}; // first get the dispatch ID for IPOutlookApp::Logon hr = pDispatchPtr->GetIDsOfNames(IID_NULL, names, 1, GetUserDefaultLCID(), &dispLogon); if(FAILED(hr)) { cout _T("Could not get dispid for Logon") endl; return FALSE; } VariantInit(&varg); varg.vt = VT_I4; varg.lVal = (LONG)hWnd; hr = pDispatchPtr->Invoke(dispLogon, IID_NULL, GetUserDefaultLCID(), DISPATCH_METHOD, &disparms, NULL, NULL, NULL); if(FAILED(hr)) { cout _T("Could not invoke Logon") endl; return FALSE; } cout _T("Logon invoked") endl; return TRUE; }
|