• Aucun résultat trouvé

Cahier des charges L'application comportera deux classes :

La classe Application() sera obtenue par dérivation d'une des classes de base de Tkinter : elle mettra en place la fenêtre principale, son canevas et ses deux boutons.

Une classe Wagon(), indépendante, permettra d'instancier dans le canevas 4 objets-wagons similaires, dotés chacun d'une méthode perso(). Celle-ci sera destinée à provoquer l'apparition d'un petit personnage à l'une quelconque des trois fenêtres du wagon. L'application principale invoquera cette méthode différemment pour différents objets-wagons, afin de faire apparaître un choix de quelques personnages.

Implémentation

from Tkinter import*

def cercle(can, x, y, r):

"dessin d'un cercle de rayon <r> en <x,y> dans le canevas <can>"

can.create_oval(x-r, y-r, x+r, y+r) class Application(Tk):

def __init__(self):

Tk.__init__(self) # constructeur de la classe parente self.can =Canvas(self, width =475, height =130, bg ="white")

self.can.pack(side =TOP, padx =5, pady =5)

Button(self, text ="Train", command =self.dessine).pack(side =LEFT) Button(self, text ="Hello", command =self.coucou).pack(side =LEFT)

def dessine(self):

"instanciation de 4 wagons dans le canevas" self.w1 = Wagon(self.can, 10, 30)

self.w2 = Wagon(self.can, 130, 30)

self.w3 = Wagon(self.can, 250, 30)

self.w4 = Wagon(self.can, 370, 30)

def coucou(self):

"apparition de personnages dans certaines fenêtres" self.w1.perso(3) # 1er wagon, 3e fenêtre self.w3.perso(1) # 3e wagon, 1e fenêtre self.w3.perso(2) # 3e wagon, 2e fenêtre self.w4.perso(1) # 4e wagon, 1e fenêtre

class Wagon:

31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54

"dessin d'un petit wagon en <x,y> dans le canevas <canev>" # mémorisation des paramètres dans des variables d'instance : self.canev, self.x, self.y = canev, x, y

# rectangle de base : 95x60 pixels :

canev.create_rectangle(x, y, x+95, y+60)

# 3 fenêtres de 25x40 pixels, écartées de 5 pixels : for xf in range(x+5, x+90, 30):

canev.create_rectangle(xf, y+5, xf+25, y+40)

# 2 roues de rayon égal à 12 pixels :

cercle(canev, x+18, y+73, 12) cercle(canev, x+77, y+73, 12)

def perso(self, fen):

"apparition d'un petit personnage à la fenêtre <fen>" # calcul des coordonnées du centre de chaque fenêtre :

xf = self.x + fen*30 -12 yf = self.y + 25

cercle(self.canev, xf, yf, 10) # visage

cercle(self.canev, xf-5, yf-3, 2) # oeil gauche

cercle(self.canev, xf+5, yf-3, 2) # oeil droit

cercle(self.canev, xf, yf+5, 3) # bouche

app = Application() app.mainloop()

Commentaires

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 Tk() importée du module Tkinter[2]. 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 Wagon(), nous devrons associer à sa méthode constructeur un nombre égal de paramètres pour réceptionner ces arguments.

Lignes 22 à 27 : Cette méthode est invoquée lorsque l'on actionne le second bouton. Elle invoque elle-même la méthode perso() de certains objets-wagons, avec des arguments différents, afin de faire apparaître les personnages aux fenêtres indiquées.

Remarque : ces quelques lignes de code vous montrent donc comment un objet peut communiquer avec un autre

en faisant appel à l'une ou l'autre de ses méthodes. Il s'agit là du mécanisme central de la programmation par objets : les objets sont des entités programmées qui s'échangent des messages et interagissent par l'intermédiaire de leurs méthodes.

Idéalement, la méthode coucou() devrait comporter quelques instructions complémentaires, lesquelles vérifieraient d'abord si les objets-wagons concernés existent bel et bien, avant d'autoriser l'activation d'une de leurs méthodes. Nous n'avons pas inclus ce genre de garde-fou afin que l'exemple reste aussi simple que possible, mais cela entraîne la conséquence que vous ne pouvez pas actionner le second bouton avant le premier. (Pouvez-vous ajouter un correctif ?) Lignes 29-30 : La classe Wagon() ne dérive d'aucune autre classe préexistante. Cependant, étant donné qu'il s'agit d'une classe d'objets graphiques, nous devons munir sa méthode constructeur de paramètres, afin de recevoir la référence du canevas auquel les dessins sont destinés, ainsi que les coordonnées de départ de ces dessins. Dans vos expérimentations éventuelles autour de cet exercice, vous pourriez bien évidemment ajouter encore d'autres paramètres : taille du dessin, orientation, couleur, vitesse, etc.

Lignes 31 à 51 : Ces instructions ne nécessitent guère de commentaires. La méthode perso() est dotée d'un paramètre qui indique celle des 3 fenêtres où il faut faire apparaître un petit personnage. Ici aussi nous n'avons pas prévu de garde-fou : vous pouvez invoquer cette méthode avec un argument égal à 4 ou 5, par exemple, ce qui produira des effets incorrects.

