Recherche de site Web

Comment construire une application Tkinter en utilisant une approche orientée objet


Dans un tutoriel précédent, nous avons vu les concepts de base derrière l’utilisation de Tkinter, une bibliothèque utilisée pour créer des interfaces utilisateur graphiques avec Python. Dans cet article, nous voyons comment créer une application complète mais simple. Au cours de ce processus, nous apprenons à utiliser des threads pour gérer des tâches de longue durée sans bloquer l’interface, à organiser une application Tkinter en utilisant une approche orientée objet et à utiliser les protocoles Tkinter.

Dans ce tutoriel, vous allez apprendre :

  • Comment organiser une application Tkinter en utilisant une approche orientée objet
  • Comment utiliser les threads pour éviter de bloquer l’interface de l’application
  • Comment utiliser faire communiquer les fils de discussion à l’aide d’événements
  • Comment utiliser les protocoles Tkinter

Configuration logicielle requise et conventions utilisées

Introduction

Dans ce tutoriel nous allons coder une application simple « composée » de deux widgets : un bouton et une barre de progression. Ce que notre application va faire, c’est simplement télécharger l’archive contenant la dernière version de Wordpress une fois que l’utilisateur a cliqué sur le bouton « télécharger » ; Le widget de la barre de progression sera utilisé pour suivre la progression du téléchargement. L’application sera codée à l’aide d’une approche orientée objet ; au cours de l’article, je supposerai que le lecteur est familier avec les concepts de base de la POO.

Organisation de l’application

La toute première chose que nous devons faire pour construire notre application est d’importer les modules nécessaires. Pour commencer, nous devons importer :

  • La classe Tk de base
  • La classe Button que nous devons instancier pour créer le widget de bouton
  • La classe Progressbar dont nous avons besoin pour créer le widget de barre de progression

Les deux premiers peuvent être importés à partir du module tkinter, tandis que le dernier, Progressbar, est inclus dans le module tkinter.ttk. Ouvrons notre éditeur de texte préféré et commençons à écrire le code :

#!/usr/bin/env python3

from tkinter import Tk, Button
from tkinter.ttk import Progressbar

Nous voulons construire notre application en tant que classe, afin de garder les données et les fonctions bien organisées, et d’éviter d’encombrer l’espace de noms global. La classe représentant notre application (appelons-la WordpressDownloader), va étendre la classe de base Tk, qui, comme nous l’avons vu dans le tutoriel précédent, sert à créer la fenêtre « root » :

class WordpressDownloader(Tk):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.title('Wordpress Downloader')
        self.geometry("300x50")
        self.resizable(False, False)

Voyons ce que fait le code que nous venons d’écrire. Nous avons défini notre classe comme une sous-classe de Tk. À l’intérieur de son constructeur, nous avons initialisé le parent, puis nous avons défini le titre et la géométrie de notre application en appelant le titre et la géométrie des méthodes héritées, respectivement. Nous avons passé le titre en argument à la méthode title, et la chaîne indiquant la géométrie, avec la syntaxe <with>x<height>, en argument à la méthode geometry.

Nous définissons ensuite la fenêtre racine de notre application comme non redimensionnable. Nous y sommes parvenus en appelant la méthode redimensionnable. Cette méthode accepte deux valeurs booléennes comme arguments : elles déterminent si la largeur et la hauteur de la fenêtre doivent être redimensionnables. Dans ce cas, nous avons utilisé False pour les deux.

A ce stade, nous pouvons créer les widgets qui doivent « composer » notre application : la barre de progression et le bouton « télécharger ». Nous ajoutons le code suivant à notre constructeur de classe (code précédent omis) :

The progressbar widget
self.progressbar = Progressbar(self)
self.progressbar.pack(fill='x', padx=10)

The button widget
self.button = Button(self, text='Download')
self.button.pack(padx=10, pady=3, anchor='e')

