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é.
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.)
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 Ω.
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 :
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
from Tkinter import *
from math import log10 # logarithmes en base 10
f = Application() # instanciation de l'objet application
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).
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.
« 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 à codice_1. 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. 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.
L'application comportera deux classes :
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()
« 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 formula_1, 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 codice_25. 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é
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 codice_26 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.
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 :
Après importation des classes du module codice_27, nous instancions un premier objet codice_28, de la classe codice_29.
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 !
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 codice_30 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 "φ".
Pour comprendre la configuration de ce troisième "widget", il faut nous rappeler que la classe codice_29 a été construite par dérivation de la classe codice_32. 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 codice_30, à laquelle nous fournissons des arguments pour la phase et la couleur.
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.
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 codice_30 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).
Nous devons transmettre à ce constructeur la référence de l'instance présente (self) comme premier argument.
« 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 codice_25. 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 ».
Commençons d'abord par découvrir un widget de base, que nous n'avions pas encore utilisé jusqu'ici :
Le "widget" codice_52 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" codice_52 de n'importe quelle taille (option codice_54), en orientation horizontale (comme dans notre exemple) ou verticale (option codice_55).
Les options codice_56 (attention : n'oubliez pas le caractère 'souligné' !) et codice_57 définissent la plage de réglage. L'intervalle entre les repères numériques est défini dans l'option codice_58, etc.
La fonction désignée dans l'option codice_59 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 codice_60 de la fonction codice_61, dans notre exemple.
Le "widget" codice_52 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" codice_63 construit dans les pages précédentes.
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 codice_64. 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>')
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 codice_29 construite précédemment, comme nous le montrerons dans l'application de synthèse.
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 codice_1. 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 codice_67.
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" codice_29, canevas spécialisé pour le dessin de sinusoïdes, et le "widget" codice_81, panneau de contrôle à trois curseurs permettant de choisir les paramètres d'une vibration.
Ces "widgets" sont désormais disponibles dans les modules codice_25 et codice_64.
Nous allons à présent les utiliser dans une application synthèse, qui pourrait illustrer votre cours de physique : un "widget" codice_29 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" codice_81 :
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" codice_81, 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 codice_87 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) 
if __name__ == '__main__': 
 ShowVibra().mainloop()