« Programmation Python/Classes et Interfaces graphiques » : différence entre les versions

Contenu supprimé Contenu ajouté
Aucun résumé des modifications
Ligne 36 :
Au niveau principal du programme, nous nous contentons d'instancier un objet de la classe ainsi construite (aucune méthode de cet objet n'est activée de l'extérieur).
 
<source lang=python line>
<pre>
class Application: #1
def __init__(self): #2
Ligne 106 :
from math import log10 # logarithmes en base 10 #68
f = Application() # instanciation de l'objet application #69
</presource>
 
;Commentaires
Ligne 112 :
* Ligne 1 : La classe est définie sans référence à une classe parente (pas de parenthèses). Il s'agira donc d'une nouvelle classe indépendante.
 
* Lignes 2 à 14 : Le constructeur de la classe instancie les ''widgets'' nécessaires : pour améliorer la lisibilité du programme, on a placé l'instanciation du canevas (avec le dessin de la résistance) dans une méthode séparée <code>dessineResistance()</code>. Les boutons et le libellé ne sont pas mémorisés dans des variables, parce que l'on ne souhaite pas y faire référence ailleurs dans le programme. Le positionnement des ''widgets'' dans la fenêtre utilise la méthode <code>grid()</code>, décrite à la page {{todo}}.
 
* Lignes 15-17 : Le code des couleurs est mémorisé dans une simple liste.
Ligne 158 :
Nous allons à présent passer à la vitesse supérieure et réaliser une petite application sur la base de plusieurs classes, afin d'examiner ''comment différents objets peuvent s'échanger des informations par l'intermédiaire de leurs méthodes''. Nous allons également profiter de cet exercice pour vous montrer comment vous pouvez définir la classe principale de votre application graphique par ''dérivation'' d'une classe ''Tkinter'' préexistante, mettant ainsi à profit le mécanisme d'héritage.
 
Le projet développé ici très simple, mais il pourrait constituer une première étape dans la réalisation d'un logiciel de jeu : nous en fournissons d'ailleurs des exemples plus loin (voir page {{todo}}). Il s'agit d'une fenêtre contenant un canevas et deux boutons. Lorsque l'on actionne le premier de ces deux boutons, un petit train apparaît dans le canevas. Lorsque l'on actionne le second bouton, quelques petits personnages apparaissent à certaines fenêtres des wagons.
 
[[Image:Apprendre à programmer avec Python 38.png|center]]
Ligne 172 :
;Implémentation
 
<source lang=python line>
<pre>
1.from Tkinter import *
 
2.
3.def cercle(can, x, y, r):
4. "dessin d'un cercle de rayon <r> en <x,y> dans le canevas <can>"
5. can.create_oval(x-r, y-r, x+r, y+r)
 
6.
7.class Application(Tk):
8. def __init__(self):
9. Tk.__init__(self) # constructeur de la classe parente
10. self.can =Canvas(self, width =475, height =130, bg ="white")
11. self.can.pack(side =TOP, padx =5, pady =5)
12. Button(self, text ="Train", command =self.dessine).pack(side =LEFT)
13. Button(self, text ="Hello", command =self.coucou).pack(side =LEFT)
14.
15. def dessine(self):
16. "instanciation de 4 wagons dans le canevas"
17. self.w1 = Wagon(self.can, 10, 30)
18. self.w2 = Wagon(self.can, 130, 30)
19. self.w3 = Wagon(self.can, 250, 30)
20. self.w4 = Wagon(self.can, 370, 30)
21.
22. def coucou(self):
23. "apparition de personnages dans certaines fenêtres"
24. self.w1.perso(3) # 1er wagon, 3e fenêtre
25. self.w3.perso(1) # 3e wagon, 1e fenêtre
26. self.w3.perso(2) # 3e wagon, 2e fenêtre
27. self.w4.perso(1) # 4e wagon, 1e fenêtre
28.
29.class Wagon:
30. def __init__(self, canev, x, y):
31. "dessin d'un petit wagon en <x,y> dans le canevas <canev>"
32. # mémorisation des paramètres dans des variables d'instance :
33. self.canev, self.x, self.y = canev, x, y
34. # rectangle de base : 95x60 pixels :
35. canev.create_rectangle(x, y, x+95, y+60)
36. # 3 fenêtres de 25x40 pixels, écartées de 5 pixels :
37. for xf in range(x+5, x+90, 30):
38. canev.create_rectangle(xf, y+5, xf+25, y+40)
39. # 2 roues de rayon égal à 12 pixels :
40. cercle(canev, x+18, y+73, 12)
41. cercle(canev, x+77, y+73, 12)
42.
43. def perso(self, fen):
44. "apparition d'un petit personnage à la fenêtre <fen>"
45. # calcul des coordonnées du centre de chaque fenêtre :
46. xf = self.x + fen*30 -12
47. yf = self.y + 25
48. cercle(self.canev, xf, yf, 10) # visage
49. cercle(self.canev, xf-5, yf-3, 2) # oeil gauche
50. cercle(self.canev, xf+5, yf-3, 2) # oeil droit
51. cercle(self.canev, xf, yf+5, 3) # bouche
 
