Voila ce qui va t'être utile
Les collections
Jusqu'à présent, nous avons toujours considéré les contrôles individuellement. Un contrôle, un nom de contrôle, et au moins une ligne de code. Dix contrôles à traiter, au moins dix lignes de code. Cinquante contrôles à traiter, au moins cinquante lignes. Pas moyen d'y couper.
La seule combine qui nous a permis, dans certaines circonstances, de faire une entorse à ce principe, a été de brancher le même événement survenant sur plusieurs contrôles, sur une procédure événementielle unique, via l'instruction Handles. Ceci nous a également permis de manipuler les propriétés du contrôle ayant appelé la procédure, via le paramètre sender.
Ce que nous allons voir à présent, c'est comment on peut généraliser les occasions de traiter les contrôles en série, et plus seulement pièce par pièce.
1. La notion de Collection
Dans les versions précédentes de Visual Basic, existait un outil que tout programmeur débutant pouvait maîtriser en quelques coups de cuiller à pot, et qui rendait des services inestimables : les tableaux de contrôles. Cela fonctionnait de la même manière que les tableaux de variables, et cela ouvrait les mêmes possibilités.
Bref, c'était tellement simple, tellement intuitif et tellement efficace que dans la dernière version de VB, à savoir VB.Net, celle-là même que vous êtes en train d'étudier, bande de petits veinards, eh bien les tableaux de contrôles ont tout simplement disparu.
En lieu et place, nous voilà à présent pourvus d'une série d'ustensiles et d'un bric à brac plus ou moins heureux et maniable, dans lequel nous allons devoir piocher tant bien que mal en fonction des circonstances. Au passage, si quelqu'un de chez Microsoft comprend la logique de cette évolution du langage, et qu'il se sent capable de me l'expliquer, qu'il ne se gêne surtout pas. Il a néanmoins intérêt à être sacrément convaincant, parce qu'autant lui dire tout de suite, ce n'est pas gagné.
Bref, exit les tableaux de contrôles, que nous reste-t-il ? Un machin qui s'appelle les collections, machin qui se rapproche furieusement des tableaux de contrôles... sans jamais leur ressembler complètement.
Commençons par dire que par définition, sans même que nous ayons besoin de faire quoi que ce soit pour cela, tout contrôle posé sur une Form devient aussi sec un membre d'une collection, en l'occurrence la collection Controls. C'est-à-dire que ce contrôle devient le membre d'un ensemble, que nous allons traiter comme tel, et sur lequel nous allons pouvoir programmer des boucles. Car il est possible de balayer l'intégralité des membres d'une collection par une boucle, tout comme on peut balayer l'intégralité des éléments d'un tableau. Sauf que tout n'est pas toujours exactement simple dans cette affaire...
2. Désigner les contrôles par leur indice
La manière la plus évidente de désigner les contrôles dans une collection, va être de se référer à leur indice dans cette collection.
"Faudrait savoir !" Tel est le cri que j'entends déjà sourdre parmi les masses estudiantines opprimées, suite à la lecture de la phrase précédente. En effet, il y a à peine quelques paragraphes de cela, j'expliquais que les tableaux de contrôles n'existaient plus en VB.Net. Et là, je me ramène avec des collections où les contrôles sont désignés par des indices... Alors, incompétence manifeste ? Absence totale de conscience professionnelle ? Alzheimer précoce ? Schizophrénie caractérisée ?
Rien de tout cela. Dans la collection Controls, les contrôles possèdent certes un indice, et cela constitue un point commun avec les tableaux. Mais il reste tout de même de sérieuses différences :
On ne choisit pas toujours quels contrôles sont membres de la collection : comme on l'a vu, tous les contrôles d'une Form sont d'office membres de la collection Controls.
On ne choisit pas non plus, du moins en mode Design, l'indice attribué à chaque contrôle. Pour info, celui-ci est l"inverse de l'ordre de création des contrôles dans la Form : celui qui a été créé en dernier a l'indice 0, l'avant-dernier l'indice 1, etc. Ce qui s'avère extrêmement déroutant pour le novice.
En l'occurrence, pour balayer les contrôles d'une Form, on pourra donc faire une boucle sur l'indice de ces contrôles dans la collection Controls. Le premier indice est par définition 0. Quant au dernier, nous pouvons le déduire de la propriété Controls.Count, qui renvoie le nombre de contrôles dans la collection Controls. Dans la forme la plus étendue du code, il faudra, pour accéder à l'indice, passer par la propriété Item. Le code permettant par exemple de rendre visible tous les contrôles d'une Form sera donc :
- Code:
For i = 0 to Controls.Count - 1
Controls.Item(i).Visible = True
Next i
Le plupart des collections - car je le rappelle, il existe bien d'autres collections que la collection Controls - autorisent toutefois l'omission de la propriétés Item. On peut donc souvent écrire en abrégé, et pour le même résultat :
- Code:
For i = 0 to Controls.Count - 1
Controls(i).Visible = True
Next i
Remarque instructive :
Tous les contrôles membres d'un contrôle conteneur, tel Groupbox ou Panel, sont d'office membres de la collection que représente ce conteneur. Une ruse peut donc consister à placer sur la Form un conteneur (au besoin invisible) dans le seul but de pouvoir traiter les contrôles qui s'y trouvent comme une collection.
Remarque perfide :
Les contrôles membres d'un contrôle conteneur ne sont du même coup plus considérés comme membres de la collection Controls !
En fait, les contrôles d'une Form sont organisés en quelque sorte dans des collections de différents niveaux.
Dit d'une autre manière, les différentes collections d'une Form représentent une véritable arborescence, dont la collection Controls n'est que la racine.
Voilà une remarque à méditer longuement.
Bon. Certes. Mais il y a d'autres moyens de parcourir une collection...
3. La boucle For Each ... Next
Cette structure de boucle permet spécifiquement de parcourir une série de contrôles. Tout repose sur le fait que la variable qui va servir de compteur à la boucle n'est ici plus un nombre, comme dans toutes les boucles For ... Next que nous avons écrites jusque là, mais que cette variable représente directement un contrôle. Autrement dit, la boucle va se servir d'une variable qui va désigner successivement plusieurs contrôles. Donc d'une variable de type Control.
Le code précédent pourra ici être réécrit comme :
- Code:
For Each Truc in Controls
Truc.Visible = False
Next Truc
La variable Truc étant de type Control, sa déclaration aura auparavant été :
Dim Truc as Control
En soi, et sur l'exemple choisi, cette technique n'apporte rien de spécial par rapport à la précédente. Mais comme on le verra plus loin, il y a des circonstances où elle révèlera des avantages notables.
Il faut noter que dans le cas où la collection parcourue ne comporte que des contrôles de la même classe, on peut tout à fait déclarer la variable objet directement comme un membre de la classe en question. Ainsi, au lieu d'avoir un :
Dim Truc as Control
On pourrait très bien déclarer Truc comme Label :
Dim Truc as Label
ou comme bouton :
Dim Truc as Button
...etc.
Encore une fois, cela suppose que tous les contrôles de la collection soient bien de la classe spécifiée. Dans le cas contraire, il y aura forcément une erreur à l'exécution...
4. Tester le type d'un contrôle
Avançant à l'aveuglette parmi les membres divers et variés de la collection Controls de la Form (ou parmi toute autre sous-collection, telle celle d'un contrôle conteneur), nous pourrions bien avoir parfois besoin d'un moyen pour identifier ne serait-ce que la classe des ces différents membres. C'est possible grâce à l'instruction
Typeof ... Is
Par exemple, le code suivant met à blanc toutes les zones de textes (et uniquement elles) d'une Form :
- Code:
For Each bidule in Controls
If TypeOf bidule is Textbox Then
bidule.Text = ""
Endif
Next bidule
Jouons donc un peu...
On peut donc reprendre le dernier exercice proposé (Options) et utiliser les connaissances que nous venons d'acquérir afin de faire subir au code une cure d'amaigrissement.
Exercice Exécutable Sources
Options
Sondage
5. Créer ses propres collections par du code
Il y a la collection Controls, créée et gérée automatiquement par VB, avec toutes les limites que nous venons de découvrir. Il y a les collections que constituent automatiquement les contrôles dits "conteneurs" (tels que GroupBox ou Panel). Mais il existe aussi la possibilité de créer ses propres collections par du code. Bien que cela soit un peu fastidieux, c'est parfois le seul moyen de gérer correctement certains problèmes.
5.1 Déclarer une collection
La première chose à faire sera de déclarer la collection, en lui donnant un nom (exactement comme on déclare une variable, ou un tableau). Par exemple :
Dim Ensemble as New Collection()
5.2 Remplir et vider une collection
Ensuite, il va falloir entrer dans cette collection, un par un, tous les éléments qui devront en faire partie, via la méthode Add. Par exemple :
- Code:
Ensemble.Add(Me.Textbox1)
Ensemble.Add(Me.Textbox2)
Ensemble.Add(Me.Label5)
etc.
Disons tout de suite qu'on peut, de même, retirer un contrôle d'une collection par la méthode Remove :
- Code:
Ensemble.Remove(Me.Label5)
Arrivés à ce stade, nous savons donc gérer comme bon nous semble les membres d'une collection. Cela nous permet notamment de pouvoir procéder sans problèmes à des balayages systématiques (par exemple pour initialiser les valeurs de toute une série de contrôles, pour les rendre visibles ou invisibles, etc.). Cela nous permet également de désigner chaque contrôle membre de notre collection par son indice dans cette collection, exactement comme nous le faisions avec la collection prédéfinie Controls. Enfin... presque !
Remarque bonne pour la santé mentale :
Les concepteurs de Visual Basic .Net ont décidé que si les indices des collections prédéfinies, telle Controls, commençaient en toute logique à zéro, les indices des collections créées par le programmeur commenceraient quant à eux à 1 !
Si le gars de chez Microsoft dont j'ai parlé tout à l'heure m'appelle, tant qu'il y est, qu'il prépare aussi un argumentaire pour m'expliquer cela. Et qu'il fasse preuve d'imagination...
5.3 "Brancher" les événements sur une procédure unique
Passons. Un autre avantage considérable de disposer de tableaux de contrôles, était de pouvoir traiter un même événement survenant aux différents membres du tableau avec une seule procédure. Dans l'état actuel de nos connaissances, c'est certes possible, via l'instruction Handles qui figure dans les titres des procédures : il suffit de faire suivre cette instruction Handles de la liste complète des membres de la collection. C'est d'ailleurs cette technique que nous avons déjà eu l'occasion d'utiliser dans plusieurs exercices, sans même pour autant avoir créé de collection. Toutefois, cette combine a des limites :
dans le cas d'un ensemble formé de très nombreux contrôles, il va s'avérer infernal de devoir énumérer à la main tous ces contrôles après l'instruction Handles (pensez au démineur de Windows, dont le plateau de jeu est formé d'un damier de boutons pouvant compter plusieurs centaines d'exemplaires !)
même si nous n'avons pas encore étudié cette situation, les contrôles ne sont pas toujours créés au départ du programme, via la fenêtre Design. Dans bien des cas, on va souhaiter créer des contrôles par du code, en cours d'exécution. Dès lors, comment rattacher les événements survenant à ces contrôles - qui n'existent pas encore - à telle ou telle procédure événementielle, lorsqu'on écrit celle-ci ?
La parade à ces problèmes tient dans la possibilité de rattacher par du code, donc en cours d'exécution, tel événement survenant à tel contrôle, à telle procédure existante. C'est le sens de l'instruction AddHandler, qui donnera par exemple :
AddHandler Button2.Click, AddressOf MiseAJour
Je fais remarquer qu'il y a une virgule après l'événement, et deux "d" à AddressOf. Sinon, bing, VB ne comprend rien. Dans cet exemple, MiseAJour est le nom de la procédure sur laquelle nous programmons le "branchement" de l'événement Button2.Click.
Se pose la question de savoir où doit figurer cette instruction Addhandler. Cela va sans dire (mais cela va mieux en le disant), elle doit obligatoirement être située dans une procédure qui sera exécutée avant l'événement concerné par Addhandler. Si j'utilise Addhandler pour que le clic sur Bouton2 déclenche la procédure MiseaJour, il faut obligatoirement que ce Addhandler soit exécuté avant que l'utilisateur ait pu cliquer sur Bouton2. Faute de quoi la procédure MiseaJour ne sera pas branchée au clic sur Bouton2 et l'utilisateur aura beau massacrer le bouton de sa souris, il ne se passera rien du tout.
On peut donc dire que l'instruction Addhandler devra figurer dans une procédure qui, du point de vue de la chronologie de l'exécution de l'application, se situera :
avant l'événement qu'elle permet de gérer
mais après la création du contrôle concerné (ceci paraît une remarque étrange, les contrôles étant a priori toujours créés avant même le début de l'application. Mais nous allons bientôt voir qu'on peut créer des contrôles en cours d'exécution. Cette remarque prend alors tout son sens).
Nous sommes presque au bout de nos peines. Nous savons à présent :
regrouper les contrôles de notre choix dans une collection
parcourir cette collection pour la traiter de manière systématique
associer par du code un événement concernant l'ensemble des contrôles d'une collection à une seule procédure événementielle, afin d'en simplifier la gestion.
5.4 Identifier le contrôle qui a déclenché l'événement
Il ne nous reste plus qu'un seul souci, mais de taille. Imaginons que nous ayons 125 boutons sur notre Form, sur lequel un clic mène infailliblement à une procédure unique. Comment faire pour récupérer, au sein de cette procédure, le bouton précis qui a provoqué l'événement (ce qui est souvent indispensable) ? Nous disposons certes du paramètre Sender, que nous avons déjà utilisé lors d'exercices précédents. Sender est une variable objet, qui contiendra le nom du contrôle ayant déclenché l'événement.
Il y a alors de fortes chances pour que nous ayons besoin de savoir à quel indice dans la collection correspond le contrôle désigné par cette variable Sender. Pour cela, nous pouvons utiliser la méthode IndexOf, qui renvoie l'indice de n'importe quel contrôle au sein de n'importe quelle collection... enfin, presque. J'y reviens dans un instant. Ainsi, imaginons que nous ayons créé un Panel comprenant une série de 100 boutons, pour lesquels une boucle a habilement renvoyé l'événement Click vers une procédure Yaclic. Supposons enfin que nous souhaitions afficher en titre de la Form l'indice du bouton sur lequel l'utilisateur vient de cliquer. La procédure Yaclic aurait alors la tête suivante :
- Code:
Private Sub Yaclic(ByVal sender As System.Object, ByVal e As System.EventArgs)
Dim i As Integer
i = Panel1.Controls.IndexOf(sender)
Me.Text = i
End Sub
Cette technique, simple et efficace, ne fonctionnera toutefois pas au sein des collections que nous aurons créées et remplies par du code, les collections créées par le programmeur ignorant la méthode IndexOf ! Pour celles-ci, il faudra avoir recours à une voie détournée : programmer une boucle qui compare, par exemple, la propriété Name de chaque membre de la collection Macollec avec celui du sender, sur le modèle suivant :
- Code:
Private Sub Yaclic(ByVal sender As System.Object, ByVal e As System.EventArgs)
Dim i As Integer
For i = 1 to Macollec.Count
If Macollec.Item(i).Name = sender.Name Then
Me.Text = i
Endif
Next i
End Sub
En conclusion : VB.Net permet de faire tout ce que nous permettaient les tableaux de contrôles des versions précédentes de VB, mais souvent au prix d'un code plus compliqué. Et par-dessus le marché, au lieu d'établir une règle simple fonctionnant dans toutes les situations, les gars de chez Microsoft se sont amusés à multiplier les exceptions. Comme quoi, les Shadoks ont incontestablement inspiré Bill Gates lorsqu'ils proclamaient solennellement : "En essayant continuellement, on finit par réussir. Donc : plus ça rate, plus on a de chance que ça marche"
Exercice Exécutable Sources
Village de cases
Le maillon faible
6. Créer dynamiquement des contrôles
J'y ai fait rapidement allusion un peu plus haut, alors autant ne pas vous laisser dans les ténèbres de l'ignorance. D'autant qu'une fois que vous aurez appris cette technique, vous aurez à votre disposition un ensemble de mécanismes qui vous permettra de faire face à bien des situations.
Alors voilà, l'affaire est toute bête, et tient en peu de mots : il est possible de créer et de détruire des contrôles en cours d'exécution, via les instructions appropriées.
Créer un nouveau contrôle par du code n'est pas plus compliqué que créer une nouvelle variable : on retrouve le mot-clé Dim, et la spécification de la classe du contrôle. Sans omettre le constructeur New, indispensable dès que la déclaration porte sur autre chose qu'un type simple. On aura ainsi une ligne du genre :
Dim Toto as New Button
Où Toto sera la propriété Name du nouveau bouton créé. Mais à ce stade, nous n'avons créé qu'un bouton désincarné, virtuel, si j'ose dire, et qui n'appartient à aucun ensemble de notre application. Si l'on veut par exemple que le nouveau bouton soit visible (et en général, c'est vivement recommandé), il est indispensable de l'incorporer dans la collection Controls de la Form :
Controls.Add(Toto)
Toto est dorénavant un contrôle comme un autre de la Form. Si, pour libérer de l'espace, on veut à un moment où à un autre détruire le contrôle, il suffira de lui appliquer la méthode Dispose.
Remarque pas piquée des hannetons :
Si l'on crée un contrôle dynamiquement, par du code, celui-ci se voit donc déclaré par l'instruction Dim au sein d'une procédure. Par conséquent, ce contrôle va être considéré par le langage comme une variable (objet) locale à la procédure. En toute logique, il sera donc impossible de faire référence à ce contrôle par son nom (sa propriété Name), dans une autre procédure !
La parade sera d'incorporer, comme nous l'avons fait, ce contrôle à la collection Controls de la Form (et éventuellement, à d'autres collections créées par nos soins). De là, nous pourrons accéder au contrôle, depuis n'importe quelle procédure, via son indice dans la collection... Ouf.
Le seul détail qui nous reste à régler - mais par rapport à ce qu'on s'est cogné jusque là, ça va être du cake - c'est de savoir comment on va faire pour attribuer automatiquement des noms (des propriétés Name) à toute une série de contrôles créées à la chaîne par du code.
Pour cela, il suffit par exemple de concaténer un nom (mettons, Toto), et un nombre généré par une boucle. De toutes façons, l'essentiel est que les noms soient tous différents : le reste, on s'en fiche un peu, puisqu'on ne se resservira des contrôles qu'en les désignant par leur indice dans la collection dans laquelle nous les aurons rangés.
Voilà, pour résumer, un code qui accomplit les tâches suivantes :
il crée et affiche les unes en dessous des autres 20 nouvelles Checkbox
il les intègre dans la collection Mescases
il branche le changement d'état de chacune de ces cases sur une procédure unique, ClicMesCases
- Code:
Dim MesCases As New Collection
Private Sub Button1_Click (ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim i As Integer
For i = 1 To 20
Dim x As New CheckBox
x.Name = "Macase" & Str(i)
Controls.Add(x)
x.Left = 50
x.Top = i * 20
x.Width = 150
x.Text = "Je suis une case"
MesCases.Add(x)
AddHandler x.CheckedChanged, AddressOf ClicMesCases
Next i
End Sub
Pour gérer le clic sur ces cases et afficher laquelle a déclenché l'événement, nous retrouverons donc un code déjà vu, sur un mode un brin laborieux (puisque l'emploi d'une collection personnalisée MesCases nous interdit de récupérer directement l'indice via IndexOf). Il va de soi que dans certaines situation, on pourra tout de même optimiser un peu la boucle :
- Code:
Private Sub ClicMesCases(ByVal sender As System.Object, ByVal e As System.EventArgs)
Dim i, tut As Integer
For i = 1 To MesCases.Count
If MesCases(i).name = sender.name Then
tut = MsgBox("vous avez cliqué sur la case n°" & i, vbOK)
End If
Next i
End Sub
Et voilà. Bon, avec ça, on va pouvoir faire des noeuds très jolis (et très gros) à nos neurones.
Exercice Exécutable Sources
7. Remarque finale
Si les collections ont beaucoup d'inconvénients, elles possèdent néanmoins un avantage :c'est qu'elles représentent un concept que l'on va retrouver à tous les coins de rues en VB.Net. En fait, si j'ai jusqu'ici parlé uniquement des collections au sens de série de boutons, de labels, etc. c'était parce qu'il fallait bien prendre le problème par un bout. Mais en VB.Net, dès que l'on a affaire à une série d'éléments qui sont "inclus" dans un autre élément, on peut être sûr que le langage considère qu'il s'agit d'une collection. Ainsi, je pourrais citer en vrac :
les éléments d'une liste
les menus, les sous-menus, et les sous-sous menus.
les éléments d'un Treeview, avec leurs sous-éléments, leurs sous-sous-éléments.
les différentes Form au sein d'une application MDI (pensez à Word ou Excel, avec les différentes fenêtres "document" au sein de la fenêtre principale de l'application).
etc.
Tout cela, nous en reparlerons. Mais si vous avez compris le concept de collection et les outils avec lesquels on les manie, vous devriez vous en sortir sans trop de dégâts.