Rappel : Ce cours est enseigné dans la
spécialité PISE du Master MECI (Université Paris 7)
par Christophe Darmangeat

Partie 10
Les contrôles à la chaîne
1. La gestion dynamique des contrôles
Jusqu'à présent, lorsque nous voulions utiliser des contrôles dans une application, nous avons toujours procédé de la même manière : au départ, on crée tous les contrôles nécessaires à un moment ou à un autre dans l'application, quitte à en cacher provisoirement quelques uns hors de la vue de l'utilisateur. Et ensuite, on permet à l'utilisateur d'effectuer certaines actions sur ces contrôles.
Cette stratégie, dans 99 % des cas, donne des résultats tout à fait convenables. Mais pour le 1% restant, il est bon de connaître une autre manière de s'y prendre : celle qui va nous permettre de créer et de faire disparaître les contrôles au cours de l'exécution de l'application.
Un exemple de ce 1%, nous est donné par un jeu comme le démineur, que tout le monde connaît pour l'avoir pratiqué de longues heures durant, à l'insu de son chef de bureau. Au démineur, les règles du jeu sont invariables. Mais en revanche, l'utilisateur a le droit de choisir entre plusieurs tailles de damier. Pire, il peut même choisir librement le nombre de cases en ligne et colonne.
Une manière barbare de programmer cela, serait de créer au départ le plus grand damier autorisé pour le jeu ; puis ensuite, à la lumière du choix effectué par le joueur, de masquer les cases qui n'ont pas lieu d'être. Mais c'est une solution d'une part bien peu élégante (et, question philosophique, l'élégance n'est-elle pas la marque de tout bon programmeur ?), d'autre part très lourde en termes de ressources mémoire, puisqu'on va mobiliser de la place pour gérer des dizaines de contrôles qui s'avèreront le plus souvent aussi superflus que le peigne de Fabien Barthez.
Il convient donc de posséder la technique permettant de créer, de manipuler et de détruire des contrôles par des instructions figurant dans l'application elle-même. Ce n'est pas vraiment difficile, et en plus, c'est une excellente manière de se préparer à la programmation objet proprement dite.
Alors, comment procéder ? Réfléchissons un peu, ça ne pourra pas nous faire de mal.
Si nous créons un contrôle de toutes pièces au cours de l'exécution, et si l'on veut que ce contrôle puisse servir à quelque chose, il va bien falloir qu'existent les procédures d'événements qui lui sont liées. Or, on n'a aucun moyen de créer en cours de route lesdites procédures (un programme ne peut pas écrire des lignes de programme). Ainsi, si l'on veut que notre contrôle soit autre chose qu'un simple élément décoratif, il faut que les procédures événementielles qui s'y rapportent aient été crées à l'avance. Comment est-ce possible ? Tout simplement en ne créant dynamiquement que des éléments de groupes, dont on aura préalablement défini le premier élément et les procédures associées.
Quatre-vingt-dix-neuf fois sur cent, il faut donc appliquer la stratégie suivante :
  1. On crée à la main l'élément numéro zéro d'un groupe de contrôles (quitte à le masquer provisoirement), et on définit ses propriétés.
  2. On écrit les procédures événementielles liées à ce groupe
  3. On engendre de manière dynamique les autres éléments du groupe, dans la quantité souhaitée.
Pour créer un élément supplémentaire d'un groupe de contrôles, on emploiera le code suivant :
Load NomduGroupe(i)
où "NomduContrôle" est évidemment le nom du groupe, et "i" l'index de l'élément qui sera créé.
Remarque importante : tout nouvel élément, créé par Load, d'un groupe, est situé par défaut à l'emplacement exact de l'original, et invisible. Il faudra donc le plus souvent modifier ses propriétés Top et Left, ainsi que sa propriété Visible.
Pour supprimer un élément d'un groupe de contrôles, on écrira :
Unload NomduGroupe(i)
C'est aussi simple que cela ! Pour peu qu'on ne fasse pas n'importe quoi, la création et la destruction dynamiques de contrôles ne posent donc pas la moindre difficulté. En fin de compte, pour conclure sur ce point : même si le cheminement diffère un peu, les instructions Load et Unload nous permettent de parvenir au même résultat avec les groupes de contrôles que l'instruction Redim avec les tableaux de variables.
 