52.
53.app = Application()
54.app.mainloop()
</presource>
 
;Commentaires
Ligne 233 :
* Lignes 3 à 5 : Nous projetons de dessiner une série de petits cercles. Cette petite fonction nous facilitera le travail en nous permettant de définir ces cercles à partir de leur centre et leur rayon.
 
* Lignes 7 à 13 : La classe principale de notre application est construite par dérivation de la classe de fenêtres <code>Tk()</code> importée du module Tkinter<ref>Nous verrons plus loin que ''Tkinter'' autorise également de construire la fenêtre principale d'une application par dérivation d'une classe de ''widget'' (le plus souvent, il s'agira d'un ''widget'' <code>Frame()</code>). La fenêtre englobant ce ''widget'' sera automatiquement ajoutée. (Voir page {{todo}}).</ref>. Comme nous l'avons expliqué au chapitre précédent, le constructeur d'une classe dérivée doit activer lui-même le constructeur de la classe parente, en lui transmettant la référence de l'instance comme premier argument. Les lignes 10 à 13 servent à mettre en place le canevas et les boutons.
 
* Lignes 15 à 20 : Ces lignes instancient les 4 objets-wagons, produits à partir de la classe correspondante. Ceci pourrait être programmé plus élégamment à l'aide d'une boucle et d'une liste, mais nous le laissons ainsi afin de ne pas alourdir inutilement les explications qui suivent. Nous voulons placer nos objets-wagons dans le canevas, à des emplacements bien précis : il nous faut donc transmettre quelques informations au constructeur de ces objets : au moins la référence du canevas, ainsi que les coordonnées souhaitées. Ces considérations nous font également entrevoir, que lorsque nous définirons la classe <code>Wagon()</code> un peu plus loin, nous devrons associer à sa méthode constructeur un nombre égal de paramètres pour réceptionner ces arguments.
Ligne 260 :
<ol>
<li>
<source lang=python>
<pre>
from Tkinter import *
 
Ligne 327 :
 
