Une remarque pertinente ? Une critique impertinente ? Un lynchage en règle ? Une invitation sous les tropiques ? Ecrivez-moi ! |
|
Conçu et enseigné tel qu'en lui même, avec pertes, fracas et
humour de qualité supérieure par Christophe Darmangeat dans le M2 PISE du Master MECI (Université Paris 7) |
|
|
Partie 6
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, en écrivant ces événements à la queue-leu-leu derrière l'instruction Handles.
Cette technique 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
le traitement des 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 lorsque VB6 est devenu VB7, autrement dit VB.Net,
(je parle bien du langage 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'un concept un peu
étrange, qui évoquera de loin les défunts tableaux de contrôles, tout en
s'en différenciant par certains aspects - mais qui, c'est son avantage, va se retrouver absolument à
tous les coins de code. Donc, il va falloir au début un peu de gamberge pour piger
le truc ; mais une fois que ce sera fait, ça n'arrêtera pas de servir. Le concept en question
c'est celui de collection.
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
conteneur définit automatiquement une collection - et le contenu de ce conteneur
rassemble donc les membres de cette collection. Par exemple, tout
contrôle posé sur une Form devient aussi sec membre de la collection définie par la Form.
En l'occurrence, comme toute collection définie par un contrôle conteneur, celle-ci
porte le nom de Controls. Simplement puisqu'il s'agit de la Form,
il n'est pas besoin de précision supplémentaire. Si l'on parle de la collection définie par
le contrôle conteneur TrucMuche, alors il faudra préciser qu'il s'agit de :
TrucMuche.Controls
Remarque sylvestre :
Les collections fonctionnent de manière hiérarchique, comme une arborescence, exactement de la même façon que les répertoires et les sous-répertoires. Ainsi, si sur une Form j'ai deux boutons et un GroupBox, et que dans ce GroupBox j'ai trois RadioButton, ces RadioButton font partie de la collection définie par la GroupBox... mais pas de celle définie par la Form, qui ne compte pour sa part que les deux boutons et la GroupBox. Tout l'intérêt des collections, et c'est là un des points communs avec les tableaux,
c'est que les contrôles devenant des membres d'un ensemble, on va pouvoir les traiter
en écrivant des boucles, en balayant
l'intégralité des membres d'une collection tout comme on peut balayer l'intégralité des éléments d'un tableau.
Mais comment, vous demandez-vous, haletants ?
Pour le savoir, il vous suffit de poursuivre votre lecture...
Remarque instructive :
Par conséquent, en cas de besoin, le moyen le plus simple pour regrouper des contrôles dans une collection consiste à les créer au sein d'un conteneur. Le Panel, du fait qu'il est invisible pour l'utilisateur, est un candidat idéal à ce rôle. 2. Désigner les contrôles par leur indice
2.1 Principes généraux et syntaxe
La manière la plus évidente (mais pas toujours la plus pratique, comme nous le verrons)
de désigner les contrôles dans une collection, est de se référer à leur indice dans cette
collection.
Car c'est un autre point commun entre les collections et les tableaux :
tout
membre d'une collection y possède un indice qui l'identifie de manière unique. Le truc, c'est qu'on ne
choisit pas cet indice : VB le gère tout seul, comme un grand, avec une règle un peu étrange :
les indices sont attribués dans l'ordre inverse de la création des contrôles.
Autrement dit, le dernier contrôle créé dans une collection y possède l'indice zéro. Et si l'on crée un nouveau
contrôle, l'ancien contrôle d'indice 0 devient l'indice 1, l'ancien indice 1 devient l'indice 2, etc.
Il en va de même en sens inverse, en cas de suppression d'un contrôle : tous les membres de la
collection sont alors renumérotés automatiquement par VB. Sans qu'on ait à s'occuper
de quoi que ce soit, les indices commencent donc toujours à
zéro, et il n'y a jamais de « trous » dans les indices.
Pour désigner un contrôle individuel dans une collection
(et on a vu que tout contrôle, hormis la Form elle-même,
est nécessairement membre d'une collection), il existe ainsi une alternative à
la propriété Name employée jusqu'ici. Il s'agit de l'écriture suivante,
qui traite des collections littéralement comme de tableaux :
NomCollection.Item(i)
En fait, lorsque la collection est définie par un contrôle conteneur, et que son
nom se termine donc par Controls, on peut omettre la propriété
Item et écrire directement :
NomCollection(i)
Ainsi, si je veux parler de ce qui est écrit sur le 8e contrôle (dans l'ordre décrété par VB)
de la collection constituée par la Form, j'écrirai :
Controls(7).Text
Si à présent on traite d'un autre conteneur que la Form (par exemple, un Panel appelé Ensemble,
et que l'on souhaite positionner le 4e contrôle de cette collection à 200 pixels du bord gauche de son conteneur, on écrira :
Ensemble.Controls(3).Left = 200
Tout cela ne pose aucune difficulté particulière.
2.2 Une boucle sur l'indice : For ... Next
Qui dit éléments indicés dit boucles, et possibilité d'effectuer un traitement systématique (un balayage)
de l'ensemble. Pour cela, une propriété s'avère bien indispensable : il s'agit de Count, qui
donne le nombre d'éléments d'une collection donnée. L'écriture de boucle de balayage typique est donc :
For i = 0 to Ensemble.Controls.Count - 1
... Ensemble.Controls(i) ... Next i Cette technique simple semble imparable, et on se demande bien pourquoi il faudrait se creuser davantage la tête.
Eh bien, le problème avec cette approche, c'est que dans la boucle, on pourra seulement accéder à celles des propriétés qui
sont des propriétés générales des contrôles. Les propriétés propres à une classe précise nous seront refusées. Par exemple, si je veux rendre
une série de contrôles invisibles, aucun souci, Visible étant une propriété commune à tous les contrôles. On poura donc
écrire sans aucun problème :
For i = 0 to Ensemble.Controls.Count - 1
Ensemble.Controls(i).Visible = False Next i Admettons qu'en revanche, mon problème soit de cocher une série de CheckBox. Je serais tenté d'écrire,
en toute logique :
For i = 0 to Ensemble.Controls.Count - 1 Ensemble.Controls(i).Checked = True Next i Sauf que là, ça va coincer : le compilateur VB nous jette sans ménagements, arguant du fait que la propriété Checked
ne s'applique pas à un objet de la classe Controls.
Damned, nous voilà coincés.
Enfin, pas tout à fait... (quel suspense dans ce cours, un vrai thriller).
3. Une boucle (presque) sans indice : For Each ... Next
Voici en effet qu'arrive une nouvelle structure de boucle,
typique d'un langage objet, spécifiquement conçue pour permettre de
parcourir une série d'ojets - et donc, de contrôles. Dans ce type de boucle, tout repose sur le fait que la
variable qui va servir de compteur n'est 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 l'ensemble des contrôles de la collection.
Le tout est de savoir quel doit être le type de ladite variable...
3.1 Le cas le plus simple (et le plus souhaitable)
Ce cas simple est celui où tous les contrôles de la collection appartiennent
à une même classe. Par exemple, ce sont tous des boutons, ou des cases à cocher, etc. Dans ce cas, on va pouvoir
créer une variable du type de la classe voulue, et s'en servir pour effectuer la boucle. Si l'on reprend l'exemple
précédent, celui du cochage systématique d'une série de CheckBox, on aura :
Dim Truc As CheckBox
For Each Truc in Ensemble.Controls Truc.Checked = True Next Truc Cette écriture appelle plusieurs remarques.
Cette technique est si simple et si pratique qu'on y aura recours aussi souvent que possible.
Comme je le disais précédemment, on cherchera à constituer des collections homogènes, en particulier
via un contrôle Panel, afin de s'ouvrir toutes les possibilités que cet agencement
nous offre.
3.2 Le cas plus compliqué (mais qu'on ne peut pas toujours éviter)
Il s'agit bien sûr de la situation où une collection comprend des contrôles de classes différentes.
Par exemple, en plus de nos cases à cocher, quelques boutons, zones de texte et labels.
Là, évidemment, on a deux possibilités, mais qui posent deux problèmes symétriques.
Alors, serions-nous condamnés à un désespoir sans fin ? Nenni, vous vous doutez bien qu'avec le problème, arrive
la solution.
Toute l'astuce va consister à effectuer la boucle via une variable de type général Controls
(pas le choix). Mais au sein de la boucle, on détecte le type du contrôle pointé par la variable. S'il
s'agit du type souhaité (case à cocher, en l'occurrence), on recopie alors la variable de boucle vers une variable de type
Checkbox, et c'est cette dernière qui va nous donner accès aux propriétés voulues. Démonstration par
l'exemple :
Dim Truc As Control
Dim Toto As CheckBox For Each Truc in Ensemble.Controls If TypeOf Truc Is CheckBox Then Toto = Truc Toto.Checked = True End If Next Truc Elle est pas belle, la vie ?
Remarque « d'une pierre deux coups » (comme disait Bertin) :
Cette technique peut s'avérer tout aussi précieuse pour utiliser un Sender rétif. Cette variable étant obligatoirement déclarée en Control, on ne peut normalement, par son intermédiaire, avoir accès à bien des propriétés utiles. Là encore, il sufira de créer une variable dans le type voulu et de recopier Sender dans cette variable pour peu que son type s'y prête, et le tour sera joué. On peut donc reprendre les deux derniers exercices et utiliser les connaissances que nous venons d'acquérir
afin de faire subir au code une cure d'amaigrissement.
4. Créer des collections par du code
Tous les contrôles conteneurs définissent automatiquement une collection.
Mais toutes les collections ne sont pas définies par des contrôles conteneurs. En effet, il est
possible de créer une collection uniquement par du code, et de regrouper ainsi des contrôles
sans aucune espèce de relation avec leur disposition sur la Form. Soyons honnêtes, on n'a pas besoin
de cette technique tous les jours, et en réalité, c'est même une solution assez rare.
Mais ce n'est pas très compliqué, et cela va nous permettre de découvrir quelques instructions
qui nous seront fort utiles dans d'autres circonstances. Alors, pourquoi se priver ?
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
Cette fois, Collection étant considéré comme un objet à part entière -
et non comme une simple variable - on n'échappe pas à l'indispensable constructeur New.
À part ça, comme vous le voyez, il n'y a vraiment pas de quoi fouetter un chat (si tant est qu'on doive
jamais en arriver à de telles maltraitances envers ces braves petites boules de poils).
5.2 Remplir et vider une collection
Si l'on crée une collection par du code, cela signifie qu'a priori, elle est vide.
Pour qu'elle contienne des contrôles, ceux-ci devront lui être rattachés via la
méthode Add. Tiens, direz-vous ! Enfin une méthode ! Eh oui, et
celle-ci réclame un argument (et un seul), le nom du contrôle qui doit être ajouté
à la collection. On aura par exemple :
Ensemble.Add(Textbox1)
Ensemble.Add(Textbox2) Ensemble.Add(Label5) etc. De même, on peut tout aussi facilement retirer un
contrôle d'une collection par la méthode Remove qui, elle
aussi, réclame le même argument pour les mêmes raisons :
Ensemble.Remove(Label5)
Il existe néanmoins un moyen plus rapide et radical de vider une collection :
il consiste à employer la méthode Clear (sans arguments), qui purge purement
et simplement une collection de l'ensemble de son contenu :
Ensemble.Clear
Tout se passe donc pour le mieux dans le meilleur des mondes
microsoftiens possibles, et nous pouvons naturellement présumer que tout comme pour
une collection définie par un conteneur, nous pouvons désigner les membres d'une
collection définie par du code en utilisant leurs indices. Là, cependant,
plus question d'utiliser la propriété Controls. En revanche, obligation
de passer par Item.
Cela donnera un machin du genre :
...
Ensemble.Item(i).Visible = True ... Oui mais voilà, la bande à Bill avait décidé que l'oisiveté est la mère de tous
les vices, et qu'un programmeur qui s'endort, c'est un programmeur qui est déjà à moitié mort. Alors,
pour nous tenir éveillés, ils ont décidé un truc marrant : si les
indices des collections définies par des conteneurs commencent en
toute logique à zéro, les indices des collections définies par du code
commencent pour leur part à 1 ! Amusant, non ? Alors, il n'y a là-dedans qu'une
seule difficulté... c'est de s'en souvenir, et en pareil cas, de bien écrire :
For i = 1 to Ensemble.Count
6. Associer des événements à une procédure par du code
Tout le monde est encore avec moi ? Alors asseyez-vous confortablement,
attachez bien vos ceintures, on va passer à la vitesse supérieure.
6.1 Branchements en cours d'exécution
Jusqu'ici, quand nous avons voulu associer un événement donné à une procédure,
nous n'avons guère eu le choix : il fallait entrer de nos petits doigts vigoureux, dans la
ligne de titre de la procédure, juste après l'instruction Handles,
la liste des associations Objet.Action voulue. C'était jouable... tant qu'on ne voulait
associer à une procédure donnée que deux, trois ou quatre événements. Mais s'il fallait en associer
plusieurs dizaines ou centaines, on voit que cette technique deviendrait vite un enfer.
Or, depuis que nous savons ce qu'est une collection, nous savons aussi qu'il est
possible de parcourir ces dizaines, ou ces centaines de contrôles, par une boucle. Jusque-là, nous
n'avons envisagé ces boucles que dans le but de tripatouiller les propriétés de nos contrôles. Mais
là où ça devient carrément excitant, c'est qu'il est également possible d'en profiter pour
rattacher, par des instructions, des événements à des procédures. Wow.
L'instruction qui permet d'accomplr cet exploit est AddHandler,
littéralement « ajouter une gestion ». Elle s'emploie selon la syntaxe suivante :
AddHandler NomObjet.Action, AddressOf NomProcédure
Imaginons ainsi que ma collection Ensemble (créée par du code)
soit composée d'une série de boutons, et que
je veuille « brancher » le clic sur tous ces boutons sur une procédure appelée Shiva.
Cela donnera le code suivant :
Dim Truc As Button
For Each Truc in Ensemble AddHandler Truc.Click, AddressOf Shiva Next Truc
Remarque orthographique :
Ne pas oublier la virgule après l'événement, et les deux « d » à AddressOf. Sinon, patatras, VB ne comprend rien. Une question capitale est celle de savoir à quel endroit du code
doit figurer l'instruction Addhandler. Cela va sans dire (mais
cela va mieux en le disant), elle doit obligatoirement être exécutuée avant
qu'on ait besoin de ses effets. Autrement dit, elle doit être placée
dans une procédure qui sera exécutée avant l'événement concerné par
AddHandler. A priori, et sauf raison contraire (nous en
verrons une excellente dès la partie suivante), une solution pratique est de mettre
cette instruction quelque part dans la procédure Form.Load,
dont on sait qu'elle se déclenchera dès le lancement de l'application.
Remarque « le fil rouge sur le bouton rouge »
Lorsqu'on « branche » ainsi un événement sur une procédure, il est essentiel de s'assurer que cette procédure a bien, au départ, été créée pour gérer le même type d'événements. En effet, dans le cas contraire, il y a aura non-correspondance entre le type du paramètre en entrée e attendu et celui qui aura été déclaré... et par conséquent, paf, une erreur. Une bonne manière de procéder consiste :
6.2 Un indice pour identifier le coupable ?
Si nous avons, à la main ou par une boucle, branché plusieurs contrôles différents sur une même
procédure (par exemple, des tas de boutons.click vers Shiva), il se posera évidemment le problème, à chaque
exécution de Shiva, de savoir lequel, parmi tous ces boutons, vient de recevoir le clic.
Nous savons depuis longtemps récupérer son nom : puisque la variable Sender
pointe sur le contrôle en question, son nom nous est donné par la propriété Sender.Name.
Oui, c'est bien gentil, direz-vous légitimement, mais bien davantage que son nom, on a souvent
davantage besoin de connaître son indice dans la collection dont il fait partie.
Dans ce cas, de deux choses l'une, selon la manière dont
la collection qui rassemble nos boutons a été définie.
MonIndice = MonPanel.IndexOf(Sender)
For i = 1 to Ensemble.Count
If Ensemble.Item(i) = Sender Then MonIndice = i End If Next i Bon, et si on se faisait quelques exos histoire de faire infuser tout cela ?
7. Créer des contrôles dynamiquement
Nous en arrivons à l'apothéose, au couronnement, bref, à l'extase ultime.
Je vous annonce donc la nouvelle sans plus attendre : il est possible de créer et de détruire des contrôles en
cours d'exécution, via les instructions appropriées.
Cette nouvelle qui nous laisse sans voix nous ouvre néanmoins des perspectives
grandioses, puisque elle nous libère de la nécessité de connaître à l'avance, avant même le démarrage de
l'application, l'ensemble des contrôles dont celle-ci pourra avoir besoin. Si vous voulez un exemple,
pensez au célèbre Démineur de Windows, où l'on propose au joueur différents modes (débutant, intermédiaire, avancé)
pour lesquels varie le nombre de cases à déminer (respectivement 81, 256 et 480). Il va de soi que ces cases (en fait,
des boutons), sont créés au moment où le joueur choisit son mode, c'est-à-dire alors que l'exécution de l'application
est déjà lancée.
En fait, créer un nouveau contrôle par du code n'est pas beaucoup plus compliqué
que créer une nouvelle variable : comme d'habitude, 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. Toto est un contrôle, mais un contrôle qui plane dans une sorte
d'espace ethéré, et qui ne fait pas vraiment partie de notre application (il n'est
d'ailleurs même pas visible à l'écran). Pour cela,
il est indispensable que Toto soit rattaché à une collection.
Celle-ci pourra par exemple être Controls, définie tout
bêtement par la Form. Quant à l'instruction qui effectue ce rattachement, elle ne constitue en rien une surprise
puisque nous retrouvons la désormais familière méthode Add :
Controls.Add(Toto)
Toto est dorénavant un contrôle comme un autre de la Form, sur lequel l'utilisateur
va pouvoir agir... à condition que nous ayons prévu cette interaction. Autrement dit, il nous faut écrire autant d'instructions
AddHandler que d'événements concernant Toto que nous souhaitons « brancher »
sur différentes procédures.
Remarque enfonceuse de portes ouvertes :
Est-il besoin de le préciser ? Dans le cas où l'on a créé un contrôle par du code, les instructions AddHandler qui gèreront son comportement ne pourront figurer qu'après cette instruction de création (car pour que la machine comprenne qu'elle doit gérer une action sur un contrôle, encore faut-il que ce contrôle existe). Du coup, l'instruction AddHandler ne se trouvera pas forcément dans la procédure Form.Load - 999 fois sur 1000, elle se trouvera juste après l'instruction ayant créé le contrôle, quelle que soit la procédure où celle-ci se trouve. Profitons de l'occasion pour découvrir une autre instruction.
Si, pour une raison ou pour une autre, on veut à un moment ou à un autre
détruire le contrôle, il suffira de lui appliquer la méthode Dispose.
Voilà, pour résumer, un exemple de code qui accomplit les tâches suivantes :
Dim i As Integer
For i = 1 To 20 Dim MaCase As New CheckBox MonGroupBox.Add(MaCase) MaCase.Left = 50 MaCase.Top = i * 20 MaCase.Width = 150 MaCase.Text = "Je suis la case numéro" & i AddHandler MaCase.CheckedChanged, AddressOf ClicMesCases Next i
Remarque la tronche à l'envers :
Si l'on se souvien bien de tout ce qui précède, on doit faire remarquer que dans la collection MonGroupBox, les cases possèdent un indice « inversé » par rapport au texte qui figure à leur côté : la case dite n°1 possède l'indice 20, la n°2 l'indice 19, etc. Car, faut-il le rappeler, les indices sont attribués automatiquement par VB dans l'ordre inverse de la création - plus exactement, de l'affectation des contrôles à la collection. Ah, ce Bilou, quand même, quel plaisantin. Et voilà. Bon, avec ça, on va pouvoir de très jolis noeuds à nos neurones.
8. Remarque finale
Je terminerai ce chapitre par là où je l'ai commencé : en rappelant
que les collections représentent un concept que l'on va retrouver absolument partout.
Elles sont très loin de se limiter aux seuls contrôles inclus dans des conteneurs.
En fait, en VB.Net, il y a une collection dès que l'on a affaire à une série d'éléments
qui sont « inclus » dans un autre. Ainsi, peut-on évoquer en vrac :
|