« Programmation Python/Classes et Interfaces graphiques » : différence entre les versions

Contenu supprimé Contenu ajouté
Tavernier (discussion | contributions)
Tavernier (discussion | contributions)
découpage en paragraphes
Ligne 1 :
{{todo|mettre en forme}}
 
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.
 
1.1== « 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 :
Ligne 12 ⟶ 16 :
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.)
 
a);Cahier des charges de notre programme :
 
Notre application doit faire apparaître une fenêtre comportant un dessin de la résistance, ainsi qu'un champ d'entrée dans lequel l'utilisateur peut encoder une valeur numérique. Un bouton « Montrer » déclenche la modification du dessin de la résistance, de telle façon que les trois bandes de couleur se mettent en accord avec la valeur numérique introduite.
Contrainte : Le programme doit accepter toute entrée numérique fournie sous forme entière ou réelle, dans les limites de 10 à 1011 . Par exemple, une valeur telle que 4.78e6 doit être acceptée et arrondie correctement, c'est-à-dire convertie en 4800000 .
 
b);Mise en œuvre concrète
 
Nous construisons cette application simple sous la forme d'une classe. Sa seule utilité présente consiste à nous fournir un espace de noms commun dans lequel nous pouvons encapsuler nos variables et nos fonctions, ce qui nous permet de nous passer de variables globales. En effet :
Les variables auxquelles nous souhaitons pouvoir accéder de partout sont déclarées comme des attributs d'instance (nous attachons chacune d'elles à l'instance à l'aide de self).
Ligne 90 ⟶ 97 :
69.f = Application() # instanciation de l'objet application
 
;Commentaires :
 
Commentaires :
Ligne 1 : La classe est définie sans référence à une classe parente (pas de parenthèses). Il s'agira donc d'une nouvelle classe indépendante.
Lignes 2 à 14 : Le constructeur de la classe instancie les widgets nécessaires : pour améliorer la lisibilité du programme, on a placé l'instanciation du canevas (avec le dessin de la résistance) dans une méthode séparée dessineResistance(). Les boutons et le libellé ne sont pas mémorisés dans des variables, parce que l'on ne souhaite pas y faire référence ailleurs dans le programme.
Ligne 119 ⟶ 126 :
1.5.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.
 
1.2== « 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érieur1. 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 : nous en fournissons d'ailleurs des exemples plus loin (voir page 229). 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.
 
a);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.
 
b);Implémentation :
 
1.from Tkinter import *
Ligne 186 ⟶ 196 :
54.app.mainloop()
 
;Commentaires :
 
Commentaires :
Lignes 3 à 5 : Nous projetons de dessiner une série de petits cercles. Cette petite fonction nous facilitera le travail en nous permettant de définir ces cercles à partir de leur centre et leur rayon.
Lignes 7 à 13 : La classe principale de notre application est construite par dérivation de la classe de fenêtres Tk() importée du module Tkinter.2 Comme nous l'avons expliqué au chapitre précédent, le constructeur d'une classe dérivée doit activer lui-même le constructeur de la classe parente, en lui transmettant la référence de l'instance comme premier argument.
Ligne 207 ⟶ 217 :
Ajoutez un bouton à la fenêtre principale, qui puisse déclencher cet allumage. Profitez de l'amélioration de la fonction cercle() pour teinter le visage des petits personnages en rose (pink), leurs yeux et leurs bouches en noir, et instanciez les objets-wagons avec des couleurs différentes.
 
1.3== « 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 :
Ligne 263 ⟶ 274 :
Lancez donc l'exécution du script de la manière habituelle. Vous devriez obtenir un affichage similaire à celui qui est reproduit à la page précédente.
 
;Expérimentation :
 
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 une fenêtre de terminal (« Python shell »), et entrez les instructions ci-dessous directement à la ligne de commande :
Ligne 291 ⟶ 303 :
1.7.Créez un quatrième widget, de taille 400 x 300, couleur de fond jaune, et faites-y apparaître plusieurs courbes correspondant à des fréquences et des amplitudes différentes.
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 (voir page 183).
 
a);Cahier des charges :
 
Nous souhaitons définir une nouvelle classe de widget, capable d'afficher automatiquement les graphiques élongation/temps correspondant à divers mouvements vibratoires harmoniques.
Ce widget doit pouvoir être dimensionné à volonté au moment de son instanciation. Il fait apparaître deux axes cartésiens X et Y munis de flèches. L'axe X représente l'écoulement du temps pendant une seconde au total, et il est muni d'une échelle comportant 8 intervalles.
Une méthode traceCourbe() est associée à ce widget. Elle provoque le tracé du graphique élongation/temps pour un mouvement vibratoire dont on fournit la fréquence (entre 0.25 et 10 Hz), la phase (entre 0 et 2 radians) et l'amplitude (entre 1 et 10 ; échelle arbitraire).
 
b);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 320 ⟶ 336 :
1.10.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.
1.11.Complétez encore votre widget en y faisant apparaître des repères numériques.
 
1.4== « 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 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.
 
1.4.1Présentation=== Présentation du widget « Scale » ===
 
Commençons d'abord par découvrir un widget de base, que nous n'avions pas encore utilisé jusqu'ici :
Le widget Scale se présente comme un curseur qui coulisse devant une échelle. Il permet à l'utilisateur de choisir rapidement la valeur d'un paramètre quelconque, d'une manière très attrayante.
Ligne 347 ⟶ 367 :
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.
 
1.4.2Construction=== 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 loin3. Nous attirons votre attention sur le fait que le code ci-dessous peut être raccourci de différentes manières (Nous y reviendrons). Nous ne l'avons pas optimisé d'emblée, parce que cela nécessiterait d'y incorporer un concept supplémentaire (les expressions lambda), ce que nous préférons éviter pour l'instant.
Vous savez déjà que les lignes de code placées à la fin du script permettent de tester son fonctionnement. Vous devriez obtenir une fenêtre semblable à celle-ci :
Ligne 407 ⟶ 429 :
Ce panneau de contrôle permettra à vos utilisateurs de régler aisément la valeur des paramètres indiqués (fréquence, phase & amplitude), lesquels pourront alors servir à commander l'affichage de graphiques élongation/temps dans un widget de la classe OscilloGraphe() construite précédemment, comme nous le montrerons dans l'application de synthèse.
 
;Commentaires :
 
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 437 ⟶ 460 :
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().
 
;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.
Ligne 446 ⟶ 470 :
1.14.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.
1.15.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()).
 
1.5Intégration== 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.py5
Ligne 504 ⟶ 530 :
45. ShowVibra().mainloop()
 
;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 classe, dérivée de la classe Frame() : ainsi nous pourrons plus tard l'intégrer toute entière dans d'autres projets, si le cœur nous en dit.