IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Apprendre Python et s'initier à la programmation

Partie 2 : Programmation avancée


précédentsommairesuivant

VI. Interface graphique

Tous les programmes que l'on a écrits jusqu'à présent fonctionnent en mode console. Dans ce chapitre, on va voir comment construire des interfaces graphiques permettant une tout autre interaction avec l'utilisateur. Pour cela, on va utiliser la bibliothèque Kivy qui permet de construire des interfaces modernes et adaptables. On va également voir la programmation évènementielle qui permet de programmer les interactions avec l'utilisateur. Enfin, le chapitre se termine par la réalisation de dessins et l'application de simples transformations.

VI-A. bibliothèque Kivy

Il existe de nombreuses bibliothèques pour réaliser des interfaces graphiques avec Python. Les principales différences qu'il y a entre ces bibliothèques sont le support de plusieurs systèmes d'exploitation (Linux, Windows, MacOS…) ainsi que les composants graphiques proposés (bouton, zone de texte, menu, liste déroulante…). Dans la bibliothèque de base fournie avec Python, on retrouve la bibliothèque graphique Tk, très basique et pas forcément esthétique. Dans les autres choix majeurs, portables sur les principaux systèmes d'exploitation, on retrouve PyQt et WxPython. Dans ce livre, on va utiliser la bibliothèque Kivy (la bibliothèque Kivy peut être téléchargée sur son site officiel : https://kivy.org/), open source, portable, moderne et qui tourne également sur Android et iOS.

VI-A-1. Hello World!

Voyons immédiatement comment créer un premier programme avec une interface graphique à l'aide de la bibliothèque Kivy. Pour ne pas changer, on va simplement créer un Hello World. Le programme suivant affiche une fenêtre qui contient la phrase « Hello World! » :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
from kivy.app import App
from kivy.uix.label import Label

class HelloApp(App):
    def build(self):
        return Label(text='Hello World!', font_size='100sp')

HelloApp().run()

L'exécution de ce programme crée une nouvelle fenêtre qui ne contient qu'une zone de texte non éditable avec la phrase « Hello World! », en taille kitxmlcodeinlinelatexdvp100finkitxmlcodeinlinelatexdvp pixels, comme on peut le voir sur la figure 1.

Image non disponible
Figure 1. La bibliothèque Kivy permet de créer des programmes basés sur une interface graphique, comme celle-ci qui montre la phrase « Hello World! » dans une zone de texte non éditable.

Une interface graphique se définit à l'aide d'une nouvelle classe de type App, comme on l'a indiqué entre parenthèses après le nom de la classe. On a déjà vu cette construction précédemment, pour définir ses propres exceptions. Dans cette classe, il faut, au minimum, définir une méthode build qui va construire, et renvoyer, les composants à placer dans la fenêtre. Dans notre cas, on va se limiter à un composant de type Label, c'est-à-dire une zone de texte non éditable. Enfin, pour lancer le programme, il suffit de créer une instance de la classe qui a été définie, et d'appeler run.

Cette méthode run, spécifique aux objets de type App, initialise toute une série de choses permettant d'obtenir une interface graphique. Elle s'occupe notamment de mettre en place l'interaction avec le hardware de l'écran, de discuter avec les dispositifs d'entrée/sortie tels que l'écran multitouch, le clavier, l'accéléromètre, etc. et enfin, elle planifie plusieurs tâches à exécuter de manière régulière.

VI-A-1-a. Fenêtre

Le principal élément d'une application qui possède une interface graphique est la fenêtre. Par défaut, une application Kivy se charge de créer une fenêtre, dans laquelle les composants créés par la méthode build seront placés.

On peut facilement personnaliser quelques caractéristiques de cette fenêtre. Tout d'abord, on peut modifier son titre et l'icône associée à la fenêtre et à l'application à l'aide des variables d'instance title et icon, à modifier dans la méthode build. On peut également modifier la taille, en pixels, de la fenêtre en configurant deux options. L'exemple suivant modifie ces trois éléments :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
from kivy.app import App
from kivy.config import Config 
from kivy.uix.button import Label

class HelloApp(App):
    def build(self):
        self.title = 'Hello World!'
        self.icon = 'hello.png'
        return Label(text='Hello World!')

Config.set('graphics', 'width', '300') 
Config.set('graphics', 'height', '150')

HelloApp().run()

La modification de la taille de la fenêtre passe donc par une fonction dans la classe Config du module kivy.config et elle doit être faite avant le lancement de l'application.

VI-A-1-b. Composant graphique

