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

Contenu supprimé Contenu ajouté
Tavernier (discussion | contributions)
découpage en paragraphes
Tavernier (discussion | contributions)
pause
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.
 
== « 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 :
{{remarque|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.
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. Par Exempleexemple concretsupposons :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Ω.
 
Supposons qu'à partir de la gauche, les bandes colorées soient jaune, violette et verte.
La valeur de cette résistance est 4700000 , ou 4700 k , ou encore 4,7 M..
Ce système ne permet évidemment de préciser une valeur numérique qu'avec deux chiffres significatifs seulement. Il est toutefois considéré comme largement suffisant pour la plupart des applications électroniques « ordinaires » (radio, TV, etc.)
 
;Cahier des charges de notre programme
 
{{image manquante}}
 
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 .
'''Contrainte''' : Le programme doit accepter toute entrée numérique fournie sous forme entière ou réelle, dans les limites de 10 à 10<sup>11</sup> Ω. Par exemple, une valeur telle que 4.78e6 doit être acceptée et arrondie correctement, c'est-à-dire convertie en 4800000 Ω.
 
;Mise en œuvre concrète
 
Nous construisons cette application simple sous la forme d'une ''classe''. Sa seule utilité présente consiste à nous fournir ''un espace de noms commun'' dans lequel nous pouvons ''encapsuler'' nos variables et nos fonctions, ce qui nous permet de nous passer de variables globales. En effet :
 
Les variables auxquelles nous souhaitons pouvoir accéder de partout sont déclarées comme des attributs d'instance (nous attachons chacune d'elles à l'instance à l'aide de self).
* Les fonctionsvariables auxquelles nous souhaitons pouvoir accéder de partout sont déclarées comme des méthodes,attributs etd'instance donc(nous attachéesattachons chacune d'elles aussià l'instance à l'aide de <code>self</code>).
 
* Les fonctions sont déclarées comme des méthodes, et donc attachées elles aussi à <code>self</code>.
 
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).
 
<pre>
1.class Application:
2. def __init__(self):
Ligne 96 ⟶ 106 :
68.from math import log10 # logarithmes en base 10
69.f = Application() # instanciation de l'objet application
</pre>
 
;Commentaires
 
* Ligne 1 : La classe est définie sans référence à une classe parente (pas de parenthèses). Il s'agira donc d'une nouvelle classe indépendante.
Lignes 2 à 14 : Le constructeur de la classe instancie les widgets nécessaires : pour améliorer la lisibilité du programme, on a placé l'instanciation du canevas (avec le dessin de la résistance) dans une méthode séparée dessineResistance(). Les boutons et le libellé ne sont pas mémorisés dans des variables, parce que l'on ne souhaite pas y faire référence ailleurs dans le programme.
Le positionnement des widgets dans la fenêtre utilise la méthode grid(), décrite à la page 97.
Lignes 15-17 : Le code des couleurs est mémorisé dans une simple liste.
Ligne 18 : La dernière instruction du constructeur démarre l'application.
Lignes 20 à 30 : Le dessin de la résistance se compose d'une ligne et d'un premier rectangle gris clair, pour le corps de la résistance et ses deux fils. Trois autres rectangles figureront les bandes colorées que le programme devra modifier en fonction des entrées de l'utilisateur. Ces bandes sont noires au départ ; elles sont référencées dans la liste self.ligne.
Lignes 32 à 53 : Ces lignes contiennent l'essentiel de la fonctionnalité du programme.
L'entrée brute fournie par l'utilisateur est acceptée sous la forme d'une chaîne de caractères.
A la ligne 36, on essaie de convertir cette chaîne en une valeur numérique de type float. Si la conversion échoue, on mémorise l'erreur. Si l'on dispose bien d'une valeur numérique, on vérifie ensuite qu'elle se situe effectivement dans l'intervalle autorisé (de 10  à 1011 ). Si une erreur est détectée, on signale à l'utilisateur que son entrée est incorrecte en colorant de rouge le fond du champ d'entrée, qui est ensuite vidé de son contenu (lignes 55 à 61).
Lignes 45-46 : Les mathématiques viennent à notre secours pour extraire de la valeur numérique son ordre de grandeur (c'est-à-dire l'exposant de 10 le plus proche). Veuillez consulter votre cours de mathématiques pour de plus amples explications concernant les logarithmes.
Lignes 47-48 : Une fois connu l'ordre de grandeur, il devient relativement facile d'extraire du nombre traité ses deux premiers chiffres significatifs. Exemple : Supposons que la valeur entrée soit 31687. Le logarithme de ce nombre est 4,50088... dont la partie entière (4) nous donne l'ordre de grandeur de la valeur entrée (soit 104). Pour extraire de celle-ci son premier chiffre significatif, il suffit de la diviser par 104, soit 10000, et de conserver seulement la partie entière du résultat (3).
Lignes 49 à 51 : Le résultat de la division effectuée dans le paragraphe précédent est 3,1687.
Nous récupérons la partie décimale de ce nombre à la ligne 49, soit 0,1687 dans notre exemple.
Si nous le multiplions par dix, ce nouveau résultat comporte une partie entière qui n'est rien d'autre que notre second chiffre significatif (1 dans notre exemple).
Nous pourrions facilement extraire ce dernier chiffre, mais puisque c'est le dernier, nous souhaitons encore qu'il soit correctement arrondi. Pour ce faire, il suffit d'ajouter une demi unité au produit de la multiplication par dix, avant d'en extraire la valeur entière. Dans notre exemple, en effet, ce calcul donnera donc 1,687 + 0,5 = 2,187 , dont la partie entière (2) est bien la valeur arrondie recherchée.
Ligne 53 : Le nombre de zéros à accoler aux deux chiffres significatifs correspond au calcul de l'ordre de grandeur. Il suffit de retirer une unité au logarithme.
Ligne 56 : Pour attribuer une nouvelle couleur à un objet déjà dessiné dans un canevas, on utilise la méthode itemconfigure(). Nous utilisons donc cette méthode pour modifier l'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.
 