Nous avons utilisé la classe Progressbar pour créer le widget de barre de progression, puis nous avons appelé la méthode pack sur l’objet résultant pour créer un minimum de configuration. Nous avons utilisé l’argument fill pour faire en sorte que le widget occupe toute la largeur disponible de la fenêtre parent (axe x), et l’argument padx pour créer une marge de 10 pixels à partir de ses bordures gauche et droite.

Le bouton a été créé en instanciant la classe Button. Dans le constructeur de la classe, nous avons utilisé le paramètre text pour définir le texte du bouton. Nous avons ensuite configuré la disposition des boutons avec pack : avec le paramètre anchor, nous avons déclaré que le bouton devait être conservé à droite du widget principal. La direction de l’ancrage est spécifiée à l’aide de points cardinaux ; Dans ce cas, le E signifie « est » (cela peut également être spécifié en utilisant les constantes incluses dans le module tkinter. Dans ce cas, par exemple, nous aurions pu utiliser tkinter. E). Nous avons également défini la même marge horizontale que celle que nous avons utilisée pour la barre de progression.

Lors de la création des widgets, nous avons passé self comme premier argument de leurs constructeurs de classes afin de définir la fenêtre représentée par notre classe comme leur parent.

Nous n’avons pas encore défini de rappel pour notre bouton. Pour l’instant, voyons à quoi ressemble notre application. Pour ce faire, nous devons ajouter la sentinelle principale à notre code, créer une instance de la classe WordpressDownloader et appeler la méthode mainloop sur celle-ci :

if __name__ == '__main__':
    app = WordpressDownloader()
    app.mainloop()

À ce stade, nous pouvons rendre notre fichier de script exécutable et le lancer. Supposons que le fichier soit nommé app.py, dans notre répertoire de travail actuel, nous exécuterions :

chmod +x app.py
./app.py

Nous devrions obtenir le résultat suivant :

Tout semble bon. Maintenant, faisons en sorte que notre bouton fasse quelque chose ! Comme nous l’avons vu dans le tutoriel de base de tkinter, pour attribuer une action à un bouton, nous devons passer la fonction que nous voulons utiliser comme rappel comme valeur du paramètre de commande du constructeur de la classe Button. Dans notre classe d’application, nous définissons la méthode handle_download, écrivons le code qui effectuera le téléchargement, puis attribuons la méthode comme rappel du bouton.

Pour effectuer le téléchargement, nous allons utiliser la fonction urlopen qui est incluse dans le module urllib.request. Importons-le :

from urllib.request import urlopen

Voici comment nous mettons en œuvre la méthode handle_download :

def handle_download(self):
    with urlopen("https://wordpress.org/latest.tar.gz") as request:
        with open('latest.tar.gz', 'wb') as tarball:
            tarball_size = int(request.getheader('Content-Length'))
            chunk_size = 1024
            read_chunks = 0

            while True:
                chunk = request.read(chunk_size)
                if not chunk:
                    break

                read_chunks += 1
                read_percentage = 100 * chunk_size * read_chunks / tarball_size
                self.progressbar.config(value=read_percentage)

                tarball.write(chunk)

Le code à l’intérieur de la méthode handle_download est assez simple. Nous émettons une requête get pour télécharger la dernière version de l’archive tarball de Wordpress et nous ouvrons/créons le fichier que nous utiliserons pour stocker l’archive localement en mode wb (binary-write).

Pour mettre à jour notre barre de progression, nous devons obtenir la quantité de données téléchargées en pourcentage : pour ce faire, nous obtenons d’abord la taille totale du fichier en lisant la valeur de l’en-tête Content-Length et en la convertissant en int, puis nous établissons que les données du fichier doivent être lues en morceaux de 1024 octets, et conservez le nombre de morceaux que nous lisons à l’aide de la variable read_chunks.

À l’intérieur de la boucle while infinie, nous utilisons la méthode read de l’objet request pour lire la quantité de données que nous avons spécifiée avec chunk_size. Si les méthodes de lecture renvoient une valeur vide, cela signifie qu’il n’y a plus de données à lire, donc nous cassons la boucle ; Sinon, nous mettons à jour la quantité de morceaux que nous lisons, calculons le pourcentage de téléchargement et le référenceons via la variable read_percentage. Nous utilisons la valeur calculée pour mettre à jour la barre de progression en appelant sa méthode config. Enfin, nous écrivons les données dans le fichier local.

