From 3f615ae86d64e3e3a5e118a2121ac5ae7919ca49 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Derrode?= <stephane.derrode@ec-lyon.fr>
Date: Sat, 6 Mar 2021 08:00:26 +0100
Subject: [PATCH] update BE #4

---
 seance4_4h/seance4_4h.md | 99 +++++++++++++++++++++-------------------
 1 file changed, 52 insertions(+), 47 deletions(-)

diff --git a/seance4_4h/seance4_4h.md b/seance4_4h/seance4_4h.md
index 8d03662..fb4979a 100644
--- a/seance4_4h/seance4_4h.md
+++ b/seance4_4h/seance4_4h.md
@@ -1,10 +1,10 @@
+
 **Sommaire**
 
 [[_TOC_]]
 
 # BE #4 : Application de dessin vectoriel
 
-
 L'objectif de ce BE est d'apprendre à manipuler quelques composants du module _Python_ _Tkinter_ permettant de créer des interfaces graphiques. Vous allez créer une application simple de dessin vectoriel, qui permettra de tracer à la souris les formes définies dans le BE #2.
 
 ---
@@ -17,33 +17,39 @@ Durant ce BE, nous vous recommandons de conserver la [documentation de _Tkinter_
 Voici un premier exemple de code _Tkinter_ :
 
 ```python
-import random
 from tkinter import *
+from random import randint
+
+class FenPrincipale(Tk):
+    def __init__(self):
+        Tk.__init__(self)
+        
+        # paramètres de la fenêtre
+        self.title('Tirage aléatoire')
+        self.geometry('300x100+400+400')
+        
+        # constitution de son arbre de scène
+        boutonLancer = Button(self, text='Tirage')
+        boutonLancer.pack(side=LEFT, padx=5, pady=5)
+        self.__texteResultat = StringVar()
+        labelResultat = Label(self, textvariable=self.__texteResultat)
+        labelResultat.pack(side=LEFT, padx=5, pady=5)
+        boutonQuitter = Button(self, text='Quitter')
+        boutonQuitter.pack(side=LEFT, padx=5, pady=5)
+        
+        # association des widgets aux fonctions
+        boutonLancer.config(command=self.tirage) # appel "callback" (pas de parenthèses)
+        boutonQuitter.config(command=self.quit)  # idem
+    
+    # tire un entier au hasard et l'affiche dans self.__texteResultat
+    def tirage(self):
+        nb = randint(1, 100)
+        self.__texteResultat.set('Nombre : ' + str(nb))
 
-def tirage():
-    nb = random.randint(1, 100)
-    texteResultat.set('Nombre : ' + str(nb))
 
 if __name__ == '__main__':
-    
-    # création de l'arbre de scène
-    racine = Tk() # Appel à une méthode de classe (et non un constructeur, cf slide cours #2)
-    racine.title('Tirage aléatoire')
-    racine.geometry('300x100+400+400')
-    
-    # Les widgets de la scène
-    boutonLancer = Button(racine, text='Tirage')
-    boutonLancer.pack(side=LEFT, padx=5, pady=5)
-    texteResultat = StringVar()
-    labelResultat = Label(racine, textvariable=texteResultat)
-    labelResultat.pack(side=LEFT, padx=5, pady=5)
-    boutonQuitter = Button(racine, text='Quitter')
-    boutonQuitter.pack(side=LEFT, padx=5, pady=5)
-
-    # association des commandes aux widgets
-    boutonLancer.config(command=tirage) # appel dit callback (pas de parenthèses)
-    boutonQuitter.config(command=racine.quit) # idem
-    racine.mainloop() # affichage de l'interface jusqu'à l'appui de Quitter
+    app = FenPrincipale()
+    app.mainloop()
 ```
 
 
@@ -52,28 +58,25 @@ __Exercice 1 -__ Copiez le code suivant dans un fichier appelé *Exo1.py* et ex
 __Attention, utilisateurs de Mac__ : l'association _Spyder_+_Tkinter_ ne fonctionne pas bien sous Mac ! Lorsque vous quitterez l'interface (par le biais du bouton _Quitter_), la fenêtre va se bloquer (_freeze_). Deux solutions: 
 
  - soit vous forcez l'application à s’arrêter à chaque fois (utilisez le menu contextuel sur l'icône de l'application concernée dans la barre d'outils);