* 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 <code>dessineResistance()</code>. Les boutons et le libellé ne sont pas mémorisés dans des variables, parce que l'on ne souhaite pas y faire référence ailleurs dans le programme. Le positionnement des ''widgets'' dans la fenêtre utilise la méthode <code>grid()</code>, décrite à la page {{todo}}.
(1)Exercices :
 
* Lignes 15-17 : Le code des couleurs est mémorisé dans une simple liste.
1.1.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.
 
1.2.Modifiez le script ci-dessus de telle manière que l'image dessinée soit deux fois plus grande.
* Ligne 18 : La dernière instruction du constructeur démarre l'application.
1.3.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' .
 
1.4.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.
* 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 <code>self.ligne</code>.
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.
 
* 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.<br />À la ligne 36, on essaie de convertir cette chaîne en une valeur numérique de type ''float''. Si la conversion échoue, on mémorise l'erreur. Si l'on dispose bien d'une valeur numérique, on vérifie ensuite qu'elle se situe effectivement dans l'intervalle autorisé (de 10 Ω à 1011 Ω). Si une erreur est détectée, on signale à l'utilisateur que son entrée est incorrecte en colorant de rouge le fond du champ d'entrée, qui est ensuite vidé de son contenu (lignes 55 à 61).
 
* Lignes 45-46 : Les mathématiques viennent à notre secours pour extraire de la valeur numérique son ordre de grandeur (c'est-à-dire l'exposant de 10 le plus proche). Veuillez consulter votre cours de mathématiques pour de plus amples explications concernant les logarithmes.
 
* Lignes 47-48 : Une fois connu l'ordre de grandeur, il devient relativement facile d'extraire du nombre traité ses deux premiers chiffres significatifs. Exemple : Supposons que la valeur entrée soit 31687. Le logarithme de ce nombre est 4,50088... dont la partie entière (4) nous donne l'ordre de grandeur de la valeur entrée (soit 10<sup>4</sup>). Pour extraire de celle-ci son premier chiffre significatif, il suffit de la diviser par 10<sup>4</sup>, 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.<br />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).<br />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 <code>itemconfigure()</code>. Nous utilisons donc cette méthode pour modifier l'option <code>fill</code> de chacune des bandes colorées, en utilisant les noms de couleur extraits de la liste <code>self.cc</code> grâce à aux trois indices <code>li[1]</code>, <code>li[2]</code> et <code>li[3]</code> qui contiennent les 3 chiffres correspondants.
 