Une interface graphique est construite à partir de composants que l'on place sur une fenêtre. On retrouve plusieurs types de composants tels que des zones de texte éditables ou non, des boutons, des listes, des cases à cocher, etc. Les composants sont placés les uns dans les autres, permettant ainsi de les organiser visuellement sur la fenêtre. Par conséquent, chaque composant possède un composant parent. La figure 2 montre une fenêtre remplie de composants, ainsi qu'une vue explosée où on voit comment ils ont été imbriqués.

Image non disponible
Figure 2. Les composants graphiques sont placés les uns dans les autres dans une fenêtre, créant ainsi une hiérarchie de composants.

Certains composants, appelés conteneurs, n'ont aucun rendu visuel. Ils permettent simplement d'en accueillir d'autres, afin d'organiser l'aspect visuel global de l'interface graphique. Dans l'exemple de la figure 3, on en a utilisé deux.

Pour comprendre comment sont organisés les composants d'une fenêtre, on peut représenter les liens entre ces derniers sous forme d'un arbre, comme le montre la figure 4. Un flèche arrive sur chaque composant, reliant son composant parent à lui. De plus, une ou plusieurs flèches partent de chaque composant, le reliant à ses composants enfants, c'est-à-dire ceux qui y sont imbriqués.

Image non disponible
Figure 3. On peut représenter la hiérarchie qu'il y a entre les composants d'une fenêtre, reliant les composants parents à leurs composants enfants.
VI-A-1-b-i. Widget

Un widget est un composant qui offre une interaction spécifique avec l'utilisateur, en pouvant lui montrer une information et en offrant une ou des interactions possibles avec lui. Un bouton permet, par exemple, de montrer un texte et de recevoir un clic gauche. On ne va pas passer en revue tous les widgets proposés par Kivy, mais uniquement les principaux.

Commençons avec un exemple d'une simple interface graphique qui regroupe trois composants de base.

  • Le label est une zone permettant d'accueillir un texte qui ne peut pas être modifié. On l'utilise, par exemple, pour renseigner le nom d'un champ à remplir dans un formulaire.
  • La zone de texte est une zone dans laquelle l'utilisateur peut entrer un texte. On l'utilise, par exemple, comme champ d'un formulaire.
  • Le bouton est une zone sur laquelle l'utilisateur peut cliquer, pour déclencher une action. On l'utilise, par exemple, pour permettre à l'utilisateur de valider un formulaire.

La figure 5 montre le résultat que l'on souhaite obtenir, à savoir aligner horizontalement un label, une zone de texte et un bouton. Pour ce faire, on va utiliser un conteneur de type BoxLayout qui aligne les composants qui lui sont ajoutés, de gauche à droite et horizontalement.

Image non disponible
Figure 4. Pour aligner trois widgets horizontalement, on utilise un conteneur de type BoxLayout qui permet de gérer la mise en page.

Voici le programme qui permet de construire l'interface graphique que l'on souhaite pour la fenêtre de connexion :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
from kivy.app import App
from kivy.config import Config 
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.textinput import TextInput

class LoginApp(App):
    def build(self):
        self.title = 'Se connecter'
        box = BoxLayout(orientation='horizontal')
        box.add_widget(Label(text='Pin code'))
        box.add_widget(TextInput())
        box.add_widget(Button(text='Entrer'))
        return box

Config.set('graphics', 'width', '350') 
Config.set('graphics', 'height', '50')

LoginApp().run()

Le premier composant que l'on crée, et qui sera renvoyé par la méthode build, est le BoxLayout. Ce dernier va contenir tous les autres composants qui lui seront ajoutés à l'aide de sa méthode add_widget. De plus, ils seront alignés horizontalement comme on l'a précisé dans le constructeur lors de la création du conteneur. Les composants sont placés dans le BoxLayout en suivant l'ordre des appels à add_widget.

Malgré que la fenêtre que l'on vient de construire soit très simple, le nombre de lignes de code et d'imports a déjà bien augmenté. On va voir, plus loin dans ce chapitre, comment facilement décrire les composants à mettre dans une fenêtre et leur agencement, à l'aide d'un fichier KV.

On peut proposer un choix à l'utilisateur de différentes manières, selon le type de choix que l'on souhaite lui proposer.

  • La case à cocher propose un choix booléen à l'utilisateur, qui peut la cocher ou non. On l'utilise, par exemple, pour proposer à l'utilisateur d'activer ou non une option.
  • Le bouton radio permet de sélectionner un choix parmi un groupe d'options. On l'utilise, par exemple, pour proposer plusieurs choix à l'utilisateur qui doit en choisir un seul.
  • La liste déroulante permet de proposer toute une liste de choix à l'utilisateur qui pourra en choisir un seul. On l'utilise lorsqu'on a plus de choix qu'avec le bouton radio, pour permettre à l'utilisateur de sélectionner son pays, par exemple.