Lignes 53-54 : Pour démarrer l'application, il ne suffit pas d'instancier un objet de la classe Application() comme dans l'exemple de la rubrique précédente. Il faut également invoquer la méthode mainloop() qu'elle a hérité de sa classe parente. Vous pourriez cependant condenser ces deux instructions en une seule, laquelle serait alors : Application().mainloop()

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58

Perfectionnez le script décrit ci-dessus, en ajoutant un paramètre couleur au constructeur de la classe Wagon(), lequel déterminera la couleur de la cabine du wagon. Arrangez-vous également pour que les fenêtres soient noires au départ, et les roues grises (pour réaliser ce dernier objectif, ajoutez aussi un paramètre couleur à la fonction cercle()). À cette même classe Wagon(), ajoutez encore une méthode allumer(), qui servira à changer la couleur des 3 fenêtres (initialement noires) en jaune, afin de simuler l'allumage d'un éclairage intérieur. Ajoutez un bouton à la fenêtre principale, qui puisse déclencher cet allumage. Profitez de l'amélioration de la fonction cercle() pour teinter le visage des petits personnages en rose (pink), leurs yeux et leurs bouches en noir, et instanciez les objets-wagons avec des couleurs différentes.

1.

Solution

from Tkinter import *

def cercle(can, x, y, r, coul ='white'):

"dessin d'un cercle de rayon <r> en <x,y> dans le canevas <can>" can.create_oval(x-r, y-r, x+r, y+r, fill =coul)

class Application(Tk): def __init__(self):

Tk.__init__(self) # constructeur de la classe parente self.can =Canvas(self, width =475, height =130, bg ="white") self.can.pack(side =TOP, padx =5, pady =5)

Button(self, text ="Train", command =self.dessine).pack(side =LEFT) Button(self, text ="Hello", command =self.coucou).pack(side =LEFT) Button(self, text ="Ecl34", command =self.eclai34).pack(side =LEFT)

def dessine(self):

"instanciation de 4 wagons dans le canevas" self.w1 = Wagon(self.can, 10, 30)

self.w2 = Wagon(self.can, 130, 30, 'dark green') self.w3 = Wagon(self.can, 250, 30, 'maroon') self.w4 = Wagon(self.can, 370, 30, 'purple')

def coucou(self):

"apparition de personnages dans certaines fenêtres" self.w1.perso(3) # 1er wagon, 3e fenêtre self.w3.perso(1) # 3e wagon, 1e fenêtre self.w3.perso(2) # 3e wagon, 2e fenêtre self.w4.perso(1) # 4e wagon, 1e fenêtre

def eclai34(self):

"allumage de l'éclairage dans les wagons 3 & 4" self.w3.allumer()

self.w4.allumer()

class Wagon:

def __init__(self, canev, x, y, coul ='navy'):

"dessin d'un petit wagon en <x,y> dans le canevas <canev>" # mémorisation des paramètres dans des variables d'instance : self.canev, self.x, self.y = canev, x, y

# rectangle de base : 95x60 pixels :

canev.create_rectangle(x, y, x+95, y+60, fill =coul) # 3 fenêtres de 25x40 pixels, écartées de 5 pixels : self.fen =[] # pour mémoriser les réf. des fenêtres for xf in range(x +5, x +90, 30):

self.fen.append(canev.create_rectangle(xf, y+5, xf+25, y+40, fill ='black')) # 2 roues de rayon égal à 12 pixels :

cercle(canev, x+18, y+73, 12, 'gray') cercle(canev, x+77, y+73, 12, 'gray')

def perso(self, fen):

"apparition d'un petit personnage à la fenêtre <fen>" # calcul des coordonnées du centre de chaque fenêtre : xf = self.x + fen*30 -12

yf = self.y + 25

cercle(self.canev, xf, yf, 10, "pink") # visage cercle(self.canev, xf-5, yf-3, 2) # oeil gauche cercle(self.canev, xf+5, yf-3, 2) # oeil droit

59 60 61 62 63 64 65 66 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

cercle(self.canev, xf, yf+5, 3) # bouche

def allumer(self):

"déclencher l'éclairage interne du wagon" for f in self.fen:

self.canev.itemconfigure(f, fill ='yellow') Application().app.mainloop()

Le projet qui suit va nous entraîner encore un petit peu plus loin. Nous allons y construire une nouvelle classe de widget, qu'il sera possible d'intégrer dans nos projets futurs comme n'importe quel widget standard. Comme la classe principale de l'exercice précédent, cette nouvelle classe sera construite par dérivation d'une classe Tkinter préexistante.

Le sujet concret de cette application nous est inspiré par le cours de physique. Pour rappel :

Un mouvement vibratoire harmonique se définit comme étant la projection d'un mouvement circulaire uniforme sur une droite. Les positions successives d'un

mobile qui effectue ce type de mouvement sont traditionnellement repérées par rapport à une position centrale : on les appelle alors des élongations. L'équation qui décrit l'évolution de l'élongation d'un tel mobile au cours du temps est toujours de la forme , dans laquelle e représente l'élongation du mobile à tout instant t. Les constantes A, f et φ désignent respectivement l'amplitude, la fréquence et la phase du mouvement vibratoire. Le but du présent projet est de fournir un instrument de visualisation simple de ces différents concepts, à

