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 5

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 astuce qui nous a permis, dans certaines circonstances, de faire une entorse à ce principe, a été de brancher un événement commun à plusieurs contrôles (par exemple, un clic) sur une procédure événementielle unique. Ce qui ne nous a pas empêchés de pouvoir récupérer ou manipuler les propriétés du contrôle ayant appelé la procédure via le paramètre sender.

Le but de ce chapitre est de découvrir comment on peut généraliser le traitement des contrôles en série.

1. La notion de Collection

1.1 Quelques généralités

C# est un langage riche. Très riche, même. Ainsi, pour regrouper des données (ou des objets), il met à disposition du programmeur une série impressionnante d'outils. Parmi ceux-ci, on trouve les grands classiques que sont les tableaux. Mais on trouve aussi des choses plus inhabituelles, comme des listes, des tableaux de listes, j'en passe et des meilleures.

La première chose à comprendre, c'est que l'ensemble de ces variantes sont les déclinaisons d'un outil fondamental, qu'on va retrouver à tous les coins de ligne en C# : la collection. Un tableau, ce n'est donc qu'une forme particulière de collection (pour ce qui est des caractéristiques précises des tableaux et de leur maniement, des points communs et différences entre tableaux et collection, je renvoie à cette page de l'aide-mémoire). Dans les lignes qui suivent, j'ignorerai tableaux, listes, etc., pour m'en tenir aux collections proprement dites : elles suffiront largement à notre bonheur.

Une collection peut rassembler absolument tout et n'importe quoi, à commencer par des données. On peut donc avoir des collections d'entiers, de chaînes, de booléens... ou de tout cela mélangé. Mais ce qui va nous intéresser au premier chef, ce sont les collections d'objets – plus précisément encore, les collections de contrôles.

1.2 Contrôles conteneurs et collections

Il existe une catégorie un peu particulière de contrôles, qu'on appelle des contrôles conteneurs. Ce sont tout simplement des contrôles qui contiennent d'autres contrôles. Par exemple, les Form, les GroupBox ou les Panel sont des contrôles conteneurs. Eh bien, 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 : les contrôles qui se trouvent à l'intérieur du conteneur s'appellent les membres de cette collection. Ainsi, tout contrôle posé sur une Form devient aussi sec membre de la collection définie par la Form. Tous les contrôles qui se trouvent dans une GroupBox sont de ce fait membres de la collection définie par cette GroupBox, etc.

La collection définie par un contrôle conteneur est désignée par sa propriété Controls. Donc, pour manipuler la collection définie par le contrôle conteneur TrucMuche, on écrira :

TrucMuche.Controls

Quant à la collection définie par la Form, elle s'écrit tout naturellement :

this.Controls

Remarque sylvestre :
Les collections s'imbriquent de manière hiérarchique et forment 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 est de permettre de traiter les contrôles qui en sont membres en écrivant des boucles, exactement comme on le fait pour les éléments d'un tableau.

Remarque « astuce pas chère » :
En cas de besoin, le moyen le plus simple pour regrouper des contrôles dans une collection consiste à les rassembler dans un conteneur. Le Panel, du fait qu'il est invisible pour l'utilisateur, est un candidat idéal à ce rôle.

2. Écrire des boucles sur des collections de contrôles

2.1 Désigner les contrôles par leur indice : la boucle for...

2.1.1 Principes généraux et syntaxe

La manière la plus évidente (mais pas toujours la plus pratique) de désigner les contrôles dans une collection, est de se référer à leur indice dans cette collection.

Encore une fois, rien de surprenant, vu la proximité entre les collections et les tableaux : tout membre d'une collection possède un indice qui l'identifie de manière unique au sein de la collection. En ce qui concerne les collections de contrôles, on ne gère pas cet indice. C# s'en occupe tout seul comme un grand, avec toutefois une règle un peu étrange : quand on crée les contrôles à la main, dans la fenêtre design, 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 C#. La bonne nouvelle (et cela vaut aussi pour les collections qu'on aura créées ou gérées par des lignes de code), c'est que sans qu'on ait à s'occuper de quoi que ce soit, les indices commencent ainsi toujours à zéro, et il n'y a jamais de « trous » dans les indices.