{{Exercices}}
 
# Modifiez le script ci-dessus de telle manière que le fond d'image devienne bleu clair (<code>'light blue'</code>), que le corps de la résistance devienne beige (<code>'beige'</code>), 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.
{{fin}}
 
== « 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 à <code>self</code>. 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.
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<ref>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).
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.
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. </ref>. 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 {{todo}}). 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.
 
{{image manquante}}
 
;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.
*La classe <code>Application()</code> 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 <code>Wagon()</code>, indépendante, permettra d'instancier dans le canevas 4 objets-wagons similaires, dotés chacun d'une méthode <code>perso()</code>. Celle-ci sera destinée à provoquer l'apparition d'un petit personnage à l'une quelconque des trois fenêtres du wagon. L'application principale invoquera cette méthode différemment pour différents objets-wagons, afin de faire apparaître un choix de quelques personnages.
 
;Implémentation
 
<pre>
1.from Tkinter import *
2.
Ligne 195 ⟶ 222 :
53.app = Application()
54.app.mainloop()
</pre>
 
;Commentaires
 
* Lignes 3 à 5 : Nous projetons de dessiner une série de petits cercles. Cette petite fonction nous facilitera le travail en nous permettant de définir ces cercles à partir de leur centre et leur rayon.
Lignes 7 à 13 : La classe principale de notre application est construite par dérivation de la classe de fenêtres Tk() importée du module Tkinter.2 Comme nous l'avons expliqué au chapitre précédent, le constructeur d'une classe dérivée doit activer lui-même le constructeur de la classe parente, en lui transmettant la référence de l'instance comme premier argument.
Les lignes 10 à 13 servent à mettre en place le canevas et les boutons.
Lignes 15 à 20 : Ces lignes instancient les 4 objets-wagons, produits à partir de la classe correspondante. Ceci pourrait être programmé plus élégamment à l'aide d'une boucle et d'une liste, mais nous le laissons ainsi afin de ne pas alourdir inutilement les explications qui suivent.
Nous voulons placer nos objets-wagons dans le canevas, à des emplacements bien précis : il nous faut donc transmettre quelques informations au constructeur de ces objets : au moins la référence du canevas, ainsi que les coordonnées souhaitées. Ces considérations nous font également entrevoir, que lorsque nous définirons la classe Wagon() un peu plus loin, nous devrons associer à sa méthode constructeur un nombre égal de paramètres pour réceptionner ces arguments.
Lignes 22 à 27 : Cette méthode est invoquée lorsque l'on actionne le second bouton. Elle invoque elle-même la méthode perso() de certains objets-wagons, avec des arguments différents, afin de faire apparaître les personnages aux fenêtres indiquées. Ces quelques lignes de code vous montrent donc comment un objet peut communiquer avec un autre en faisant appel à l'une ou l'autre de ses méthodes. Il s'agit là du mécanisme central de la programmation par objets :
 
* Lignes 7 à 13 : La classe principale de notre application est construite par dérivation de la classe de fenêtres <code>Tk()</code> importée du module Tkinter<ref>Nous verrons plus loin que ''Tkinter'' autorise également de construire la fenêtre principale d'une application par dérivation d'une classe de ''widget'' (le plus souvent, il s'agira d'un ''widget'' <code>Frame()</code>). La fenêtre englobant ce ''widget'' sera automatiquement ajoutée. (Voir page {{todo}}).</ref>. 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.
Les objets sont des entités programmées qui s'échangent des messages et interagissent par l'intermédiaire de leurs méthodes.
 
