Tutoriel 8 - Le rendre lisse

A moins que vous ne disposiez d’une connexion internet très rapide, vous remarquerez peut-être que lorsque vous appuyez sur le bouton, l’interface graphique de votre application se bloque pendant un petit moment. C’est parce que la requête web que nous avons faite est synchrone. Lorsque notre application effectue la requête web, elle attend que l’API renvoie une réponse avant de continuer. Pendant cette attente, l’API ne permet pas à l’application de se redessiner, ce qui a pour effet de bloquer l’application.

Boucles d’événements de l’interface graphique

Pour comprendre pourquoi cela se produit, nous devons entrer dans les détails du fonctionnement d’une application GUI. Les spécificités varient en fonction de la plate-forme, mais les concepts de haut niveau sont les mêmes, quelle que soit la plate-forme ou l’environnement d’interface graphique que vous utilisez.

Une application GUI est, fondamentalement, une boucle unique qui ressemble à quelque chose comme: :

while not app.quit_requested():
    app.process_events()
    app.redraw()

Cette boucle est appelée boucle d’événements. (Il ne s’agit pas de noms de méthodes réels, mais d’une illustration de ce qui se passe dans le « pseudo-code »).

Lorsque vous cliquez sur un bouton, faites glisser une barre de défilement ou tapez une touche, vous générez un « événement ». Cet « événement » est placé dans une file d’attente, et l’application traitera la file d’événements lorsqu’elle en aura l’occasion. Le code utilisateur déclenché en réponse à l’événement est appelé « gestionnaire d’événement ». Ces gestionnaires d’événements sont invoqués dans le cadre de l’appel process_events().

Une fois qu’une application a traité tous les événements disponibles, elle va redraw() l’interface graphique. Cela prend en compte tous les changements que les événements ont causés à l’affichage de l’application, ainsi que tout ce qui se passe dans le système d’exploitation - par exemple, les fenêtres d’une autre application peuvent masquer ou révéler une partie de la fenêtre de notre application, et le redessin de notre application devra refléter la partie de la fenêtre qui est actuellement visible.

Détail important : pendant qu’une application traite un événement, elle ne peut pas redessiner, et elle ne peut pas traiter d’autres événements.

Cela signifie que toute logique utilisateur contenue dans un gestionnaire d’événements doit être exécutée rapidement. Tout retard dans l’exécution du gestionnaire d’événements sera observé par l’utilisateur sous la forme d’un ralentissement (ou d’un arrêt) des mises à jour de l’interface graphique. Si ce délai est suffisamment long, votre système d’exploitation peut signaler qu’il s’agit d’un problème - les icônes macOS « beachball » et Windows « spinner » indiquent que votre application prend trop de temps dans un gestionnaire d’événements.

Des opérations simples comme « mettre à jour une étiquette » ou « recalculer le total des entrées » sont faciles à réaliser rapidement. Cependant, de nombreuses opérations ne peuvent pas être effectuées rapidement. Si vous effectuez un calcul mathématique complexe, si vous indexez tous les fichiers d’un système de fichiers ou si vous effectuez une requête réseau importante, vous ne pouvez pas « faire vite » - les opérations sont intrinsèquement lentes.

Alors, comment effectuer des opérations à long terme dans une application GUI ?

Programmation asynchrone

Ce dont nous avons besoin, c’est d’un moyen de dire à une application au milieu d’un gestionnaire d’événements de longue durée qu’il est acceptable de relâcher temporairement le contrôle dans la boucle d’événements, tant que nous pouvons reprendre là où nous nous sommes arrêtés. C’est à l’application de déterminer quand cette libération peut avoir lieu ; mais si l’application libère le contrôle dans la boucle d’événements régulièrement, nous pouvons avoir un gestionnaire d’événements de longue durée et maintenir une interface utilisateur réactive.

Nous pouvons le faire en utilisant la programmation asynchrone. La programmation asynchrone est une façon de décrire un programme qui permet à l’interpréteur d’exécuter plusieurs fonctions en même temps, en partageant les ressources entre toutes les fonctions qui s’exécutent simultanément.

Les fonctions asynchrones (appelées co-routines) doivent être explicitement déclarées comme étant asynchrones. Elles doivent également déclarer en interne lorsqu’il est possible de changer de contexte et de passer à une autre co-routine.

En Python, la programmation asynchrone est implémentée à l’aide des mots-clés async et await, et du module asyncio <https://docs.python.org/3/library/asyncio.html>`__ dans la bibliothèque standard. Le mot-clé ``async nous permet de déclarer qu’une fonction est une co-routine asynchrone. Le mot-clé await permet de déclarer qu’il existe une opportunité de changer de contexte vers une autre co-routine. Le module asyncio fournit d’autres outils et primitives utiles pour le codage asynchrone.

Rendre le didacticiel asynchrone

Pour rendre notre tutoriel asynchrone, modifiez le gestionnaire d’événement say_hello() pour qu’il ressemble à ceci: :

async def say_hello(self, widget):
    async with httpx.AsyncClient() as client:
        response = await client.get("https://jsonplaceholder.typicode.com/posts/42")

    payload = response.json()

    self.main_window.info_dialog(
        greeting(self.name_input.value),
        payload["body"],
    )

Il n’y a que 4 changements dans ce code par rapport à la version précédente :

  1. La méthode est définie comme async def, plutôt que comme def. Cela indique à Python que la méthode est une co-routine asynchrone.

  2. Le client créé est un AsyncClient() asynchrone, plutôt qu’un Client() synchrone. Cela indique à httpx qu’il doit fonctionner en mode asynchrone, plutôt qu’en mode synchrone.

  3. Le gestionnaire de contexte utilisé pour créer le client est marqué comme async. Cela indique à Python qu’il y a une opportunité de relâcher le contrôle lorsque le gestionnaire de contexte est entré et sorti.

  4. L’appel get est fait avec un mot-clé await. Cela indique à l’application que pendant que nous attendons la réponse du réseau, l’application peut laisser le contrôle à la boucle d’événements.

Toga vous permet d’utiliser des méthodes normales ou des co-programmes asynchrones en tant que gestionnaires ; Toga gère tout en coulisses pour s’assurer que le gestionnaire est invoqué ou attendu selon les besoins.

Si vous sauvegardez ces changements et relancez l’application (soit avec briefcase dev en mode développement, soit en mettant à jour et en relançant l’application packagée), il n’y aura pas de changements évidents dans l’application. Cependant, lorsque vous cliquez sur le bouton pour déclencher le dialogue, vous pouvez remarquer un certain nombre d’améliorations subtiles :

  • Le bouton revient à l’état « décliqué » au lieu d’être bloqué à l’état « cliqué ».

  • L’icône « beachball »/ »spinner » n’apparaît pas

  • Si vous déplacez ou redimensionnez la fenêtre de l’application en attendant que la boîte de dialogue s’affiche, la fenêtre se redessinera.

  • Si vous essayez d’ouvrir un menu d’application, le menu s’affiche immédiatement.

Étapes suivantes

Nous disposons désormais d’une application élégante et réactive, même lorsqu’elle est en attente d’une API lente. Mais comment s’assurer que l’application continue de fonctionner au fur et à mesure que nous la développons ? Comment tester notre application ? Consultez Tutoriel 9 pour le découvrir…