Programmation Python/Classes et Interfaces graphiques
La programmation orientée objet convient particulièrement bien au développement d'applications avec interface graphique. Des bibliothèques de classes comme Tkinter ou wxPython fournissent une base de widgets très étoffée, que nous pouvons adapter à nos besoins par dérivation. Dans ce chapitre, nous allons utiliser à nouveau la bibliothèque Tkinter, mais en appliquant les concepts décrits dans les pages précédentes, et en nous efforçant de mettre en évidence les avantages qu'apporte l'orientation objet dans nos programmes.
« Code des couleurs » : un petit projet bien encapsulé
[modifier | modifier le wikicode]Nous allons commencer par un petit projet qui nous a été inspiré par le cours d'initiation à l'électronique. L'application que nous décrivons ci-après permet de retrouver rapidement le code de trois couleurs qui correspond à une résistance électrique de valeur bien déterminée.
Pour rappel, la fonction des résistances électriques consiste à s'opposer (à résister) plus ou moins bien au passage du courant. Les résistances se présentent concrètement sous la forme de petites pièces tubulaires cerclées de bandes de couleur (en général 3). Ces bandes de couleur indiquent la valeur numérique de la résistance, en fonction du code suivant :
On oriente la résistance de manière telle que les bandes colorées soient placées à gauche. La valeur de la résistance – exprimée en ohms (Ω) - s'obtient en lisant ces bandes colorées également à partir de la gauche : les deux premières bandes indiquent les deux premiers chiffres de la valeur numérique ; il faut ensuite accoler à ces deux chiffres un nombre de zéros égal à l'indication fournie par la troisième bande. Par exemple supposons qu'à partir de la gauche, les bandes colorées soient jaune, violette et verte et que la valeur de cette résistance est 4700000 Ω, ou 4700 kΩ, ou encore 4,7 MΩ.
Ce système ne permet évidemment de préciser une valeur numérique qu'avec deux chiffres significatifs seulement. Il est toutefois considéré comme largement suffisant pour la plupart des applications électroniques « ordinaires » (radio, TV, etc.)
- Cahier des charges de notre programme
Notre application doit faire apparaître une fenêtre comportant un dessin de la résistance, ainsi qu'un champ d'entrée dans lequel l'utilisateur peut encoder une valeur numérique. Un bouton « Montrer » déclenche la modification du dessin de la résistance, de telle façon que les trois bandes de couleur se mettent en accord avec la valeur numérique introduite.
Contrainte : Le programme doit accepter toute entrée numérique fournie sous forme entière ou réelle, dans les limites de 10 à 1011 Ω. Par exemple, une valeur telle que 4.78e6 doit être acceptée et arrondie correctement, c'est-à-dire convertie en 4800000 Ω.
- Mise en œuvre concrète
Nous construisons cette application simple sous la forme d'une classe. Sa seule utilité présente consiste à nous fournir un espace de noms commun dans lequel nous pouvons encapsuler nos variables et nos fonctions, ce qui nous permet de nous passer de variables globales. En effet :
- Les variables auxquelles nous souhaitons pouvoir accéder de partout sont déclarées comme des attributs d'instance (nous attachons chacune d'elles à l'instance à l'aide de
self
).
- Les fonctions sont déclarées comme des méthodes, et donc attachées elles aussi à
self
.
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).
class Application:
def __init__(self):
"""Constructeur de la fenêtre principale"""
self.root =Tk()
self.root.title('Code des couleurs')
self.dessineResistance()
Label(self.root,
text ="Entrez la valeur de la résistance, en ohms :").grid(row =2)
Button(self.root, text ='Montrer',
command =self.changeCouleurs).grid(row =3, sticky = W)
Button(self.root, text ='Quitter',
command =self.root.quit).grid(row =3, sticky = E)
self.entree = Entry(self.root, width =14)
self.entree.grid(row =3)
# Code des couleurs pour les valeurs de zéro à neuf :
self.cc = ['black','brown','red','orange','yellow',
'green','blue','purple','grey','white']
self.root.mainloop()
def dessineResistance(self):
"""Canevas avec un modèle de résistance à trois lignes colorées"""
self.can = Canvas(self.root, width=250, height =100, bg ='ivory')
self.can.grid(row =1, pady =5, padx =5)
self.can.create_line(10, 50, 240, 50, width =5) # fils
self.can.create_rectangle(65, 30, 185, 70, fill ='light grey', width =2)
# Dessin des trois lignes colorées (noires au départ) :
self.ligne =[] # on mémorisera les trois lignes dans 1 liste
for x in range(85,150,24):
self.ligne.append(self.can.create_rectangle(x, 30, x+12, 70, fill='black', width=0))
def changeCouleurs(self):
"""Affichage des couleurs correspondant à la valeur entrée"""
self.v1ch = self.entree.get() # la méthode get() renvoie une chaîne
try:
v = float(self.v1ch) # conversion en valeur numérique
except:
err =1 # erreur : entrée non numérique
else:
err =0
if err ==1 or v < 10 or v > 1e11 :
self.signaleErreur() # entrée incorrecte ou hors limites
else:
li =[0]*3 # liste des 3 codes à afficher
logv = int(log10(v)) # partie entière du logarithme
ordgr = 10**logv # ordre de grandeur
# extraction du premier chiffre significatif :
li[0] = int(v/ordgr) # partie entière
decim = v/ordgr - li[0] # partie décimale
# extraction du second chiffre significatif :
li[1] = int(decim*10 +.5) # +.5 pour arrondir correctement
# nombre de zéros à accoler aux 2 chiffres significatifs :
li[2] = logv -1
# Coloration des 3 lignes :
for n in range(3):
self.can.itemconfigure(self.ligne[n], fill =self.cc[li[n]])
def signaleErreur(self):
self.entree.configure(bg ='red') # colorer le fond du champ
self.root.after(1000, self.videEntree) # après 1 seconde, effacer
def videEntree(self):
self.entree.configure(bg ='white') # rétablir le fond blanc
self.entree.delete(0, len(self.v1ch)) # enlever les car. présents
# Programme principal :
from Tkinter import *
from math import log10 # logarithmes en base 10
f = Application() # instanciation de l'objet application
- Commentaires
- 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
dessineResistance()
. 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éthodegrid()
.
- Lignes 15-17 : Le code des couleurs est mémorisé dans une simple liste.
- Ligne 18 : La dernière instruction du constructeur démarre l'application.
- Lignes 20 à 30 : Le dessin de la résistance se compose d'une ligne et d'un premier rectangle gris clair, pour le corps de la résistance et ses deux fils. Trois autres rectangles figureront les bandes colorées que le programme devra modifier en fonction des entrées de l'utilisateur. Ces bandes sont noires au départ ; elles sont référencées dans la liste
self.ligne
.
- Lignes 32 à 53 : Ces lignes contiennent l'essentiel de la fonctionnalité du programme.
L'entrée brute fournie par l'utilisateur est acceptée sous la forme d'une chaîne de caractères.
À la ligne 36, on essaie de convertir cette chaîne en une valeur numérique de type float. Si la conversion échoue, on mémorise l'erreur. Si l'on dispose bien d'une valeur numérique, on vérifie ensuite qu'elle se situe effectivement dans l'intervalle autorisé (de 10 Ω à 1011 Ω). Si une erreur est détectée, on signale à l'utilisateur que son entrée est incorrecte en colorant de rouge le fond du champ d'entrée, qui est ensuite vidé de son contenu (lignes 55 à 61).
- Lignes 45-46 : Les mathématiques viennent à notre secours pour extraire de la valeur numérique son ordre de grandeur (c'est-à-dire l'exposant de 10 le plus proche). Veuillez consulter votre cours de mathématiques pour de plus amples explications concernant les logarithmes.
- Lignes 47-48 : Une fois connu l'ordre de grandeur, il devient relativement facile d'extraire du nombre traité ses deux premiers chiffres significatifs. Exemple : Supposons que la valeur entrée soit 31687. Le logarithme de ce nombre est 4,50088... dont la partie entière (4) nous donne l'ordre de grandeur de la valeur entrée (soit 104). Pour extraire de celle-ci son premier chiffre significatif, il suffit de la diviser par 104, soit 10000, et de conserver seulement la partie entière du résultat (3).
- Lignes 49 à 51 : Le résultat de la division effectuée dans le paragraphe précédent est 3,1687.
Nous récupérons la partie décimale de ce nombre à la ligne 49, soit 0,1687 dans notre exemple.
Si nous le multiplions par dix, ce nouveau résultat comporte une partie entière qui n'est rien d'autre que notre second chiffre significatif (1 dans notre exemple).
Nous pourrions facilement extraire ce dernier chiffre, mais puisque c'est le dernier, nous souhaitons encore qu'il soit correctement arrondi. Pour ce faire, il suffit d'ajouter une demi unité au produit de la multiplication par dix, avant d'en extraire la valeur entière. Dans notre exemple, en effet, ce calcul donnera donc 1,687 + 0,5 = 2,187 , dont la partie entière (2) est bien la valeur arrondie recherchée.
- Ligne 53 : Le nombre de zéros à accoler aux deux chiffres significatifs correspond au calcul de l'ordre de grandeur. Il suffit de retirer une unité au logarithme.
- Ligne 56 : Pour attribuer une nouvelle couleur à un objet déjà dessiné dans un canevas, on utilise la méthode
itemconfigure()
. Nous utilisons donc cette méthode pour modifier l'optionfill
de chacune des bandes colorées, en utilisant les noms de couleur extraits de la listeself.cc
grâce à aux trois indicesli[1]
,li[2]
etli[3]
qui contiennent les 3 chiffres correspondants.
Exercices
- Modifiez le script ci-dessus de telle manière que le fond d'image devienne bleu clair (
'light blue'
), que le corps de la résistance devienne beige ('beige'
), que le fil de cette résistance soit plus fin, et que les bandes colorées indiquant la valeur soient plus larges. - Modifiez le script ci-dessus de telle manière que l'image dessinée soit deux fois plus grande.
- Modifiez le script ci-dessus de telle manière qu'il devienne possible d'entrer aussi des valeurs de résistances comprises entre 1 et 10 Ω. Pour ces valeurs, le premier anneau coloré devra rester noir, les deux autres indiqueront la valeur en Ω et dixièmes d' Ω.
- Modifiez le script ci-dessus de telle façon que le bouton « Montrer » ne soit plus nécessaire. Dans votre script modifié, il suffira de frapper <Enter> après avoir entré la valeur de la résistance, pour que l'affichage s'active.
- Modifiez le script ci-dessus de telle manière que les trois bandes colorées redeviennent noires dans les cas où l'utilisateur fournit une entrée inacceptable.
Solution
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
« Petit train » : héritage, échange d'informations entre classes
[modifier | modifier le wikicode]Dans l'exercice précédent, nous n'avons exploité qu'une seule caractéristique des classes : l'encapsulation. Celle-ci nous a permis d'écrire un programme dans lequel les différentes fonctions (qui sont donc devenues des méthodes) peuvent chacune accéder à un même pool de variables : toutes celles qui sont définies comme étant attachées à self
. Toutes ces variables peuvent être considérées en quelque sorte comme des variables globales à l'intérieur de l'objet.
Comprenez bien toutefois qu'il ne s'agit pas de véritables variables globales. Elles restent en effet strictement confinées à l'intérieur de l'objet, et il est déconseillé de vouloir y accéder de l'extérieur[1]. D'autre part, tous les objets que vous instancierez à partir d'une même classe posséderont chacun leur propre jeu de ces variables, qui sont donc bel et bien encapsulées dans ces objets. On les appelle pour cette raison des attributs d'instance.
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. 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.
- 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éthodeperso()
. 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:
def __init__(self, canev, x, y):
"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) # œil gauche
cercle(self.canev, xf+5, yf-3, 2) # œil 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.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éthodecoucou()
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éthodemainloop()
qu'elle a hérité de sa classe parente. Vous pourriez cependant condenser ces deux instructions en une seule, laquelle serait alors :Application().mainloop()
Exercices
- 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 fonctioncercle()
). À cette même classeWagon()
, ajoutez encore une méthodeallumer()
, 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 fonctioncercle()
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.
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) # œil gauche cercle(self.canev, xf+5, yf-3, 2) # œil droit 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()
« OscilloGraphe » : un widget personnalisé
[modifier | modifier le wikicode]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 math import 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()
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()
Par ces instructions, nous créons un second widget de la même classe, en précisant cette fois ses dimensions (hauteur et largeur, dans n'importe quel ordre).
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.
Solution
- Réfléchissez !
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.
- Cahier des charges
Nous souhaitons définir une nouvelle classe de widget, capable d'afficher automatiquement les graphiques élongation/temps correspondant à divers mouvements vibratoires harmoniques.
Ce widget doit pouvoir être dimensionné à volonté au moment de son instanciation. Il fait apparaître deux axes cartésiens X et Y munis de flèches. L'axe X représente l'écoulement du temps pendant une seconde au total, et il est muni d'une échelle comportant 8 intervalles.
Une méthode traceCourbe()
est associée à ce widget. Elle provoque le tracé du graphique élongation/temps pour un mouvement vibratoire dont on fournit la fréquence (entre 0.25 et 10 Hz), la phase (entre 0 et 2π radians) et l'amplitude (entre 1 et 10 ; échelle arbitraire).
- Implémentation
- Ligne 4 : La classe
OscilloGraphe()
est créée par dérivation de la classeCanvas()
. Elle hérite donc toutes les propriétés de celle-ci : on pourra configurer les objets de cette nouvelle classe en utilisant les nombreuses options déjà disponibles pour la classeCanvas()
.
- 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
larg
ethaut
(largeur et hauteur) servent à assigner des valeurs aux optionswidth
etheight
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.
Nous activons donc le constructeur de la classeCanvas()
à 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 :
Canvas.__init__(self, width=larg, height=haut)
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
larg
ethaut
dans des variables d'instance, parce que nous devrons pouvoir y accéder aussi dans la méthodetraceCourbe()
.
- Lignes 13 et 14 : Pour tracer les axes X et Y, nous utilisons les paramètres larg et haut, ainsi ces axes sont automatiquement mis à dimension. L'option
arrow=LAST
permet de faire apparaître une petite flèche à l'extrémité de chaque ligne.
- 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
traceCourbe()
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.
- 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
curve
.
- Lignes 30 et 31 : La méthode
create_line()
trace alors la courbe correspondante en une seule opération, et elle renvoie le numéro d'ordre du nouvel objet ainsi instancié dans le canevas (ce numéro d'ordre nous permettra d'y accéder encore par après : pour l'effacer, par exemple). L'optionsmooth =1
améliore l'aspect final, par lissage.
Exercices
- Modifiez le script de manière à ce que l'axe de référence vertical comporte lui aussi une échelle, avec 5 tirets de part et d'autre de l'origine.
- Comme les widgets de la classe
Canvas()
dont il dérive, votre widget peut intégrer des indications textuelles. Il suffit pour cela d'utiliser la méthodecreate_text()
. Cette méthode attend au moins trois arguments : les coordonnées x et y de l'emplacement où vous voulez faire apparaître votre texte, et puis le texte lui-même, bien entendu. D'autres arguments peuvent être transmis sous forme d'options, pour préciser par exemple la police de caractères et sa taille. Afin de voir comment cela fonctionne, ajoutez provisoirement la ligne suivante dans le constructeur de la classeOscilloGraphe()
, puis relancez le script :self.create_text(130, 30, text = "Essai", anchor =CENTER)
Utilisez cette méthode pour ajouter au widget les indications suivantes aux extrémités des axes de référence : e (pour « élongation ») le long de l'axe vertical, et t (pour « temps ») le long de l'axe horizontal. Le résultat pourrait ressembler à ceci (figure de gauche) :
- Vous pouvez compléter encore votre widget, en y faisant apparaître une grille de référence, plutôt que de simples tirets le long des axes. Pour éviter que cette grille ne soit trop visible, vous pouvez colorer ses traits en gris (option
fill = 'grey'
), comme dans la figure de droite. - Complétez encore votre widget en y faisant apparaître des repères numériques.
Solution
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
« Curseurs » : un widget composite
[modifier | modifier le wikicode]Dans l'exercice précédent, vous avez construit un nouveau type de widget que vous avez sauvegardé dans le module oscillo.py
. Conservez soigneusement ce module, car vous l'intégrerez bientôt dans un projet plus complexe.
Pour l'instant, vous allez construire encore un autre widget, plus interactif cette fois. Il s'agira d'une sorte de panneau de contrôle comportant trois curseurs de réglage et une case à cocher. Comme le précédent, ce widget est destiné à être réutilisé dans une application de synthèse.
Présentation du widget « Scale »
[modifier | modifier le wikicode]Commençons d'abord par découvrir un widget de base, que nous n'avions pas encore utilisé jusqu'ici :
Le widget Scale
se présente comme un curseur qui coulisse devant une échelle. Il permet à l'utilisateur de choisir rapidement la valeur d'un paramètre quelconque, d'une manière très attrayante.
Le petit script ci-dessous vous montre comment le paramétrer et l'utiliser dans une fenêtre :
from Tkinter import *
def updateLabel(x):
lab.configure(text='Valeur actuelle = ' + str(x))
root = Tk()
Scale(root, length=250, orient=HORIZONTAL, label ='Réglage :',
troughcolor ='dark grey', sliderlength =20,
showvalue =0, from_=-25, to=125, tickinterval =25,
command=updateLabel).pack()
lab = Label(root)
lab.pack()
root.mainloop()
Ces lignes ne nécessitent guère de commentaires.
Vous pouvez créer des widgets Scale
de n'importe quelle taille (option length
), en orientation horizontale (comme dans notre exemple) ou verticale (option orient = VERTICAL
).
Les options from_
(attention : n'oubliez pas le caractère 'souligné' !) et to
définissent la plage de réglage. L'intervalle entre les repères numériques est défini dans l'option tickinterval
, etc.
La fonction désignée dans l'option command
est appelée automatiquement chaque fois que le curseur est déplacé, et la position actuelle du curseur par rapport à l'échelle lui est transmise en argument. Il est donc très facile d'utiliser cette valeur pour effectuer un traitement quelconque. Considérez par exemple le paramètre x
de la fonction updateLabel()
, dans notre exemple.
Le widget Scale
constitue une interface très intuitive et attrayante pour proposer différents réglages aux utilisateurs de vos programmes. Nous allons à présent l'incorporer en plusieurs exemplaires dans une nouvelle classe de widget : un panneau de contrôle destiné à choisir la fréquence, la phase et l'amplitude pour un mouvement vibratoire, dont nous afficherons ensuite le graphique élongation/temps à l'aide du widget oscilloGraphe
construit dans les pages précédentes.
Construction d'un panneau de contrôle à trois curseurs
[modifier | modifier le wikicode]Comme le précédent, le script que nous décrivons ci-dessous est destiné à être sauvegardé dans un module, que vous nommerez cette fois curseurs.py
. Les classes que vous sauvegardez ainsi seront réutilisées (par importation) dans une application de synthèse. Nous attirons votre attention sur le fait que le code ci-dessous peut être raccourci de différentes manières. Nous ne l'avons pas optimisé d'emblée, parce que cela nécessiterait d'y incorporer un concept supplémentaire (les expressions lambda), ce que nous préférons éviter pour l'instant.
Vous savez déjà que les lignes de code placées à la fin du script permettent de tester son fonctionnement. Vous devriez obtenir une fenêtre semblable à celle-ci :
from Tkinter import *
from math import pi
class ChoixVibra(Frame):
"""Curseurs pour choisir fréquence, phase & amplitude d'une vibration"""
def __init__(self, boss =None, coul ='red'):
Frame.__init__(self) # constructeur de la classe parente
# Initialisation de quelques attributs d'instance :
self.freq, self.phase, self.ampl, self.coul = 0, 0, 0, coul
# Variable d'état de la case à cocher :
self.chk = IntVar() # 'objet-variable' Tkinter
Checkbutton(self, text='Afficher', variable=self.chk,
fg = self.coul, command = self.setCurve).pack(side=LEFT)
# Définition des 3 widgets curseurs :
Scale(self, length=150, orient=HORIZONTAL, sliderlength =25,
label ='Fréquence (Hz) :', from_=1., to=9., tickinterval =2,
resolution =0.25,
showvalue =0, command = self.setFrequency).pack(side=LEFT)
Scale(self, length=150, orient=HORIZONTAL, sliderlength =15,
label ='Phase (degrés) :', from_=-180, to=180, tickinterval =90,
showvalue =0, command = self.setPhase).pack(side=LEFT)
Scale(self, length=150, orient=HORIZONTAL, sliderlength =25,
label ='Amplitude :', from_=1, to=9, tickinterval =2,
showvalue =0, command = self.setAmplitude).pack(side=LEFT)
def setCurve(self):
self.event_generate('<Control-Z>')
def setFrequency(self, f):
self.freq = float(f)
self.event_generate('<Control-Z>')
def setPhase(self, p):
pp =float(p)
self.phase = pp*2*pi/360 # conversion degrés -> radians
self.event_generate('<Control-Z>')
def setAmplitude(self, a):
self.ampl = float(a)
self.event_generate('<Control-Z>')
#### Code pour tester la classe : ###
if __name__ == '__main__':
def afficherTout(event=None):
lab.configure(text = '%s - %s - %s - %s' %
(fra.chk.get(), fra.freq, fra.phase, fra.ampl))
root = Tk()
fra = ChoixVibra(root,'navy')
fra.pack(side =TOP)
lab = Label(root, text ='test')
lab.pack()
root.bind('<Control-Z>', afficherTout)
root.mainloop()
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 OscilloGraphe()
construite précédemment, comme nous le montrerons dans l'application de synthèse.
Commentaires
[modifier | modifier le wikicode]- Ligne 6 : La méthode
constructeur
utilise un paramètre optionnelcoul
. Ce paramètre permettra de choisir une couleur pour le graphique soumis au contrôle du widget. Le paramètreboss
sert à réceptionner la référence d'une fenêtre maîtresse éventuelle. - Ligne 7 : Activation du constructeur de la classe parente (pour hériter sa fonctionnalité).
- Ligne 9 : Déclaration de quelques variables d'instance. Leurs vraies valeurs seront déterminées par les méthodes des lignes 29 à 40 (gestionnaires d'événements).
- Ligne 11 : Cette instruction instancie un objet de la classe
IntVar()
, laquelle fait partie du module Tkinter au même titre que les classes similairesDoubleVar()
,StringVar()
etBooleanVar()
. Toutes ces classes permettent de définir des variables Tkinter, lesquels sont en fait des objets, mais qui se comportent comme des variables à l'intérieur des widgets Tkinter. Ainsi l'objet référencé dansself.chk
contient l'équivalent d'une variable de type entier, dans un format utilisable par Tkinter. Pour accéder à sa valeur depuis Python, il faut utiliser des méthodes spécifiques de cette classe d'objets : la méthodeset()
permet de lui assigner une valeur, et la méthodeget()
permet de la récupérer (ce que l'on met en pratique à la ligne 47). - Ligne 12 : L'option variable de l'objet
checkbutton
est associée à la variable Tkinter définie à la ligne précédente. (Nous ne pouvons pas référencer directement une variable ordinaire dans la définition d'un widget Tkinter, parce que Tkinter lui-même est écrit dans un langage qui n'utilise pas les mêmes conventions que Python pour formater ses variables. Les objets construits à partir des classes de variables Tkinter sont donc nécessaires pour assurer l'interface). - Ligne 13 : L'option
command
désigne la méthode que le système doit invoquer lorsque l'utilisateur effectue un clic de souris dans la case à cocher. - Lignes 14 à 24 : Ces lignes définissent les trois widgets curseurs, en trois instructions similaires. Il serait plus élégant de programmer tout ceci en une seule instruction, répétée trois fois à l'aide d'une boucle. Cela nécessiterait cependant de faire appel à un concept que nous n'avons pas encore expliqué (les fonctions/expressions lamdba), et la définition du gestionnaire d'événements associé à ces widgets deviendrait elle aussi plus complexe. Conservons donc pour cette fois des instructions séparées : nous nous efforcerons d'améliorer tout cela plus tard.
- Lignes 26 à 40 : Les 4 widgets définis dans les lignes précédentes possèdent chacun une option
command
. Pour chacun d'eux, la méthode invoquée dans cette option command est différente : la case à cocher active la méthodesetCurve()
, le premier curseur active la méthodesetFrequency()
, le second curseur active la méthodesetPhase()
, et le troisième curseur active la méthodesetAmplitude()
. Remarquez bien au passage que l'optioncommand
des widgetsScale
transmet un argument à la méthode associée (la position actuelle du curseur), alors que la même optioncommand
ne transmet rien dans le cas du widgetCheckbutton
. 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[3], en faisant appel à la méthodeevent_generate()
. 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. Nous produisons ainsi un message-événement bien particulier, qui peut être détecté et traité par un gestionnaire d'événement associé à un autre widget (voir page suivante). De cette manière, nous mettons en place un véritable système de communication entre widgets : chaque fois que l'utilisateur exerce une action sur notre panneau de contrôle, celui-ci génère un événement spécifique, qui signale cette action à l'attention des autres widgets présents.nous aurions pu choisir une autre combinaison de touches (ou même carrément un autre type d'événement). Notre choix s'est porté sur celle-ci parce qu'il y a vraiment très peu de chances que l'utilisateur s'en serve alors qu'il examine notre programme. Nous pourrons cependant produire nous-mêmes un tel événement au clavier à titre de test, lorsque le moment sera venu de vérifier le gestionnaire de cet événement, que nous mettrons en place par ailleurs. - Lignes 42 à 54 : Comme nous l'avions déjà fait pour
oscillo.py
, nous complétons ce nouveau module par quelques lignes de code au niveau principal. Ces lignes permettent de tester le bon fonctionnement de la classe : elles ne s'exécutent que si on lance le module directement, comme une application à part entière. Veillez à utiliser vous-même cette technique dans vos propres modules, car elle constitue une bonne pratique de programmation : l'utilisateur de modules construits ainsi peut en effet (re)découvrir très aisément leur fonctionnalité (en les exécutant) et la manière de s'en servir (en analysant ces quelques lignes de code). Dans ces lignes de test, nous construisons une fenêtre principale root qui contient deux widgets : un widget de la nouvelle classeChoixVibra()
et un widget de la classeLabel()
. À la ligne 53, nous associons à la fenêtre principale un gestionnaire d'événement : tout événement du type spécifié déclenche désormais un appel de la fonctionafficherTout()
. Cette fonction est donc notre gestionnaire d'événement spécialisé, qui est sollicité chaque fois qu'un événement de type <Maj-Ctrl-Z> est détecté par le système d'exploitation. Comme nous l'avons déjà expliqué plus haut, nous avons fait en sorte que de tels événements soient produits par les objets de la classeChoixVibra()
, chaque fois que l'utilisateur modifie l'état de l'un ou l'autre des trois curseurs, ou celui de la case à cocher. Conçue seulement pour effectuer un test, la fonctionafficherTout()
ne fait rien d'autre que provoquer l'affichage des valeurs des variables associées à chacun de nos quatre widgets, en (re)configurant l'option text d'un widget de classeLabel()
. - Ligne 47, expression
fra.chk.get()
: nous avons vu plus haut que la variable mémorisant l'état de la case à cocher est un objet-variable Tkinter. Python ne peut pas lire directement le contenu d'une telle variable, qui est en réalité un objet-interface. Pour en extraire la valeur, il faut donc faire usage d'une méthode spécifique de cette classe d'objets : la méthodeget()
.
Propagation des évènements
[modifier | modifier le wikicode]Le mécanisme de communication décrit ci-dessus respecte la hiérarchie de classes des widgets. Vous aurez noté que la méthode qui déclenche l'événement est associée au widget dont nous sommes en train de définir la classe, par l'intermédiaire de self
. En général, un message-événement est en effet associé à un widget particulier (par exemple, un clic de souris sur un bouton est associé à ce bouton), ce qui signifie que le système d'exploitation va d'abord examiner s'il existe un gestionnaire pour ce type d'événement, qui soit lui aussi associé à ce widget. S'il en existe un, c'est celui-là qui est activé, et la propagation du message s'arrête. Sinon, le message-événement est « présenté » successivement aux widgets maîtres, dans l'ordre hiérarchique, jusqu'à ce qu'un gestionnaire d'événement soit trouvé, ou bien jusqu'à ce que la fenêtre principale soit atteinte.
Les événements correspondant à des frappes sur le clavier (telle la combinaison de touches <Maj-Ctrl-Z> utilisée dans notre exercice) sont cependant toujours expédiés directement à la fenêtre principale de l'application. Dans notre exemple, le gestionnaire de cet événement doit donc être associé à la fenêtre root
.
Exercices
- Votre nouveau widget hérite des propriétés de la classe
Frame()
. Vous pouvez donc modifier son aspect en modifiant les options par défaut de cette classe, à l'aide de la méthodeconfigure()
. Essayez par exemple de faire en sorte que le panneau de contrôle soit entouré d'une bordure de 4 pixels ayant l'aspect d'un sillon (bd = 4, relief = GROOVE). Si vous ne comprenez pas bien ce qu'il faut faire, inspirez-vous du scriptoscillo.py
(ligne 10). - Si l'on assigne la valeur
1
à l'optionshowvalue
des widgetsScale()
, la position précise du curseur par rapport à l'échelle est affichée en permanence. Activez donc cette fonctionnalité pour le curseur qui contrôle le paramètrephase
. - L'option
troughcolor
des widgetsScale()
permet de définir la couleur de leur glissière. Utilisez cette option pour faire en sorte que la couleur des glissières des 3 curseurs soit celle qui est utilisée comme paramètre lors de l'instanciation de votre nouveau widget. - Modifiez le script de telle manière que les widgets curseurs soient écartés davantage les uns des autres (options
padx
etpady
de la méthodepack()
).
Solution
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
Intégration de widgets composites dans une application synthèse
[modifier | modifier le wikicode]Dans les exercices précédents, nous avons construit deux nouvelles classes de widgets : le widget OscilloGraphe()
, canevas spécialisé pour le dessin de sinusoïdes, et le widget ChoixVibra()
, panneau de contrôle à trois curseurs permettant de choisir les paramètres d'une vibration.
Ces widgets sont désormais disponibles dans les modules oscillo.py
et curseurs.py
[4].
Nous allons à présent les utiliser dans une application synthèse, qui pourrait illustrer votre cours de physique : un widget OscilloGraphe()
y affiche un, deux, ou trois graphiques superposés, de couleurs différentes, chacun d'entre eux étant soumis au contrôle d'un widget ChoixVibra()
:
Le script correspondant est reproduit ci-après.
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 ».
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>.
Lorsque nous avons construit la classe de widgets ChoixVibra()
, 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 montreCourbes()
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.
from oscillo import *
from curseurs import *
class ShowVibra(Frame):
"""Démonstration de mouvements vibratoires harmoniques"""
def __init__(self, boss =None):
Frame.__init__(self) # constructeur de la classe parente
self.couleur = ['dark green', 'red', 'purple']
self.trace = [0]*3 # liste des tracés (courbes à dessiner)
self.controle = [0]*3 # liste des panneaux de contrôle
# Instanciation du canevas avec axes X et Y :
self.gra = OscilloGraphe(self, larg =400, haut=200)
self.gra.configure(bg ='white', bd=2, relief=SOLID)
self.gra.pack(side =TOP, pady=5)
# Instanciation de 3 panneaux de contrôle (curseurs) :
for i in range(3):
self.controle[i] = ChoixVibra(self, self.couleur[i])
self.controle[i].pack()
# Désignation de l'événement qui déclenche l'affichage des tracés :
self.master.bind('<Control-Z>', self.montreCourbes)
self.master.title('Mouvements vibratoires harmoniques')
self.pack()
def montreCourbes(self, event):
"""(Ré)Affichage des trois graphiques élongation/temps"""
for i in range(3):
# D'abord, effacer le tracé précédent (éventuel) :
self.gra.delete(self.trace[i])
# Ensuite, dessiner le nouveau tracé :
if self.controle[i].chk.get():
self.trace[i] = self.gra.traceCourbe(
coul = self.couleur[i],
freq = self.controle[i].freq,
phase = self.controle[i].phase,
ampl = self.controle[i].ampl)
#### Code pour tester la classe : ###
if __name__ == '__main__':
ShowVibra().mainloop()
Commentaires
[modifier | modifier le wikicode]- Lignes 1-2 : Nous pouvons nous passer d'importer le module Tkinter : chacun de ces deux modules s'en charge déjà.
- Ligne 4 : Puisque nous commençons à connaître les bonnes techniques, nous décidons de construire l'application elle-même sous la forme d'une classe, dérivée de la classe
Frame()
: ainsi nous pourrons plus tard l'intégrer toute entière dans d'autres projets, si le cœur nous en dit. - Lignes 8-10 : Définition de quelques variables d'instance (3 listes) : les trois courbes tracées seront des objets graphiques, dont les couleurs sont pré-définies dans la liste
self.couleur
; nous devons préparer également une listeself.trace
pour mémoriser les références de ces objets graphiques, et enfin une listeself.controle
pour mémoriser les références des trois panneaux de contrôle. - Lignes 13 à 15 : Instanciation du widget d'affichage. Étant donné que la classe
OscilloGraphe()
a été obtenue par dérivation de la classeCanvas()
, il est toujours possible de configurer ce widget en redéfinissant les options spécifiques de cette classe (ligne 13). - Lignes 18 à 20 : Pour instancier les trois widgets « panneau de contrôle », on utilise une boucle. Leurs références sont mémorisées dans la liste
self.controle
préparée à la ligne 10. Ces panneaux de contrôle sont instanciés comme esclaves du présent widget, par l'intermédiaire du paramètreself
. Un second paramètre leur transmet la couleur du tracé à contrôler. - Lignes 23-24 : Au moment de son instanciation, chaque widget Tkinter reçoit automatiquement un attribut master qui contient la référence de la fenêtre principale de l'application. Cet attribut se révèle particulièrement utile si la fenêtre principale a été instanciée implicitement par Tkinter, comme c'est le cas ici.
Rappelons en effet que lorsque nous démarrons une application en instanciant directement un widget tel que
Frame
, par exemple (c'est ce que nous avons fait à la ligne 4), Tkinter instancie automatiquement une fenêtre maîtresse pour ce widget (un objet de la classeTk()
). Comme cet objet a été créé automatiquement, nous ne disposons d'aucune référence dans notre code pour y accéder, si ce n'est par l'intermédiaire de l'attributmaster
que Tkinter associe automatiquement à chaque widget. Nous nous servons de cette référence pour redéfinir le bandeau-titre de la fenêtre principale (à la ligne 24), et pour y attacher un gestionnaire d'événement (à la ligne 23). - Lignes 27 à 40 : La méthode décrite ici est le gestionnaire des événements <Maj-Ctrl-Z> spécifiquement déclenchés par nos widgets
ChoixVibra()
(ou « panneaux de contrôle »), chaque fois que l'utilisateur exerce une action sur un curseur ou une case à cocher. Dans tous les cas, les graphiques éventuellement présents sont d'abord effacés (ligne 28) à l'aide de la méthodedelete()
: le widgetOscilloGraphe()
a hérité cette méthode de sa classe parenteCanvas()
. Ensuite, de nouvelles courbes sont retracées, pour chacun des panneaux de contrôle dont on a coché la case « Afficher ». Chacun des objets ainsi dessinés dans le canevas possède un numéro de référence, renvoyé par la méthodetraceCourbe()
de notre widgetOscilloGraphe()
. Les numéros de référence de nos dessins sont mémorisés dans la listeself.trace
. Ils permettent d'effacer individuellement chacun d'entre eux (cfr. instruction de la ligne 28). - Lignes 38-40 : Les valeurs de fréquence, phase & amplitude que l'on transmet à la méthode
traceCourbe()
sont les attributs d'instance correspondants de chacun des trois panneaux de contrôle, eux-mêmes mémorisés dans la listeself.controle
. Nous pouvons récupérer ces attributs en utilisant la qualification des noms par points.
Exercices
- Modifiez le script, de manière à obtenir l'aspect ci-dessous (écran d'affichage avec grille de référence, panneaux de contrôle entourés d'un sillon) :
- Modifiez le script, de manière à faire apparaître et contrôler 4 graphiques au lieu de trois. Pour la couleur du quatrième graphique, choisissez par exemple :
'blue'
,'navy'
,'maroon'
, ... - Aux lignes 33-35, nous récupérons les valeurs des fréquence, phase & amplitude choisies par l'utilisateur sur chacun des trois panneaux de contrôle, en accédant directement aux attributs d'instance correspondants. Python autorise ce raccourci - et c'est bien pratique – mais cette technique est dangereuse. Elle enfreint l'une des recommandations de la théorie générale de la « programmation orientée objet », qui préconise que l'accès aux propriétés des objets soit toujours pris en charge par des méthodes spécifiques. Pour respecter cette recommandation, ajoutez à la classe
ChoixVibra()
une méthode supplémentaire que vous appellerezvaleurs()
, et qui renverra un tuple contenant les valeurs de la fréquence, la phase et l'amplitude choisies. Les lignes 33 à 35 du présent script pourront alors être remplacées par quelque chose comme :freq, phase, ampl = self.control[i].valeurs()</li>
- Écrivez une petite application qui fait apparaître une fenêtre avec un canevas et un widget curseur (
Scale
). Dans le canevas, dessinez un cercle, dont l'utilisateur pourra faire varier la taille à l'aide du curseur. - Écrivez un script qui créera deux classes : une classe « Application », dérivée de
Frame()
, dont le constructeur instanciera un canevas de 400x400 pixels, ainsi que deux boutons. Dans le canevas, vous instancierez un objet de la classe « Visage » décrite ci-après. 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. - Exercice de synthèse : élaboration d'un dictionnaire de couleurs.
But : réaliser un petit programme utilitaire, qui puisse vous aider à construire facilement et rapidement un nouveau dictionnaire de couleurs, lequel permettrait l'accès technique à une couleur quelconque par l'intermédiaire de son nom usuel en français.
Contexte : En manipulant divers objets colorés avec Tkinter, vous avez constaté que cette bibliothèque graphique accepte qu'on lui désigne les couleurs les plus fondamentales sous la forme de chaînes de caractères contenant leur nom en anglais :
'red'
,'blue'
, etc. Vous savez cependant qu'un ordinateur ne peut traiter que des informations numérisées. Cela implique que la désignation d'une couleur quelconque doit nécessairement tôt ou tard être encodée sous la forme d'un nombre. Il faut bien entendu adopter pour cela une convention, et celle-ci peut varier d'un système à un autre. L'une de ces conventions, parmi les plus courantes, consiste à représenter une couleur à l'aide de trois octets, qui indiqueront respectivement les intensités des trois composantes rouge, verte et bleue de cette couleur. Cette convention peut être utilisée avec Tkinter pour accéder à n'importe quelle nuance colorée. Vous pouvez en effet lui indiquer la couleur d'un élément graphique quelconque, à l'aide d'une chaîne de 7 caractères telle que'#00FA4E'
. Dans cette chaîne, le premier caractère (#) signifie que ce qui suit est une valeur hexadécimale. Les six caractères suivants représentent les 3 valeurs hexadécimales des 3 composantes R, V et B. Pour visualiser concrètement la correspondance entre une couleur quelconque et son code, vous pouvez essayer le petit programme utilitaire tkColorChooser.py (qui se trouve généralement dans le sous-répertoire /lib-tk de votre installation de Python). Étant donné qu'il n'est pas facile pour les humains que nous sommes de mémoriser de tels codes hexadécimaux, Tkinter est également doté d'un dictionnaire de conversion, qui autorise l'utilisation de noms communs pour un certain nombre de couleurs parmi les plus courantes, mais cela ne marche que pour des noms de couleurs exprimés en anglais. Le but du présent exercice est de réaliser un logiciel qui facilitera la construction d'un dictionnaire équivalent en français, lequel pourrait ensuite être incorporé à l'un ou l'autre de vos propres programmes. Une fois construit, ce dictionnaire serait donc de la forme :{'vert':'#00FF00', 'bleu':'#0000FF', ... etc ...}
.- Cahier des charges
- Le script ci-dessous correspond à une ébauche de projet dessinant des ensembles de dés à jouer disposés à l'écran de plusieurs manières différentes (cette ébauche pourrait être une première étape dans la réalisation d'un logiciel de jeu).
L'exercice consistera à analyser ce script et à le compléter. Vous vous placerez ainsi dans la situation d'un programmeur chargé de continuer le travail commencé par quelqu'un d'autre, ou encore dans celle de l'informaticien prié de participer à un travail d'équipe.
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 :
from Tkinter import * class FaceDom: def __init__(self, can, val, pos, taille =70): self.can =can # *** x, y, c = pos[0], pos[1], taille/2 can.create_rectangle(x -c, y-c, x+c, y+c, fill ='ivory', width =2) d = taille/3 # *** self.pList =[] # *** pDispo = [((0,0),), ((-d,d),(d,-d)), ((-d,-d), (0,0), (d,d))] disp = pDispo[val -1] # *** for p in disp: self.cercle(x +p[0], y +p[1], 5, 'red') def cercle(self, x, y, r, coul): # *** self.pList.append(self.can.create_oval(x-r, y-r, x+r, y+r, fill=coul)) def effacer(self): # *** for p in self.pList: self.can.delete(p) class Projet(Frame): def __init__(self, larg, haut): Frame.__init__(self) self.larg, self.haut = larg, haut self.can = Canvas(self, bg='dark green', width =larg, height =haut) self.can.pack(padx =5, pady =5) # *** bList = [("A", self.boutA), ("B", self.boutB), ("C", self.boutC), ("D", self.boutD), ("Quitter", self.boutQuit)] for b in bList: Button(self, text =b[0], command =b[1]).pack(side =LEFT) self.pack() def boutA(self): self.d3 = FaceDom(self.can, 3, (100,100), 50) def boutB(self): self.d2 = FaceDom(self.can, 2, (200,100), 80) def boutC(self): self.d1 = FaceDom(self.can, 1, (350,100), 110) def boutD(self): # *** self.d3.effacer() def boutQuit(self): self.master.destroy() Projet(500, 300).mainloop()
Modifiez ensuite ce script, afin qu'il corresponde au cahier des charges suivant :
- Le canevas devra être plus grand : 600 x 600 pixels.
- Les boutons de commande devront être déplacés à droite et espacés davantage.
- La taille des points sur une face de dé devra varier proportionnellement à la taille de cette face
Variante 1 : Ne conservez que les 2 boutons A et B. Chaque utilisation du bouton A fera apparaître 3 nouveaux dés (de même taille, plutôt petits) disposés sur une colonne (verticale), les valeurs de ces dés étant tirées au hasard entre 1 et 6. Chaque nouvelle colonne sera disposée à la droite de la précédente. Si l'un des tirages de 3 dés correspond à 4, 2, 1 (dans n'importe quel ordre), un message « gagné » sera affiché dans la fenêtre (ou dans le canevas). Le bouton B provoquera l'effacement complet (pas seulement les points !) de tous les dés affichés.
Variante 2 : Ne conservez que les 2 boutons A et B. Le bouton A fera apparaître 5 dés disposés en quinconce (c.à.d. comme les points d'une face de valeur 5). Les valeurs de ces dés seront tirées au hasard entre 1 et 6, mais il ne pourra pas y avoir de doublons. Le bouton B provoquera l'effacement complet (pas seulement les points !) de tous les dés affichés.
Variante 3 : Ne conservez que les 3 boutons A, B et C. Le bouton A fera apparaître 13 dés de même taille disposés en cercle. Chaque utilisation du bouton B provoquera un changement de valeur du premier dé, puis du deuxième, du troisième, etc. La nouvelle valeur d'un dé sera à chaque fois égale a sa valeur précédente augmentée d'une unité, sauf dans le cas ou la valeur précédente était 6 : dans ce cas la nouvelle valeur est 1, et ainsi de suite. Le bouton C provoquera l'effacement complet (pas seulement les points !) de tous les dés affichés.
Variante 4 : Ne conservez que les 3 boutons A, B et C. Le bouton A fera apparaître 12 dés de même taille disposés sur deux lignes de 6. Les valeurs des dés de la première ligne seront dans l'ordre 1, 2, 3, 4, 5, 6. Les valeurs des dés de la seconde ligne seront tirées au hasard entre 1 et 6. Chaque utilisation du bouton B provoquera un changement de valeur aléatoire du premier dé de la seconde ligne, tant que cette valeur restera différente de celle du dé correspondant dans la première ligne. Lorsque le 1er dé de la 2e ligne aura acquis la valeur de son correspondant, c'est la valeur du 2e dé de la seconde ligne qui sera changée au hasard, et ainsi de suite, jusqu'à ce que les 6 faces du bas soient identiques à celles du haut. Le bouton C provoquera l'effacement complet (pas seulement les points !) de tous les dés affichés.
Solution
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
-
# Dictionnaire de couleurs from Tkinter import * # Module donnant accès aux boîtes de dialogue standard pour # la recherche de fichiers sur disque : from tkFileDialog import asksaveasfile, askopenfile class Application(Frame): '''Fenêtre d'application''' def __init__(self): Frame.__init__(self) self.master.title("Création d'un dictionnaire de couleurs") self.dico ={} # création du dictionnaire # Les widgets sont regroupés dans deux cadres (Frames) : frSup =Frame(self) # cadre supérieur contenant 6 widgets Label(frSup, text ="Nom de la couleur :", width =20).grid(row =1, column =1) self.enNom =Entry(frSup, width =25) # champ d'entrée pour self.enNom.grid(row =1, column =2) # le nom de la couleur Button(frSup, text ="Existe déjà ?", width =12, command =self.chercheCoul).grid(row =1, column =3) Label(frSup, text ="Code hexa. corresp. :", width =20).grid(row =2, column =1) self.enCode =Entry(frSup, width =25) # champ d'entrée pour self.enCode.grid(row =2, column =2) # le code hexa. Button(frSup, text ="Test", width =12, command =self.testeCoul).grid(row =2, column =3) frSup.pack(padx =5, pady =5) frInf =Frame(self) # cadre inférieur contenant le reste self.test = Label(frInf, bg ="white", width =45, # zone de test height =7, relief = SUNKEN) self.test.pack(pady =5) Button(frInf, text ="Ajouter la couleur au dictionnaire", command =self.ajouteCoul).pack() Button(frInf, text ="Enregistrer le dictionnaire", width =25, command =self.enregistre).pack(side = LEFT, pady =5) Button(frInf, text ="Restaurer le dictionnaire", width =25, command =self.restaure).pack(side =RIGHT, pady =5) frInf.pack(padx =5, pady =5) self.pack() def ajouteCoul(self): "ajouter la couleur présente au dictionnaire" if self.testeCoul() ==0: # une couleur a-t-elle été définie ? return nom = self.enNom.get() if len(nom) >1: # refuser les noms trop petits self.dico[nom] =self.cHexa else: self.test.config(text ="%s : nom incorrect" % nom, bg ='white') def chercheCoul(self): "rechercher une couleur déjà inscrite au dictionnaire" nom = self.enNom.get() if self.dico.has_key(nom): self.test.config(bg =self.dico[nom], text ="") else: self.test.config(text ="%s : couleur inconnue" % nom, bg ='white') def testeCoul(self): "vérifier la validité d'un code hexa. - afficher la couleur corresp." try: self.cHexa =self.enCode.get() self.test.config(bg =self.cHexa, text ="") return 1 except: self.test.config(text ="Codage de couleur incorrect", bg ='white') return 0 def enregistre(self): "enregistrer le dictionnaire dans un fichier texte" # Cette méthode utilise une boîte de dialogue standard pour la # sélection d'un fichier sur disque. Tkinter fournit toute une série # de fonctions associées à ces boîtes, dans le module tkFileDialog. # La fonction ci-dessous renvoie un objet-fichier ouvert en écriture : ofi =asksaveasfile(filetypes=[("Texte",".txt"),("Tous","*")]) for clef, valeur in self.dico.items(): ofi.write("%s %s\n" % (clef, valeur)) ofi.close() def restaure(self): "restaurer le dictionnaire à partir d'un fichier de mémorisation" # La fonction ci-dessous renvoie un objet-fichier ouvert en lecture : ofi =askopenfile(filetypes=[("Texte",".txt"),("Tous","*")]) lignes = ofi.readlines() for li in lignes: cv = li.split() # extraction de la clé et la valeur corresp. self.dico[cv[0]] = cv[1] ofi.close() if __name__ == '__main__': Application().mainloop()
-
(variante 3) :
from Tkinter import * from random import randrange from math import sin, cos, pi class FaceDom: def __init__(self, can, val, pos, taille =70): self.can =can x, y, c = pos[0], pos[1], taille/2 self. carre = can.create_rectangle(x -c, y-c, x+c, y+c, fill ='ivory', width =2) d = taille/3 # disposition des points sur la face, pour chacun des 6 cas : self.pDispo = [((0,0),), ((-d,d),(d,-d)), ((-d,-d), (0,0), (d,d)), ((-d,-d),(-d,d),(d,-d),(d,d)), ((-d,-d),(-d,d),(d,-d),(d,d),(0,0)), ((-d,-d),(-d,d),(d,-d),(d,d),(d,0),(-d,0))] self.x, self.y, self.dim = x, y, taille/15 self.pList =[] # liste contenant les points de cette face self.tracer_points(val) def tracer_points(self, val): # créer les dessins de points correspondant à la valeur val : disp = self.pDispo[val -1] for p in disp: self.cercle(self.x +p[0], self.y +p[1], self.dim, 'red') self.val = val def cercle(self, x, y, r, coul): self.pList.append(self.can.create_oval(x-r, y-r, x+r, y+r, fill=coul)) def effacer(self, flag =0): for p in self.pList: self.can.delete(p) if flag: self.can.delete(self.carre) class Projet(Frame): def __init__(self, larg, haut): Frame.__init__(self) self.larg, self.haut = larg, haut self.can = Canvas(self, bg='dark green', width =larg, height =haut) self.can.pack(padx =5, pady =5) # liste des boutons à installer, avec leur gestionnaire : bList = [("A", self.boutA), ("B", self.boutB), ("C", self.boutC), ("Quitter", self.boutQuit)] bList.reverse() # inverser l'ordre de la liste for b in bList: Button(self, text =b[0], command =b[1]).pack(side =RIGHT, padx=3) self.pack() self.des =[] # liste qui contiendra les faces de dés self.actu =0 # réf. du dé actuellement sélectionné def boutA(self): if len(self.des): return # car les dessins existent déjà ! a, da = 0, 2*pi/13 for i in range(13): cx, cy = self.larg/2, self.haut/2 x = cx + cx*0.75*sin(a) # pour disposer en cercle, y = cy + cy*0.75*cos(a) # on utilise la trigono ! self.des.append(FaceDom(self.can, randrange(1,7) , (x,y), 65)) a += da def boutB(self): # incrémenter la valeur du dé sélectionné. Passer au suivant : v = self.des[self.actu].val v = v % 6 v += 1 self.des[self.actu].effacer() self.des[self.actu].tracer_points(v) self.actu += 1 self.actu = self.actu % 13 def boutC(self): for i in range(len(self.des)): self.des[i].effacer(1) self.des =[] self.actu =0 def boutQuit(self): self.master.destroy() Projet(600, 600).mainloop()
Notes
[modifier | modifier le wikicode]- ↑ Comme nous l'avons déjà signalé précédemment, Python vous permet d'accéder aux attributs d'instance en utilisant la qualification des noms par points. D'autres langages de programmation l'interdisent, ou bien ne l'autorisent que moyennant une déclaration particulière de ces attributs (distinction entre attributs privés et publics). Sachez en tous cas que ce n'est pas recommandé : le bon usage de la programmation orientée objet stipule en effet que vous ne devez pouvoir accéder aux attributs des objets que par l'intermédiaire de méthodes spécifiques.
- ↑ 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
Frame()
). La fenêtre englobant ce widget sera automatiquement ajoutée. - ↑ En fait, on devrait plutôt appeler cela un message (qui est lui-même la notification d'un événement). Programmes pilotés par des événements.
- ↑ Il va de soi que nous pourrions rassembler toutes les classes que nous construisons dans un seul module.