- - soit vous exécutez votre programme en ligne de commande. Pour cela, ouvrez un terminal dans le répertoire de travail (clic-droit dessus → Nouveau terminal au dossier). Puis lancer la commande : `python3 Exo1.py`. Vous devriez pouvoir quitter l'application sans difficulté. N'oubliez pas de sauvegarder votre fichier sous _Spyder_ avant toute exécution de cette manière !
+ - soit vous exécutez votre programme en ligne de commande. Pour cela, ouvrez un terminal dans le répertoire de travail (clic-droit sur le répertoire → Nouveau terminal au dossier). Puis lancer la commande : `python3 Exo1.py`. Vous devriez pouvoir quitter l'application sans difficulté. N'oubliez pas de sauvegarder votre fichier sous _Spyder_ avant toute exécution de cette manière !
 
 Prenez le temps d'étudier cet exemple, et répondez aux questions suivantes :
 
 * Combien d'éléments contient l'arbre de scène ?
-* Que se passe-t-il lorsqu'on clique sur le bouton ?
+* Que se passe-t-il lorsqu'on clique sur le bouton `Tirage` ?
 * Comment peut-on inverser les positions des deux boutons ?
 * Comment peut-on augmenter l'espace à gauche et à droite du label ?
 * Comment peut-on colorier le texte du label en rouge ?
 
 
 ---
-## Squelette de l'application de dessin (45 min.)
+## Squelette de l'application de dessin (60 min.)
 
 On souhaite obtenir l'interface ci-dessous, dans laquelle les utilisateurs sélectionneront le type de forme à dessiner avec les boutons, et créeront une forme en cliquant dans la zone située sous la barre d'outils (_widget_ __Canvas__ de _Tkinter_). On a donné une couleur grise au fond de la fenêtre pour vous aider à déterminer les différents _widgets_ présents.
 
 <center><img src="figures/interface.png" style="width:60%"/></center>
 