Pour désigner un contrôle individuel dans une collection, il existe donc une alternative à la propriété Name employée jusqu'ici. Il s'agit de l'écriture suivante :

NomConteneur.Controls[i]

Ainsi, si je veux parler de ce qui est écrit sur le 8e contrôle (dans l'ordre décrété par C#) de la collection constituée par la Form, j'écrirai :

this.Controls[7].Text

De la même façon, si l'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

Comme je le disais à l'instant, qui dit « éléments indicés » dit « boucles », et possibilité d'effectuer un traitement systématique (un balayage). Pour cela, une propriété s'avère bien utile : il s'agit de Count, qui fournit le nombre d'éléments d'une collection donnée. L'écriture d'une boucle de balayage typique d'une collection est donc :

for (i=0 ; i<ensemble.Controls.Count ; i++)
{
   ... ensemble.Controls[i] ...
}

Tout cela ne pose aucune difficulté particulière.

2.1.2 Les limites de la boucle for...

Cette technique semble aussi simple qu'imparable, et on se demande bien pourquoi il faudrait se creuser davantage la tête. En fait, on va retomber très exactement sur le souci déjà rencontré à propos du paramètre sender, défini par C# dans le type général object. Avec une boucle for..., on parcourt les éléments de la collection Controls, dont la classe est Control, certes un peu plus définie que la classe Object, mais encore très générale. Ainsi, les mêmes causes vont produire les mêmes effets : en procédant ainsi, on ne pourra accéder qu'à celles des propriétés qui sont des propriétés générales des contrôles. L'accès à des propriétés propres à une classe précise nous seront refusées.

Par exemple, s'il s'agit de rendre une série de contrôles invisibles, tout va bien : Visible étant une propriété appartenant à la classe Control, on pourra écrire sans problème :

for (i=0 ; i<ensemble.Controls.Count ; i++)
{
   ensemble.Controls[i].Visible = false;
}

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 ; i<ensemble.Controls.Count ; i++)
{
   ensemble.Controls[i].Checked = true;
}

Sauf que là, ça va coincer : le compilateur C# nous jette sans ménagements, arguant du fait que la propriété Checked ne s'applique pas à un objet de la classe Control.

Damned, nous voilà coincés.

Enfin, pas tout à fait... (quel suspense dans ce cours, un vrai thriller).

2.1.3 Une première issue : le bon vieux cast

Nous connaissons déjà un moyen de nous dépêtrer du problème, puisque nous l'avions déjà employé avec sender : il suffit, à chaque tout de boucle, de caster l'objet désigné par l'indice vers un type plus précis (en l'occurrence, CheckBox) pour avoir accès à la propriété voulue. En supposant que tous les membres de ensemble soient effectivement des CheckBox, et qu'il ne m'arrivera donc pas de misères lors du cast, je peux ainsi écrire :

CheckBox MaCase;
for (i=0 ; i<Ensemble.Controls.Count ; i++)
{
   MaCase = (CheckBox)ensemble.Controls[i];
   MaCase.Checked = true;
}

2.2 Une boucle (presque) sans indice : foreach...

C# propose une écriture de boucle alternative, typique des langages objet, et spécifiquement conçue pour permettre de faciliter le balayage d'une collection. Dans cette écriture, tout repose sur le fait que la variable qui sert de compteur n'est plus un nombre, comme dans toutes les boucles for... de l'univers connu, mais que cette variable représente directement un contrôle. Autrement dit, la boucle foreach... utilise une variable qui va désigner successivement l'ensemble des contrôles de la collection.

2.2.1 Le cas le plus simple

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 tous 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, où il s'agit de cocher systématiquement une série de CheckBox, on aura :

foreach (CheckBox maCase in Ensemble.Controls)
{
   maCase.Checked = true;
}