Application().app.mainloop()
</presource>
</li>
</ol>
Ligne 348 :
Veuillez donc encoder le script ci-dessous et le sauvegarder dans un fichier, auquel vous donnerez le nom <code>oscillo.py</code>. Vous réaliserez ainsi un véritable ''module'' contenant une classe (vous pourrez par la suite ajouter d'autres classes dans ce même module, si le cœur vous en dit).
 
<source lang=python line>
{{todo|num à droite}}
from Tkinter import *
from math import sin, pi
 
class OscilloGraphe(Canvas):
<pre>
"Canevas spécialisé, pour dessiner des courbes élongation/temps"
1.from Tkinter import *
def __init__(self, boss =None, larg=200, haut=150):
2.from math import sin, pi
"Constructeur du graphique : axes et échelle horiz."
3.
# construction du widget parent :
4.class OscilloGraphe(Canvas):
Canvas.__init__(self) # appel au constructeur
5. "Canevas spécialisé, pour dessiner des courbes élongation/temps"
self.configure(width=larg, height=haut) # de la classe parente
6. def __init__(self, boss =None, larg=200, haut=150):
self.larg, self.haut = larg, haut # mémorisation
7. "Constructeur du graphique : axes et échelle horiz."
8. # constructiontracé dudes widgetaxes parentde référence :
self.create_line(10, haut/2, larg, haut/2, arrow=LAST) # axe X
9. Canvas.__init__(self) # appel au constructeur
10. self.configurecreate_line(width=larg10, height=haut)-5, 10, 5, arrow=LAST) # de la classe parente# axe Y
# tracé d'une échelle avec 8 graduations :
11. self.larg, self.haut = larg, haut # mémorisation
pas = (larg-25)/8. # intervalles de l'échelle horizontale
12. # tracé des axes de référence :
for t in range(1, 9):
13. self.create_line(10, haut/2, larg, haut/2, arrow=LAST) # axe X
14. self.create_line(10, haut-5, 10, 5, arrowstx =LAST) 10 + t*pas # axe+10 pour partir de Yl'origine
self.create_line(stx, haut/2-4, stx, haut/2+4)
15. # tracé d'une échelle avec 8 graduations :
16. pas = (larg-25)/8. # intervalles de l'échelle horizontale
def traceCourbe(self, freq=1, phase=0, ampl=10, coul='red'):
17. for t in range(1, 9):
"tracé d'un graphique élongation/temps sur 1 seconde"
18. stx = 10 + t*pas # +10 pour partir de l'origine
curve =[] # liste des coordonnées
19. self.create_line(stx, haut/2-4, stx, haut/2+4)
pas = (self.larg-25)/1000. # l'échelle X correspond à 1 seconde
20.
for t in range(0,1001,5): # que l'on divise en 1000 ms.
21. def traceCourbe(self, freq=1, phase=0, ampl=10, coul='red'):
e = ampl*sin(2*pi*freq*t/1000 - phase)
22. "tracé d'un graphique élongation/temps sur 1 seconde"
23. curve =[] x = 10 + # liste des coordonnéest*pas
24. pas y = (self.larg-25)haut/1000.2 - # l'échelle X correspond à 1 secondee*self.haut/25
curve.append((x,y))
25. for t in range(0,1001,5): # que l'on divise en 1000 ms.
26. en = ampl*sinself.create_line(2*pi*freq*t/1000curve, -fill=coul, phasesmooth=1)
27. return n x # n = 10numéro d'ordre +du t*pastracé
 
28. y = self.haut/2 - e*self.haut/25
#### Code pour tester la classe : ####
29. curve.append((x,y))
 
30. n = self.create_line(curve, fill=coul, smooth=1)
if __name__ == '__main__':
31. return n # n = numéro d'ordre du tracé
root = Tk()
32.
gra = OscilloGraphe(root, 250, 180)
33.#### Code pour tester la classe : ####
gra.pack()
34.
gra.configure(bg ='ivory', bd =2, relief=SUNKEN)
35.if __name__ == '__main__':
gra.traceCourbe(2, 1.2, 10, 'purple')
36. root = Tk()
root.mainloop()
37. gra = OscilloGraphe(root, 250, 180)
</source>
38. gra.pack()
39. gra.configure(bg ='ivory', bd =2, relief=SUNKEN)
40. gra.traceCourbe(2, 1.2, 10, 'purple')
41. root.mainloop()
</pre>
Le niveau principal du script est constitué par les lignes 35 à 41. Comme nous l'avons déjà expliqué à la page {{todo}}, lesLes lignes de code situées après l'instruction <code>if __name__ == '__main__':</code> ne sont pas exécutées si le script est importé en tant que module. Si on lance le script comme application principale, par contre, ces instructions sont exécutées.
 
Nous disposons ainsi d'un mécanisme intéressant, qui nous permet d'intégrer des instructions de test à l'intérieur des modules, même si ceux-ci sont destinés à être importés dans d'autres scripts.
Ligne 446 ⟶ 444 :
{{fin}}
 
Il est temps à présent que nous analysions la structure de la classe qui nous a permis d'instancier tous ces ''widgets''. Nous avons enregistré cette classe dans le module oscillo.py (voir page {{todo}}).
 
;Cahier des charges
Ligne 462 ⟶ 460 :
* Ligne 6 : La méthode « constructeur » utilise 3 paramètres, qui sont tous optionnels puisque chacun d'entre eux possède une valeur par défaut. Le paramètre boss ne sert qu'à réceptionner la référence d'une fenêtre maîtresse éventuelle (voir exemples suivants). Les paramètres <code>larg</code> et <code>haut</code> (largeur et hauteur) servent à assigner des valeurs aux options <code>width</code> et <code>height</code> du canevas parent, au moment de l'instanciation.
 