savoir un système d'affichage automatique de graphiques élongation/temps. L'utilisateur pourra choisir librement les valeurs des paramètres A, f et φ, et observer les courbes qui en résultent.

Le widget que nous allons construire d'abord s'occupera de l'affichage proprement dit. Nous construirons ensuite d'autres widgets pour faciliter l'entrée des paramètres A, f et φ.

Veuillez donc encoder le script ci-dessous et le sauvegarder dans un fichier, auquel vous donnerez le nom oscillo.py. 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).

from Tkinter import*

from mathimport sin, pi class OscilloGraphe(Canvas):

"Canevas spécialisé, pour dessiner des courbes élongation/temps" def __init__(self, boss =None, larg=200, haut=150):

"Constructeur du graphique : axes et échelle horiz." # construction du widget parent :

Canvas.__init__(self) # appel au constructeur

self.configure(width=larg, height=haut) # de la classe parente self.larg, self.haut = larg, haut # mémorisation # tracé des axes de référence :

self.create_line(10, haut/2, larg, haut/2, arrow=LAST) # axe X self.create_line(10, haut-5, 10, 5, arrow=LAST) # axe Y # tracé d'une échelle avec 8 graduations :

pas = (larg-25)/8. # intervalles de l'échelle horizontale for t in range(1, 9):

stx = 10 + t*pas # +10 pour partir de l'origine self.create_line(stx, haut/2-4, stx, haut/2+4)

def traceCourbe(self, freq=1, phase=0, ampl=10, coul='red'):

"tracé d'un graphique élongation/temps sur 1 seconde"

curve =[] # liste des coordonnées

pas = (self.larg-25)/1000. # l'échelle X correspond à 1 seconde for t in range(0,1001,5): # que l'on divise en 1000 ms.

e = ampl*sin(2*pi*freq*t/1000 - phase) x = 10 + t*pas

y = self.haut/2 - e*self.haut/25 curve.append((x,y))

n = self.create_line(curve, fill=coul, smooth=1)

return n # n = numéro d'ordre du tracé #### Code pour tester la classe : ####

if __name__ == '__main__': root = Tk()

37 38 39 40 41 gra = OscilloGraphe(root, 250, 180) gra.pack()

gra.configure(bg ='ivory', bd =2, relief=SUNKEN) gra.traceCourbe(2, 1.2, 10, 'purple')

root.mainloop()

Le niveau principal du script est constitué par les lignes 35 à 41. Les lignes de code situées après l'instruction if __name__ == '__main__': 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.

Lancez donc l'exécution du script de la manière habituelle. Vous devriez obtenir un affichage similaire à celui qui est reproduit à la page précédente.

Expérimentation

Commençons d'abord par expérimenter quelque peu la classe que nous venons de construire. Ouvrez une fenêtre de terminal (Python shell), et entrez les instructions ci-dessous directement à la ligne de commande :

>>> from oscillo import * >>> g1 = OscilloGraphe() >>> g1.pack()

Après importation des classes du module oscillo, nous instancions un premier objet g1, de la classe OscilloGraphe().

Puisque nous ne fournissons aucun argument, l'objet possède les dimensions par défaut, définies dans le constructeur de la classe. Remarquons au passage que nous n'avons même pas pris la peine de définir d'abord une fenêtre maître pour y placer ensuite notre widget. Tkinter nous pardonne cet oubli et nous en fournit une automatiquement !

>>> g2 = OscilloGraphe(haut=200, larg=250) >>> g2.pack()

>>> g2.traceCourbe()

Ensuite, nous activons la méthode traceCourbe() associée à ce widget. Étant donné que nous ne lui fournissons aucun argument, la sinusoïde qui apparaît correspond aux valeurs prévues par défaut pour les paramètres A, f et φ.

>>> g3 = OscilloGraphe(larg=220)

>>> g3.configure(bg='white', bd=3, relief=SUNKEN) >>> g3.pack(padx=5,pady=5)

>>> g3.traceCourbe(phase=1.57, coul='purple') >>> g3.traceCourbe(phase=3.14, coul='dark green')

Pour comprendre la configuration de ce troisième widget, il faut nous rappeler que la classe OscilloGraphe() a été construite par dérivation de la classe Canvas(). Elle hérite donc de toutes les propriétés de celle-ci, ce qui nous permet de choisir la couleur de fond, la bordure, etc., en utilisant les mêmes arguments que ceux qui sont à notre disposition lorsque nous configurons un canevas.

Nous faisons ensuite apparaître deux tracés successifs, en faisant appel deux fois à la méthode traceCourbe(), à laquelle nous fournissons des arguments pour la phase et la couleur.

Exercices

Créez un quatrième widget, de taille 400 x 300, couleur de fond jaune, et faites-y apparaître plusieurs courbes correspondant à des fréquences et des amplitudes différentes.

1.

Solution

Réfléchissez ! 1.

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.