Nom de l'exercice
Exécutable
Sources
Damier Extensible



2. La notion de collection
Rien à voir avec la haute couture de saison, voici pour conclure un concept fort utile dès que l'on est amené à gérer à la queue leu leu des hordes sauvages de contrôles.
Dans cet esprit, on disposait déjà d'un outil simple, mais très performant : les groupes
, qui comme le disait le sage, à savoir moi-même, sont aux contrôles ce que les tableaux sont aux variables.  Eh bien, on pourrait dire, pour filer la métaphore, que les collections sont aux contrôles ce que les variables structurées sont aux variables : un assemblage d'éléments au besoin hétéroclite, permettant un traitement global là où il fallait auparavant prendre les éléments un par un.
Première bonne nouvelle : tous les contrôles posés sur une feuille appartiennent de droit à la collection de la feuille. Au sein de cette collection, exactement comme dans un groupe, chaque élément est désigné par un numéro d'indice (correspondant ici à l'ordre de création de l'élément dans la feuille).
Pour accéder aux éléments de la collection, on pourra employer la propriété Controls de la Form, par exemple de la manière suivante :
Form1.Controls(5).Visible = False
...qui masquera le sixième contrôle créé sur Form1.
A noter que la propriété Controls n'est pas accessible depuis la fenêtre des propriétés du mode conception, mais uniquement par le code.
Evidemment, prendre les contrôles un par un par leur numéro d'indice, c'est bien, mais le problème se pose vite de savoir à quel numéro s'arrêter (sinon, vlan, c'est le dépassement d'indice direct garanti sur facture). Pour cela, deux solutions :

  1. La propriété Count de la collection Controls, qui dénombre le nombre de contrôles qu'elle contient. Pour rendre visibles tous les contrôles d'une feuille, nous pourrons donc écrire :
    For i = 0 to Form1.Controls.Count - 1
      Form1.Controls(i).Visible = True
    Next i
    Mais il y a une autre ruse, bien plus redoutable...
  2. C'est qu'en VB, on dispose d'une instruction de boucle spéciale pour les collections de contrôles. Il s'agit de l'instruction
    For Each Truc in Machin
    où "Machin" est la collection de contrôles (donc Form1.Controls, par exemple), et "Truc" est une variable qui prendra successivement la valeur de chaque contrôle de la collection. Une telle variable, qui ne contient pas une valeur mais un objet, est appelée... une variable objet. Etonnant, non ? Ce qui nous permettra d'écrire une boucle aussi simple que :
    For Each Bidule in Form1.Controls
      Bidule.Visible = True
    Next Bidule
Quelle que soit la méthode utilisée, parcourir automatiquement toute une série, c'est bien, mais pouvoir déterminer à quel type de contrôle on a affaire, ce serait nettement mieux. Car pour certains traitements, on a besoin de savoir si le contrôle est un bouton, une zone de texte ou tout autre chose avant de pouvoir lui infliger quoi que ce soit.
Eh bien, une fois de plus, VB a tout prévu, par l'instruction :
Typeof Bidule Is Nomtype
Où "Bidule" est un contrôle (ou une variable objet), et "Nomtype" un mot-clé désignant un type (une classe) de contrôles : CommandButton, TextBox, ListBox, etc.
Pour conclure, imaginons un fragment de code verrouillant toutes les zones de texte d'une feuille, sans que ces zones de texte aient nécessairement été créées comme un groupe de contrôle :
For Each Machin In Form1.Controls
If Machin Is TextBox Then
  Machin.Locked = True
Endif
Next Machin
Et le tour est joué. Puissant, n'est-il pas ?