Cette écriture appelle plusieurs remarques.

  1. maCase est une simple variable, même si elle fait référence à un objet d'un type précis. Autrement dit, quand nous la déclarons, nous créons un emplacement capable de pointer vers un objet existant ; nous ne créons pas un nouvel objet. Voilà pourquoi la déclaration de la variable ne comporte pas le constructeur new.
  2. du fait que maCase est déclarée en type Checkbox, le compilateur nous donne accès, par son intermédiaire, à toutes les propriétés et méthodes de cette classe.
  3. il faut savoir qu'en réalité, une boucle foreach est gérée par un indice. Autrement dit, derrière toute boucle foreach... se cache une boucle ordinaire for... : C# épargne simplement au programmeur de gérer lui-même le compteur de boucle. Cela explique le pourquoi du comment de certaines erreurs, du genre « index out of range », c'est-à-dire « indice-qui-est-allé-trop-loin », qui ne manqueront pas de vous tomber sur le coin du museau durant certains exercices – le tout est de se souvenir de ce petit détail au moment voulu...

2.2.2 Le cas à peine plus compliqué

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, plus moyen de faire la boucle sur la collection en déclarant la variable comme une CheckBox : dès que l'application va tomber sur un contrôle d'un autre type, elle va s'arrêter net en nous envoyant une exception dans les gencives.

La seule issue va donc être de construire la boucle en utilisant une variable du type le plus général (control), et de tester à chaque tour de boucle si cette variable pointe ou non sur une CheckBox. Si oui, on effectue le traitement voulu (en ayant préalablement effectrué un cast). Si non, on passe au contrôle suivant sans rien faire.

Pour tester ainsi le type d'un contrôle (ou, ce qui revient au même, d'un contrôle auquel une variable fait référence), C# demande de passer par un opérateur de comparaison particulier, is :

CheckBox maCase;
foreach (Control bidule in ensemble.Controls)
{
   if (bidule is CheckBox)
      {
         maCase = (CheckBox)bidule;
         maCase.Checked = true;
      }
}

Et il y a encore plus fort ! En effet, il est possible de restreindre d'emblée le balayage de la collection aux seuls contrôles du type voulu, en utilisant la méthode OfType. La syntaxe pique un peu les yeux, mais elle permet d'économiser un test :

foreach (CheckBox maCase in ensemble.Controls.OfType<CheckBox>())
{
   maCase.Checked = true;
}

Avec tout cela, on peut à présent reprendre les deux derniers exercices et utiliser nos connaissances toutes fraîches afin de faire subir au code une cure d'amaigrissement.

Exercice
Exécutable
Sources
Options
Sondage

3. Manipuler des collections par du code

3.1 Déclarer une collection

Tous les contrôles conteneurs définissent automatiquement une collection. Mais en C#, la notion de collection est beaucoup plus vaste, et toutes les collections ne sont pas définies par des contrôles conteneurs. Il est donc possible de créer une collection uniquement par du code, et d'y regrouper des contrôles et, plus généralement, toutes sortes de données.

Je n'aborderai pas véritablement ces techniques autrement que par la bande. Sachez simplement que si les ensembles définis par des contrôles conteneurs sont, comme on vient de le voir, des collections au sens général du terme, lorsqu'on va créer une collection par du code, on utilisera plutôt une des sous-classes de collections : les tableaux, bien sûr, mais aussi (et surtout ?), les list (listes).

3.2 Ajouter des éléments

L'instruction qui permet d'ajouter un élément à une collection, est la méthode Add. Celle-ci réclame un argument unique, à savoir 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.

3.3 Retirer des éléments

3.3.1 Les retirer un par un

Pour retirer un membre d'une collection, il existe deux méthodes :

  • Remove qui demande en argument le nom du membre.
  • RemoveAt qui demande en argument l'indice du membre.

Ainsi, pour retirer boutonAnnuler de la collection ensemble, dont il est le membre d'indice 4, on n'a que l'embarras du choix entre :

ensemble.Remove(boutonAnnuler)

et

ensemble.RemoveAt(4)

On choisira, bien sûr, ce qui nous arrange en fonction du contexte. Qu'importe le flacon pourvu qu'on ait l'ivresse.

3.3.2 Les retirer tous d'un coup

S'il s'agit de vider une collection, il y a un moyen beaucoup plus simple que de faire une boucle pour supprimer les membres un par un : la redoutable et dévastratrice méthode Clear. Pour purger ensemble de la totalité de son contenu, il suffira donc d'écrire :

Ensemble.Clear()

