News letter : Risques
R2O Pilotage | R2O Rating | R2O Side Business | R2O Crédit | R2O CPM | R2O Stress | R2O RiskOp

Connecter une DLL à Excel

1. Principe

Le Visual Basic est un langage interprété, c'est à dire qu'il traduit chaque commande au fur et à mesure qu'il les rencontre. La simplicité d'utilisation milite pour ce type d'utilisation. Cependant, les principaux problèmes sont la lenteur, d'une part, et la nécessité de l'interpréteur de commande pour chaque exécution d'autre part. Dans le cas de fonctions Excel, le critère déterminant est la lenteur d'exécution des boucles. On cherchera donc a utiliser au maximum les fonctions intégrées qui sont optimisées, mais ce n'est pas toujours possible. Aussi trouvera-t-on avantage à utiliser des programmes compilés (au contraire d'interprété), c'est à dire que l'on lance une fois pour toute la compilation, puis on obtient un programme exécutable, mais non modifiable. Là, les boucles deviennent très rapides et globalement, les programmes sont plus performants. Cependant, si l'on obtient des gains en temps d'exécution de l'ordre de 5 à 10 pour des programmes utilisant massivement des boucles, il faut savoir que la structure est plus lourde, donc plus consommatrice de mémoire, et que l'implémentation présente certaines difficultés. Aussi n'utilisera-t-on cette solution que pour des fonctions particulièrement longues à calculer (type simulations). Le programme se décompose alors en deux parties : l'une en VBA sous Excel, l'autre en C++ dans une DLL. Il n'est pas possible d'appeler directement une fonction C++ dans la feuille de calcul, aussi devrat- on passer par une fonction publique VBA qui appelle la fonction de la DLL. Cette étape n'est d'ailleurs pas inutile : en effet, le passage des paramètres en entrée comme en sortie de la DLL constitue une phase critique. Une petite erreur peut générer une erreur importante, qui forcera la fermeture d'Excel (le debogage ne marche que partiellement sous Excel). On pourra donc vérifier les paramètres de la fonction sous VBA avant de les transmettre à la DLL. Les fonctions seront déclarées des deux côtés : sous VBA, on donnera la liste des fonctions des DLL utilisées, et sous C++ on donnera la liste des fonctions accessibles dans la DLL. On peut passer tous les types d'arguments simples, mais les tableaux requièrent un traitement spécifique que nous verrons par la suite.

2. Comment créer la DLL ?

Nous ferons ici l'hypothèse que le lecteur possède des connaissances sur la programmation en C. Rien d'autre n'est véritablement nécessaire. Visual C++ 5.0 offre un assistant qui permet de créer une DLL. Pour cela, lancez Visual C++ , puis faites File/New et cliquez sur MFC AppWizard (dll) dans l'onglet Projects. Choisir un nom pour votre DLL dans la case "Project Name" et le répertoire de travail. Cliquez sur OK, puis choisissez "Regular DLL using shared MFC DLL", et laissez les options par défaut. Cliquez alors sur Finish, puis sur OK dans la fenêtre résumé qui apparaît ensuite. Votre projet est maintenant initialisé. Supposons que notre projet s'appelle "Manuel". La fenêtre Workspace qui affiche la structure de votre projet doit apparaître (sinon faites View/Workspace). Dans les onglets, choisissez FileView, puis descendez l'arbre (ici Manuel Files, puis Source Files) pour atteindre Manuel.cpp. Vous avez alors en face de vous le code source du programme. Rien d'intéressant n'est à observer, et vous n'avez pas besoin de comprendre le code. Vous pouvez alors commencer à entrer vos fonctions directement (sans faire de programmation objet) après l'instruction END_MESSAGE_MAP(). Il faut simplement ajouter dans votre déclaration l'instruction stdcall. Par exemple :

double _stdcall Test(double z)
{
return z+2 ;
}


Il n'est pas nécessaire de faire de déclaration dans le fichier.h, l'intérêt de le faire étant simplement de pouvoir appeler une fonction en amont du code de la fonction appelée. Par contre, chaque fonction créée doit être déclarée dans le fichier .def, juste après le mot clé EXPORTS. Ne doit figurer que le nom de la procédure. Par exemple (après EXPORT) :

Test

Votre fonction est alors exploitable. Compilez (F7), puis vous pouvez passer à la partie Excel (la DLL se trouve dans le sous répertoire Debug du répertoire de travail). Lors d'un changement du type ou du nombre de paramètre de la fonction dans le code, il faut forcer le lien entre cpp et def, en modifiant d'une manière ou d'une autre le fichier .def. On pourra effacer un caractère, et le réécrire manuellement par exemple.

3. Comment appeler la DLL ?

La fonction de la DLL doit être déclarée dans le module VBA. Pour cela, il suffit d'ajouter au début de votre programme VBA le code suivant :

Private Declare Function Test Lib "Manuel.dll" (ByVal y As Double) As Double

si la DLL créée se trouve dans le répertoire Office. L'argument ByVal est conseillé (pas de passage par paramètre dans l'appel). Private est nécessaire pour que la fonction ne soit pas accessible à partir de la feuille de calcul (ce qui donnerait une erreur). Attention, il faut veiller à l'exacte similitude des types des arguments envoyés (Le type Integer n'est pas compatible. On utilisera plutôt le type Long). Vous déclarez toutes les fonctions de la DLL de la même manière. Une fois cette ligne écrite, vous pouvez appeler la fonction de la DLL dans votre programme VBA comme s'il s'agissait d'une fonction VBA. Exemple :