* Lignes 15 à 20 : Ces lignes instancient les 4 objets-wagons, produits à partir de la classe correspondante. Ceci pourrait être programmé plus élégamment à l'aide d'une boucle et d'une liste, mais nous le laissons ainsi afin de ne pas alourdir inutilement les explications qui suivent. Nous voulons placer nos objets-wagons dans le canevas, à des emplacements bien précis : il nous faut donc transmettre quelques informations au constructeur de ces objets : au moins la référence du canevas, ainsi que les coordonnées souhaitées. Ces considérations nous font également entrevoir, que lorsque nous définirons la classe <code>Wagon()</code> un peu plus loin, nous devrons associer à sa méthode constructeur un nombre égal de paramètres pour réceptionner ces arguments.
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. (Pouvez-vous ajouter un correctif ?)
<ul>
Lignes 29-30 : La classe Wagon() ne dérive d'aucune autre classe préexistante. Cependant, étant donné qu'il s'agit d'une classe d'objets graphiques, nous devons munir sa méthode constructeur de paramètres, afin de recevoir la référence du canevas auquel les dessins sont destinés, ainsi que les coordonnées de départ de ces dessins. Dans vos expérimentations éventuelles autour de cet exercice, vous pourriez bien évidemment ajouter encore d'autres paramètres : taille du dessin, orientation, couleur, vitesse, etc.
<li>Lignes 22 à 27 : Cette méthode est invoquée lorsque l'on actionne le second bouton. Elle invoque elle-même la méthode <code>perso()</code> de certains objets-wagons, avec des arguments différents, afin de faire apparaître les personnages aux fenêtres indiquées.
Lignes 31 à 51 : Ces instructions ne nécessitent guère de commentaires. La méthode perso() est dotée d'un paramètre qui indique celle des 3 fenêtres où il faut faire apparaître un petit personnage. Ici aussi nous n'avons pas prévu de garde-fou : vous pouvez invoquer cette méthode avec un argument égal à 4 ou 5, par exemple, ce qui produira des effets incorrects.
Lignes 53-54 : Pour démarrer l'application, il ne suffit pas d'instancier un objet de la classe Application() comme dans l'exemple de la rubrique précédente. Il faut également invoquer la méthode mainloop() qu'elle a hérité de sa classe parente. Vous pourriez cependant condenser ces deux instructions en une seule, laquelle serait alors : Application().mainloop()
 
{{remarque|Ces quelques lignes de code vous montrent donc comment un objet peut communiquer avec un autre en faisant appel à l'une ou l'autre de ses méthodes. Il s'agit là du mécanisme central de la programmation par objets : les objets sont des entités programmées qui s'échangent des messages et interagissent par l'intermédiaire de leurs méthodes.}}
Exercice :
 
1.6.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()).
Idéalement, la méthode <code>coucou()</code> devrait comporter quelques instructions complémentaires, lesquelles vérifieraient d'abord si les objets-wagons concernés existent bel et bien, avant d'autoriser l'activation d'une de leurs méthodes. Nous n'avons pas inclus ce genre de garde-fou afin que l'exemple reste aussi simple que possible, mais cela entraîne la conséquence que vous ne pouvez pas actionner le second bouton avant le premier. (Pouvez-vous ajouter un correctif ?)</li>
À 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.
* Lignes 29-30 : La classe <code>Wagon()</code> ne dérive d'aucune autre classe préexistante. Cependant, étant donné qu'il s'agit d'une classe d'objets graphiques, nous devons munir sa méthode constructeur de paramètres, afin de recevoir la référence du canevas auquel les dessins sont destinés, ainsi que les coordonnées de départ de ces dessins. Dans vos expérimentations éventuelles autour de cet exercice, vous pourriez bien évidemment ajouter encore d'autres paramètres : taille du dessin, orientation, couleur, vitesse, etc.
 
* Lignes 31 à 51 : Ces instructions ne nécessitent guère de commentaires. La méthode <code>perso()</code> est dotée d'un paramètre qui indique celle des 3 fenêtres où il faut faire apparaître un petit personnage. Ici aussi nous n'avons pas prévu de garde-fou : vous pouvez invoquer cette méthode avec un argument égal à 4 ou 5, par exemple, ce qui produira des effets incorrects.
 
* Lignes 53-54 : Pour démarrer l'application, il ne suffit pas d'instancier un objet de la classe <code>Application()</code> comme dans l'exemple de la rubrique précédente. Il faut également invoquer la méthode <code>mainloop()</code> qu'elle a hérité de sa classe parente. Vous pourriez cependant condenser ces deux instructions en une seule, laquelle serait alors : <code>Application().mainloop()</code>
 
{{Exercices}}
<ol>
<li>Perfectionnez le script décrit ci-dessus, en ajoutant un paramètre couleur au constructeur de la classe <code>Wagon()</code>, 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 <code>cercle()</code>).
 