L'exemple suivant crée une interface graphique pour gérer des feux de signalisation et reprend ces trois composants :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.checkbox import CheckBox
from kivy.uix.label import Label
from kivy.uix.spinner import Spinner

class SignallingApp(App):
    def build(self):
        self.title = 'Feu de signalisation'
        # Sélection de la ville du feu
        city = BoxLayout(orientation='horizontal')
        city.add_widget(Label(text='Feu'))
        cities = ('Bruxelles', 'Gent', 'Namur')
        city.add_widget(Spinner(values=cities))
        # Activation ou non du feu
        activation = BoxLayout(orientation='horizontal')
        activation.add_widget(CheckBox())
        activation.add_widget(Label(text='Activer le feu'))
        # Choix de l'état du feu (rouge, orange ou vert)
        state = BoxLayout(orientation='horizontal')
        state.add_widget(CheckBox(group='state'))
        state.add_widget(Label(text='Rouge'))
        state.add_widget(CheckBox(group='state'))
        state.add_widget(Label(text='Orange'))
        state.add_widget(CheckBox(group='state'))
        state.add_widget(Label(text='Vert'))
        # Composant principal
        box = BoxLayout(orientation='vertical')
        box.add_widget(city)
        box.add_widget(activation)
        box.add_widget(state)
        return box

SignallingApp().run()

La figure 6 montre le résultat obtenu en exécutant ce programme. La liste déroulante s'obtient avec un objet Spinner à qui on spécifie la liste des choix à proposer sous forme d'un tuple, avec le paramètre nommé values du constructeur. On voit d'ailleurs qu'elle est dépliée en l'attente d'un choix de l'utilisateur. Les cases à cocher et les boutons radio sont tous les deux représentés par un objet de type CheckBox. Pour obtenir des boutons radio, il faut les placer dans un même groupe que l'on définit à l'aide du paramètre nommé group du constructeur.

Image non disponible
Figure 5. Plusieurs widgets peuvent être utilisés pour proposer un choix à l'utilisateur, parmi lesquels les cases à cocher et boutons radio (CheckBox) et les listes déroulantes (Spinner).

La construction de cette interface graphique est encore plus complexe et plus lourde que la construction de la précédente. On a, en effet, dû utiliser plusieurs conteneurs imbriqués les uns dans les autres pour arriver à la mise en page désirée. La figure 7 montre la hiérarchie des composants utilisés dans cette fenêtre.

Image non disponible
Figure 6. L'interface graphique du programme de gestion de feux de signalisation possède trois niveaux d'imbrication de composants, permettant un arrangement vertical de lignes de composants arrangés horizontalement.
VI-A-1-b-ii. Gestionnaire de mise en page

Si on revient un moment sur l'exemple précédent, on se rend compte que les composants de l'interface graphique sont organisés en trois lignes et une colonne. Pour la construire, on a utilisé un conteneur de type BoxLayout, qui n'est sans doute pas le plus adapté pour cela. Un tel conteneur n'a aucun aspect visuel particulier, son seul but étant d'accueillir d'autres composants en les organisant dans l'espace d'une façon particulière. Ce type de composant est un conteneur gestionnaire de mise en page. Une fois le composant créé, on lui en ajoute d'autres grâce à la méthode add_widget.

Un conteneur de type BoxLayout positionne ses composants les uns à côté des autres, soit horizontalement, soit verticalement. On précise le sens que l'on désire à l'aide du paramètre nommé orientation du constructeur, qui peut valoir horizontal ou vertical.

Un autre conteneur très utilisé est le GridLayout, qui permet d'organiser les composants qu'il héberge sous forme d'une grille. Le nombre de lignes et de colonnes que l'on souhaite est spécifié par les paramètres nommés rows et cols du constructeur. Les éléments que l'on ajoute à ce conteneur sont placés dans la grille de gauche à droite et de haut en bas. L'exemple suivant crée une interface graphique avec kitxmlcodeinlinelatexdvp12finkitxmlcodeinlinelatexdvp boutons placés dans une grille de trois lignes et quatre colonnes :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
from kivy.app import App
from kivy.config import Config 
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout

class GridGameApp(App):
    def build(self):
        self.title = 'Grid Game'
        grid = GridLayout(rows=3, cols=4)
        for i in range(12):
            grid.add_widget(Button(text=str(i + 1)))
        return grid

