.. _gui: Interfaces graphiques ===================== Selon `Wikipedia `_, *une interface graphique (en anglais GUI pour graphical user interface) ou environnement graphique est un dispositif de dialogue homme-machine*. Le dialogue s'effectue grâce à : - un écran qui affiche ou recueille l'information dans des fenêtres à travers des composants programmables (widgets) ; - et des dispositifs d'entrée-sortie (clavier, souris, trackpad, joystick, etc...) qui permettent à l'humain de fournir cette information à la machine. Le développement d'interfaces graphiques peut se décomposer en 2 étapes principales : - positionner les widgets nécessaires dans la fenêtre graphique ; - puis paramétrer ces widgets pour les associer à une action spécifique lorsqu'ils sont activés (clic sur un bouton par exemple). Ces opérations sont conduites en utilisant une bibliothèque graphique qui fournit l'environnement nécessaire à la programmation. Il existe de nombreuses bibliothèques graphiques. Pour ce cours, nous travaillerons avec la bibliothèque `Tcl/Tk `_ interfacée avec Python via le module :mod:`tkinter`. L'interfaçage permet d'utiliser des objets Python qui se chargent de produire les commandes Tcl/Tk de plus bas niveau. Installation ------------ Pour Windows et MacOS, les installations officielles de Python incluent le module :mod:`tkinter` et aucune autre opération n'est nécessaire. Pour Linux/Ubuntu, pour économiser de la mémoire, :mod:`tkinter` n'est pas installé par défaut. Un package additionnel doit être installé :: $ sudo apt-get install python3-tk Quel que soit le système d'exploitation, si l'installation est opérationnelle, l'import de :mod:`tkinter` doit se passer sans erreur:: $ python Python 3.9.5 (tags/v3.9.5:0a7dcbd, May 3 2021, 17:27:52) [MSC v.1928 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> import tkinter >>> L'exécution de la commande suivante doit provoquer l'apparition d'une fenêtre minimale permettant également de valider l'installation:: $ python -m tkinter Une première fenêtre -------------------- Une fois tkinter opérationnel, on crée une fenêtre graphique : #. en important le package ; #. en instanciant la classe :class:`tkinter.Tk`. Dans un interpréteur interactif : >>> import tkinter as tk >>> window = tk.Tk() Une fenêtre vide apparait à l'écran. | .. image:: ../images/13-first-window.png :align: center | Pour le moment, une telle fenêtre ne dispose que des interactions avec le système d'exploitation (minimiser, agrandir, fermer) et n'est donc pas très utile. Dans ce qui suit, on va ajouter des widgets permettant de la doter de capacités supplémentaires. Les widgets ----------- L'élément principal de l'interface graphique (GUI) est la fenêtre (window) construite à l'étape précédente. Pour ajouter des fonctionnalités à cette fenêtre, Tkinter dispose de nombreux widgets permettant de mettre en oeuvre des interactions avec l'utilisateur. La fenêtre est donc un container hébergeant l'ensemble des éléments graphiques (widgets) qui composent la GUI. Les principaux widgets sont : .. list-table:: Les principaux widgets :widths: 25 50 :header-rows: 1 * - Widget Class - Description * - Label - Utilisé pour afficher du texte ou des images * - Button - peut être associé à une action lorsqu'il est cliqué * - Entry - permet à l'utilisateur d'entrer une ligne de texte * - Text - permet à l'utilisateur d'entrer plusieurs lignes de texte * - Frame - un container rectangulaire permettant de grouper des widgets .. note:: Les widgets de Tkinter sont parfaitement fonctionnels mais ont un look un peu daté. Les widgets :mod:`tkinter.ttk` sont plus modernes mais pour des raisons de simplicité, ils ne seront pas utilisés ici. Cependant, à la fin du cours, vous disposerez des connaissances nécessaires pour les mettre en oeuvre sans difficulté. Chaque widget dispose de nombreuses propriétés permettant de définir leur apparence graphique ou leur comportement. Nous illustrerons ça dans les paragraphes qui suivent. Le widget :class:`Label` ------------------------ Un :class:`Label` est utilisé pour afficher du texte et des images. L'ajout d'un :class:`Label` se fait en deux temps : - on crée une instance de la classe ; - et on l'ajoute à la fenêtre graphique avec un gestionnaire graphique. Ici on utilise la méthode `pack() `_ (on verra d'autres gestionnaires graphiques ultérieurement). Ceci se fait avec les deux instructions suivantes :: >>> label = tk.Label(text="Label") >>> label.pack() .. note:: La construction d'un widget est une condition nécessaire mais pas suffisante pour le faire apparaître à l'écran. Il faut que celui ci soit positionné avec un gestionnaire graphique. Un des problèmes importants de la création de GUI est le dimensionnement des window et des widget. Ici un dimensionnement par défaut a été utilisé, consistant à adapter la taille de la fenêtre au widget qu'elle contient. .. image:: ../images/13-label.png Le paradigme événementiel ------------------------- La construction de GUI utilise le paradigme événementiel qui scrute indéfiniment les évènements liés à la fenêtre (clic sur un bouton, appui sur une touche du clavier, fermeture de la fenêtre, etc.) pour déclencher l'action liée à cet évènement. .. important:: Le paradigme événementiel est très différent du paradigme impératif ou objet utilisés jusque là. La boucle de scrutation est interne à :mod:`tkinter`. Ceci dispense l'utilisateur d'écrire sa propre boucle ``while`` : - dans un interpréteur interactif, la scrutation est déclenchée automatiquement. C'est cette scrutation qui fait que l'on peut fermer la fenêtre avec l'icone en haut à droite (interaction avec le système d'exploitation). - si le code est stocké dans un fichier, on rentre dans la scrutation des évènements en appelant explicitement la méthode :meth:`mainloop`. Si cette méthode n'est pas appelée, la fenêtre n'apparait pas à l'écran. .. note:: Pour le moment, la seule façon de mettre fin à la boucle de scrutation est de fermer la fenêtre . Le code complet est stocké dans le fichier ci dessous. .. code-block:: python :linenos: # first_window.py import tkinter as tk window = tk.Tk() label = tk.Label(text="Hello world !") label.pack() window.mainloop() On dispose maintenant d'une structure minimale qui : - crée une fenêtre principale (ligne 5); - lui ajoute un widget (lignes 7 et 8); - et démarre la scrutation d'évènements (ligne 10). Paramétrer un :class:`Label` ------------------------------------- Un `Label ` dispose de nombreuses propriétés. .. note:: La documentation décrit l'ensemble des propriétés du widget mais les exemples utilisent le langage de script `Tcl `_. La transposition en Python est immédiate. - Tcl : :code:`label .t -text "Hello World !" -bg red` ; - Python : :code:`Label(text="Hello world !", bg="red")`. Certaines propriétés sont communes à plusieurs widgets : - ``anchor`` : position du texte [n/ne/e/se/s/sw/w/nw/center] - ``background`` or ``bg`` : couleur de fond - ``borderwidth`` or ``bd`` : largeur de la bordure - ``font`` : police de caractères - ``foreground`` or ``fg`` : couleur du texte - ``image`` : affichage d'une image - ``padx`` : espace additionnel en X - ``pady`` : espace additionnel en Y - ``text`` : texte à afficher D'autres sont spécifiques au :class:`tk.Label` : - ``height`` : hauteur - ``state`` : état (normal/active/disabled) - ``width`` : largeur .. note:: Les `couleurs `_` sont définies par leur nom ou par leur code hexadecimal RGB:: hello = tk.Label(text="Hello world !", bg="aquamarine4") hello = tk.Label(text="Hello world !", bg="#458B74") Pour le choix des couleurs, on peut travailler avec des `palettes `_ prédéterminées, ou utiliser une `Color Wheel `_ pour plus de flexibilité. .. tip:: Les propriétés ``width`` et ``height`` sont spécifiées en unités de texte. La largeur et la hauteur du caractère ``0`` sont utilisées comme références. Comme ce caractère est plus haut que large, définir une largeur et une hauteur identique ne conduit pas à un widget carré ! Le widget :class:`Button` ------------------------- Un `Button `_ est un widget cliquable, permettant une interaction avec l'utilisateur. Quelques propriétés (communes à d'autres widgets, dont le :class:`Label`): - ``anchor`` : position du texte [n/ne/e/se/s/sw/w/nw/center] - ``background`` or ``bg`` : couleur de fond - ``borderwidth`` or ``bd`` : largeur de la bordure - ``font`` : police de caractères - ``foreground`` or ``fg`` : couleur du texte - ``image`` : affichage d'une image - ``padx`` : espace additionnel en X - ``pady`` : espace additionnel en Y - ``text`` : texte à afficher Quelques propriétés spécifiques : - ``command`` : la fonction associée au bouton - ``default`` : normal / active / disabled - ``height`` : hauteur - ``state`` : état (normal/active/disabled) - ``width`` : largeur Le widget :class:`Entry` --------------------------------- Un `Entry ` est un widget permettant à l'utilisateur d'entrer un texte court. Quelques propriétés (communes à d'autres widgets): - ``background`` or ``bg`` : couleur de fond ; - ``borderwidth`` or ``bd`` : largeur de la bordure ; - ``font`` : police de caractères ; - ``foreground`` or ``fg`` : couleur du texte. Quelques propriétés spécifiques : - ``state`` : état (normal/active/disabled) - ``width`` : largeur Le widget :class:`Entry` possède également des méthodes permettant d'interagir avec le contenu : - la méthode :meth:`~tk.Entry.get` permet de récupérer le texte entré par l'utilisateur ; - la méthode :meth:`~tk.Entry.delete` permet de supprimer le texte ; - et la méthode :meth:`~tk.Entry.insert` permet d'insérer du texte. Le widget :class:`Frame` ------------------------ Une `Frame ` est un container contenant d'autres widgets. Son rôle est de structurer (et de simplifier) l'affichage en manipulant un ensemble de widgets à travers la :class:`Frame` dans laquelle ils sont placés. La fenêtre principale de l'application est également un container et se comporte donc comme une :class:`Frame`. Une :class:`Frame` possède un gestionnaire graphique, dont le rôle est de positionner les widgets selon un algorithme qui dépend de la méthode employée. La méthode :meth:`pack` utilisée précédemment est un type de gestionnaire graphique. Quelques propriétés d'une :class:`Frame` : - ``background`` or ``bg`` : couleur de fond ; - ``borderwidth`` or ``bd`` : largeur de la bordure ; - ``cursor`` : le pointeur de souris utilisé lorsque la souris est à l’intérieur de la :class:`Frame` ; - ``height`` : la hauteur de la :class:`Frame` ; - ``padx`` : ajoute de l’espace supplémentaire horizontalement ; - ``pady`` : ajoute de l’espace supplémentaire verticalement ; - ``width`` : la largeur de la :class:`Frame` ; Les gestionnaires graphiques ---------------------------- La disposition des widgets dans une :class:`Frame` (ou dans la fenêtre principale s'il n'y a pas de :class:`Frame`) peut se faire suivant 3 méthodes: - :meth:`pack`; - :meth:`grid` ; - :meth:`place`; La méthode :meth:`pack` ....................... La méthode :meth:`pack` permet de positionner les widgets les uns **par rapport** aux autres et est très efficace lorsqu'on ne souhaite pas un contrôle strict sur le layout de l'application. Considérons le code ci dessous qui construit une fenêtre avec deux labels. .. code-block:: python :linenos: # 13-pack.py import tkinter as tk window = tk.Tk() window.title('La méthode pack()') window.geometry("350x200") label1 = tk.Label(window, text="Label 1", bg="green", fg="white") label2 = tk.Label(window, text="Label 2", bg="red", fg="white") label1.pack() label2.pack() window.mainloop() | .. image:: ../images/13-pack-1.png :align: center | Le gestionnaire :meth:`pack` place les labels dans l'ordre où il les rencontre. Il suffit d'intervertir les lignes 12 et 13 pour s'en convaincre. | .. image:: ../images/13-pack-2.png :align: center | On peut passer des arguments à la méthode :meth:`pack`, par exemple pour insérer un espacement à l'intérieur ou à l'extérieur des widgets. .. code-block:: python :linenos: :emphasize-lines: 12-13 # 13-pack.py import tkinter as tk window = tk.Tk() window.title('La méthode pack()') window.geometry("350x200") label1 = tk.Label(window, text="Label 1", bg="green", fg="white") label2 = tk.Label(window, text="Label 2", bg="red", fg="white") label1.pack(ipadx=10, ipady=10) label2.pack(padx=10, pady=10) window.mainloop() | .. image:: ../images/13-pack-3.png :align: center | Les principales propriétés de la méthode :meth:`pack` : - ``padx`` : ajoute de l’espace supplémentaire horizontalement (à l'extérieur du widget) ; - ``pady`` : ajoute de l’espace supplémentaire verticalement (à l'extérieur du widget) ; - ``ipadx`` : ajoute de l’espace supplémentaire horizontalement (à l'intérieur du widget) ; - ``ipady`` : ajoute de l’espace supplémentaire verticalement (à l'intérieur du widget) ; - ``fill`` : option de remplissage de l'espace alloué au widget. 3 valeurs possibles : - ``tkinter.X`` : remplissage selon l'axe des x ; - ``tkinter.Y`` : remplissage selon l'axe des y ; - ``tkinter.BOTH`` : remplissage selon les deux axes. - ``expand`` : alloue de l'espace supplémentaire au widget ; - ``anchor`` : positionne le widget dans l'espace alloué. Plusieurs valeurs possibles : - ``N`` : en haut ; - ``S`` : en bas ; - ``E`` : à droite ; - ``W`` : à gauche ; - ``NW`` : en haut à gauche ; - ``NE`` : en haut à droite ; - ``SE`` : en bas à droite ; - ``SW`` : en bas à gauche ; - ``CENTER`` : au centre. - ``side`` : positionne l'espace alloué au widget dans la :class:`Frame`. 4 valeurs possibles : - ``tkinter.TOP`` ; - ``tkinter.LEFT`` ; - ``tkinter.RIGHT`` ; - ``tkinter.BOTTOM``. .. admonition:: Exercice Combiner les propriétés de la méthode :meth:`pack` pour obtenir les figures ci dessous : .. image:: ../images/13-pack-4.png :align: center .. image:: ../images/13-pack-exercice-2.png :align: center Une première application ------------------------ Application ----------- Développer une application permettant d'afficher le nombre de clics effectué sur un bouton. Ajouter une fonctionnalité permettant de mesurer l'intervalle de temps entre deux clics. Application ----------- Développer une application calculette permettant d'afficher le résultat des 4 opérations élémentaires.