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 7

Événements insolites

Il est temps à présent d'examiner plus en détail différents autres événements que nous permet de gérer C#. Car il faut bien l'avouer, jusqu'ici, à part des clics, des clics et encore des clics, on n'a pas vu grand chose. Or, il serait bien dommage de s'en tenir là, vu les possibilités qu'offre C# en la matière.

1. La notion de Focus

En parlant du Focus, ne pas oublier de prononcer le « s » final, sans quoi cela risque de prêter à confusion.

A part ça, le focus, dans une application Windows, désigne le curseur, au sens le plus général du terme. C'est lorsqu'un contrôle possède le « focus » qu'il devient concerné par la frappe d'une touche au clavier (la touche Entrée produisant l'enfoncement d'un bouton, par exemple). Selon les contrôles, le focus se matérialise à l'écran par un curseur clignotant (dans une Textbox), ou par un liseré sombre (sur un Button).

Du point de vue de l'utilisateur, il y a deux moyens de déplacer le focus :

  • en cliquant directement avec la souris sur le contrôle désiré (certains contrôles, tels le bouton, ne peuvent toutefois pas recevoir le focus de cette manière, car le clic de la souris y produit directement... un Click)
  • en appuyant sur la touche de tabulation, qui fait circuler le focus d'un contrôle à l'autre.

L'ordre de passage du focus d'un contrôle à l'autre de la Form est régi par la propriété TabIndex de chaque contrôle : le contrôle qui reçoit le focus par défaut, au lancement de la Form, est celui dont le TabIndex vaut zéro. Il va de soi que C# veille à ce que deux contrôles de la même Form ne puissent jamais posséder la même valeur de TabIndex (il empêche automatiquement qu'il y ait des doublons, ou des trous dans la numérotation).

Voyons maintenant le point de vue du programmeur. À tout moment, celui-ci peut placer d'autorité le focus sur un contrôle par du code, en lui appliquant la méthode... Focus. Celle-ci est disponible pour presque tous les contrôles... excepté pour ceux qui ne peuvent recevoir le focus. Étonnant, non ?

Mais le code permet également de détecter l'arrivée du focus sur un contrôle, ou son départ. Il suffit pour cela de gérer respectivement les événements Enter et Leave, eux aussi disponibles pour la quasi-totalité des contrôles.

2. Les événements clavier

2.1 Une touche, trois événements possibles !

Dans un certain nombre d'applications, on peut souhaiter attribuer certaines conséquences à la frappe de certaines touches du clavier. Par exemple, la touche F1 doit ouvrir le fichier d'aide. Autre exemple, vous pilotez en temps réel les mouvements de Zorglubator, le grandiose vaisseau de l'hyperespace, grâce aux touches de direction.

Tout cela suppose que la frappe de telle ou telle touche du clavier soit interprétée par le logiciel comme un événement. Aucun problème, Bilou s'occupe de nous, et pour ce faire nous propose trois événements, pas un de moins.

  • Keypress : cet événement détecte le fait qu'un caractère a été frappé au clavier.
  • Keydown et Keyup : ces deux événements, qui fonctionnent de pair, se déclenchent lorsqu'une touche du clavier est enfoncée (Keydown) ou relâchée (Keyup). La caractéristique de ces deux événements est qu'ils détectent l'état physique du clavier.
Remarque finaude :
Cela signifie que les touches ne produisant pas de caractères, telles les touches de fonction ou les touches de direction, ne génèrent pas l'événement Keypress. Elles génèrent en revanche les événements KeyDown et KeyUp

2.2 Où l'on parle (enfin) du paramètre e

C'est le moment ou jamais de revenir sur un point que nous avions jusqu'à présent laissé de côté : le rôle du second paramètre en entrée des procédures événementielles, le mystérieux e. Tout comme le désormais familier sender, e est une variable qui désigne un objet. Mais cet objet n'est pas le contrôle qui a déclenché la procédure (évidemment, puisque c'est Sender, C# ne va pas mettre deux fois la même information sous deux noms différents, eh, patate). D'ailleurs, l'objet pointé par e n'est pas un contrôle du tout. Il représente, si l'on veut, les conditions, les circonstances, ou les résultats, comme on préfère, de l'événement lui-même. La nature de ses propriétés varie donc d'un événement à l'autre.