Config.set('graphics', 'width', '200') 
Config.set('graphics', 'height', '150')

GridGameApp().run()

Le résultat produit par l'exécution de ce programme est montré à la figure 8. Comme on a utilisé les nombres de kitxmlcodeinlinelatexdvp1finkitxmlcodeinlinelatexdvp à kitxmlcodeinlinelatexdvp12finkitxmlcodeinlinelatexdvp comme textes sur les boutons, on voit explicitement l'ordre dans lequel ils ont été ajoutés.

Image non disponible
Figure 7. Un conteneur de type GridLayout organise les composants qu'il contient sous la forme d'une grille dont on spécifie le nombre de lignes et de colonnes.

Enfin, on peut positionner les éléments exactement là où on le souhaite, avec la taille que l'on souhaite en utilisant un conteneur de type FloatLayout. Pour cela, il faut tout d'abord spécifier la taille du conteneur avec le paramètre nommé size_hint de son constructeur. Ensuite, lorsqu'on lui ajoute des composants, ceux-ci seront placés à la position qui a été spécifiée par le paramètre nommé pos. L'exemple suivant, dont le résultat se trouve à la figure 9, crée une fenêtre avec un conteneur de type FloatLayout qui contient deux boutons qui ont été placés à des endroits bien précis. Ces boutons ont par ailleurs une taille fixée à (0.3, 0.2), c'est-à-dire kitxmlcodeinlinelatexdvp30finkitxmlcodeinlinelatexdvp% de la fenêtre en largeur et kitxmlcodeinlinelatexdvp20finkitxmlcodeinlinelatexdvp% en hauteur :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
from kivy.app import App
from kivy.config import Config 
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout

class FreePosApp(App):
    def build(self):
        self.title = 'Free Positioning'
        box = FloatLayout(size=(200, 150))
        box.add_widget(Button(text='A', size_hint=(0.3, 0.2), pos=(0, 0)))
        box.add_widget(Button(text='B', size_hint=(0.3, 0.2), pos=(50, 80)))
        return box

Config.set('graphics', 'width', '200') 
Config.set('graphics', 'height', '150')

FreePosApp().run()
Image non disponible
Figure 8. Un conteneur de type FloatLayout permet de placer les composants qu'il contient à n'importe quelle position que l'on précise lors de leur création.
VI-A-1-c. Fichier KV

Simplifier la construction de l'interface graphique, c'est-à-dire établir la liste des composants avec leurs propriétés et leur placement, peut se faire plus facilement à l'aide du langage KV. La description se fait dans un fichier séparé, qui porte le même nom que celui de l'application (à savoir celui de la classe sans App), et avec l'extension .kv. Reprenons l'exemple précédent de la fenêtre de login ; le programme principal devient :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
from kivy.app import App
from kivy.config import Config

class LoginApp(App):
    pass

Config.set('graphics', 'width', '350') 
Config.set('graphics', 'height', '50')

LoginApp().run()

À côté du fichier contenant ce programme, on crée donc un autre fichier, nommé login.kv, avec le contenu suivant :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
BoxLayout:
    orientation: 'horizontal'
    Label:
        text: 'Pin code'
    TextInput
    Button:
        text: 'Entrer'

Le langage KV permet en fait de décrire un genre de dictionnaire de manière textuelle. Le premier composant déclaré dans ce fichier est celui qui sera inséré dans la fenêtre, c'est-à-dire celui qui est renvoyé par la méthode build, c'est-à-dire un conteneur de type BoxLayout dans notre cas. Vient ensuite un bloc indenté qui reprend les propriétés du composant et les autres composants qu'on veut lui imbriquer. Dans notre cas, on imbrique, dans l'ordre, un label, une zone de texte et un bouton, dans le conteneur.

VI-B. Programmation évènementielle

Maintenant que l'on sait construire l'interface graphique, c'est-à-dire placer les composants dans la fenêtre, on va voir comment définir l'interaction avec l'utilisateur. Lors de cette interaction, de nombreux évènements seront générés tels que des clics, des déplacements de souris, des pressions sur des touches du clavier, des activations de timer, des changements de valeurs de l'accéléromètre, etc. À ces différents évènements, il est possible d'associer des gestionnaires, c'est-à-dire une portion de code qui définit quoi faire en réaction à leur occurrence.

VI-B-1. Boucle de gestion d'évènements