* Lignes 9 et 10 : La première opération que doit accomplir le constructeur d'une classe dérivée, c'est activer le constructeur de sa classe parente. En effet : nous ne pouvons hériter toute la fonctionnalité de la classe parente, que si cette fonctionnalité a été effectivement mise en place.<br />Nous activons donc le constructeur de la classe <code>Canvas()</code> à la ligne 9 , et nous ajustons deux de ses options à la ligne 10. Notez au passage que nous pourrions condenser ces deux lignes en une seule, qui deviendrait en l'occurrence :
<pre>Canvas.__init__(self, width=larg, height=haut)</pre>Rappel : comme cela a été expliqué à la page {{todo}}, nous
Nous devons transmettre à ce constructeur la référence de l'instance présente (self) comme premier argument.
 
* Ligne 11 : Il est nécessaire de mémoriser les paramètres <code>larg</code> et <code>haut</code> dans des variables d'instance, parce que nous devrons pouvoir y accéder aussi dans la méthode <code>traceCourbe()</code>.
Ligne 470 :
* Lignes 16 à 19 : Pour tracer l'échelle horizontale, on commence par réduire de 25 pixels la largeur disponible, de manière à ménager des espaces aux deux extrémités. On divise ensuite en 8 intervalles, que l'on visualise sous la forme de 8 petits traits verticaux.
 
* Ligne 21 : La méthode <code>traceCourbe()</code> pourra être invoquée avec quatre arguments. Chacun d'entre eux pourra éventuellement être omis, puisque chacun des paramètres correspondants possède une valeur par défaut. Il sera également possible de fournir les arguments dans n'importe quel ordre, comme nous l'avons déjà expliqué à la page {{todo}}.
 
* Lignes 23 à 31 : Pour le tracé de la courbe, la variable ''t'' prend successivement toutes les valeurs de 0 à 1000, et on calcule à chaque fois l'élongation ''e'' correspondante, à l'aide de la formule théorique (ligne 26). Les couples de valeurs ''t'' et ''e'' ainsi trouvées sont mises à l'échelle et transformées en coordonnées x, y aux lignes 27 & 28, puis accumulées dans la liste <code>curve</code>.
Ligne 516 :
Le petit script ci-dessous vous montre comment le paramétrer et l'utiliser dans une fenêtre :
 
<source lang=python>
<pre>
from Tkinter import *
 
Ligne 531 :
 
root.mainloop()
</presource>
 
Ces lignes ne nécessitent guère de commentaires.
Ligne 551 :
[[Image:Apprendre à programmer avec Python 44.png|center]]
 
<source lang=python line>
<pre>
1.from Tkinter import *
2.from math import pi
 
3.
4.class ChoixVibra(Frame):
5. """Curseurs pour choisir fréquence, phase & amplitude d'une vibration"""
6. def __init__(self, boss =None, coul ='red'):
7. Frame.__init__(self) # constructeur de la classe parente
8. # Initialisation de quelques attributs d'instance :
9. self.freq, self.phase, self.ampl, self.coul = 0, 0, 0, coul
10. # Variable d'état de la case à cocher :
11. self.chk = IntVar() # 'objet-variable' Tkinter
12. Checkbutton(self, text='Afficher', variable=self.chk,
13. fg = self.coul, command = self.setCurve).pack(side=LEFT)
14. # Définition des 3 widgets curseurs :
15. Scale(self, length=150, orient=HORIZONTAL, sliderlength =25,
16. label ='Fréquence (Hz) :', from_=1., to=9., tickinterval =2,
17. resolution =0.25,
18. showvalue =0, command = self.setFrequency).pack(side=LEFT)
19. Scale(self, length=150, orient=HORIZONTAL, sliderlength =15,
20. label ='Phase (degrés) :', from_=-180, to=180, tickinterval =90,
21. showvalue =0, command = self.setPhase).pack(side=LEFT)
22. Scale(self, length=150, orient=HORIZONTAL, sliderlength =25,
23. label ='Amplitude :', from_=1, to=9, tickinterval =2,
24. showvalue =0, command = self.setAmplitude).pack(side=LEFT)
25.
26. def setCurve(self):
27. self.event_generate('<Control-Z>')
 