Remarque « tout se tient »
Voilà pourquoi chaque type d'action va de pair avec un type différent de e, et que les procédures gérant une action donnée ne peuvent être utilisée pour en gérer une autre : dans une procédure gérant un Click, le e est de la classe EventArgs ; mais dans une procédure gérant un KeyDown, e est de la classe KeyEventArgs, etc.

S'il s'agit d'un événement Click, disons-le tout net, il n'y a pour ainsi dire aucune propriété dans e, car rien n'est plus tristement banal et sans caractéristiques particulières qu'un clic. En revanche, s'il s'agit d'un événement clavier, l'objet e est tout de suite beaucoup plus intéressant.

Par exemple, lors d'un KeyDown ou d'un KeyUp, e possèdera plusieurs propriétés booléennes (Shift, Alt) ou numériques qui vont nous permettre de connaître en détail l'état du clavier lors du déclenchement de l'événement. La propriété numérique Keycode, par exemple, identifie, sous forme d'un code, quelle touche vient d'être enfoncée ou relâchée.

Lors d'un KeyPress, l'objet e possède une propriété de type caractère, KeyChar, qui contient le caractère généré par la touche pressée.

Ainsi, le code qui afficherait une à une, dans une MessageBox, les touches frappées au clavier serait :

private void Form1_KeyPress(object sender, KeyPressEventArgs e)
{
   MessageBox.Show("Touche frappée : " + e.KeyChar, "Paf !", MessageBoxButtons.OK);
}

Et voilà le travail, enveloppez, c'est pesé.

2.3 Un dernier détail de pure Form

Il nous reste toutefois une petite chose à régler avant d'en avoir définitivement terminé avec les événements clavier. Imaginons que nous voulions réaliser un « appel à l'aide » avec la touche F1. Pas le choix, nous devrons passer par un KeyDown (car la touche F1 n'engendrant aucun caractère, elle ne produit donc pas d'événement Keypress). Mais là où ça coince, c'est quand on réfléchit à quel contrôle il faudra affecter l'événement. En effet, lorsqu'on appuiera sur la touche F1, cet événement concernera a priori le contrôle actif (xcelui qui possède le focus). Il faudrait donc, en bonne logique, créer une procédure Chmoll.KeyDown pour chacun des contrôles Chmoll de la Form susceptibles de posséder le focus. On n'est pas rendu.

