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).