28.
29. def setFrequency(self, f):
30. self.freq = float(f)
31. self.event_generate('<Control-Z>')
32.
33. def setPhase(self, p):
34. pp =float(p)
35. self.phase = pp*2*pi/360 # conversion degrés -> radians
36. self.event_generate('<Control-Z>')
 
37.
38. def setAmplitude(self, a):
39. self.ampl = float(a)
40. self.event_generate('<Control-Z>')
 
41.
42.#### Code pour tester la classe : ###
43.
44.if __name__ == '__main__':
45. def afficherTout(event=None):
46. lab.configure(text = '%s - %s - %s - %s' %
47. (fra.chk.get(), fra.freq, fra.phase, fra.ampl))
48. root = Tk()
49. fra = ChoixVibra(root,'navy')
50. fra.pack(side =TOP)
51. lab = Label(root, text ='test')
52. lab.pack()
53. root.bind('<Control-Z>', afficherTout)
54. root.mainloop()
</presource>
 
Ce panneau de contrôle permettra à vos utilisateurs de régler aisément la valeur des paramètres indiqués (fréquence, phase et amplitude), lesquels pourront alors servir à commander l'affichage de graphiques élongation/temps dans un ''widget'' de la classe <code>OscilloGraphe()</code> construite précédemment, comme nous le montrerons dans l'application de synthèse.
Ligne 631 :
<li>Lignes 26 à 40 : Les 4 ''widgets'' définis dans les lignes précédentes possèdent chacun une option <code>command</code>. Pour chacun d'eux, la méthode invoquée dans cette option command est différente : la case à cocher active la méthode <code>setCurve()</code>, le premier curseur active la méthode <code>setFrequency()</code>, le second curseur active la méthode <code>setPhase()</code>, et le troisième curseur active la méthode <code>setAmplitude()</code>. Remarquez bien au passage que l'option <code>command</code> des ''widgets'' <code>Scale</code> transmet un argument à la méthode associée (la position actuelle du curseur), alors que la même option <code>command</code> ne transmet rien dans le cas du ''widget'' <code>Checkbutton</code>.
 