3. Insérer des menus
Une application Windows digne de ce nom est fréquemment pilotée par des menus. Avant d'aller plus loin, examinons quelques points de vocabulaire :

Concevoir des menus pour une application VB est d'une simplicité confondante, dans la mesure où il existe un outil spécial : le créateur de menus, accessible par la commande Outils - Créateur de Menus. Ensuite, c'est une affaire de quelques minutes de prise en main.
Cet outil fait apparaître l'ensemble de la structure des menus d'une Form donnée, et nous permet de la gérer. Le seul point à comprendre est que chaque élément de menu, pour VB, est un objet, et à ce titre possède donc un certain nombre de propriétés :
  • Caption : le texte du menu, tel qu'il apparaît à l'écran (ex : Fichier, Edition, Affichage, etc.)
  • Name : le nom de cet élément de menu pour l'application.
  • Checked : indique si l'élément de menu est coché par un petit "v" à sa gauche
  • Enabled : indique si l'élément de menu est actif ou inactif
Dans la liste des menus, les niveaux des retraits indiquent le niveau des menus. Les séparateurs correspondent tout simplement à des éléments qui ont un Name, mais dont le Caption est un simple trait d'union.
Ensuite, chaque élément de menu, qui je le rappelle, est un objet, va pouvoir déclencher un traitement par la programmation de la procédure qui lui sera associée :
Private Sub NameElementMenu_Click()
...
End Sub
Et voilà, c'est aussi simple que cela. Un petit entraînement ?

Nom de l'exercice
Exécutable
Sources
Au Tord-Boyaux

VB permet également de programmer des menus dits pop-up ou contextuels. En deux mots : après avoir créé le menu par le créateur de menus, il suffit en quelque sorte de l'appeler par le code adéquat, en l'occurrence :
Popupmenu NameDuMenu, ConstanteVb
Cette instruction Popupmenu sera le plus souvent placée dans une procédure MouseDown, après un test vérifiant que c'est bien le bouton droit qui a été enfoncé. Mais on peut imaginer des menus contextuels apparaissant à d'autres occasions. Quant à la constante vb qui conclut l'instruction, elle indique le positionnement du menu par rapport à la souris (voir l'aide VB sur ce point précis).
Enfin, pour qu'un menu soit exclusivement contextuel et n'apparaisse pas en permanence dans la barre de menus du haut de la Form, il suffit de mettre dans le créateur de menus sa propriété Visible à False, et le tour est joué.
Le bonheur, cela tient parfois à peu de choses.


4. Les interfaces à Form multiples (MDI)
Nous voilà armés pour faire sauter une nouvelle limite dans VB : celle de la form unique.
En effet, jusqu'à présent, nos applications ne comportaient qu'une Form et une seule à la fois. Même si nous disposions de plusieurs Form, celles-ci n'étaient disponibles pour l'utilisateur que successivement : le code cachait une Form, en rendait une autre visible, et c'était tout ce que l'on pouvait faire.
Or, dans Windows, la plupart des applications utilisent plusieurs Form en même temps, ou tout au moins laissent cette possibilité ouverte. Qu'on pense à Word : il y a la fenêtre principale, celle de Word lui-même. Mais ensuite, au sein de cette fenêtre, l'utilisateur peut ouvrir autant de documents qu'il le souhaite, et chacun de ces documents apparaîtra dans une nouvelle fenêtre.
Il est donc grand temps que nous apprenions à utiliser cette possibilité avec VB.
Une application VB gérant des fenêtres multiples est dite application M.D.I. (pour Multiple Document Interface). Dans une telle application, il y a obligatoirement une Form mère (et une seule) et un certain nombre de Form filles, contenues dans la Form mère.
Tout ceci peut être créé à la main, ou par du code... et vous comprenez pourquoi nous abordons ceci après avoir vu les instructions Load, Unload et la notion de Collection.
4.1 Création de la Form MDI
Commençons par la création à la main de la Form mère. On choisira dans le menu adéquat une nouvelle Form, mais d'un type particulier : la Form MDI. Celle-ci apparaît avec un fond spécial, plus sombre qu'une Form classique :