Public Function TestVBA(z As Double) as Double
TestVBA=Test(z)
End Function


Attention de ne pas utiliser le même nom pour votre fonction DLL et votre fonction VBA.

4. Le passage de tableaux

4.1 En sortie :

Les tableaux ne sont pas échangeables simplement entre les deux applications (VBA ignore la construction sous forme de pointeurs du C). Pour pouvoir obtenir un résultat sous forme de tableau, il faut utiliser un objet spécifique de C++, COleSafeArray. Vous pouvez vous reporter à la documentation C++ pour en apprendre plus sur cette fonction. Rien ne parle mieux ici qu'un exemple !

La procédure c_passe_tableau prends un entier long i en paramètre, et renvoi un tableau 10*10 dont les cases sont remplies ici par i*(numéro de colonne).
Voici le code C :

VARIANT _stdcall c_passe_tableau(long i){
//Exemple de passage de tableau en sortie de fonction
COleSafeArray saRet ;
//Crée un objet COleSafeArray qui permet de placer un tableau dans un Variant
DWORD numElements[] = {10, 10} ; //Taille du tableau 10x10

//Création du safe-array
saRet.Create(VT_R8, 2, numElements) ;
//VT_R8 = données numériques sur 8 octets, dimension 2, taille = numElements (10*10)

//Initialisation avec des valeurs
long index[2] ;
//Tableau à deux éléments contenant pour l'un i et l'autre j pour pointer sur Aij
for(index[0]=0 ; index[0]<10 ; index[0]++) {
for(index[1]=0 ; index[1]<10 ; index[1]++) {
double val = index[1]*i ;
saRet.PutElement(index, val) ;

//Place la valeur pointée par val, ie val, dans la case repérée par index[0],index[1]
}
}


//Retourne le safe-array encapsulé dans le VARIANT
return saRet.Detach() ;
}


En VBA, on aura d'une part la déclaration :

Private Declare Function c_passe_tableau Lib "Manuel.dll" (ByVal i As Long) As Variant

Et la fonction appelante :

Function EXEMPLE() As Double
Dim donj As Variant
Dim k As Long
k = 5
donj = c_passe_tableau(k)
EXEMPLE = donj(1, 4)
End Function


Qui renvoie le contenu de la case (1,4).

4.2 En entrée :

La procédure est légèrement différente, mais utilise le même objet C++ ColeSafeArray et aussi un VARIANT du côté VBA. Le programme suivant calcul le test de Cramer Von Mise pour une loi uniforme :