Nous pouvons maintenant attribuer le callback au bouton :

self.button = Button(self, text='Download', command=self.handle_download)

Il semble que tout devrait fonctionner, cependant, une fois que nous exécutons le code ci-dessus et cliquons sur le bouton pour lancer le téléchargement, nous nous rendons compte qu’il y a un problème : l’interface graphique ne répond plus et la barre de progression est mise à jour d’un seul coup lorsque le téléchargement est terminé. Pourquoi cela se produit-il ?

Notre application se comporte de cette façon puisque la méthode handle_download s’exécute à l’intérieur du thread principal et bloque la boucle principale : pendant le téléchargement, l’application ne peut pas réagir aux actions de l’utilisateur. La solution à ce problème consiste à exécuter le code dans un thread séparé. Voyons comment faire.

Utilisation d’un thread distinct pour effectuer des opérations de longue durée

Qu’est-ce qu’un fil de discussion ? Un thread est essentiellement une tâche de calcul : en utilisant plusieurs threads, nous pouvons faire en sorte que des parties spécifiques d’un programme soient exécutées indépendamment. Python rend très facile de travailler avec des threads via le module de threading. La toute première chose que nous devons faire est d’importer la classe Thread à partir de celle-ci :

from threading import Thread

Pour qu’un morceau de code soit exécuté dans un thread séparé, nous pouvons soit :

  1. Créez une classe qui étend la classe Thread et implémente la méthode run
  2. Spécifier le code que nous voulons exécuter via le paramètre cible du constructeur de l’objet Thread

Ici, pour mieux organiser les choses, nous allons utiliser la première approche. Voici comment nous changeons notre code. Tout d’abord, nous créons une classe qui étend Thread. Tout d’abord, dans son constructeur, nous définissons une propriété que nous utilisons pour garder une trace du pourcentage de téléchargement, puis, nous implémentons la méthode run et nous déplaçons le code qui effectue le téléchargement de l’archive tar dans celle-ci :

class DownloadThread(Thread):
    def __init__(self):
        super().__init__()
        self.read_percentage = 0

    def run(self):
        with urlopen("https://wordpress.org/latest.tar.gz") as request:
            with open('latest.tar.gz', 'wb') as tarball:
                tarball_size = int(request.getheader('Content-Length'))
                chunk_size = 1024
                read_chunks = 0

                while True:
                    chunk = request.read(chunk_size)
                    if not chunk:
                        break

                    read_chunks += 1
                    self.read_percentage = 100 * chunk_size * read_chunks / tarball_size

                    tarball.write(chunk)

Maintenant, nous devons changer le constructeur de notre classe WordpressDownloader afin qu’il accepte une instance de DownloadThread comme argument. Nous pourrions également créer une instance de DownloadThread à l’intérieur du constructeur, mais en la passant en argument, nous déclarons explicitement que WordpressDownloader en dépend :