Un programme doté d'une interface graphique, une fois démarré, ne sera normalement quitté que suite à une action de l'utilisateur (il clique par exemple sur un bouton « quitter »). La figure 9 illustre les étapes de la vie d'un tel programme :

  1. Le programme commence par une phase d'initialisation qui va notamment récupérer des informations sur les composants de hardware tels que l'écran, ou les divers senseurs. Cette phase initialise également des structures de données qui seront nécessaires au bon fonctionnement et à la gestion de l'interface graphique ;
  2. Le programme démarre ensuite une boucle de gestion d'évènements. Dans cette boucle, potentiellement infinie, le programme reçoit des notifications de tous les évènements qui le concernent et il les traite. Pour les évènements auxquels sont associés des gestionnaires, il exécute ces derniers. La gestion des évènements se fait de manière séquentielle, ils sont traités les uns à la suite des autres ;
  3. Lorsqu'un des évènements reçus demande au programme de se quitter, il rentre dans une phase de terminaison durant laquelle il va libérer toutes les ressources allouées avant de se terminer.

Cette boucle de gestion d'évènements est précisément démarrée par la méthode run, appelée sur l'instance de la classe de type App représentant l'application avec interface graphique nouvellement définie. Elle s'exécute jusqu'à ce que le programme soit quitté, avec l'appel sys.exit(0) qui serait exécuté dans un gestionnaire, par exemple.

Image non disponible
Figure 9. L'exécution d'un programme avec une interface graphique commence par une initialisation, puis vient une boucle « infinie » qui gère les évènements avant une éventuelle terminaison du programme.
VI-B-1-a. Gestionnaire d'évènements

Lorsqu'un évènement se produit, et si aucun gestionnaire d'évènements ne lui a été associé dans le programme, il sera juste ignoré. Pour associer une action à exécuter lors de l'occurrence d'un évènement, il va donc falloir en définir. Trois éléments vont intervenir pour cela :

  • la source de l'évènement est le composant sur lequel l'interaction a eu lieu ou celui qui l'a causé ;
  • l'évènement à proprement parler est d'un certain type selon ce qui s'est produit ;
  • et le gestionnaire d'évènements est la méthode à exécuter pour chaque occurrence de l'évènement.

En fonction du type d'évènement, certaines informations complémentaires peuvent être disponibles. Voici quelques exemples d'évènements :

  • un bouton peut avoir être enfoncé suite à un clic dessus ;
  • un clic gauche ou droit peut avoir été fait sur une zone de dessin, aux coordonnées kitxmlcodeinlinelatexdvp(x, y)finkitxmlcodeinlinelatexdvp ;
  • un élément d'une liste déroulante peut avoir été sélectionné ;
  • un élément à l'intérieur d'un composant peut avoir été glissé-déposé (drag and drop) d'une position de départ kitxmlcodeinlinelatexdvp(x_0, y_0)finkitxmlcodeinlinelatexdvp à une position d'arrivée kitxmlcodeinlinelatexdvp(x_1, y_1)finkitxmlcodeinlinelatexdvp ;
  • une touche du clavier peut avoir été enfoncée ou relâchée ;
  • la souris peut être rentrée dans ou avoir quitté une zone donnée.

Un gestionnaire d'évènements est donc une méthode que l'on va attacher à un évènement sur une source donnée. L'exemple suivant permet de quitter le programme lorsqu'on clique sur un bouton :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
import sys

from kivy.app import App
from kivy.config import Config 
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.button import Button

class QuitApp(App):
    def build(self):
        self.title = 'Quit me'
        box = AnchorLayout(anchor_x='center', anchor_y='center')
        quit = Button(text='Quitter', size_hint=(0.7,0.3))
        quit.bind(on_press=self._quit)
        box.add_widget(quit)
        return box

    def _quit(self, source):
        sys.exit(0)

Config.set('graphics', 'width', '200') 
Config.set('graphics', 'height', '200')

QuitApp().run()

La première chose que l'on peut voir, c'est l'utilisation d'un conteneur de type AnchorLayout. Ce dernier permet de positionner le composant qu'il contient à une position prédéterminée comme au centre, au nord… Dans cet exemple, on l'a centré selon les deux axes kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp. On crée ensuite le bouton, comme d'habitude, et on l'ajoute dans le conteneur qui est renvoyé par la méthode build.

La nouveauté, par rapport aux précédents exemples, c'est l'ajout d'une méthode dont on a préfixé le nom avec _ pour signaler qu'elle est à usage privé dans la classe. Il s'agit en fait d'un gestionnaire d'évènements que l'on va associer au clic gauche sur le bouton en appelant dessus la méthode bind et en la passant au paramètre nommé on_press :

 
Sélectionnez
quit.bind(on_press=self._quit)
Image non disponible
Figure 10. Un bouton peut être centré au milieu de son composant mère si ce dernier est un conteneur de type AnchorLayout.

Pendant l'exécution du programme, lorsque l'utilisateur clique sur le bouton, un évènement de type press avec ce dernier en source est enregistré. Une fois que c'est à son tour d'être traité par la boucle d'évènements, puisqu'un gestionnaire lui a été associé, le programme l'exécute et, dans ce cas-ci, le programme est donc quitté.

VI-B-1-a-i. Gestionnaire d'évènements dans le fichier KV

On peut évidemment associer des gestionnaires d'évènements à des composants directement depuis le fichier KV. Pour ce faire, on va devoir importer l'application dans le fichier KV, afin d'avoir accès aux méthodes représentant les gestionnaires d'évènements.

Néanmoins, une différence notable avec l'utilisation de la méthode bind est qu'on ne doit pas spécifier la méthode à appeler, mais bien faire l'appel que l'on désire. Une première conséquence est qu'il faut soi-même fournir le paramètre qui identifie la source de l'évènement.

On commence donc par complètement supprimer la méthode build de la classe QuitApp ainsi que tous les imports qui y sont liés. Pour le reste, on ne doit toucher à rien. La nouvelle version de la classe est donc :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
import sys

from kivy.app import App
from kivy.config import Config 

class QuitApp(App):
    def _quit(self, source):
        print(source)
        sys.exit(0)

Config.set('graphics', 'width', '200') 
Config.set('graphics', 'height', '200')

QuitApp().run()

Vient ensuite le fichier quit.kv, dont le composant principal est donc un conteneur de type AnchorLayout :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
#:import App kivy.app.App

AnchorLayout:
    anchor_x: 'center'
    anchor_y: 'center'
    Button:
        text: 'Quit'
        size_hint: (0.7,0.3)
        on_press: App.get_running_app()._quit(self)

On commence par importer la classe App, ce qui se fait comme en Python, mais préfixé des caractères \#:. On récupère ensuite l'instance de la classe QuitApp qui représente le programme qui est exécuté avec l'appel App.get_running_app(). Sur cette instance, on appelle la méthode quit en lui passant en paramètre self qui représente le bouton qui a produit l'évènement. On a donc bien dû appeler la méthode manuellement.

VI-B-1-b. Composant personnalisé

Il est parfois utile de vouloir définir un nouveau composant personnalisé, qui est construit à partir d'autres existants. Il suffit, pour cela, de définir une nouvelle classe dont le type est, par exemple, BoxLayout. Pour illustrer cela, construisons un programme qui calcule automatiquement l'âge de l'utilisateur à partir de son année de naissance, qu'il doit fournir.

La figure 11 montre le look de l'interface graphique du calculateur d'âge. Une zone de texte permet à l'utilisateur d'entrer son année de naissance et, une fois qu'il l'aura validée avec la touche ENTER, l'âge calculé sera affiché dans le label inférieur.

Image non disponible
Figure 11. Après avoir entré son année de naissance, l'utilisateur peut obtenir son âge en pressant la touche ENTER.

Pour réaliser cette interface graphique, on va définir un nouveau composant appelé AgeCalculatorForm et, comme d'habitude, la classe de type App qui décrit le programme :

 
Sélectionnez
1.
2.
3.
4.
5.
class AgeCalculatorForm(BoxLayout):
    pass

class AgeCalculatorApp(App):
    pass

Il s'agit donc simplement d'une classe de type BoxLayout, dont le corps est vide. Tel quel, un composant de type AgeCalculatorForm est donc complètement équivalent à un composant BoxLayout. Associé à cela, un fichier agecalculator.kv décrit les composants de l'interface :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
AgeCalculatorForm:
    orientation: 'vertical'
    BoxLayout:
        orientation: 'horizontal'
        Label:
            text: 'Année de naissance'
        TextInput:
            multiline: False
    Label

Comme on peut le voir, le composant principal, c'est-à-dire celui qui est ajouté à la fenêtre, est le composant AgeCalculatorForm. La zone de texte est limitée à une ligne grâce à l'option multiline fixée à False. Comme on va le voir à la section suivante, l'avantage de créer son propre composant est qu'on va pouvoir récupérer facilement les informations entrées par l'utilisateur dans la zone de texte.

VI-B-1-c. État des widgets

Lorsqu'on a une interface graphique avec des widgets qui demandent de l'information, il est évidemment utile de savoir la récupérer. De même, lorsqu'un widget présente de l'information à l'utilisateur, il est utile de pouvoir la mettre à jour. Pour ce faire, on peut procéder de différentes manières, selon qu'on utilise ou non un fichier KV. Comme la bonne pratique consiste à en utiliser un, on se limitera à la façon de faire utilisant ce fichier.

La première chose à faire consiste à définir des propriétés dans le nouveau composant qu'on a créé, afin de pouvoir faire le lien avec les composants qui sont définis dans le fichier KV. Comme le lien à faire est vers des composants, on doit utiliser une propriété de type objet, à savoir une instance de la classe ObjectProperty. On ajoute donc deux variables d'instance à la classe AgeCalculatorForm :

 
Sélectionnez
class AgeCalculatorForm(BoxLayout):
    birthyear_input = ObjectProperty()
    age_label = ObjectProperty()

La première variable va servir à faire le lien avec la zone de texte et la seconde avec le label où sera affiché l'âge calculé. Les liens sont établis dans le fichier agecalculator.kv qui devient comme suit :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
AgeCalculatorForm:
    birthyear_input: birthyear
    age_label: age
    orientation: 'vertical'
    BoxLayout:
        orientation: 'horizontal'
        Label:
            text: 'Année de naissance'
        TextInput:
            id: birthyear
            multiline: False
    Label:
        id: age

La première chose à faire est d'associer un identifiant à l'aide de l'attribut id aux composants que l'on souhaite lier. On ajoute ensuite les deux propriétés qui sont définies dans la classe AgeCalculatorForm dans le fichier KV en leur donnant comme valeurs les identifiants choisis.

La dernière étape consiste à récupérer l'année de naissance, calculer l'âge et l'afficher dans le label. On ajoute pour cela une méthode _compute dans la classe AgeCalculatorForm :

 
Sélectionnez
def _compute(self, source):
    age = 2016 - int(self.birthyear_input.text)
    self.age_label.text = 'Vous avez {} ans.'.format(age)

Il ne reste plus qu'à appeler cette méthode lorsque l'utilisateur enfonce la touche ENTER dans la zone de texte. On modifie pour cela le fichier KV en ajoutant la ligne suivante dans le bloc du TextInput :

 
Sélectionnez
on_text_validate: root._compute(self)

L'évènement auquel on associe un gestionnaire est donc on_text_validate. De plus, on peut directement utiliser la variable root qui représente le composant de type AgeCalculatorForm qui est défini, sans devoir passer par App.get_running_app() comme on a dû le faire précédemment.

VI-C. Dessin

Terminons ce chapitre en examinant brièvement comment réaliser des dessins et animations à l'aide de Kivy. Cette bibliothèque permet de réaliser des dessins sophistiqués statiques ou animés en exploitant les capacités d'OpenGL et de SDL.

OpenGL est une collection de fonctions permettant de réaliser des calculs d'images 2D et 3D comme de la géométrie d'objets et du calcul de projection à l'écran, par exemple. SDL est une bibliothèque que l'on utilise pour créer des jeux 2D permettant notamment de gérer l'affichage vidéo, l'audio numérique et les périphériques tels que la souris et le clavier, par exemple.

VI-C-1. Composant canvas

On peut dessiner dans un composant en utilisant sa propriété canvas. Il suffit d'y définir une séquence d'opérations graphiques à réaliser, comme dessiner un rectangle ou changer la couleur du pinceau. Pour que le résultat soit intéressant, il vaut évidemment mieux utiliser un conteneur pour avoir une zone de dessin vierge au départ.

La figure 12 montre un exemple d'une fenêtre avec un conteneur de type BoxLayout sur lequel on a dessiné différents éléments. Pour ce faire, on a défini un nouveau composant de type BoxLayout, dans lequel on fera les dessins. Voici le code Python du programme :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout

class DrawForm(BoxLayout):
    pass

class DrawApp(App):
    pass

DrawApp().run()

On définit donc un nouveau composant DrawForm qui est un conteneur de type BoxLayout. C'est dans ce dernier que le dessin sera réalisé, à l'aide de la propriété canvas.

Image non disponible
Figure 12. La propriété canvas d'un composant permet de définir des dessins qui sont réalisés dans le composant.

Vient ensuite le fichier KV qui contient la description du dessin à réaliser. Dans ce dernier, on définit la séquence des opérations de dessin à faire grâce à la propriété canvas. Ces opérations seront réalisées dans l'ordre dans lequel elles sont définies :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
DrawForm:
    canvas:
        Color:
            rgb: [1, 1, 1]
        Rectangle:
            pos: (0, 0)
            size: self.size
        Color:
            rgb: [0.7, 0.2, 0]
        Rectangle:
            pos: (50, 200)
            size: (400, 150)

Quatre opérations graphiques seront donc réalisées au moment où le programme est lancé et que l'interface graphique s'affiche :

  1. La première opération change la couleur qui est utilisée à blanc ;
  2. La deuxième dessine un rectangle dont le coin inférieur gauche est en kitxmlcodeinlinelatexdvp(0, 0)finkitxmlcodeinlinelatexdvp, c'est-à-dire le coin inférieur gauche de la zone de dessin, et dont la taille est la même que celle du composant. En gros, on peint l'arrière-fond du composant intégralement en blanc ;
  3. On change ensuite de nouveau la couleur de dessin, en la fixant à un rouge composé de kitxmlcodeinlinelatexdvp70%finkitxmlcodeinlinelatexdvp de rouge et kitxmlcodeinlinelatexdvp20%finkitxmlcodeinlinelatexdvp de bleu ;
  4. On termine en dessinant un plus petit rectangle, qui sera donc en rouge, dont le coin inférieur droit est en kitxmlcodeinlinelatexdvp(50, 200)finkitxmlcodeinlinelatexdvp et de kitxmlcodeinlinelatexdvp400finkitxmlcodeinlinelatexdvp pixels de largeur et kitxmlcodeinlinelatexdvp150finkitxmlcodeinlinelatexdvp pixels de hauteur.
VI-C-1-a. Transformation

Il est possible d'appliquer trois différentes transformations aux dessins réalisés. Une rotation d'un certain angle peut se faire autour d'un axe donné par rapport à un point d'origine. Une translation peut se faire suivant un certain vecteur. Enfin, une mise à l'échelle selon les différents axes peut se faire par rapport à un point d'origine.

Voici une modification de l'exemple précédent qui utilise les trois transformations pour dessiner un nouveau rectangle bleu :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
DrawForm:
    canvas:
        Color:
            rgb: [1, 1, 1]
        Rectangle:
            pos: (0, 0)
            size: self.size
        Color:
            rgb: [0.7, 0.2, 0]
        Rectangle:
            pos: (50, 200)
            size: (400, 150)
        Translate:
            x: 200
            y: 0
        Rotate:
            origin: (450, 200)
            angle: -45
            axis: (0, 0, 1)
        Scale:
            origin: (450, 200)
            x: 0.5
            y: 0.5
        Color:
            rgb: [0, 0.2, 0.7]
        Rectangle:
            pos: (50, 200)
            size: (400, 150)

On repart donc du rectangle rouge qu'on va redessiner à la dernière étape, après avoir changé la couleur en bleu, mais surtout après avoir défini une séquence de transformations à appliquer. Sans rentrer dans les détails mathématiques, il faut savoir que les opérations de transformations sont appliquées dans l'ordre inverse de leur apparition. Dans cet exemple, on en définit trois qui vont donc s'appliquer dans l'ordre suivant :

  1. On commence avec une mise à l'échelle diminuant de moitié les dimensions de l'objet selon les deux axes (kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp), grâce au facteur kitxmlcodeinlinelatexdvp0.5finkitxmlcodeinlinelatexdvp, et par rapport au point d'origine kitxmlcodeinlinelatexdvp(450, 200)finkitxmlcodeinlinelatexdvp qui correspond au coin inférieur droit du rectangle rouge ;
  2. On enchaine ensuite avec une rotation, autour du même point d'origine, de kitxmlcodeinlinelatexdvp45finkitxmlcodeinlinelatexdvp degrés dans le sens antihorloger, selon l'axe kitxmlcodeinlinelatexdvpzfinkitxmlcodeinlinelatexdvp (la rotation se fait donc uniquement dans le plan kitxmlcodeinlinelatexdvpxyfinkitxmlcodeinlinelatexdvp) ;
  3. On termine enfin avec une translation de kitxmlcodeinlinelatexdvp200finkitxmlcodeinlinelatexdvp pixels vers la droite (axe kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp) et de kitxmlcodeinlinelatexdvp0finkitxmlcodeinlinelatexdvp en hauteur (axe kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp).

Les transformations, une fois définies, s'appliquent à tous les objets qui seront dessinés par après. La figure 13 montre le résultat produit par l'exécution de l'exemple. L'ordre des transformations est très important et on n'obtient, en général, pas le même résultat final lorsqu'on modifie l'ordre des transformations. Il faut également faire attention à l'origine qui est utilisée pour les rotations et mises à l'échelle.

Image non disponible
Figure 13. L'utilisation de transformations permet de modifier la manière avec laquelle un objet est dessiné dans le canvas.

précédentsommairesuivant

Copyright © 2019 Sébastien Combéfis. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.