double _stdcall c_cramer_uniforme(COleVariant Objet, long Elem){
//Cramer Von Mise pour la loi uniforme, prend le tableau et sa taille
COleSafeArray Arrey ;
//cf. ci-dessus
double val=0, y = 0,a ;
long index[1] ;
Arrey.Attach(Objet) ;
a = Elem ;
if (a==0) exit(0) ;
for (index[0]=0 ;index[0]{
Arrey.GetElement(index,val) ;
y = y + ((index[0] + 0.5)/a - val)*((index[0] + 0.5)/a - val) ;

//Calcul du facteur
}
y = y + 1/(12 *a) ;

//Fait la somme
return y ;
}


Sous VBA, on a la déclaration :

Private Declare Function c_cramer_uniforme Lib "c :.dll" (ByVal plage As Variant, ByVal taille As Long) As Double

Puis l'appel :

Function CRAMER_UNIFORME2(plage As Range) As Double
'Méthode C++
Dim i, NBL As Integer
Dim z() As Double
Dim c As Range
NBL = plage.Rows.Count
If plage.Columns.Count <> 1 Then Exit Function
'Si plus d'une colonne, sort de la procédure
ReDim z(NBL) As Double
i = 0
For Each c In plage
'Parcourt la plage
z(i) = c.Value 'Affecte au tableau
i = i + 1
Next c
CRAMER_UNIFORME2 = c_cramer_uniforme(z, NBL)
'Appel de la DLL
End Function

5. Quelques solutions et recommendations

5.1 Sous VBA :

Comme expliqué plus haut, VBA est un langage interprété, et a donc comme principal défaut sa lenteur d'exécution et comme principal avantage sa souplesse. En effet, notamment en terme de débogage, les possibilités offertes sont très nombreuses. Voici quelques points méritant d'être cités :

  • La commande "Option Explicit" au début du code peut être utilisée pour obliger le programmeur à déclarer par un Dim ("Dim A As Double" par exemple) toutes les variables utilis ées. On évite ainsi des erreurs d'orthographe dans le nom de variable du type " tzmp " au lieu de " temp " dont les conséquences peuvent être très graves.
  • Par défaut, les tableaux en VBA déclarés par "Dim t() As Double" et dimensionné par "Redim t(100)" sont indexés à partir de 0, comme en C. Pour d'évidentes raisons de compatibilité on gardera ce mode d'indexation, même si la commande "Option 1" permet de parcourir à partir de 1.
  • Le débogage d'une macro se fait très simplement en pressant F8 tandis que le curseur se trouve dans la macro. Pour une fonction, on aura intérêt à créer une macro appelant la fonction en question, et de deboger la macro et donc la fonction.
  • Le choix entre macro et fonction est généralement simple. Les macro ne seront utilisées que lorsqu'une sortie multiple est indispensable, c'est à dire que plusieurs cases doivent être modifiées en sortie. C'est le cas de la génération de suites aléatoires.
  • On utilisera au maximum les fonctions intégrées sous Excel, car celles-ci sont généralement optimisées (comme pour le tri par exemple).
  • Il sera plus intéressant de parcourir des cellules grâce à une commande du type "For Each Cellule in Plage" (avec Cellule étant un objet Range et Plage la plage de données consid érées) que d'utiliser une vraie boucle du type "For i = 1 to N". On fera notamment le choix de la première méthode pour l'affectation des valeurs dans un tableau.
  • Dans le cadre d'une fonction, les cellules de la feuille ne sont pas modifiables (sauf celle qui prend le résultat de la fonction et qui contient en fait l'appel).
  • Une méthode d'affectation directe de tableau à plage existe pour les macros. Il s'agit de la commande Plage.Value = t, où t est le tableau. Les plages sont remplies en fonction de leur dimension de haut en bas et de gauche à droite. Cette procédure est vraiment optimisée. La réciproque n'existe apparemment pas.

