L'interface IDispatch
Avertissement
Dans ce document j'utilise une terminologie provenant d'un autre texte sur les méthodes virtuelles et COM. Le sujet c'est l'implémentation et l'appel de IDispatch en C++, et non pas tous les détails liés à cette technologie fournie par Microsoft
Plus récemment, j'ai développé un nouvel outil pour ma société Acordyn, où l'usage de IDispatch dans un éditeur d'objets mène à des déploiements de composants Web. Des bases de données sont ainsi mises en ligne sur https://www.acordyn.com concernant des graphiques actualisés sur le Covid-19, les transactions immobilières en France depuis 2018, les sociétés actives en France actuellement, les produits alimentaires référencés sur l'application Yuca par Open Food Facts, l'inventaire des films, séries et téléfilms de IMDb Movies, etc...
Définition du contrat
L'interface (contrat) IDispatch est un contrat COM défini par Microsoft, dont voici la classe purement abstraite :
// ...
class IDispatch : public IUnknown
{
public:
virtual HRESULT PASCAL GetTypeInfoCount (UINT* _pctinfo) = 0;
virtual HRESULT PASCAL GetTypeInfo (UINT _iTInfo, LCID _lcid, ITypeInfo** _ppTInfo) = 0;
virtual HRESULT PASCAL GetIDsOfNames (REFIID _riid, LPOLESTR* _rgszNames, UINT _cNames, LCID _lcid, DISPID* _rgDispId) = 0;
virtual HRESULT PASCAL Invoke (DISPID _dispIdMember, REFIID _riid, LCID _lcid, WORD _wFlags, DISPPARAMS* _pDispParams, VARIANT* _pVarResult, EXCEPINFO* _pExcepInfo, UINT* _puArgErr) = 0;
};On a là un rude contrat... La seule méthode importante c'est Invoke (la dernière), dont le but est de permettre à un programme client d'appeler des fonctions du serveur d'une manière "doublement indirecte". Il ne s'agit plus de l'appel indirect d'une méthode de VTable, méthode qui "existe" et dont l'adresse relative est fixée lors du linking du module, puis placée dans la VTable fournie au client lors de l'instanciation d'un composant (la classe purement abstraite de la VTable est connue par le client lors de la compilation). Il s'agit ici de pouvoir appeler des fonctions qui ne sont pas connues lors de la compilation du client, voire des fonctions qui n'existent pas ou plus, auquel cas un code d'erreur est retourné par Invoke lors de l'exécution. Le programme client tenant un IDispatch* suppose qu'une fonction portant le numéro du premier paramètre de type DISPID existe dans le composant et que dans le paramètre de type DISPPARAMS il y a de bonnes informations sur les arguments à passer à la fonction, puis "essaie" d'appeler cette fonction. Le serveur vérifie tout cela et, si l'appel est réalisable avec ce que le client fournit, il se charge de retrouver l'adresse de la VTable et de la méthode à appeler dans cette VTable, puis retourne un résultat dans le paramètre de type VARIANT* pour le client.
Ainsi, les services offerts par un composant à travers IDispatch ne sont pas obligatoirement connus par le programme client sous la forme de classes purement abstraites, mais sont découvertes par le développeur du programme client en lisant la documentation "papier" du composant. Le développeur essaie ensuite d'utiliser le composant en appelant la méthode Invoke, éventuellement après avoir utilisé la méthode GetIDsOfNames pour obtenir des DISPID à partir des noms en clair des fonctions. Microsoft propose avec IDispatch une façon standard pour un programme client d'appeler n'importe quelle fonction de n'importe quel composant qui joue à ce jeu (et ils sont nombreux).
Automation
Automation signifie automatisation, mais sous Windows ce mot est un nom propre, désignant l'ensemble de la technologie fournie autour du concept IDispatch. C'est bien plus que le seul contrat IDispatch, c'est tout un ensemble de fonctions système implémentées dans ole32.dll et oleaut32.dll, c'est le compilateur MIDL (générateur de librairies de types), c'est les objets en Visual Basic et VBScript, c'est les utilitaires browsers de registry. Et Microsoft Office est la série de composants serveurs basés IDispatch qui représente un modèle du genre.
Dans ce document, je parle des difficultés que j'ai rencontré avec l'interface IDispatch en C++, en me concentrant sur le contrat lui-même, sans approfondir la partie sur les sources d'événements en callback.
Implémenter IDispatch
Lorsqu'on développe un composant en C++ (par exemple un contrôle ActiveX, mais pas forcément), on peut vouloir rendre certaines fonctionnalités accessibles par des programmes clients de type Automation, pour faciliter l'intégration du composant dans des applications VB et permettre sa manipulation par des scripts VBScript ou JavaScript (on appelle cela un composant "programmable"). Cela ne doit pas empêcher d'autres programmes clients C++ d'utiliser le composant d'une manière à exécution plus rapide, avec des VTables (on appelle cela un composant programmable "dual").
L'élément "clé" pour exposer des propriétés et méthodes à travers Automation est la librairie de types (type library), et ce n'est rien d'autre qu'un format binaire Microsoft persistant qui stocke des définitions de fonctions et qui, surtout, peut être manipulé par une couche d'abstraction fournie par le système. On crée généralement une librairie de types en écrivant d'abord un source en IDL Microsoft (un langage de définition d'interfaces ressemblant au C++). Je prends l'exemple simplifié, qui n'a pas d'utilité en pratique, d'un composant dual dont la librairie de type est générée par le source IDL que voici :
[ uuid (E2667A2A-4487-4F0B-B2DB-5A5FBB9D203E) ]
library ExampleTypeLib
{
importlib ("stdole32.tlb");
coclass ExampleClass;
interface IDispExample;
interface IDispSubExample;
// "classe" du composant
[ uuid (AED7DAB8-46A0-453D-9245-7D0BFFFA2021) ]
coclass ExampleClass
{
[default] interface IDispExample;
};
// interface principale
[ uuid (EE46031F-A2A1-4345-B3FE-5887EC8C58BF), oleautomation, dual ]
interface IDispExample : IDispatch
{
[propget, id (1)] HRESULT Count ([out, retval] long* _pCount);
[id (2)] HRESULT GetItem ([in] long _nIndex, [out, retval] IDispSubExample** _ppItem);
[id (3)] HRESULT ResetItems ([in] long _nCount, [in] BSTR _pText );
[id (4)] HRESULT ShowMessage ( );
};
// interface d'un petit composant imbriqué
[ uuid (650B648D-93D3-41D5-95E3-31FFEE526AAD), oleautomation, dual ]
interface IDispSubExample : IDispatch
{
[propget, id (1)] HRESULT Hidden ([out, retval] VARIANT_BOOL* _pHidden);
[propput, id (1)] HRESULT Hidden ([in] VARIANT_BOOL _nHidden);
[id (2)] HRESULT SwitchText ([in, optional] BSTR _pNewText, [out, retval] BSTR* _ppOldText);
};
}On a ci-dessus une petite librairie de type, une "classe d'objet" pour l'instanciation de type COM (à base de IClassFactory, registry et CLSID) et deux interfaces dérivées de IDispatch. Le composant correspondant à la coclass implémente un contrat de type collection d'objets, où chaque petit objet implémente à son tour un autre contrat. Les "propriétés" ont l'attribut propget et/ou propput et génèrent dans la classe purement abstraite en C++ des méthodes préfixées (dans l'exemple, get_Count, puis get_Hidden et put_Hidden), tandis que les méthodes apparaîtront avec le même nom que dans l'IDL (par exemple ShowMessage).
On compile ce fichier avec le compilateur MIDL.EXE (utilitaire Visual Studio 6) pour générer un header, un source C contenant les GUIDs et un fichier TLB au format librairie de types (par exemple, à partir de example.idl, on produit example.h, example.c et example.tlb). Si le source en IDL est dans un projet de Visual Studio 6, le compilateur MIDL lui est associé par défaut et on peut contrôler les fichiers générés dans les "Settings" de l'IDL accessibles par clic droit. Une fois la première compilation de l'IDL terminée, il ne faut pas oublier d'ajouter au projet le source C généré, contenant les GUIDs sous la forme de variables globales.
L'étape suivante consiste à faire en sorte que la librairie de types soit une ressource de la DLL du composant, pour éviter d'avoir à livrer deux fichiers à installer, example.dll et example.tlb. Il vaut mieux livrer la DLL seule, et pour cela l'astuce consiste à éditer en texte les ressources de la DLL (disons example.rc) et y insérer à la main la ligne de texte suivante :
1 TYPELIB "example.tlb"
Pour implémenter le composant, il faut implémenter une class factory, les fonctions standard à exporter (DllGetClassObject, DllCanUnloadNow, DllRegisterServer et DllUnregisterServer) et, enfin, écrire le code pour les deux composants. Dans tout cela, ce qui nous intéresse ici c'est comment implémenter IDispatch. Dans un premier temps, il faut se servir du système pour construire un composant qui implémente le contrat standard ITypeLib et garder celui-ci en mémoire pendant toute la durée d'utilisation de la DLL dans le processus. Le secret de cette opération est dans l'utilisation de la fonction système LoadTypeLibEx pour lire la librairie de types stockée en tant que ressource dans la DLL :
#include "example.h"
ITypeLib* g_pTypeLib = NULL;
ITypeInfo* g_pClsidInfo = NULL;
ITypeInfo* g_pExampleInfo = NULL;
ITypeInfo* g_pSubExInfo = NULL;
// ...
BOOL WINAPI DllMain (HINSTANCE _hinstDLL, DWORD _fdwReason, LPVOID _lpvReserved)
{
if (_fdwReason == DLL_PROCESS_ATTACH)
{
WCHAR l_pDllFullName [_MAX_PATH];
GetModuleFileName (_hinstDLL, l_pDllFullName, _MAX_PATH);
LoadTypeLibEx (l_pDllFullName, REGKIND_NONE, &g_pTypeLib);
g_pTypeLib->GetTypeInfoOfGuid (CLSID_ExampleClass, &g_pClsidInfo);
g_pTypeLib->GetTypeInfoOfGuid (IID_IDispExample, &g_pExampleInfo);
g_pTypeLib->GetTypeInfoOfGuid (IID_IDispSubExample, &g_pSubExInfo);
}
else if (_fdwReason == DLL_PROCESS_DETACH)
{
g_pSubExInfo->Release ();
g_pExampleInfo->Release ();
g_pClsidInfo->Release ();
g_pTypeLib->Release ();
}
return TRUE;
}Une fois que les implémentations système des contrats ITypeInfo sont disponibles en permanence, on est parfaitement bien armé pour implémenter IDispatch avec beaucoup d'aisance, puisque certaines méthodes de ITypeInfo ressemblent étrangement à celles de IDispatch. Si j'ai choisi d'avoir deux interfaces duales dans l'exemple, c'est pour montrer que le composant n'est pas limité à la publication d'un ensemble de propriétés et de méthodes, globales à une instance, mais que l'on peut construire un composant complexe avec de nombreux composants imbriqués, à la manière des "object models" des produits Microsoft Office. Pour montrer comment on implémente IDispatch, il n'est pas nécessaire de voir le code complet des deux composants imbriqués, une partie du code du sous-composant suffit :
#include "example.h"
extern ITypeInfo* g_pSubExInfo;
class ImplDispSubExample : public IDispSubExample
{
public:
// partie IUnknown (répondra à IID_IUnknown, IID_IDispatch et IID_IDispSubExample)
virtual HRESULT PASCAL QueryInterface (REFGUID _iid, void** _ppObject);
virtual ULONG PASCAL AddRef ();
virtual ULONG PASCAL Release ();
// partie IDispatch
virtual HRESULT PASCAL GetTypeInfoCount (UINT* _pctinfo);
virtual HRESULT PASCAL GetTypeInfo (UINT _iTInfo, LCID _lcid, ITypeInfo** _ppTInfo);
virtual HRESULT PASCAL GetIDsOfNames (REFIID _riid, LPOLESTR* _rgszNames, UINT _cNames, LCID _lcid, DISPID* _rgDispId);
virtual HRESULT PASCAL Invoke (DISPID _dispIdMember, REFIID _riid, LCID _lcid, WORD _wFlags, DISPPARAMS* _pDispParams, VARIANT* _pVarResult, EXCEPINFO* _pExcepInfo, UINT* _puArgErr);
// reste de IDispSubExample
virtual HRESULT PASCAL get_Hidden (VARIANT_BOOL* _pHidden);
virtual HRESULT PASCAL put_Hidden (VARIANT_BOOL _nHidden);
virtual HRESULT PASCAL SwitchText (BSTR _pNewText, BSTR _pOldText);
public:
~ImplDispSubExample () { SysFreeString (m_pText); } // destructeur
private:
long m_nRefCount;
VARIANT_BOOL m_nHidden;
BSTR m_pText;
// ... etc
};
// ... début de l'implémentation omis ici
// implémentation de IDispatch
HRESULT ImplDispSubExample::GetTypeInfoCount (UINT* _pctinfo)
{
if (_pctinfo) *_pctinfo = 1; // indique que le ITypeInfo est fourni
return S_OK;
}
HRESULT ImplDispSubExample::GetTypeInfo (UINT _iTInfo, LCID _lcid, ITypeInfo** _ppTInfo)
{
if (_iTInfo) return E_DISP_BADINDEX; // on répond seulement pour l'index 0
if (_ppTInfo)
{
*_ppTInfo = g_pSubExInfo; // ITypeInfo fourni (en général pour les browsers d'objets)
g_pSubExInfo->AddRef ();
}
return S_OK;
}
HRESULT ImplDispSubExample::GetIDsOfNames (REFIID _riid, LPOLESTR* _rgszNames, UINT _cNames, LCID _lcid, DISPID* _rgDispId)
{
// on délègue tout le travail (seul le _lcid ne peut être géré, le _riid n'étant jamais utilisé)
return g_pSubExInfo->GetIDsOfNames (_rgszNames, _cNames, _rgDispId);
};
HRESULT ImplDispSubExample::Invoke (DISPID _dispIdMember, REFIID _riid, LCID _lcid, WORD _wFlags, DISPPARAMS* _pDispParams, VARIANT* _pVarResult, EXCEPINFO* _pExcepInfo, UINT* _puArgErr);
{
// on délègue tout le travail (sauf _lcid), du moment qu'on fournit une VTable IDispSubExample* !
return g_pSubExInfo->Invoke ((IDispSubExample*) this, _dispIdMember, _wFlags, _pDispParams, _pVarResult, _pExcepInfo, _puArgErr);
}
// la véritable implémentation commence ici
HRESULT ImplDispSubExample::get_Hidden (VARIANT_BOOL* _pHidden)
{
if (_pHidden) *_pHidden = m_nHidden;
return S_OK;
}
// ... etc, le reste de l'implémentation est omis
// fonction d'instanciation
HRESULT CreateSubExampleObject (REFGUID _iid, void** _ppObject);
{
ImplDispSubExample* l_pObject = new ImplDispSubExample;
HRESULT l_nRes = l_pObject->QueryInterface (_iid, _ppObject);
if (l_nRes != S_OK)
delete l_pObject;
return l_nRes;
}L'implémentation de IDispatch ci-dessus est très facile à réaliser. Quant au problème de la localisation LCID, il peut être résolu de deux façons : soit on utilise uniquement l'anglais pour les noms des propriétés et méthodes (et, à mon avis, il vaut mieux faire comme ça), soit on crée une ressource TLB pour chaque langue et on gère le multilinguisme de la même façon que pour les autres ressources (dialogues, textes, etc).
L'implémentation de IDispatch dans l'autre composant conteneur est quasiment identique au source ci-dessus, mais avec la VTable IDispExample et la variable g_nExampleInfo. Ce composant collection devra gérer un tableau d'instances du petit composant, instances qu'il créera en utilisant CreateSubExampleObject, mais les détails de tout cela sort du cadre du présent document. La seule chose à remarquer plus spécialement est le fait que, dans l'implémentation des parties spécifiques (duales) des contrats, on ne se soucie pas de l'héritage de IDispatch, on implémente comme s'il n'y avait pas cet héritage. Pour la partie IDispatch on fait confiance à l'implémentation de ITypeInfo::Invoke pour rediriger vers la bonne méthode et traduire le résultat sous la forme de VARIANT. Pour rappel, VARIANT est une structure C destinée à gérer des "valeurs" de tous types grâce à un membre contenant un code de type (VARTYPE) et à une union. Toute variable Visual Basic est, en mémoire, un VARIANT. Les types de VARIANT retournés dans l'exemple sont VT_I4, VT_BSTR, VT_BOOL et le type spécial VT_DISPATCH retourné par la méthode GetItem du composant lors du retour de la VTable IDispatch du sous-composant désigné par le paramètre _nIndex.
Supposons maintenant que le composant est correctement enregistré dans la registry, avec son CLSID sous le nom ("progid") "Example" (voir la documentation MSDN pour plus les détails). Un script tel que ci-dessous pourrait alors être inséré dans un fichier HTML (en principe pour Internet Explorer) :
<SCRIPT language=VBScript>
Dim Test
Set Test = CreateObject ("Example")
Test . ResetItems 3, "ballon"
Test . GetItem (1) . SwitchText "jouet"
Test . ShowMessage
</SCRIPT>L'exécution du script appelle exclusivement les méthodes de IDispatch et, si jamais le HRESULT retourné par Invoke est inférieur à zéro, IE affiche la fameuse dialogue de débogage de script. Dans l'implémentation des VTables spécifiques il vaut donc mieux retourner S_OK ou S_FALSE, et prévoir éventuellement dans les contrats des paramètres supplémentaires pour récupérer des erreurs dans le script.
Appeler IDispatch
On peut avoir besoin d'utiliser des composants programmables dans des programmes clients en C++ et pour cela il faut se résoudre à appeler la méthode IDispatch::Invoke. J'ai déjà vu des codes sources cauchemardesques s'essayant à cet exercice, du même genre que les tentatives d'implémenter IDispatch à la main, pourtant, en déblayant un peu, les problèmes de lisibilité en C++ dans ces cas-là sont pratiquement tous causés par le paramètre complexe de type DISPPARAMS contenant les arguments.à passer. Cette structure contient un tableau dynamique de VARIANTs pour les valeurs des arguments, dans l'ordre inverse par rapport à la définition dans l'IDL (le premier paramètre d'une méthode est le dernier VARIANT du tableau), le nombre d'éléments de ce tableau se trouvant, lui, dans un autre membre de la structure. Par ailleurs, des arguments "nommés" sont prévus, mais on peut s'en passer en se limitant à une utilisation minimale pour implémenter un "property put" (voir le code plus bas). Alors, pour simplifier le code appelant, il est utile de développer un petit composant helper facilitant la manipulation d'un DISPPARAMS, par exemple :
// ...
class IHelpInvoke : public IUnknown
{
public:
virtual HRESULT PASCAL ResetParamCount (long _nParams) = 0;
virtual HRESULT PASCAL GetParamAddress (long _nIndex, VARIANT** _ppValue) = 0;
virtual HRESULT PASCAL CallGetProp (IDispatch* _pDisp, BSTR _pName, VARIANT* _pResult) = 0;
virtual HRESULT PASCAL CallPutProp (IDispatch* _pDisp, BSTR _pName) = 0;
virtual HRESULT PASCAL CallMethod (IDispatch* _pDisp, BSTR _pName, VARIANT* _pResult) = 0;
};
// fonction d'instanciation
HRESULT CreateHelpInvoke (REFGUID _iid, void** _ppObject);L'implémentation de ce composant est relativement simple : la classe concrète contient un DISPPARAMS et un tableau de VARIANTs suffisamment grand (disons une centaine d'emplacements) où les VARIANTs sont initialisés à "vide" et détruits avec la fonction système VariantClear à chaque recommencement ResetParamCount et lors de l'autodestruction du composant. Le client peut lire et écrire une valeur dans l'un des VARIANTs en y accédant par adresse.
// ...
class ImplHelpInvoke : public IHelpInvoke
{
public:
// ... ici les virtuelles
public:
ImplHelpInvoke (); // constructeur
~ImplHelpInvoke (); // destructeur
private:
long m_nRefCount;
DISPPARAMS m_sDp;
VARIANT m_pArgs [100];
DISPID m_nDispid;
// ...
};
// ... une bonne partie de l'implémentation est omise
HRESULT ImplHelpInvoke::ResetParamCount (long _nParams)
{
// ... ici une boucle pour vider les VARIANTS actuels
// puis...
m_sDp.rgvarg = (VARIANT*) m_pArgs;
m_sDp. rgdispidNamedArgs = &m_nDispid;
m_sDp. cArgs = _nParams;
m_sDp. cNamedArgs = 0;
return S_OK;
}
HRESULT ImplHelpInvoke::GetParamAddress (long _nIndex, VARIANT** _ppValue)
{
if ((_nIndex >= 0) && (_nIndex < m_sDp.cArgs)
*_ppValue = &m_pArgs [m_sDp.cArgs - 1 - _nIndex];
else
*_ppValue = NULL;
return (*_ppValue) ? S_OK : E_INVALIDARG;
}
HRESULT ImplHelpInvoke:: CallGetProp (IDispatch* _pDisp, BSTR _pName, VARIANT* _pResult)
{
HRESULT l_nRes = S_OK;
DISPID l_nPropId;
l_nRes = _pDisp->GetIDsOfNames (IID_NULL, &_pName, 1, LANG_NEUTRAL, &l_nPropId);
if (l_nRes == S_OK)
l_nRes = _pDisp->Invoke (l_nPropId, IID_NULL, LANG_NEUTRAL, DISPATCH_PROPERTYGET,&m_sDp, _pResult, NULL, NULL);
return l_nRes;
}
HRESULT ImplHelpInvoke:: CallPutProp (IDispatch* _pDisp, BSTR _pName)
{
HRESULT l_nRes = S_OK;
DISPID l_nPropId;
l_nRes = _pDisp->GetIDsOfNames (IID_NULL, &_pName, 1, LANG_NEUTRAL, &l_nPropId);
if ((l_nRes == S_OK) && (m_sDp.cArgs > 0))
{
m_nDispid = DISPID_PROPERTYPUT;
m_sDp.cNamedArgs = 1;
l_nRes = _pDisp->Invoke (l_nPropId, IID_NULL, LANG_NEUTRAL, DISPATCH_PROPERTYPUT, &m_sDp, NULL, NULL, NULL);
m_sDp.cNamedArgs = 0;
}
return l_nRes;
}
HRESULT ImplHelpInvoke:: CallMethod (IDispatch* _pDisp, BSTR _pName, VARIANT* _pResult)
{
HRESULT l_nRes = S_OK;
DISPID l_nMethId;
l_nRes = _pDisp->GetIDsOfNames (IID_NULL, &_pName, 1, LANG_NEUTRAL, &l_nMethId);
if (l_nRes == S_OK)
l_nRes = _pDisp->Invoke (l_nMethId, IID_NULL, LANG_NEUTRAL, DISPATCH_METHOD,&m_sDp, _pResult, NULL, NULL);
return l_nRes;
}
// ... reste de l'implémentation omisVoyons maintenant comment on peut utiliser le helper pour appeler différentes implémentations de IDispatch dans Excel, avec un exemple simplifié où on instancie un serveur Excel avec un classeur vide par défaut, on écrit une valeur dans la première cellule de la première feuille et on sauvegarde le résultat dans un fichier au format XLS.
{
// ...
GUID l_sExcelClsid; // CLSID de l'application Excel
IHelpInvoke* l_pHelper;
IDispatch* l_pDispExcel; // racine de l'application
IDispatch* l_pDispWorkbooks; // collection de classeurs (?!)
IDispatch* l_pDispWorkbook; // permier classeur
IDispatch* l_pDispWorksheets; // collection des feuilles du premier classeur
IDispatch* l_pDispWorksheet; // première feuille du premier classeur
IDispatch* l_pDispRange; // première cellule
VARIANT l_vResult;
VARIANT* l_pParam;
// ...
VariantInit (&l_vResult);
CreateHelpInvoke (IID_IHelpInvoke, (void**) &l_pHelper); // instancie le helper
CLSIDFromProgID (L"Excel.Application", &l_sExcelClsid); // récupère le CLSID de l'Excel installé
CoCreateInstance (l_sEcelClsid, NULL, CLSCTX_SERVER, IID_IDispatch, &l_pDispExcel);
// à ce stade un Excel.exe est lancé avec la commande /Automation
// le système crée un fantôme IDispatch qui appelle Excel en passant par des RPCs
l_pHelper->ResetParamCount ( 0 );
l_pHelper->CallGetProp (l_pDispExcel, L"Workbooks", &l_vResult);
l_pDispWorkbooks = l_vResult. pdispVal; // accès aux workbooks
VariantInit (&l_vResult);
// accès premier Workbook
l_pHelper->ResetParamCount ( 1 );
l_pHelper->GetParamAddress ( 0, &l_pParam );
l_pParam->vt = VT_I4; l_pParam->lVal = 1;
l_pHelper->CallGetProp (l_pDispWorkbooks, L"Item", &l_vResult);
l_pDispWorkbook = l_vResult. pdispVal;
VariantInit (&l_vResult);
// accès Worksheets
l_pHelper->ResetParamCount ( 0 );
l_pHelper->CallGetProp (l_pDispWorkbook, L"Worksheets", &l_vResult);
l_pDispWorksheets = l_vResult. pdispVal;
VariantInit (&l_vResult);
// accès premier Worksheet
l_pHelper->ResetParamCount ( 1 );
l_pHelper->GetParamAddress ( 0, &l_pParam );
l_pParam->vt = VT_I4; l_pParam->lVal = 1;
l_pHelper->CallGetProp (l_pDispWorksheets, L"Item", &l_vResult);
l_pDispWorksheet = l_vResult. pdispVal;
VariantInit (&l_vResult);
// accès première cellule
l_pHelper->ResetParamCount ( 1 );
l_pHelper->GetParamAddress ( 0, &l_pParam );
l_pParam->vt = VT_BSTR; l_pParam->bstrVal = SysAllocString (L"A1");
l_pHelper->CallGetProp (l_pDispWorksheet, L"Range", &l_vResult);
l_pDispRange = l_vResult. pdispVal;
VariantInit (&l_vResult);
// affectation première cellule
l_pHelper->ResetParamCount ( 1 );
l_pHelper->GetParamAddress ( 0, &l_pParam );
l_pParam->vt = VT_I4; l_pParam->lVal = 1234;
l_pHelper->CallPutProp (l_pDispRange, L"Value");
// sauvegarde XLS
l_pHelper->ResetParamCount ( 2 );
l_pHelper->GetParamAddress ( 0, &l_pParam );
l_pParam->vt = VT_BSTR; l_pParam->bstrVal = SysAllocString (L"test.xls");
l_pParam ++; l_pParam->vt = VT_I4; l_pParam->lVal = xlWorkbookNormal; // -4143 en dur
l_pHelper->CallMethod (l_pDispWorkbook, L"SaveAs", NULL);
// informe le processus Excel.exe que c'est terminé
l_pHelper->ResetParamCount ( 0 );
l_pHelper->CallMethod (l_pDispWorkbooks, L"Close");
// ...
l_pDispRange->Release ();
l_pDispWorksheet->Release ();
l_pDispWorksheets->Release ();
l_pDispWorkbook->Release ();
l_pDispWorkbooks->Release ();
l_pDispExcel->Release ();
l_pHelper->Release ();
}Même avec un helper le code source en C++ est fastidieux, si on le compare à la ligne VB :
<excel object>.Application.Workbooks (1).Worksheets (1).Range ("A1").Value = 1234
On peut se consoler en se disant que le client C++ peut attaquer un composant programmable de manière détaillée avec accès à toutes les méthodes possibles, cachées ou non. Si on examine les librairies de types publiées par la version d'Excel installée sur le poste avec l'utilitaire OLEVIEW.EXE de Visual Studio 6 et que l'on génère l'équivalent IDL (menu contextuel "View") on voit tout ce qu'il est possible d'appeler en C++, au-delà de la documentation officielle.
Ambiguïtés du contrat IDispatch
La specification du contrat standard suggère qu'un composant implémente une seule interface de ce genre, mais le langage IDL prévoit qu'une coclass puisse implémenter autant d'interfaces IDispatch qu'on veut (duales ou pas). Or, si on demande au composant la VTable IDispatch, il ne peut y en avoir qu'une. Comme dans le cas de l'héritage multiple d'une classe concrète qui implémente plusieurs interfaces COM mais une seule fois IUnknown, ici on se retrouve avec plusieurs classes de bases dérivées de IDispatch, mais une seule implémentation IDispatch. Mais, alors, cette seule implémentation devrait répondre correctement aux invocations de toutes les propriétés et méthodes de toutes les interfaces, comme s'il n'y avait qu'une seule interface d'union en vrac.
Ce tour de passe-passe est difficile à mettre en oeuvre et à faire évoluer, parce qu'il représente un raccourci à la sauvette, sensé pallier rapidement à de graves problèmes de conception. Il est toutefois assez fréquent de rencontrer des "vracs" de propriétés et méthodes (par dizaines, ou même centaines), en lieu et place d'un object model qui devrait offrir une navigation dans des composants imbriqués, ayant chacun un réel "métier", et implémentés dans différentes classes concrètes (pas dans un seul monstre central). Il va de soi que de tels vracs sont à éviter et, personnellement, j'irais même jusqu'à les prohiber, parce qu'à travers les librairies de types, on publie, en quelque sorte, les propriétés et méthodes pour qu'elles soient appelées par des développeurs, mais aussi par des utilisateurs de scripts simples. Ce n'est pas la même chose que de fournir des headers à des collègues..., on ne peut pas se permettre de ne plus assurer le fonctionnement de scripts clients, du jour au lendemain, sous prétexte qu'on a mieux redéfini cette partie-là (et je sais de quoi je parle, hélas...). Toute erreur de conception dans les librairies de types devra être supportée pendant assez longtemps, alors il vaut mieux ne pas se précipiter.