Remarque vertigineuse
Supprimer des éléments d'une collection au sein d'une boucle peut entraîner de redoutables migraines. En effet, C# gérant automatiquement les indices d'une collection, toute supression d'élément conduit à chaque fois à diminuer le plus grand indice de 1.
Si l'on n'y prend pas garde, cela fera donc planter une boucle for ou foreach, qui commençait « bêtement » à l'indice zéro pour finir au plus grand indice de la collection.
Pour éviter le problème, deux solutions :
– ne pas supprimer d'éléments au sein d'une boucle
– si on est obligé de supprimer des éléments au sein d'une boucle, construire la boucle à l'envers, en balayant à partir du dernier indice, afin que celui-ci « recule » toujours vers des éléments existants... ou supprimer toujours l'élément d'indice zéro, dont est certain qu'il existe.

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

4.1 Avantages et limites de la méthode manuelle

Jusqu'ici, nous n'avons guère eu à réfléchir sur la manière dont nous pouvions associer la survenue d'un événement donné sur un contrôle à une procédure : la plupart du temps, cette association était faite automatiquement ou semi-automatiquement. Au mieux, il suffisait de faire un doucle-clic sur le contrôle pour voir apparaître la procédure avec le bon événement (celui qui, par défaut, est associé à la classe du contrôle). Au pire, il fallait d'abord passer par la fenêtre des événements pour préciser lequel on souhaitait gérer. Et au pire du pire, quand par exemple on avait créé une procédure commune à plusieurs événements, il fallait choisir, dans cette fenêtre des événements, la bonne procédure dans la liste déroulante pour chaque contrôle concerné.

Tout cela fonctionne à merveille... tant qu'on ne veut associer à une même procédure que deux, trois ou quatre événements. Mais imaginons une situation où il faille en associer plusieurs dizaines ou centaines, et on voit que le paradis se changera rapidement en 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à, celles-ci ne nous ont servi qu'à tripatouiller les propriétés de nos contrôles. Mais, vous me voyez venir avec mes gros sabots, il est tout à fait possible de faire par du code ce que nous faisions jusque là par des clics de souris – qui, en fait, écrivaient du code caché. Autrement dit, il est tout à fait possible, par des instructions, de rattacher des événements à des procédures ou, inversement, de détacher des événements de procédures. Wow.

4.2 Branchements en cours d'exécution

Pour accomplir cet exploit, il va falloir créer (instancier) un nouvel objet, EventHandler, le « gestionnaire d'événements ». Ainsi, pour qu'un clic sur boutonOK déclenche la procédure appelée validation, on écrira :

boutonOK.Click += new EventHandler(validation);

Décryptage : on a instancié un nouvel objet, de la classe « gestionnaire d'événements », qu'on a affecté à l'événement boutonOK.Clic, en précisant qu'il fallait effectuer le branchement sur la procédure validation. Au fond, ce n'est pas si compliqué.

Du coup, et tant qu'on y est, vous avez tout de suite deviné quelle instruction permet, inversement, de « débrancher » en cours d'exécution un événement donné d'une procédure. Là, même pas besoin d'instancier un nouvel EventHandler, il suffit de dire en quelque sorte qu'on retire la procédure de l'événement :

boutonOK.Click -= validation;

Imaginons ainsi que ma collection ensemble 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 :

foreach (Button monBouton in ensemble.Controls)
{
   monBouton.Click += new EventHandler(shiva);
}

Une question simple, mais capitale est celle de savoir à quel endroit du code il faut procéder à de tels branchements, individuels ou en série. Cela va sans dire (mais cela va mieux en le disant), elle doit obligatoirement être exécutée avant qu'on ait besoin des effets du branchement (on branche l'ampoule avant d'allumer la lumière, sinon on ne s'étonne pas que l'interrupteur ne fonctionne pas...). Autrement dit, les instructions qui procèdent aux branchements d'événements doivent toujours être placées dans une procédure qui sera exécutée avant les événements concernés. A priori, et sauf raison contraire (nous en verrons une excellente très bientôt), une solution pratique est de placer 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 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 que celui qu'on est en train de brancher. En effet, dans le cas contraire, il risque d'y avoir 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.

4.3 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 boutonMachin.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.