5.2 Sous C++ :

  • Un des points les plus délicats en C/C++ est la gestion des tableaux. Etant donné la consommation mémoire de certaines fonctions, notamment celles de test sur les pseudo-aléatoires, il faut veiller à bien allouer et désallouer les ressources. L'allocation se fait par la commande new, sur une variable déjà définie. Rien ne différencie au premier abord un pointeur sur une case mémoire d'un tableau, si ce n'est l'allocation mémoire. La taille du tableau pourra être fixée de manière dynamique, en fonction des variables définies à ce moment.
  • Les opérations sont liées aux types de données intervenant dans le calcul. Ainsi, selon le type entier ou non de A et B, C=A/B donnera pour C le quotient entier ou bien le quotient réel. On devra parfois, si l'on veut obtenir le quotient réel de deux variables définies comme entières, forcer la conversion en réel, par exemple en multipliant A/B par 1.0, soit C=1.0*A/B.
  • L'option _stdcall dans la déclaration de la fonction est indispensable pour que cette fonction soit visible pour le VBA.
  • Trois niveaux de procédures ont été implémentés pour les générateurs. Le premier niveau est celui d'un élément seul, le deuxième est celui des vecteurs et le dernier celui des matrices.
  • La fonction Rand() du C++ renvoie un entier de 0 à RAND_MAX (constante du langage). Donc, pour obtenir un réel entre 0 et 1 (1 exclu) on utilisera rand()/(RAND_MAX+1.0).
  • Pour des raisons de précisions, le type de donner _int64 a été utilisé. Les entiers sont alors représentés sur 64 bits, ce qui permet de faire fonctionner correctement les formules de récurrences des gros générateurs LCG. Une alternative pourrait être de prendre des réels avec grosses précisions, mais des erreurs d'approximations pourrait alors être commises.
  • Bien évidemment, on aura intérêt à calculer le maximum de paramètres à l'extérieur de la boucle. Si le programme en perd de la clarté, dans le cadre de simulations, les contraintes de temps rendent ces séparations indispensables.
  • Des Long peuvent être utilisé comme booléen (0=False, 1=True), car ce type n'est pas géré.

6. Comment créer un fichier d'aide

Un fichier d'aide se crée comme un programme, à savoir qu'il faudra lier et compiler un ensemble de fichiers sources pour obtenir un fichier d'aide utilisable. Trois principaux fichiers sont requis : un fichier "Projet" qui contient les paramètres principaux du fichier d'aide, l'allure des fenêtres, des boutons, etc., un fichier sommaire contenant les titres des rubriques et leur hiérarchisation et enfin un fichier RTF qui contient le texte de l'aide. Les deux premiers fichiers seront réalisés à l'aide d'un programme spécifique, Help Workshop, développé par Microsoft, qui de plus permettra la compilation de l'aide. Le troisième sera écrit par Word, en format RTF (Rich Text Format) et avec des règles précises décrites plus loin. Une courte description de la structure d'une aide est nécessaire avant de s'occuper de sa construction. Une aide se compose de topics, à savoir des sujets, qui correspondent à une page de l'aide. La particularité de l'aide par rapport à un fichier texte classique est qu'elle autorise des liens, comme un fichier HTML. Il est d'ailleurs à noter que prochainement, les fichiers d'aide seront programmés par défaut dans ce langage. Il existe deux types de page : l'une classique, et l'autre pop-up. Ces dernières viendront au-dessus d'une fenêtre classique, et disparaîtront d'un clic de souris. Il existe plusieurs types de lien : les liens Sommaire, qui utiliseront le fichier sommaire pour savoir quelle page appeler, les liens directs, qui apparaissent dans une fenêtre et en appelle une autre, les liens Index, qui appelle une page à partir d'un de ses mots clé, ou même les liens externes, qui appelle la page directement d'une application (Word, Excel) sans ouvrir le fichier d'aide. Le fichier d'aide se résume ainsi à plusieurs pages liées par différents liens. La programmation du fichier d'aide sera ainsi composée de trois principales parties : mise en place de l'aide, rédaction du texte (des topics) et établissement des liens.

6.1 Mise en place de l'aide ?

  • Ouvrir Word et Help Workshop (ver. 4.3 ou ultérieures)
  • Sous H.W. créé un nouveau Projet et sauvegarder le dans un répertoire sous, par exemple, le nom manuel (.hpj, extension par défaut)
  • Les options par défaut sont appliquées.
  • Sous Word, créé un nouveau fichier, vide, et sauvegarder le sous le format RTF, par exemple manuel.rtf. Ce fichier contiendra le texte de l'aide, garder le ouvert.
  • Dans HW, cliquer sur Files, puis Add et sélectionner le fichier RTF créé précédemment (manuel.rtf).
  • Dans HW, créer un nouveau fichier, du type Contents. Donner lui un nom (par exemple, manuel.cnt) et un titre, puis sauvegarder le (vide).
  • Revenir à la fenêtre du Projet sous HW. Faire Options, Files, et dans la case Contents File donner le fichier créé. Vous pouvez maintenant sauvegarder le fichier Projet, sans le fermer. Les trois fichiers ont été créés et liés, il suffit maintenant de les remplir.

