Le transcript ci-dessous correspond au texte que vous avez entendu dans la vidéo de ce chapitre.
Bonjour,
Comme promis, nous allons maintenant améliorer le programme de la Pythonnerie précédente. Cette amélioration va se faire en plusieurs temps. Nous allons tout d'abord corriger des problèmes visuels, améliorer un tout petit peu l'ergonomie, réorganiser le code et rendre le programme davantage robuste.
En fait, on peut avoir quelques soucis avec le programme de quizz tel qu'il est écrit à présent. Pour les mettre en évidence, j'ai ajouté à ma liste d'auteurs Edgar Poe, et François-René de Chateaubriand. Et j'ai légèrement trafiqué le programme pour que le tirage des questions ne soit plus aléatoire.
Supposons donc que le premier nom à surgir soit celui de Poe. Je rentre son prénom, et malheur, quand Chateaubriand apparaît – et bien heureusement que je sais que c'est Chateaubriand. Le problème est facile à comprendre : je vous ai dit que quand, dans la méthode
init
, j'appelais la méthode
SetSizerAndFit
, la fenêtre ajustait automatiquement sa taille au contenu. Et le contenu initial était un nom très court. Du coup, la fenêtre se trouve sous-dimensionnée dès que l'on a un nom ; même de longueur moyenne.
Je pourrais imaginer appeler des méthodes (il y en a) pour adapter la taille de la fenêtre à chaque question. Le problème est que la fenêtre passerait son temps à se redimensionner, ce qui ne serait pas très plaisant à utiliser. Il y a une autre solution. Quand je charge mon fichier, je lis toutes les questions et toutes les réponses. Je peux repérer à ce moment là les valeurs les plus longues, et essayer de dimensionner les éléments graphiques dès le départ pour que ces valeurs puissent y tenir sans déborder.
Mais qu’est-ce que c'est la valeur la plus longue ? Nous sommes en environnement graphique, et en environnement graphique on travaille généralement avec des polices variables. J'ai ici 10 i, qui occupent une certaine largeur. Il ne me faut pas beaucoup de m pour avoir une suite de caractères qui occupe davantage de place. Compter le nombre de caractères ne pourra donc donner qu'une idée approximative de la taille. Mais il y a plus ennuyeux. Connaissant le nombre de caractères, comment en déduire une taille qui dans un environnement graphique se calculera en pixels ? Parce qu'en fait, la largeur réellement occupée, c'est avant tout une question de police.
Ce que je vous propose pour régler cette question est un mécanisme relativement simple. Tout d'abord, à la lecture de mon fichier, je vais identifier les longueurs les plus grandes, tant pour les questions que pour les réponses. Je vais me constituer un pseudo-texte de cette longueur maximale, uniquement constitué de x – une lettre de taille moyenne (si j’étais paranoïaque je pourrais employer m ou w – ou je pourrais raffiner en mettant une majuscule. Après, c’est une question d’ajustement). Puis, et je vais faire tout cela pendant que je suis toujours à m'activer en coulisses, avant même de montrer quoi que ce soit à l'écran, je vais mettre ce pseudo-texte représentant ce que je peux avoir de plus long dans un élément graphique ; je vais ensuite utiliser une méthode qui me permet de connaître la taille d'un élément graphique, puis une seconde méthode qui permet de fixer la taille. Je vais donc fixer la taille au plus grand, et quand je mettrai dans l’élément une « vraie » valeur le tour sera joué.
Je vais donc, au début de ma méthode de chargement, mettre à zéro la plus grande longueur rencontrée pour les questions et pour les réponses. Je vais lire mon fichier, et quand j’ajoute quelque chose à ma liste de questions, je contrôle la longueur de la question, et si elle est plus grande que la plus grande valeur rencontrée jusqu’à présent, je la mémorise à la place. Je fais la même chose avec les réponses.
A la fin, je mets à la place de la question et de la réponse avec la méthode
SetLabel
le nombre de x qui va bien (je vous rappelle que multiplier une lettre par un nombre revient à la répéter ce nombre de fois). Puis j'appelle la méthode de redimensionnement que nous allons voir tout de suite. J'attire tout de même votre attention sur le fait que, du coup, la zone de réponse a elle aussi besoin de devenir un attribut de la classe.
Voyons donc cette méthode de redimensionnement. Les éléments graphiques ont une méthode
GetSizeTuple()
qui retourne deux valeurs à la fois - une largeur et une hauteur - dans cet ordre. Pour récupérer deux valeurs à la fois, on écrit comme ici variable1 virgule variable2 égale – appel de la méthode. La méthode
SetSizeHints
permet de spécifier la taille que l'on veut, en précisant des valeurs minimales et des valeurs maximales. Ici je mets les mêmes valeurs dans les deux cas. Après cela, je recalcule la taille des boîtes en appelant leur méthode
LayOut
- mise-en-page, si vous voulez. Évidemment, du coup, ces deux boites doivent être des attributs de la classe pour que je puisse les voir dans cette méthode. Puis j'appelle la méthode
Fit()
de la fenêtre principale pour ajuster la taille globale de la fenêtre. Si vous vous rappelez, dans les pythonneries précédentes, j'appelais
SetSizerAndFit
qui faisait deux opérations à la fois – accrocher la disposition, puis ajuster la taille. Ici, la disposition est déjà raccrochée, donc je n'appelle qu’une méthode
Fit()
, je recentre parce qu'avec tout ça j'ai toutes les chances d'être décalé, et enfin je déclenche l’événement comme quoi la taille a changé. C'est le même événement que l'on déclenche quand on redimensionne la fenêtre. En fait, c'est surtout pour le gestionnaire de fenêtre, qui fera par exemple se redessiner ce qui était caché par la fenêtre avant et ne l'est plus maintenant si sa taille a réduit. Et finalement, j’appelle la méthode
Refresh()
qui redessine, dans la fenêtre même, ce qui a besoin d'être redessiné.
Quand ma fenêtre est dimensionnée pour que même la question la plus longue y tienne, je pourrai tirer une nouvelle question et cela va être la fonction de la méthode nouvelle_question qui fait ce que nous connaissons déjà, à savoir piocher une nouvelle question, j'incrémente ici mon compteur de questions, c'est plus logique, je la mets dans l'objet graphique question, puis j'efface la réponse précédente de l’utilisateur. Je vais quand même ajouter une touche nouvelle, l'appel à la méthode
SetFocus()
de l'objet réponse utilisateur. Cette méthode rend cet objet actif et y positionne le curseur. Elle a le même effet que si l'utilisateur avait cliqué dans la zone d’entrée pour pouvoir fournir sa réponse.
Vous comprendrez, je pense, que l'introduction de ces méthodes va demander une certaine refonte de la méthode
init
. Les conseils que donnait aux apprentis poètes, il y a de cela plus de trois siècles, Nicolas Boileau dans son « Art Poétique » sont toujours en grande partie valables pour les développeurs informatiques. Et s'il ne faut pas tomber dans le perfectionnisme, il faut construire sur des bases saines qui permettront de faire évoluer le programme. Je vais donc, rapidement, restructurer la méthode
init
. Tout d'abord, les attributs qui ne correspondent pas à des éléments graphiques, les compteurs et le tableau des questions. Ensuite les éléments graphiques, dont le seul qui n'ait pas besoin d'être vu par une autre méthode est le bouton OK.
Ensuite, mes dispositions qui doivent être des attributs puisqu'on y fait référence dans la méthode redimensionne. Ensuite, je range tous mes éléments qui dans la boite verticale, qui dans la boite horizontale, la boite horizontale dans la verticale, pour les disposer comme je veux. Après, j'accroche l'action verifier_reponse au bouton, et je vais apporter une légère amélioration en rattachant la même action à l’évènement EVT_TEXT_ENTER quand il est émis par l'objet reponse_utilisateur. Qu'est-ce que c'est que c’est événement ? C'est ce que produit le fait d'appuyer sur la touche « Entrée » quand on a tapé la réponse. En d’autres termes, appuyer sur « Entrée » aura le même effet que cliquer sur OK.
Ensuite une légère modification : j’attache la boite verticale à la fenêtre, mais uniquement avec
SetSizer()
, pas
SetSizerAndFit()
, puisque l'ajustement de la taille se fera après avoir chargé les données.
Et au final, je charge le fichier questions.txt puis je tire ma première question.
Pour terminer avec la restructuration du code je peux maintenant simplifier verifier_reponse en appelant nouvelle_question après avoir vérifié et changé le score. Le comptage des questions, rappel, se fait dans nouvelle_question.
Voilà, le code est plus propre et prêt à supporter des évolutions. Mais ne peut-on rendre le programme plus robuste ? Si vous vous rappelez la Pythonnerie n°9, j’avais introduit les exceptions dans le cadre d’un fichier de questions absent. Ce n’est pas parce que l'on est en environnement graphique que les mêmes problèmes ne se produiront pas ! Voyons ce qui se passe si je renomme mon fichier. Si sous Windows je double-clique sur le .pyw, cela va donner à peu près cela... Bon, je lis vite, mais il y a des limites. En fait, si je lance le programme depuis Idle, la fenêtre va rester affichée un peu plus longtemps, et voici ce qu’elle contient. On retrouve le même genre de message mystérieux qu’en mode console, dans une fenêtre qui s’ouvre pour la circonstance.
Nous allons, comme précédemment, attraper l’exception au vol, mais mettre le message dans une fenêtre comme vous en avez probablement déjà vu beaucoup, une fenêtre qui demande de cliquer sur un bouton pour continuer.
Je vais donc commencer par modifier ma méthode chargement et mettre dans un bloc
try
la lecture du fichier et tout ce qui en découle.
Attaquons nous à la partie intéressante, la partie
except
dans laquelle je n’attrape que les erreurs auxquelles je m’attends, à savoir IOError, dont je récupère le numéro de message, que je n’utiliserai pas, et le texte du message d’erreur.
Je vais créer un objet de la classe
MessageDialog
, qui est sensiblement différent des autres éléments graphiques sur le plan des paramètres qu’il attend à la création. Toujours
self
pour indiquer la fenêtre parente, mais ensuite le texte qui doit apparaître dans le fenêtre, le titre à lui donner dans le bandeau, puis le paramètre don le nom est « style » et qui spécifie ce à quoi ressemblera la fenêtre.
J’appelle ensuite la méthode
ShowModal()
de l’objet que je viens de créer, ce qui a pour effet de l'afficher. On parle de « fenêtre modale » quand on a une fenêtre qui bloque tout le reste tant que l'on n'a pas cliqué sur ses boutons ou qu'on ne l'a pas fermée. Tant qu’elle est active, le reste de l'application est désactivé.
Cette méthode retourne soit
wx.ID_OK
(à ne pas confondre avec
wx.OK
, qui est le style) si je clique sur le bouton, soit
wx.ID_CANCEL
si je la ferme sans cliquer sur le bouton. Dans la mesure où tout ce qui m'importe c’est que l'utilisateur ait vu le message, inutile de vérifier cette réponse.
Un petit panorama très rapide des boîtes de message. Ce que nous venons juste de voir, c'est la boîte d'erreur classique : le texte, le titre, le bouton, et l'icône facultative dont l'apparence peut varier suivant le système sur lequel l’application tourne. Ce genre de boîte est purement informatif (il y a également une icône
wx.ICON_INFORMATION
si vous voulez être moins alarmant).
On peut également avoir une boîte de message pour demander une décision à l'utilisateur – le message, le titre, un style
wx.YES_NO
correspondant à deux boutons, combiné à une icône interrogative. Deux choses importantes à remarquer : d'une part, le bouton de fermeture de la fenêtre est désactivé. Ce sera oui ou non, il n'y a pas d'échappatoire possible. L'autre chose, c'est que, bien entendu, il me faut récupérer cette fois ce sur quoi l'utilisateur aura cliqué, et j'obtiendrai
wx.ID_YES
ou
wx.ID_NO
suivant le cas.
Enfin, je peux aussi avoir les deux boutons (ou simplement un bouton OK) combinés avec
wx.CANCEL
pour ajouter un bouton d'annulation en cas de fausse manœuvre. Une icône différente, et surtout ce coup-ci je peux fermer la fenêtre sans cliquer sur un bouton, ce qui sera identique en fait à cliquer sur le bouton d’annulation et retournera
wx.ID_CANCEL
. Il va sans dire que là aussi je veux contrôler la réponse.
Retournons à notre méthode chargement. Quand je clique sur OK, la fenêtre disparaît mais en fait elle est toujours là, invisible – elle ne peut pas disparaître tout à fait et retourner une valeur. C’est la raison pour laquelle il faut la détruire explicitement en appelant sa méthode
Destroy()
. Après quoi, très important, je lance de nouveau l'exception. Pourquoi cela ? Eh bien, normalement, après le chargement, je fais plein de choses, à commencer par tirer une question, ce qui, évidemment, risque de s’avérer problématique si je n'ai pas trouvé le fichier. Comme j'ai attrapé l’exception, pour Python cela devient en quelque sorte une affaire classée et pour lui tout va bien quand je retourne de la méthode. Relancer l’exception est ma manière de dire à la méthode
init
que tout n'a pas marché comme sur des roulettes.
Du coup je modifie
init
, je mets chargement et ce qui suit dans un bloc
try
, et si j'ai une exception, sachant que l’utilisateur a vu le message d’erreur, la fenêtre se détruit elle-même et l’application se termine. Nous n’en avons pas encore terminé avec ce programme, mais ce sera pour la prochaine fois.