Ces 4 méthodes (qui sont donc les gestionnaires des événements produits par la case à cocher et les trois curseurs) provoquent elles-mêmes chacune l'émission d'un nouvel événement<ref>En fait, on devrait plutôt appeler cela un message (qui est lui-même la notification d'un événement). Veuillez relire à ce sujet les explications de la page {{todo}} : Programmes pilotés par des événements.</ref>, en faisant appel à la méthode <code>event_generate()</code>.
 
Lorsque cette méthode est invoquée, Python envoie au système d'exploitation exactement le même message-événement que celui qui se produirait si l'utilisateur enfonçait simultanément les touches <Ctrl>, <Maj> et <Z> de son clavier.
Ligne 686 :
Nous attirons votre attention sur la technique mise en œuvre pour provoquer un rafraîchissement de l'affichage dans le canevas par l'intermédiaire d'un événement, chaque fois que l'utilisateur effectue une action quelconque au niveau de l'un des panneaux de contrôle.
 
Rappelez-vous que les applications destinées à fonctionner dans une interface graphique doivent être conçues comme des « programmes pilotés par les événements » (voir page {{todo}}).
 
En préparant cet exemple, nous avons arbitrairement décidé que l'affichage des graphiques serait déclenché par un événement particulier, tout à fait similaire à ceux que génère le système d'exploitation lorsque l'utilisateur accomplit une action quelconque. Dans la gamme (très étendue) d'événements possibles, nous en avons choisi un qui ne risque guère d'être utilisé pour d'autres raisons, pendant que notre application fonctionne : la combinaison de touches <Maj-Ctrl-Z>.
Ligne 692 :
Lorsque nous avons construit la classe de ''widgets'' <code>ChoixVibra()</code>, nous y avons donc incorporé les instructions nécessaires pour que de tels événements soient générés, chaque fois que l'utilisateur actionne l'un des curseurs ou modifie l'état de la case à cocher. Nous allons à présent définir le gestionnaire de cet événement et l'inclure dans notre nouvelle classe : nous l'appellerons <code>montreCourbes()</code> et il se chargera de rafraîchir l'affichage. Étant donné que l'événement concerné est du type <enfoncement d'une touche>, nous devrons cependant le détecter au niveau de la fenêtre principale de l'application.
 
<source lang=python line>
<pre>
1.from oscillo import *
2.from curseurs import *
 
3.
4.class ShowVibra(Frame):
5. """Démonstration de mouvements vibratoires harmoniques"""
6. def __init__(self, boss =None):
7. Frame.__init__(self) # constructeur de la classe parente
8. self.couleur = ['dark green', 'red', 'purple']
9. self.trace = [0]*3 # liste des tracés (courbes à dessiner)
10. self.controle = [0]*3 # liste des panneaux de contrôle
 
11.
12. # Instanciation du canevas avec axes X et Y :
13. self.gra = OscilloGraphe(self, larg =400, haut=200)
14. self.gra.configure(bg ='white', bd=2, relief=SOLID)
15. self.gra.pack(side =TOP, pady=5)
 
16.
17. # Instanciation de 3 panneaux de contrôle (curseurs) :
18. for i in range(3):
19. self.controle[i] = ChoixVibra(self, self.couleur[i])
20. self.controle[i].pack()
 
21.
22. # Désignation de l'événement qui déclenche l'affichage des tracés :
23. self.master.bind('<Control-Z>', self.montreCourbes)
24. self.master.title('Mouvements vibratoires harmoniques')
25. self.pack()
26.
27. def montreCourbes(self, event):
28. """(Ré)Affichage des trois graphiques élongation/temps"""
29. for i in range(3):
 
30.
31. # D'abord, effacer le tracé précédent (éventuel) :
32. self.gra.delete(self.trace[i])
 
33.
34. # Ensuite, dessiner le nouveau tracé :
35. if self.controle[i].chk.get():
36. self.trace[i] = self.gra.traceCourbe(
37. coul = self.couleur[i],
38. freq = self.controle[i].freq,
39. phase = self.controle[i].phase,
40. ampl = self.controle[i].ampl)
41.
42.#### Code pour tester la classe : ###
43.
44.if __name__ == '__main__':
45. ShowVibra().mainloop()
</presource>
 
=== Commentaires ===
Ligne 791 :
La classe « Visage » servira à définir des objets graphiques censés représenter des visages humains simplifiés. Ces visages seront constitués d'un cercle principal dans lequel trois ovales plus petits représenteront deux yeux et une bouche (ouverte). Une méthode "fermer" permettra de remplacer l'ovale de la bouche par une ligne horizontale. Une méthode « ouvrir » permettra de restituer la bouche de forme ovale.
 
Les deux boutons définis dans la classe « Application » serviront respectivement à fermer et à ouvrir la bouche de l'objet « Visage » installé dans le canevas.</li>
(Vous pouvez vous inspirer de l'exemple de la page {{todo}} pour composer une partie du code).</li>
 
<li>''Exercice de synthèse : élaboration d'un dictionnaire de couleurs.''
Ligne 826 ⟶ 825 :
Commencez par analyser ce script, et ajoutez-y des commentaires, en particulier aux lignes marquées : #*** , afin de montrer que vous comprenez ce que doit faire le programme à ces emplacements :
 
<source lang=python>
<pre>
from Tkinter import *
 
Ligne 885 ⟶ 884 :
Projet(500, 300).mainloop()
</presource>
 
Modifiez ensuite ce script, afin qu'il corresponde au cahier des charges suivant :
Ligne 910 ⟶ 909 :
<li>
[[Image:Apprendre à programmer avec Python 77.png|center|500px|capture d'écran de l'application]]
<source lang=python>
<pre>
# Dictionnaire de couleurs
from Tkinter import *
Ligne 1 005 ⟶ 1 004 :
if __name__ == '__main__':
Application().mainloop()
</presource>
</li>
<li>
(variante 3) :
<source lang=python>
<pre>
from Tkinter import *
from random import randrange
Ligne 1 096 ⟶ 1 095 :
Projet(600, 600).mainloop()
</presource>
</li>
</ol>
Ligne 1 104 ⟶ 1 103 :
{{références}}
 
<noinclude>[[Catégorie:Interface graphique]]</noinclude>
[[Catégorie:Apprendre à programmer avec Python (livre)|Classes]]