6.2 Le Texte :

  • Dans le fichier RTF, chaque topic est séparé par un saut manuel de page. Le texte sera reproduit dans l'aide avec le même format (police, couleur) que dans le fichier RTF. Vous pouvez donc donner au texte l'apparence que vous voulez.
  • Vous pouvez par exemple créer un premier topic appelé Introduction. Rentrer le texte, avec le titre, comme si vous rédigiez directement la page d'aide. Une fois terminée, le topic doit être référencé.
  • Pour cela, il faut lui donner un Id, un titre et éventuellement des Keywords. Le système général est d'utiliser une note en bas de page, dont le caractère de renvoi indiquera le type de référence et dont le texte de la note elle-même donnera le contenu. Pour indiquer le Id du topic, dans le document RTF, devant le titre, faire Insertion/Note puis rentrer dans la case du caractère de renvoi le signe #. Dans la note en bas de page qui s'ouvre, taper les quelques caractères qui identifieront le topic. La même procédure est à suivre pour rentrer le titre en référence (en plus de l'avoir écrit dans le texte lui-même), si ce n'est qu'au lieu du caractère #, on utilisera $. Enfin, les Keywords concernant le topic sont à rentrer de la même manière, avec le caractère K au lieu du #. On les rentrera sur une même ligne, séparés par des points virgules en veillant à ce que Word ne rajoute pas de caractère " vide " avant le point virgule (ce qu'il fera par défaut).

6.3 Les liens :

  • Une fois le fichier RTF sauvegardé, avec par exemple le topic Introduction, dont l'ID est "intro" le titre "Introduction" et les keywords "Démarrage ;Présentation", on peut passer au linkage.
  • Liens dans le sommaire : Le fichier sommaire (cnt) décrit le sommaire qui sera présenté lors de la consultation de l'aide. On donnera donc à ce sommaire l'allure que l'on désire (avec les "headers" que l'on voudra), la seule contrainte étant de lier chaque Topic créé dans le sommaire à un topic existant dans le fichier RTF. Par exemple, on pourra avec la commande Add Below rajouter un header "Général", puis ensuite ajouter un topic "Introduction" avec comme topic Id "intro" (comme défini ci-dessus).
  • A l'intérieur du fichier RTF, on peut créer des liens vers un autre topic. Deux méthodes coexistent pour l'appel, l'une ouvrant le topic appelé à la place du précédent, l'autre ouvrant une fenêtre Pop-up. Le lien est composé de deux parties, le texte du lien (en vert souligné) et l'ID du topic cible. Dans le cas d'une fenêtre pop-up, le texte sera formaté en Souligné, dans le cas normal en Double Souligné (Format Police Soulignement =>double). Puis, collé sans espace au texte, on écrira l'Id du topic cible en format Masqué. Par exemple : On pourra consulter l'Introductionintro (intro doit être ici écrit en masqué, mais bien évidemment, pour des raisons de lisibilité, on l'a écrit en Empreinte).
  • Les Keywords apparaissant dans le fichier RTF seront automatiquement incorporés à l'index, et si plusieurs occurrences existent, une liste de choix avec le nom des topics concernés comme définis dans le fichier RTF apparaîtra.
  • Pour lier à partir d'une application, un Id numérique est nécessaire. Par exemple, dans Excel, sous VBA, il est possible dans l'explorateur d'objet de définir pour le projet un fichier d'aide et pour chaque fonction un id numérique appelé contexte. Pour que l'appel se fasse, il faut dans le projet définir des correspondances. Pour cela, il suffit de cliquer le bouton Map (dans le projet sous HW), et de faire correspondre à chaque topic un id numérique.
  • Pour lancer la compilation, ne pas oublier de sauvegarder tous les fichiers avant (ce n'est pas automatique sous HW).

Finance Factory | Solution R2O | Conseil | Informatique | Formation | Recherche | Références | ©2007 Finance Factory