class WordpressDownloader(Tk):
    def __init__(self, download_thread, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.download_thread = download_thread
        [...]

Ce que nous voulons faire maintenant, c’est créer une nouvelle méthode qui sera utilisée pour suivre le pourcentage de progression et mettra à jour la valeur du widget de la barre de progression. On peut l’appeler update_progress_bar :

def update_progress_bar(self):
      if self.download_thread.is_alive():
          self.progressbar.config(value=self.download_thread.read_percentage)
          self.after(100, self.update_progress_bar)

Dans la méthode update_progress_bar, nous vérifions si le thread est en cours d’exécution à l’aide de la méthode is_alive. Si le thread est en cours d’exécution, nous mettons à jour la barre de progression avec la valeur de la propriété read_percentage de l’objet thread. Après cela, pour continuer à surveiller le téléchargement, nous utilisons la méthode after de la classe WordpressDownloader. Cette méthode permet d’effectuer un rappel après un certain nombre de millisecondes. Dans ce cas, nous l’avons utilisé pour rappeler la méthode update_progress_bar après 100 millisecondes. Cette opération sera répétée jusqu’à ce que le fil soit vivant.

Enfin, on peut modifier le contenu de la méthode handle_download qui est invoquée lorsque l’utilisateur clique sur le bouton « télécharger ». Étant donné que le téléchargement réel est effectué dans la méthode run de la classe DownloadThread, il suffit ici de démarrer le thread et d’appeler la méthode update_progress_bar que nous avons définie à l’étape précédente :

def handle_download(self):
      self.download_thread.start()
      self.update_progress_bar()

À ce stade, nous devons modifier la façon dont l’objet app est créé :

if __name__ == '__main__':
    download_thread = DownloadThread()
    app = WordpressDownloader(download_thread)
    app.mainloop()

Si nous relançons maintenant notre script et lançons le téléchargement, nous pouvons voir que l’interface n’est plus bloquée pendant le téléchargement :

Il y a cependant encore un problème. Pour le « visualiser », lancez le script, et fermez la fenêtre de l’interface graphique une fois que le téléchargement a commencé mais n’est pas encore terminé ; Voyez-vous qu’il y a quelque chose qui accroche au terminal ? Cela se produit parce que bien que le thread principal ait été fermé, celui utilisé pour effectuer le téléchargement est toujours en cours d’exécution (les données sont toujours en cours de téléchargement). Comment pouvons-nous résoudre ce problème ? La solution est d’utiliser des « événements ». Voyons comment.

Utilisation d’événements

En utilisant un objet Event, nous pouvons établir une communication entre les threads ; Dans notre cas entre le fil principal et celui que nous utilisons pour effectuer le téléchargement. Un objet « event » est initialisé via la classe Event que nous pouvons importer depuis le module de threading :

from threading import Thread, Event

Comment fonctionne un objet événement ? Un objet Event a un indicateur qui peut être défini sur True via la méthode set, et peut être réinitialisé sur False via la méthode clear ; Son état peut être vérifié via la méthode is_set. La tâche longue exécutée dans la fonction run du thread que nous avons construit pour effectuer le téléchargement, doit vérifier l’état du drapeau avant d’effectuer chaque itération de la boucle while. Voici comment nous changeons notre code. Tout d’abord, nous créons un événement et le lions à une propriété à l’intérieur du constructeur DownloadThread :

class DownloadThread(Thread):
    def __init__(self):
        super().__init__()
        self.read_percentage = 0
        self.event = Event()

Maintenant, nous devons créer une nouvelle méthode dans la classe DownloadThread, que nous pouvons utiliser pour définir l’indicateur de l’événement sur False. Nous pouvons appeler cette méthode stop, par exemple :

def stop(self):
    self.event.set()

Enfin, nous devons ajouter une condition supplémentaire dans la boucle while de la méthode run. La boucle doit être interrompue s’il n’y a plus de morceaux à lire, ou si l’indicateur d’événement est défini :

def run(self):
    [...]

    while True:
        chunk = request.read(chunk_size)
        if not chunk or self.event.is_set():
            break

        [...]

Ce que nous devons faire maintenant, c’est appeler la méthode stop du thread lorsque la fenêtre de l’application est fermée, nous devons donc attraper cet événement.

Protocoles Tkinter

La bibliothèque Tkinter permet de gérer certains événements qui arrivent à l’application à l’aide de protocoles. Dans ce cas, nous voulons effectuer une action lorsque l’utilisateur clique sur le bouton pour fermer l’interface graphique. Pour atteindre notre objectif, nous devons « attraper » l’événement WM_DELETE_WINDOW et exécuter un callback lorsqu’il est déclenché. Dans le constructeur de la classe WordpressDownloader, nous ajoutons le code suivant :

self.protocol('WM_DELETE_WINDOW', self.on_window_delete)

Le premier argument passé à la méthode de protocole est l’événement que nous voulons attraper, le second est le nom du callback qui doit être invoqué. Dans ce cas, le callback est : on_window_delete. Nous créons la méthode avec le contenu suivant :

def on_window_delete(self):
    if self.download_thread.is_alive():
        self.download_thread.stop()
        self.download_thread.join()

    self.destroy()

Comme vous pouvez vous en souvenir, la propriété download_thread de notre classe WordpressDownloader fait référence au fil de discussion que nous avons utilisé pour effectuer le téléchargement. À l’intérieur de la méthode on_window_delete, nous vérifions si le fil a été démarré. Si c’est le cas, on appelle la méthode stop que nous avons vue précédemment, puis la méthode join qui est héritée de la classe Thread. Ce dernier fait en bloque le thread appelant (dans ce cas, le principal) jusqu’à ce que le thread sur lequel la méthode est invoquée se termine. La méthode accepte un argument optionnel qui doit être un nombre à virgule flottante représentant le nombre maximum de secondes pendant lesquelles le thread appelant attendra l’autre (dans ce cas, nous ne l’utilisons pas). Enfin, nous invoquons la méthode destroy sur notre classe WordpressDownloader, qui tue la fenêtre et tous les widgets descendants.

Voici le code complet que nous avons écrit dans ce tutoriel :

#!/usr/bin/env python3

from threading import Thread, Event
from urllib.request import urlopen
from tkinter import Tk, Button
from tkinter.ttk import Progressbar

class DownloadThread(Thread):
    def __init__(self):
        super().__init__()
        self.read_percentage = 0
        self.event = Event()

    def stop(self):
        self.event.set()

    def run(self):
        with urlopen("https://wordpress.org/latest.tar.gz") as request:
            with open('latest.tar.gz', 'wb') as tarball:
                tarball_size = int(request.getheader('Content-Length'))
                chunk_size = 1024
                readed_chunks = 0

                while True:
                    chunk = request.read(chunk_size)
                    if not chunk or self.event.is_set():
                        break


                    readed_chunks += 1
                    self.read_percentage = 100 * chunk_size * readed_chunks / tarball_size

                    tarball.write(chunk)


class WordpressDownloader(Tk):
    def __init__(self, download_thread, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.download_thread = download_thread

        self.title('Wordpress Downloader')
        self.geometry("300x50")
        self.resizable(False, False)

        # The progressbar widget
        self.progressbar = Progressbar(self)
        self.progressbar.pack(fill='x', padx=10)

        # The button widget
        self.button = Button(self, text='Download', command=self.handle_download)
        self.button.pack(padx=10, pady=3, anchor='e')

        self.download_thread = download_thread
        self.protocol('WM_DELETE_WINDOW', self.on_window_delete)

    def update_progress_bar(self):
        if self.download_thread.is_alive():
            self.progressbar.config(value=self.download_thread.read_percentage)
            self.after(100, self.update_progress_bar)

    def handle_download(self):
      self.download_thread.start()
      self.update_progress_bar()

    def on_window_delete(self):
        if self.download_thread.is_alive():
            self.download_thread.stop()
            self.download_thread.join()

        self.destroy()


if __name__ == '__main__':
    download_thread = DownloadThread()
    app = WordpressDownloader(download_thread)
    app.mainloop()

Ouvrons un émulateur de terminal et lançons notre script Python contenant le code ci-dessus. Si nous fermons maintenant la fenêtre principale alors que le téléchargement est toujours en cours, l’invite du shell revient, acceptant les nouvelles commandes.

Résumé

Dans ce tutoriel, nous avons construit une application graphique complète en utilisant Python et la bibliothèque Tkinter en utilisant une approche orientée objet. Au cours du processus, nous avons vu comment utiliser les threads pour effectuer des opérations de longue durée sans bloquer l’interface, comment utiliser des événements pour permettre à un thread de communiquer avec un autre, et enfin, comment utiliser les protocoles Tkinter pour effectuer des actions lorsque certains événements d’interface sont déclenchés.

Articles connexes: