UKOnline

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.

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 hardwares 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.

Boucle d'évènements
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.

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 $(x, y)$ ;
  • 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 $(x_0, y_0)$ à une position d'arrivée $(x_1, y_1)$ ;
  • 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 :

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 $x$ et $y$. 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 :

Quit me
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é.

Gestionnaire d'évènement 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 :

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

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.

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.

Age calculator
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 :

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 :

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és par l'utilisateur dans la zone de texte.

État des widgets

Lorsqu'on a une interface graphique avec des widgets qui demande 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 :

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 :

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 :

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 :

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.