Apprendre à programmer avec Python 3

Image de couverture python 3


précédentsommairesuivant

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 , ou encore 4,7 .

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

Image non disponible

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.
 
Sélectionnez
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.

Image non disponible

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

 
Sélectionnez
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 Image non disponible, 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.

Image non disponible

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).

 
Sélectionnez
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.

Image non disponible

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 :

 
Sélectionnez
>>> 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 !

 
Sélectionnez
>>> 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 φ.

 
Sélectionnez
>>> 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

Image non disponible

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 :

 
Sélectionnez
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.

Image non disponible

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 :

 
Sélectionnez
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>.

Image non disponible
 
Sélectionnez
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):
      """()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.
  • Image non disponible
  • 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 :

 
Sélectionnez
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.


précédentsommairesuivant
Comme nous l'avons déjà signalé précédemment, Python vous permet d'accéder aux attributs d'instance en utilisant la qualification des noms par points. D'autres langages de programmation l'interdisent, ou bien ne l'autorisent que moyennant une déclaration particulière de ces attributs (distinction entre attributs privés et publics).
Sachez en tous cas que ce n'est pas recommandé : le bon usage de la programmation orientée objet stipule en effet que vous ne devez pouvoir accéder aux attributs des objets que par l'intermédiaire de méthodes spécifiques (l'interface).
Nous verrons plus loin que tkinter autorise également de construire la fenêtre principale d'une application pardérivation d'une classe de widget (le plus souvent, il s'agira d'un widgetFrame()). La fenêtre englobant ce widget sera automatiquement ajoutée (voir page ).
Vous pourriez bien évidemment aussi enregistrer plusieurs classes dans un même module.
En fait, on devrait plutôt appeler cela un message (qui est lui-même la notification d'un événement). Veuillez relire à ce sujet les explications de la page : Programmes pilotés par des événements.
Il va de soit que nous pourrions aussi rassembler toutes les classes que nous construisons dans un seul module.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Licence Creative Commons
Le contenu de cet article est rédigé par Gérard Swinnen et est mis à disposition selon les termes de la Licence Creative Commons Attribution 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2013 Developpez.com.