Heureusement, il y a une autre possibilité : demander à la Form la Form de court-circuiter tous les contrôles qui se trouvent sur elle en cas de frappe de touche au clavier. Il suffit pour cela de régler sa propriété KeyPreview (qu'on pourrait traduire approximativement par « interception du clavier ») à True. On n'a plus alors qu'à écrire une seule procédure, celle qui gère l'événement KeyDown sur la Form. Et dans cette  procédure, à tester si la touche frappée était bien F1, auquel cas on déclenche l'ouverture de l'aide. Qu'on se rassure, cela n'empêchera nullement, bien sûr, la touche du clavier d'avoir par ailleurs son effet normal, par exemple de venir écrire quelque chose dans une Textbox.

Et hop, comme qui rigole.

3. Événements Souris

3.1 Évènements peu remarquables

Dans la vie d'un informaticien, il n'y a pas que le clavier. Depuis belle lurette, il y a aussi la souris. Et la souris, petit animal vif et malicieux, ça peut faire plein de choses. Ça peut survoler un contrôle. Ça peut se faire appuyer un bouton (ou relâcher un bouton précédemment appuyé) pendant qu'elle est au-dessus d'un contrôle... Bref, une souris, c'est capable d'engendrer une foultitude d'événements aussi intéressants que variés.

Trois de ces événements ne requièrent guère de commentaires que cela. Il s'agit de MouseHover et de MouseEnter, qui détectent le passage de la souris sur un contrôle (la nuance entre les deux est si fine qu'elle m'a totalement échappé), et de MouseLeave qui détecte la sortie de la souris d'un contrôle.

De ces trois événements, qui effectuent le service minimum, et qui n'envoient quasiment aucun paramètre à la procédure qu'ils déclenchent, il n'y a pas grand chose à dire de plus. En revanche, d'autres événements vont nous permettre, via les propriétés du paramètre e, de récupérer des tas de renseignements utiles.

3.2 Évènements beaucoup plus intéressants

  • MouseDown : événement produit par le fait qu'un bouton de la souris vient d'être enfoncé au-dessus d'un contrôle (exactement sur le même principe que KeyDown)
  • MouseUp : événement produit par le fait qu'un bouton de la souris vient d'être relâché au-dessus d'un contrôle (cf. KeyUp)
  • MouseMove : événement produit par le déplacement de la souris au-dessus d' un contrôle (similaire donc à MouseHover... mais en mieux !)

Ces trois événements ont l'intérêt de génerer un objet e comportant plusieurs propriétés tout à fait utiles, parmi lesquelles :

  • Button : qui indique quel est le bouton de la souris à l'origine de l'événement.
  • X et Y : qui désignent les coordonnées de la souris par rapport au contrôle qui reçoit l'événement (et non par rapport à la Form – attention, à tous les coups on se fait avoir)

Remarque féline :
À noter qu'un contrôle peut partir à la chasse à la souris, et la « capturer » ! C'est-à-dire qu'il peut capter les événements souris, même s'ils ne se produisent pas au-dessus de lui... Il faut pour cela mettre la propriété Capture du contrôle à True.

3.3 Les folles aventures du curseur

Dans une interface bien pensée, la souris est un élément essentiel. Dans un sens, elle capte donc des actions de l'utilisateur et peut permettre à l'application d'y réagir en engendrant certains événements. Mais, on l'oublie trop souvent, la souris est aussi un moyen aussi essentiel que simple de communiquer des informations à l'utilisateur, via les changements de forme du curseur. Les applications bureautiques ou graphiques les plus célèbres passent ainsi leur temps à modifier le curseur de la souris pour dire à l'utilisateur que là il peut rétrécir une fenêtre, que là il peut élargir une colonne, que là il peut sélectionner toute une ligne, etc.

Manipuler l'apparence du curseur de la souris à bon escient est donc quelque chose qui ne coûte pas cher en termes de savoir-faire technique, et qui est très payante pour l'ergonomie d'une application.

Changer le curseur lorsqu'il se balade au-dessus d'un contrôle est extrêmement facile : il suffit de modifier la valeur de la propriété Cursor dudit contrôle. Ainsi, si je passe cette propriété à Cross pour Button1, le curseur de la souris se transformera en croix à chaque survol de Button1 (et se retransformera en flèche normale dès que je survolerai autre chjose ue ce bouton). C'est vraiment simple comme bonjour.

Pour demander au curseur de prendre une certaine tête, il y a deux possibilités :

  • soit on s'en tient aux curseurs alternatifs standards de Windows : la flèche avec le rond qui tourne, la croix, la flèche de redimensionnement, la mimine blanche, etc. Tous ces curseurs sont les membres de l'énumération Cursors, qui en compte un peu moins de trente ; de quoi voir venir ! Comme d'habitude, la propriété peut être réglée par défaut, dans la fenêtre du même nom, ou modifée en cours d'exécution par une ligne de code, du genre :

    button1.Cursor = Cursors.Hand

  • soit on veut sortir de sentiers battus, et adopter comme curseur un fichier récupéré sur le net ou fait maison. Ce n'est pas en soit très difficile. Le code ressemblera par exemple à un truc du genre :

    button1.Cursor = New Cursor("monfichier.cur")

    Le problème est que C# est extrêmement chatouilleux sur le type de fichiers capables de jouer le rôle de curseur (pas d'animation, pas de couleur, etc.). Et donc, si en théorie, la possibilité est séduisante, en pratique il est assez rare qu'on l'emploie.

Puisqu'on en est à parler du curseur, je mentionne que celui-ci possède bien d'autres propriétés, à commencer par celle qui définit son emplacement : Position. Cette propriété, tout comme Size ou Location, déjà rencontrées, est une propriété structurée – elle est formée de deux entiers qui désignent l'abcisse et l'ordonnée du curseur, sachant que l'origine du repère se situe au coin supérieur gauche de l'écran.

Position peut être utilisée, au choix, en lecture (pour récupérer la position actuelle du curseur) ou en écriture (pour la modifier).

En lecture, le plus simple est d'utiliser le fait que cette propriété se décompose en deux sous-propriétés, X et Y. Pour récupérer l'emplacement du curseur dans l'écran, on pourra ainsi écrire :

int abscisseCurseur = Cursor.Position.X
int ordonneeCurseur = Cursor.Position.Y

En écriture, on devra savoir que le nom de la structure sur laquelle est bâtie la propriété Position n'est pas, comme on pourrait s'y attendre logiquement, « Position », mais... Point. Une fois ce petit détail réglé, ça va tout seul, par exemple pour placer autoritairement le curseur dans le coin supérieur gauche de l'écran :

Cursor.Position = new Point(0, 0);

Allez, distrayons-nous un peu :
 

Exercice

Exécutable

Sources

Questionnaire

3. Le Glisser - Déposer (Drag & Drop)

Un des trucs balaises dans l'interface graphique de Windows, c'est qu'on peut se servir de sa souris pour prendre des trucs à un endroit, les trimballer et aller les mettre ailleurs. La quasi-totalité des logiciels exploitent cette possibilité, et il serait quand bien même bien dommage que nous n'apprenions pas à programmer avec C# ce qu'on appelle en français le « Glisser - Déposer », et en anglais le « Drag and Drop ». N'est-il pas ?

Cela dit, mieux vaut le savoir, mettre en oeuvre le Drag and Drop, cela suppose une fieffée dose de patience et de rigueur, car le moins qu'on puisse dire, c'est que ça ne glisse pas comme sur des roulettes et qu'à la fin, c'est souvent les armes qu'on dépose. Mais bon, en y allant le plus rationnellement et le plus méthodiquement possible, on peut espérer s'en sortir vivants.

3.1 Approche générale

3.1.1 Les préparatifs

Un Drag & Drop est un traitement un chouia complexe, composé d'un certain nombre d'instructions et de procédures évènementielles obligatoires. Et naturellement, on peut enrichir tout cela par des choses plus facultatives. Comme à chaque jour suffit sa peine, commençons par le strict nécessaire. Un Drag & Drop implique toujours au moins deux contrôles :

  1. celui qui est « draggé » (que l'Académie me pardonne ce néologisme). C''est le contrôle sur lequel on va enfoncer le bouton gauche de la souris (Drag...) afin de déclencher les opérations – je me propose de l'appeler, dans la suite de ce texte, ContrôleD (avec un D comme Départ).
  2. celui au-dessus duquel on va relâcher le bouton de la souris pour provoquer le Drop. J'appellerai ce contrôle ContrôleC (avec un C comme Cible).

Remarque de bonne méthode :
Ces définitions sont totalement indépendantes des effets précis recherchés par le Drag & Drop ; elles s'appliquent donc à toutes les situations sans exception.
La première chose à faire avant d'écrire quelque ligne que ce soit consiste donc à identifier clairement, dans le problème que l'on veut traiter, quel contrôle jouera le rôle de ContrôleD, et lequel celui de ContrôleC.

Poursuivons. Pour qu'il puisse se passer quoi que ce soit, deux conditions indispensables doivent être présentes.

  1. il faut que le Drag puisse être effectué sur ContrôleD. Pour cela, il faut qu'existe une procédure déclenchée par l'événènement qui inaugure un Drag, à savoir ContrôleD.MouseDown.
  2. il faut également que le Drop puisse être effectué sur ContrôleC. Mais là, attention : en plus de devoir gérer l'événement correspondant, à savoir ContrôleC.DragDrop, nous aurons dû préalablement fixer la propriété booléenne Allowdrop de ContrôleC à True.

3.1.2 Le décollage

À ce stade de la compétition, tout est prêt. Les rampes de lancement sont dressées, les moteurs allumés. Il n'y a plus qu'à effectuer la mise à feu. Celle-ci, le déclenchement du Drag & Drop proprement dit, s'effectue via le passage de la méthode DoDragDrop à ContrôleD. Le truc, c'est que cette méthode réclame deux arguments pas évidents à comprendre par la simple intuition :

  • Data : il s'agit d'un objet, chargé de contenir les informations qui devront être transmises lors du Drag & Drop. J'y reviendrai dans la partie suivante, mais sachez qu'on n'est pas obligé de se servir de ce praramètre « pour de vrai », et que par conséquent, on peut mettre absolument ce qu'on veut dans cet argument, comme par exemple la chaîne de caractère "AstalavistaBaby".
  • AllowedEffects : il s'agit d'un paramètre permettant d'indiquer à l'utilisateur ce que va provoquer le Drag & Drop qu'il est en train d'effectuer : les valeurs autorisées sont les membres d'une énumération (à choisir parmi All, Copy, Link, Move, None, Scroll) qui influeront sur la forme du curseur – et seulement sur elle.

Tout cela donnea donc une ligne pouvant par exemple ressembler à :

ControleD.DoDragDrop("AstalavistaBaby", DragDropEffects.All)

Remarque de mise à feu :
La méthode DoDragDrop déclenche de manière immédiate l'interruption de l'exécution de la procédure en cours (MouseDown) et le déclenchement des autres procédures liées au Drag & Drop. Ce n'est qu'une fois celles-ci terminées que C# reviendra à la procédure MouseDown interrompue, et terminera son exécution.
Des instructions placées avant le DoDragDrop seront donc exécutées avant même le décollage, des instructions placées après ne sront exécutées qu'une fois l'aterrissage effectué... et donc, possiblement, au début du Drag & Drop suivant, ce qui peut être un peu déconcertant quand on ne s'est pas déjà fait piéger une ou deux fois.

3.1.3 Le survol

À partir du moment où l'instruction DoDragDrop est exécutée, le Drag & Drop proprement dit est enclenché. On pourrait (naïvement) penser que celui-ci se limite ensuite à gérer le Drop, c'est-à-dire l'atterrissage. Mais nenni. C# nous impose de gérer un évènement supplémentaire, sans lequel rien ne fonctionnera. C'est d'autant plus déroutant que cet évènement semble ne pas servir à grand chose, mais bon, pas le choix, il faut faire, on fait.

Cet évènement en question est celui qui correspond à l'arrivée du curseur au-dessus du contrôle cible, autrement dit ControleC.DragEnter. La procédure qui lui correspond devra comporter une instruction, et une seule, consistant à affectuer la propriété Effect de l'objet e, par un membre de l'émnumération déjà rencontrée concernant les images de curseur. C'est un peu comme si on disait à la machine : « Attention, un engin volant non identifié est en approche. Nous lui donnons l'autorisation d'atterrir. »

Cela donnera, par exemple :

e.Effect = DragDropEffects.All

Et toc, un problème de plus en moins.

3.1.4 L'atterrissage

On en arrive à l'ultime étape : le Drop proprement dit. Ceci ne pose aucune difficulté : il suffit de gérer l'évènement adéquat, soit ControleC.DragDrop. C'est dans cette procédure qu'on pourra donner toutes les instructions permettant d'obtenir les effets désirés.

Remarque résumeuse :
Un Drag & Drop réclame donc au strict minimum la création de trois procédures évènementielles : ControleD.MouseDown, ControleC.DragEnter et ControleC.DragDrop.
Naturellement, il peut y en avoir davantage, par exemple si l'on souhaite gérer plusieurs cibles pour le même contrôle de départ, plusieurs contrôles de départ pour la même cible ou plusieurs contrôles de départ pour plusieurs cibles.

3.2 Programmer les effets du Drag & Drop

Jusqu'ici, nous avons confectionné un superbe écrin... vide. En effet, nous avons vu dans le détail comment programmer l'ensemble des évènements d'un Drag & Drop, mais finalement, cette belle série de cabrioles ne mène (pour l'instant) à rien du tout. On prend un contrôle (ou un élément de contrôle), on le trimbale, on relâche le bouton, et là... rien. Maintenant qu'on a le fusil, il est donc grand temps de mettre la balle dedans.

3.2.1 La méthode officielle (déconseillée)

Théoriquement, l'effet le plus courant d'un Drag & Drop, à savoir le transport d'une information depuis le contrôle D vers le contrôle C, est censé être pris en charge par un des objets qui sont générés par les évènements du Drag & Drop, à savoir le fameux paramètre e ; et plus précisément, par sa propriété Data. Toute l'affaire se déroule en deux temps.

Et d'une, au démarrage du Drag and Drop, c'est-à-dire lors du passage de la méthode DoDragDrop, l'information voulue est passée à e.Data : c'est le premier des deux arguments de cette méthode, celui pour lequel on a rentré tout à l'heure la première chose qui nous passait par la tête, à savoir "AstalavistaBaby". Mais en théorie, nous étions censés écrire ici une valeur plus significative et utile pour notre problème, comme le nom du contrôle, la valeur de l'une de ses propriétés, un de ses items si c'est une liste, etc.

Et de deux, le plus rigolo. Lors de l'atterrissage, c'est à dire dans la procédure DragDrop, on peut récupérer l'information qui se trouve dans la propriété e.Data et ainsi s'en servir. Sauf que... sauf que, cette récupération pose des problèmes de syntaxe à ingurgiter des aspirines par seaux entiers. Pour commencer, quel que soit la nature de son contenu, pas question d'accéder directement à l'information recherchée. Il va impérativement falloir recourir à la méthode GetData. Ce qui donnera donc un très joli :

e.Data.GetData

Mais les ennuis ne font que commencer. Si les informations qu'on a rangées dans cette propriété ont le bon goût d'être d'une nature simple (du texte), il faudra préciser de surcroît le format dans lequel on veut récupérer l'information, par le très joli DataFormats.Text... sans omettre de préciser que C# doit convertir le tout en type String !

On aboutit donc à l'usine à gaz suivante :

e.Data.GetData(DataFormats.Text).ToString

Tout ça pour une malheureuse donnée de type texte ! Comme disait (à peu près) Obélix : « Ils sont fous chez Redmond ! » Car, est-il besoin de le préciser, dès que la donnée stockée dans e est d'un type plus complexe (structure, image...) la récupérer dans le e.Data va devenir une vraie torture, où l'on pourra passer des heures à chercher (en vain) la syntaxe adéquate. Voilà pourquoi, très franchement, je ne vois aucun avantage à utiliser les voies officielles, et je conseille vivement d'opérer en contrebande.

3.2.2 La technique de contrebande (de loin la plus simple)

Cette technique va consister à éviter comme la peste l'objet e, et à véhiculer tout simplement la ou les informations voulues... par des variables ordinaires. Étant donné les circonstances, il faudra veiller à deux choses :

  • les variables, puisqu'on les affecte dans la procédure MouseDown et qu'on récupérera a priori leur valeur dans la procédure DragDrop, devront nécessairement être publiques au niveau Form.
  • la méthode DoDragDrop exigeant que le premier de ses deux arguments, celui qui remplira le e.Data, soit renseigné, on n'oubliera pas de lui fournir... absolument n'importe quoi, puisque cette valeur aura pour seul rôle d'éviter une erreur de C#. Donc, parmi les candidats possibles, on trouvera "AstalavistaBaby", "Zigopaf", "MerdaBill" ou, plus sobrement, "Toto".

3.3 Brancher les événements par du code

La dernière chose à dire, c'est que tous les événements souris, qu'il s'agisse ou non de drag & drop, demandent une instruction un peu spécifique pour pouvoir être programmés par du code. Il n'est en effet pas possible d'utiliser, comme nous l'avons fait jusqu'à présent, l'instruction EventHandler pour gérer ce type d'évenements. En l'occurrence, on devra donc passer par deux variantes :

  • MouseEventHandler pour gérer des événements souris simples, tels que MouseDown
  • DragEventHandler pour gérer des événements souris caractéristiques du drag & drop, tels que DragEnter ou DragDrop.

Remarque astuce de la mort qui tue :
En cas de doute sur l'instruction à utiliser pour le branchement automatique, pas de panique. Il suffit d'exploiter la vigilance du déboggeur C#. On crée donc la procédure voulue via la fenêtre design ; on supprime aussitôt cette procédure dans la fenêtre du code. C# proteste alors, car un événement pointe dorénavant sur une procédure inexistante, et il propose de régler le problème en cliquant sur un lien qui nous envoie vers le code correspondant au design. On voit donc là quelle est la bonne instructionde branchement. On supprime la ligne litigieuse, et le tour est joué, on a le renseignement manquant !

Le premier exercice, Boîte à Meuh, est une simple petite mise en jambes : aucune difficulté, aucun piège.
Picasso est un tout petit peu plus stimulant, car il faudra appliquer les raisonnements du drag & drop aux collections.
Quant aux Lapins c'est la même chose... en nettement pire !

Exercice

Exécutable

Sources

Boîte à Meuh

Picasso

Lapins