Une Form MDI ne servant que de conteneur aux Form filles, on ne peut pas poser dessus n'importe quels contrôles. En fait, une telle Form ne peut contenir qu'un nombre très limité de choses :
  • un Timer
  • une ImageBox (qui sera alors automatiquement de la largeur de la Form elle-même)
  • quelques autres contrôles exotiques que nous ne connaissons pas encore...
  • des barres d'outils
  • des menus
Si ce site n'explique pas comment créer des barres d'outils, on vient en revanche de voir comment créer des menus. Donc, pas de problèmes.
Pour finir sur les Form mères, remarquons qu'il ne peut y en avoir qu'une et une seule par projet VB. Si vous essayez d'en créer une deuxième, VB vous l'interdira aussi sec. Logique.
Passons maintenant aux Form filles. Une Form fille est une Form tout ce qu'il y a de plus normal. Elle a simplement une caractéristique, c'est que sa propriété MDIChild vaut True. C'est cela qui indique à l'application que cette Form doit apparaître au sein de la Form mère de l'application. Il peut bien entendu y avoir autant de Form filles que l'on souhaite.
Cette histoire de mère et de filles, c'est donc vraiment... un jeu d'enfant (celle-là, je ne pouvais décemment pas passer à côté).
Pour qu'à l'exécution, une Form fille apparaisse dans la Form mère, il suffit que la Form fille soit l'objet d'une instruction Load. Pour la faire disparaître, évidemment, l'instruction Unload s'impose.
4.2 Créer les Form filles par du code
Une Form étant un objet comme n'importe quel autre, on peut lui appliquer les méthodes de création dynamiques vues au début de ce chapitre. Malheureusement, il n'est pas possible de créer un groupe de Form, comme nous le faisions pour les autres contrôles. Alors, serions-nous coincés ?
Que nenni. Si la première Form de notre application s'appelle FormIntérieure (propriété Name), on pourra taper le code suivant pour créer une nouvelle Form (par exemple, lorsque l'utilisateur clique sur un éventuel menu intitulé Nouveau) :
Dim Toto As New FormIntérieure
Toto.Visible = True
La première ligne crée une copie de l'objet FormIntérieure (on parle en jargon objet de nouvelle instance), qui possède en tout point les mêmes propriétés, les mêmes contrôles, etc. Ce nouvel objet, copie conforme de FormIntérieure, est stocké dans une variable objet qui permet de le désigner, en l'occurrence Toto.
Petit souci : toutes les nouvelles Form ainsi créés vont être successivement désignées par la variable Toto. Et comme elles ne forment pas un groupe, on est a priori bien en peine de les différencier.
A cela, il existe deux parades simples, efficaces et de bon goût.
Lorsqu'il s'agit de traiter toutes les Form d'une application, on pourra utiliser la notion de collection vue plus haut, et écrire une boucle adéquate :
For Each truc In Forms
truc.BackColor = vbWhite
Next truc
...La collection Forms nous permettant de balayer toutes les Forms de l'application une par une.
Enfin, dernier problème abordé ici, lorsque nous avons besoin de localiser un contrôle d'une application MDI, nous ne savons pas forcément de quelle Form est issu ce contrôle. Imaginons que nous voulions, à un moment donné, changer ce qu'il y a écrit sur le bouton de commande Bouton1. On va alors être obligé, faute de provoquer une erreur, de préciser qu'il s'agit du contrôle Bouton1 de la Form fille active, par opposition aux autres boutons Bouton1 des Form filles non actives). Car à ce moment là de l'application, il y a autant de Bouton1 que de Form filles dans l'application ! Pour parler du Bouton1 de la Form active, rien de plus simple :
ActiveForm.Bouton1.Caption = "coucou"
Vous voyez, pour conclure, que VB nous ouvre assez facilement les portes de la réalisation d'applications au look vraiment professionnel. Après, plus ça grossit, plus il faut être méthodique pour ne pas s'y perdre...