À cette même classe <code>Wagon()</code>, ajoutez encore une méthode <code>allumer()</code>, 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 <code>cercle()</code> pour teinter le visage des petits personnages en rose (<code>pink</code>), leurs yeux et leurs bouches en noir, et instanciez les objets-wagons avec des couleurs différentes.</li>
</ol>
{{fin}}
 
== « OscilloGraphe » : un widget personnalisé ==
 
Le projet qui suit va nous entraîner encore un petit peu plus loin. Nous allons y construire une nouvelle classe de widget, qu'il sera possible d'intégrer dans nos projets futurs comme n'importe quel widget standard. Comme la classe principale de l'exercice précédent, cette nouvelle classe sera construite par dérivation d'une classe ''Tkinter'' préexistante.
 
Le sujet concret de cette application nous est inspiré par le cours de physique. Pour rappel :
Un mouvement vibratoire harmonique se définit comme étant la projection d'un mouvement circulaire uniforme sur une droite. Les positions successives d'un mobile qui effectue ce type de mouvement sont traditionnellement repérées par rapport à une position centrale : on les appelle alors des élongations. L'équation qui décrit l'évolution de l'élongation d'un tel mobile au cours du temps est toujours de la forme, dans laquelle e représente l'élongation du mobile à tout instant t . Les constantes A, f et  désignent respectivement l'amplitude, la fréquence et la phase du mouvement vibratoire.
 
''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 manquante}}, 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  .
{{image manquante}}
 
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 <code>oscillo.py </code>. 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).
 
<pre>
1.from Tkinter import *
2.from math import sin, pi
Ligne 269 ⟶ 312 :
40. gra.traceCourbe(2, 1.2, 10, 'purple')
41. root.mainloop()
</pre>
Le niveau principal du script est constitué par les lignes 35 à 41. Comme nous l'avons déjà expliqué à la page 173{{todo}}, les lignes de code situées après l'instruction <code>if __name__ == '__main__': </code> 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.
 
Ligne 277 ⟶ 323 :
 
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 :
Ouvrez une fenêtre de terminal (''Python shell''), et entrez les instructions ci-dessous directement à la ligne de commande :
 
<pre>
>>> from oscillo import *
>>> g1 = OscilloGraphe()
>>> g1.pack()
</pre>
 
Après importation des classes du module <code>oscillo</code>, nous instancions un premier objet <code>g1 </code>, de la classe <code>OscilloGraphe()</code>.
 
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 !
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 !
 
{{image manquante}}
 
<pre>
>>> g2 = OscilloGraphe(haut=200, larg=250)
>>> g2.pack()
>>> g2.traceCourbe()
</pre>
 
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  .
 
Ensuite, nous activons la méthode <code>traceCourbe()</code> 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 ''φ''.
 
<pre>
>>> g3 = OscilloGraphe(larg=220)
>>> g3.configure(bg='white', bd=3, relief=SUNKEN)
Ligne 296 ⟶ 354 :
>>> g3.traceCourbe(phase=1.57, coul='purple')
>>> g3.traceCourbe(phase=3.14, coul='dark green')
</pre>
 
Pour comprendre la configuration de ce troisième ''widget'', il faut nous rappeler que la classe <code>OscilloGraphe()</code> a été construite par dérivation de la classe <code>Canvas()</code>. Elle hérite donc de toutes les propriétés de celle-ci, ce qui nous permet de choisir la couleur de fond, la bordure, etc., en utilisant les mêmes arguments que ceux qui sont à notre disposition lorsque nous configurons un canevas.
Nous faisons ensuite apparaître deux tracés successifs, en faisant appel deux fois à la méthode traceCourbe(), à laquelle nous fournissons des arguments pour la phase et la couleur.
 
Nous faisons ensuite apparaître deux tracés successifs, en faisant appel deux fois à la méthode <code>traceCourbe()</code>, à laquelle nous fournissons des arguments pour la phase et la couleur.
Exercice :
 
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.
{{Exercices}}
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).
# 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.
{{fin}}
 
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 {{todo}}).
 
;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.
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).
 
Une méthode <code>traceCourbe()</code> est associée à ce ''widget''. Elle provoque le tracé du graphique élongation/temps pour un mouvement vibratoire dont on fournit la fréquence (entre 0.25 et 10 Hz), la phase (entre 0 et 2π radians) et l'amplitude (entre 1 et 10 ; échelle arbitraire).
 
;Implémentation