15. 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.
15-A. Code des couleurs : un petit projet bien encapsulé▲
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 :
Chaque couleur correspond conventionnellement à l'un des chiffres de zéro à neuf :
Noir = 0 ; Brun = 1 ; Rouge = 2 ; Orange = 3 ; Jaune = 4 ;
Vert = 5 ; Bleu = 6 ; Violet = 7 ; Gris = 8 ; Blanc = 9.
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.
Exemple concret : supposons qu'à partir de la gauche, les bandes colorées soient jaune, violette et verte.
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.).
15-A-1. 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
Ω.
15-A-2. Mise en œuvre concrète▲
Nous construisons le corps de cette application simple sous la forme d'une classe. Nous voulons vous montrer ainsi comment une classe peut servir d'espace de noms commun, dans lequel vous pouvez encapsuler vos variables et nos fonctions. Le principal intérêt de procéder ainsi est que cela vous permet de vous passer de variables globales. En effet :
- Mettre en route l'application se résumera à instancier un objet de cette classe.
- Les fonctions que l'on voudra y mettre en œuvre seront les méthodes de cet objet-application.
- À l'intérieur de ces méthodes, il suffira de rattacher un nom de variable au paramètre self pour que cette variable soit accessible
de partout à l'intérieur de l'objet. Une telle variable d'instance est donc tout à fait l'équivalent d'une variable globale (mais
seulement à l'intérieur de l'objet), puisque toutes les autres méthodes de cet objet peuvent y accéder par l'intermédiaire de
self.
class
Application
(
object):
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
, column =
1
, columnspan =
3
)
Button
(
self.root, text =
'Montrer'
, command =
self.changeCouleurs).\
grid
(
row =
3
, column =
1
)
Button
(
self.root, text =
'Quitter'
, command =
self.root.quit).\
grid
(
row =
3
, column =
3
)
self.entree =
Entry
(
self.root, width =
14
)
self.entree.grid
(
row =
3
, column =
2
)
# 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
, column =
1
, columnspan =
3
, 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
(
) # cette méthode 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 :
if
__name__
==
'__main__'
:
from
tkinter import
*
from
math import
log10 # logarithmes en base 10
f =
Application
(
) # instanciation de l'objet application
15-A-3. Commentaires▲
- Ligne 1 : La classe est définie comme une nouvelle classe indépendante (elle ne dérive d'aucune classe parente préexistante, mais seulement de object, « ancêtre » de toutes les classes).
- Lignes 2 à 14 : Le constructeur de la classe instancie les widgets nécessaires : espace graphique, libellés et boutons. Afin d'améliorer la lisibilité du programme, cependant, nous avons placé l'instanciation du canevas (avec le dessin de la résistance) dans une méthode distincte : dessineResistance(). Veuillez remarquer aussi que pour obtenir un code plus compact, nous ne mémorisons pas les boutons et le libellé dans des variables (comme cela a été expliqué à la page ), parce que nous ne souhaitons pas y faire référence ailleurs dans le programme. Le positionnement des widgets dans la fenêtre utilise la méthode grid() décrite à la page .
- 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. Si vous préférez démarrer l'application indépendamment de sa création, vous devez supprimer cette ligne, et reporter l'appel à mainloop() au niveau principal du programme, en ajoutant une instruction : f.root.mainloop() à la ligne 71.
- 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 un ouvrage 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'option fill de chacune des bandes colorées, en utilisant les noms de couleur extraits de la liste self.cc grâce à aux trois indices li[1], li[2] et li[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.
15-B. Petit train : héritage, échange d'informations entre classes▲
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(75). 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 attributsd'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 est 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 ). 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.
15-B-1. 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.
15-B-2. 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
(
object):
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
) # oeil gauche
cercle
(
self.canev, xf+
5
, yf-
3
, 2
) # oeil droit
cercle
(
self.canev, xf, yf+
5
, 3
) # bouche
app =
Application
(
)
app.mainloop
(
)
15-B-3. 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.(76) 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 pour 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(), un peu plus loin, nous devrons associer à sa méthode constructeur un nombre égal de paramètres afin de 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 à 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.
- Lignes 29-30 : La classe Wagon() ne dérive d'aucune autre classe préexistante. Étant donné qu'il s'agit d'une classe d'objets graphiques, nous devons cependant 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 cette application, contrairement à la précédente, nous avons préféré séparer la création de l'objet app, et son démarrage par invocation de mainloop(), dans deux instructions distinctes (en guise d'exemple). Vous pourriez également condenser ces deux instructions en une seule, laquelle serait alors : Application().mainloop(), et faire ainsi l'économie d'une variable.
Exercice
.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.
.Ajoutez des correctifs au programme précédent, afin que l'on puisse utiliser n'importe quel bouton dans le désordre, sans que cela ne déclenche une erreur ou un effet bizarre.
15-C. OscilloGraphe : un widget personnalisé▲
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 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.
Comme nous l'avons déjà expliqué à la page , 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 dans une
autre application. Si on lance le script comme application principale, par contre, ces instructions s'exécutent. 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.
15-C-1. Expérimentation▲
Nous commenterons les lignes importantes du script un peu plus loin dans ce texte. Mais commençons d'abord par expérimenter quelque peu la classe que nous venons de construire.
Ouvrez donc votre terminal, 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 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.
Exercice
.Créez un quatrième widget, de taille : 400 × 300, couleur de fond : jaune, et faites-y apparaître plusieurs courbes correspondant à des fréquences et des amplitudes différentes.
Il est temps à présent que nous analysions la structure de la classe qui nous a permis d'instancier tous ces widgets. Nous avons donc enregistré cette classe dans le module oscillo.py (voir page ).
15-C-2. 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 doit faire apparaître deux axes cartésiens X et Y munis de flèches. L'axe X représentera l'écoulement du temps pendant une seconde au total, et il sera muni d'une échelle comportant 8 intervalles.
Une méthode traceCourbe() sera associée à ce widget. Elle provoquera le tracé du graphique élongation/temps pour un mouvement vibratoire, dont on aura fourni 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).
15-C-3. Implémentation▲
- Ligne 4 : La classe OscilloGraphe() est créée par dérivation de la classe Canvas(). 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 classe Canvas().
- 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 et haut (largeur et hauteur) servent à assigner des valeurs aux options width et height du canevas parent, au moment de l'instanciation.
- Lignes 9-10 : Comme nous l'avons déjà dit à plusieurs reprises, le constructeur d'une classe dérivée doit presque toujours
commencer par activer le constructeur de sa classe parente. Nous ne pouvons en effet hériter toute la fonctionnalité de la classe parente, que si cette
fonctionnalité a été effectivement mise en place et initialisée.
Nous activons donc le constructeur de la classe Canvas() à 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).
Et comme cela a également déjà été expliqué (cf. page ), 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 et haut dans des variables d'instance, parce que nous devrons pouvoir y accéder aussi dans la méthode traceCourbe().
- Lignes 13-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, comme nous l'avons déjà expliqué à la page .
- 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 et 28, puis accumulées dans la liste curve.
- Lignes 30-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'option smooth =1amé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éthode create_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 classe OscilloGraphe(), 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 à la figure de gauche page .
.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 de la page .
.Complétez encore votre widget en y faisant apparaître des repères numériques.
15-D. Curseurs : un widget composite▲
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 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.
15-D-1. Présentation du widget Scale▲
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é', lequel est nécessaire afin d'éviter la confusion avec le mot
réservé from !) 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.
15-D-2. Construction d'un panneau de contrôle à trois curseurs▲
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 que nous décrirons un peu plus loin(77). Nous attirons votre attention sur le fait que le code ci-dessous peut être raccourci de différentes manières (nous y reviendrons). 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.
15-D-2-A. Commentaires▲
- Ligne 6 : La méthode « constructeur » utilise un paramètre optionnel coul. Ce paramètre permettra de choisir une couleur pour le graphique soumis au contrôle du widget. Le paramètre boss sert à réceptionner la référence d'une fenêtre maîtresse éventuelle (voir plus loin).
- 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
similaires DoubleVar(), StringVar() et BooleanVar(). 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 (voir ci-après).
Ainsi l'objet référencé dans self.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éthode set() permet de lui assigner une valeur, et la méthode get() permet de la récupérer (ce que l'on mettra 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 ou expressionslamdba), 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éthode setCurve(), le
premier curseur active la méthode setFrequency(), le second curseur active la méthode setPhase(), et le troisième curseur active la
méthode setAmplitude(). Remarquez bien au passage que l'option command des widgets Scale transmet un argument à la méthode
associée (la position actuelle du curseur), alors que la même option command ne transmet rien dans le cas du widget Checkbutton.
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(78), en faisant appel à la méthode event_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.
Note : 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 classe ChoixVibra() et un widget de la classe Label().
À 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 fonction afficherTout().
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 classe ChoixVibra(), 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 fonction afficherTout() 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 classe Label().
- 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éthode get().
15-D-2-B. Propagation des événements▲
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éthode configure(). 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 script oscillo.py (ligne 10).
.Si l'on assigne la valeur 1 à l'option showvalue des widgets Scale(), 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ètre « phase ».
.L'option troughcolor des widgets Scale() 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 et pady de la méthode pack()).
15-E. Intégration de widgets composites dans une application synthèse▲
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(79).
Nous allons à présent les utiliser dans une application synthèse : 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 » (voir page ).
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>.
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
(
)
15-E-1. Commentaires▲
- 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 nouvelle classe de widget, 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 liste self.trace pour mémoriser les références de ces objets graphiques, et enfin une liste self.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 classe Canvas(), 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ètre self. Un second paramètre leur transmet la couleur du tracé à contrôler.
- 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éthode delete() : le widget OscilloGraphe() a hérité cette méthode de sa classe parente Canvas().
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éthode traceCourbe() de notre widget OscilloGraphe().
Les numéros de référence de nos dessins sont mémorisés dans la liste self.trace. Ils permettent d'effacer individuellement chacun d'entre eux (cf. instruction de la ligne 28). - Lignes 38-40 : Les valeurs de fréquence, phase et 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 liste self.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 et 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 appellerez valeurs(), 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()
.É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 400 × 400 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. Vous pouvez vous inspirer de l'exemple de la page pour composer une partie du code.
.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, yellow, 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 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 rouge, vert et bleu.
Pour visualiser concrètement la correspondance entre une couleur quelconque et son code, vous pouvez explorer les ressources de divers programmes de traitement d'images, tels par exemple les excellents programmes libres « Gimp » et « Inkscape ».
É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 :
L'application à réaliser sera une application graphique, construite autour d'une classe. Elle comportera une fenêtre avec un certain nombre de
champs d'entrée et de boutons, afin que l'utilisateur puisse aisément encoder de nouvelles couleurs en indiquant à chaque fois son nom français
dans un champ, et son code hexadécimal dans un autre.
Lorsque le dictionnaire contiendra déjà un certain nombre de données, il devra être possible de le tester, c'est-à-dire d'entrer un nom
de couleur en français et de retrouver le code hexadécimal correspondant à l'aide d'un bouton (avec affichage éventuel d'une zone
colorée).
Un bouton provoquera l'enregistrement du dictionnaire dans un fichier texte. Un autre permettra de reconstruire le dictionnaire à partir du
fichier.
.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.
A) Commencez par analyser ce script et ajoutez-y des commentaires, en particulier aux lignes marquées : #***, pour
montrer que vous comprenez ce que doit faire le programme à ces emplacements :
from
tkinter import
*
class
FaceDom
(
object):
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
(
)
B) Modifiez ensuite ce script, afin qu'il corresponde au cahier des charges suivant :
Le canevas devra être plus grand : 600 × 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'est-à-dire 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.