-__Exercice 2 -__ Dessinez l'arbre de scène correspondant à cette capture d'écran.
-
-
-Une pratique courante dans les interfaces graphiques est de créer des classes qui _remplacent_ des nœuds de l'arbre de scène, et d'y mettre le code de l'application. Ces classes héritent des classes de _Tkinter_ (pour pouvoir les remplacer dans l'arbre), et nous leur ajouterons des attributs et méthodes spécifiques à leurs responsabilités dans l'application de dessin. Nous allons ainsi introduire deux classes :
+Une pratique courante dans les interfaces graphiques est d'intégrer le code de l'application dans l'arbre de scène, en créant des classes qui s'y intégreront comme des nœuds. Ces classes héritent des classes de _Tkinter_ (pour êtres autorisées à les remplacer dans l'arbre), et nous leur ajouterons des attributs et méthodes spécifiques à leurs responsabilités dans l'application de dessin. Nous allons ainsi introduire deux classes :
 
 * la classe __ZoneAffichage__, qui hérite de __Canvas__ et gère toutes les opérations de dessin spécifiques à votre application.
 * la classe __FenPrincipale__, qui hérite de __Tk__ et gère l'initialisation de l'arbre de scène et des _callbacks_ des _widgets_.
@@ -82,7 +85,10 @@ Voici le diagramme UML correspondant :
 
 <center><img src="figures/Fenetre_ZoneAffichage_1.svg" style="width:50%"/></center>
 
-__Exercice 3 -__ Complétez le code ci-dessous avec l'initialisation de votre arbre de scène. Vous utiliserez une instance de __ZoneAffichage__ à la place de __Canvas__. À ce stade, on ne vous demande pas de programmer les actions, uniquement de mettre en place le design de l'interface. Vous trouverez des exemples d'utilisation de chacun des _widgets_ dans la documentation référencée plus haut.
+__Exercice 2 -__ Dessinez l'arbre de scène correspondant à la capture d'écran. Pour chaque nœud vous indiquerez la classe, et s'il y a lieu sa classe parente également.
+
+
+__Exercice 3 -__ Complétez le code ci-dessous avec l'initialisation de votre arbre de scène. Vous utiliserez une instance de __ZoneAffichage__ pour implémenter le canevas. À ce stade, on ne vous demande pas de programmer les actions, uniquement de mettre en place le design de l'interface. Vous trouverez des exemples d'utilisation de chacun des _widgets_ dans la documentation référencée plus haut.
 
 ```python
 from tkinter import *
@@ -94,6 +100,7 @@ class ZoneAffichage(Canvas):
 class FenPrincipale(Tk):
     def __init__(self):
         Tk.__init__(self)
+        self.configure(bg="grey")
         # L'initialisation de l'arbre de scène se fait ici
 
 if __name__ == "__main__":
@@ -101,8 +108,9 @@ if __name__ == "__main__":
     fen.mainloop()
 ```
 
+
 ---
-## Dessin de formes dans le canevas (60 min.)
+## Dessin de formes dans le canevas (75 min.)
 
 Vous trouverez dans le dossier de ce BE le fichier [formes.py](formes.py) développé durant le BE #2. Nous avons agrémenté les classes __Rectangle__ et __Ellipse__ pour qu'elles reçoivent un canevas en argument et se dessinent dessus lors de leur initialisation. Téléchargez ce fichier dans votre répertoire de travail.
 
@@ -110,27 +118,24 @@ Les classes seront intégrées selon le diagramme UML suivant :
 
 <center><img src="figures/Fenetre_ZoneAffichage_2.svg" style="width:90%"/></center>
 
-__Exercice 4 -__ Créez une méthode `ajout_forme(..., x, y)` dans __ZoneAffichage__ qui crée un __Rectangle__ dont le centre sera donné par les 2 arguments _x_ et _y_, de largeur 10 et de hauteur 20. N'oubliez pas de stocker ce rectangle dans __ZoneAffichage__ !
+__Exercice 4 -__ À l'aide de la méthode `bind` vue en cours, reliez les clics de la souris dans le canevas (événements `<ButtonRelease-1>`) à une nouvelle méthode de __ZoneAffichage__ qui imprime les coordonnées de chaque clic avec `print`.
 
-__Exercice 5 -__ À l'aide de la méthode `bind` vue en cours, reliez les clics de souris sur le canevas (évènements `<ButtonRelease-1>`) à la méthode `ajout_forme`. Attention, pour utiliser une méthode comme fonction de _callback_, il faut la précéder de `self.`.
+__Exercice 5 -__ Modifiez cette méthode pour créer un nouveau __Rectangle__ centré sur la souris chaque fois que la méthode est exécutée (choisissez des dimensions arbitraires). N'oubliez pas de stocker ce rectangle dans __ZoneAffichage__ !
 
-__Exercice 6 -__ Ajoutez un attribut à __ZoneAffichage__ qui stocke le type de forme actuellement sélectionné, et associez les boutons Rectangle/Ellipse au type de forme qui est dessiné lorsqu'on clique dans le canevas.
+__Exercice 6 -__ Lorsqu'on clique sur le bouton `Ellipse`, l'outil "Ellipse" est sélectionné et tous les futurs clics dans le canevas doivent créer une nouvelle __Ellipse__. Lorsqu'on clique ensuite sur le bouton `Rectangle`, les clics suivants créeront un __Rectangle__. L'outil sélectionné par défaut est "Rectangle". Modifiez votre code pour implémenter ce comportement.
 
 
 ---
-## Quelques opérations de dessin supplémentaires (90 min.)
+## Opérations de dessin supplémentaires (60 min.)
 
-Nous allons à présent intégrer quelques commandes simples dans l'application de dessin :
+Nous allons à présent intégrer deux commandes simples dans l'application de dessin :
 
-* Lorsqu'on clique sur une forme en maintenant la touche CTRL enfoncée, elle doit être effacée du canevas.
-* Lorsqu'on déplace la souris avec le bouton enfoncé sur une forme, on déplace la forme en même temps que la souris.
-* Lorsqu'on clique sur le bouton _Couleur_, un sélecteur de couleur apparaît pour choisir la couleur de l'outil de dessin.
+* Lorsqu'on clique sur une forme en maintenant la touche CTRL enfoncée, elle doit s'effacer du canevas.
+* Lorsqu'on clique sur le bouton _Couleur_, un sélecteur de couleurs apparaît pour choisir la couleur de l'outil de dessin.
 
 __Exercice 7 -__ Implémentez l'effacement des formes avec CTRL-clic (événement `<Control-ButtonRelease-1>`). Vous pourrez faire appel aux méthodes `contient_point(...)` des classes __Rectangle__ et __Ellipse__ pour déterminer si la position de la souris au moment de l’événement est dans le périmètre d'une forme donnée, ainsi qu'à la méthode `effacer(...)` de la classe __Forme__.
 
-__Exercice 8 -__ À l'aide du module _colorchooser_ de _Tkinter_ (```from tkinter import colorchooser```), liez les clics sur le bouton Couleur à l'affichage d'un sélecteur de couleur, et utilisez la couleur renvoyée pour tous les ajouts de formes suivants.
-
-__Exercice 9 -__ À l'aide des types d’événements `<Button-1>`, `<B1-Motion>` et `<ButtonRelease-1>`, implémentez la translation des formes lors des actions d'appui-déplacement de la souris. Comment faire pour qu'elles n'interfèrent pas avec la création de nouvelles formes ?
+__Exercice 8 -__ À l'aide du module _colorchooser_ de _Tkinter_ (```from tkinter import colorchooser```), liez les clics sur le bouton _Couleur_ à l'affichage d'un sélecteur de couleur, et utilisez la couleur renvoyée pour tous les ajouts de formes suivants.
 
 
 ---
@@ -138,8 +143,8 @@ __Exercice 9 -__ À l'aide des types d’événements `<Button-1>`, `<B1-Motion>
 
 Il n'y a pas d'ordre prédéfini pour ces trois exercices supplémentaires, choisissez celui dont la fonctionnalité vous semble la plus intéressante.
 
-__Bonus 1 -__ Durant le BE #2 vous avez conçu un troisième type de forme. Il est temps de l'intégrer à votre application de dessin ! Inspirez-vous du code du fichier _formes.py_ de ce BE pour adapter la classe que vous aviez développée. Vous trouverez également les instructions de dessin dans la documentation de Tkinter sur __Canvas__.
+__Bonus 1 -__ Dans tout programme de dessin respectable, on doit pouvoir dessiner des formes de tailles arbitraires (pas prédéfinies). À l'aide des types d’événements `<Button-1>`, `<B1-Motion>` et `<ButtonRelease-1>`, faites qu'un mouvement de souris avec le bouton enfoncé dessine une forme en tirant ses coins (lorsqu'il ne déplace pas une forme existante). Vous pourrez utiliser les méthodes `redimension_par_points` des classes __Rectangle__ et __Ellipse__.
 
-__Bonus 2 -__ Maintenant que votre programme de dessin vectoriel est fonctionnel, il devrait être possible d'exporter chaque image produite dans un fichier. On utilise pour cela le format SVG, qui est un fichier texte contenant des instructions de dessin. Il suffit d'écrire `<svg width=600 height=400 xmlns=http://www.w3.org/2000/svg>` au début du fichier, `</svg>` à la fin, et d'insérer des balises [`rect`](https://developer.mozilla.org/fr/docs/Web/SVG/Element/rect) et [`ellipse`](https://developer.mozilla.org/fr/docs/Web/SVG/Element/ellipse) entre les deux. C'est à vous de jouer !
+__Bonus 2 -__ Il serait aussi pratique de pouvoir déplacer les formes présentes sur le canevas. À l'aide des types d’événements `<Button-1>`, `<B1-Motion>` et `<ButtonRelease-1>`, implémentez la translation des formes lors des actions d'appui-déplacement de la souris. Comment faire pour qu'elles n'interfèrent pas avec la création de nouvelles formes ?
 
-__Bonus 3 -__ Dans tout programme de dessin respectable, on doit pouvoir dessiner des formes de tailles arbitraires (pas prédéfinies). À l'aide des types d’événements `<Button-1>`, `<B1-Motion>` et `<ButtonRelease-1>`, faites qu'un mouvement de souris avec le bouton enfoncé dessine une forme en tirant ses coins (lorsqu'il ne déplace pas une forme existante). Vous pourrez utiliser les méthodes `redimension_par_points` des classes __Rectangle__ et __Ellipse__.
+__Bonus 3 -__ Maintenant que votre programme de dessin vectoriel est fonctionnel, on devrait pouvoir exporter chaque image produite dans un fichier. On utilise pour cela le format SVG, qui est un fichier texte contenant des instructions de dessin. Il suffit d'écrire `<svg width=600 height=400 xmlns=http://www.w3.org/2000/svg>` au début du fichier, `</svg>` à la fin, et d'insérer des balises [`rect`](https://developer.mozilla.org/fr/docs/Web/SVG/Element/rect) et [`ellipse`](https://developer.mozilla.org/fr/docs/Web/SVG/Element/ellipse) entre les deux. C'est à vous de jouer !
-- 
GitLab