Cela est bel et bon, sauf que bien souvent, son nom ne nous est guère utile, alors qu'en revanche, on aurait bien davantage besoin de connaître son indice dans la collection dont il fait partie.

Pas de problème. La méthode IndexOf permet précisément de récupérer l'indice d'un élément dans une collection à partir de son nom. Illustration :

private void shiva(object sender, EventArgs e)
{
   Control leBouton = (Control)sender;
   int indice = ensemble.Controls.IndexOf(leBouton);
   string texte = Convert.ToString(indice);
   MessageBox.Show("Vous avez cliqué sur le bouton d'indice " + texte);
}

Bon, et si on se faisait quelques exos histoire de faire infuser tout cela ?
 

Exercice Exécutable Sources
Village de cases

5. 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 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 jeu du 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 à la volée, au moment où le joueur choisit son mode, c'est-à-dire alors que l'exécution de l'application est déjà lancée.

5.1 Créer un individu

En fait, créer un nouveau contrôle par du code n'est presque pas plus compliqué que créer une nouvelle variable objet, chose que nous avons déjà faite maintes fois, pour faire une boucle sur une collection ou caster le sender (NB : vous voyez comment on en arrive à parler au bout d'à peine six mois d'informatique ? Ça fait peur, hein ?). Simplement, il ne faudra pas oublier le constructeur new ‐ c'est lui qui différencie l'instanciation d'un nouveau contrôle de celle d'une simple variable pointant sur un contrôle. Et comme new est en fait un appel de méthode, on n'oubliera pas les parenthèses finales. Cela donnera une ligne du genre :

Button monBouton = new Button();

...où monBouton sera la propriété Name du nouveau bouton créé.

Le truc essentiel à savoir, c'est qu'à ce stade, nous n'avons créé qu'un bouton désincarné, virtuel, si j'ose dire. monBouton est certes 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 monBouton soit rattaché à une collection définie par un contrôle conteneur. Par exemple, pour prendre le cas le plus simple, on rattachera le contrôle à la collection définie par la Form par une instruction désormais familière  :

this.Controls.Add(monBouton)

monBouton 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 EventHandler que d'événements concernant monBouton que nous souhaitons brancher sur différentes procédures.

Tant qu'on y est, j'en profite pour mentionner l'instruction qui permet de supprimer un contrôle – que celui-ci ait été créé à la main ou par du code n'a aucune importance. Il s'agit de la méthode Dispose :

monBouton.Dispose()

Et couic, plus de monBouton. Éparpillé par petits bouts, façon puzzle, comme disait le sage.

5.2 Créer une série de contrôles

Ces quelques lignes finales ne sont là que pour taper sur des clous déjà rivés et enfoncer des portes déjà ouvertes. Nous savons faire des boucles ; nous savons aussi créer des contrôles un par un. Il suffit de mélanger les deux pour créer des séries de contrôles viables et capables de réagir au doigt et à l'oeil aux sollicitations de l'utilisateur.

Juste histoire de pour dire, voici un exemple de code qui accomplit les tâches suivantes :

  1. il crée 20 nouvelles Checkbox
  2. il les rassemble au sein d'une GroupBox appelée mesCases
  3. il les dispose convenablement, les unes en-dessous des autres
  4. il écrit à côté de chaque case « je suis la case numéro ... »
  5. il branche le changement d'état de chacune de ces cases sur une procédure unique, changeMesCases

// boucle de création
for (int i=1 ; i<=20 ; i++)
{
   // création d'un nouveau contrôle
   CheckBox maCase = new CheckBox();
   // rattachement du contrôle à la collection adéquate
   mesCases.Controls.Add(maCase);
   // positionnement du contrôle
   maCase.Left = 20;
   maCase.Top = 20 + i * 20;
   // affichage du texte sur le contrôle
   string numéro = Convert.ToString(i);
   maCase.Text = "Je suis la case n°" + numéro;
   // branchement de la procédure événementielle
   maCase.CheckedChanged += new EventHandler(changeMesCases);
}

Voilà un joli exercice qui soulève quelques problèmes. Parmi ceux-ci, on n'oubliera pas celui qui se pose lorsqu'on redemande un damier plus petit que le précédent.

Exercice
Exécutable
Sources
Damier