So erstellen Sie eine Tkinter-Anwendung mit einem objektorientierten Ansatz
In einem früheren Tutorial haben wir die grundlegenden Konzepte hinter der Verwendung von Tkinter gesehen, einer Bibliothek, die zum Erstellen grafischer Benutzeroberflächen mit Python verwendet wird. In diesem Artikel sehen wir, wie Sie eine vollständige, wenn auch einfache Anwendung erstellen. Dabei lernen wir, wie man Threads verwendet, um lang laufende Aufgaben zu bewältigen, ohne die Schnittstelle zu blockieren, wie man eine Tkinter-Anwendung mit einem objektorientierten Ansatz organisiert und wie man Tkinter-Protokolle verwendet.
In diesem Tutorial lernen Sie:
- So organisieren Sie eine Tkinter-Anwendung mit einem objektorientierten Ansatz
- Verwenden von Threads zum Vermeiden einer Blockierung der Anwendungsschnittstelle
- Verwenden der Kommunikation von Threads mithilfe von Ereignissen
- So verwenden Sie Tkinter-Protokolle
Verwendete Softwareanforderungen und Konventionen
Einleitung
In diesem Tutorial werden wir eine einfache Anwendung programmieren, die aus zwei Widgets besteht: einer Schaltfläche und einem Fortschrittsbalken. Was unsere Anwendung tun wird, ist einfach den Tarball herunterzuladen, der die neueste Wordpress-Version enthält, sobald der Benutzer auf die Schaltfläche "Herunterladen" klickt. Das Fortschrittsbalken-Widget wird verwendet, um den Download-Fortschritt zu verfolgen. Die Anwendung wird unter Verwendung eines objektorientierten Ansatzes codiert. Im Laufe des Artikels gehe ich davon aus, dass der Leser mit den Grundbegriffen von OOP vertraut ist.
Organisieren der Anwendung
Das allererste, was wir tun müssen, um unsere Anwendung zu erstellen, ist das Importieren der benötigten Module. Für den Anfang müssen wir importieren:
- Die Basisklasse Tk
- Die Button-Klasse, die wir instanziieren müssen, um das Button-Widget zu erstellen
- Die Progressbar-Klasse, die wir benötigen, um das Fortschrittsbalken-Widget zu erstellen
Die ersten beiden können aus dem tkinter-Modul importiert werden, während das letztere, Progressbar, im tkinter.ttk-Modul enthalten ist. Öffnen wir unseren bevorzugten Texteditor und beginnen mit dem Schreiben des Codes:
#!/usr/bin/env python3
from tkinter import Tk, Button
from tkinter.ttk import Progressbar
Wir möchten unsere Anwendung als Klasse erstellen, um Daten und Funktionen gut zu organisieren und eine Überlastung des globalen Namespace zu vermeiden. Die Klasse, die unsere Anwendung repräsentiert (nennen wir sie WordpressDownloader), erweitert die Tk-Basisklasse, die, wie wir im vorherigen Tutorial gesehen haben, verwendet wird, um das "root"-Fenster zu erstellen:
class WordpressDownloader(Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title('Wordpress Downloader')
self.geometry("300x50")
self.resizable(False, False)
Schauen wir uns an, was der Code tut, den wir gerade geschrieben haben. Wir haben unsere Klasse als eine Unterklasse von Tk definiert. Im Konstruktor haben wir das übergeordnete Element initialisiert, dann haben wir den Titel und die Geometrie unserer Anwendung festgelegt, indem wir die geerbten Methoden title bzw. geometry aufgerufen haben. Wir haben den Titel als Argument an die title-Methode übergeben und die Zeichenfolge, die die Geometrie angibt, mit der Syntax <with>x<height> als Argument an die geometry-Methode.
Dann setzen wir das Stammfenster unserer Anwendung auf nicht veränderbar. Dies haben wir erreicht, indem wir die Methode resizable aufgerufen haben. Diese Methode akzeptiert zwei boolesche Werte als Argumente: Sie legen fest, ob die Breite und Höhe des Fensters in der Größe geändert werden kann. In diesem Fall haben wir False für beide verwendet.
An dieser Stelle können wir die Widgets erstellen, aus denen unsere Anwendung "bestehen" soll: der Fortschrittsbalken und die Schaltfläche "Herunterladen". Wir fügen unserem Klassenkonstruktor den folgenden Code hinzu (vorheriger Code weggelassen):
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')
Wir haben die Progressbar-Klasse verwendet, um das Fortschrittsbalken-Widget zu erstellen, und dann die pack-Methode für das resultierende Objekt aufgerufen, um ein Minimum an Setup zu erstellen. Wir haben das fill-Argument verwendet, um das Widget die gesamte verfügbare Breite des übergeordneten Fensters (x-Achse) einnehmen zu lassen, und das padx-Argument, um einen Rand von 10 Pixeln vom linken und rechten Rand zu erstellen.
Die Schaltfläche wurde durch Instanziieren der Button-Klasse erstellt. Im Klassenkonstruktor haben wir den text-Parameter verwendet, um den Schaltflächentext festzulegen. Wir haben dann das Button-Layout mit pack eingerichtet: Mit dem anchor-Parameter haben wir deklariert, dass der Button rechts vom Haupt-Widget bleiben soll. Die Ankerrichtung wird mithilfe von Kompasspunkten angegeben. In diesem Fall steht das E für "Ost" (dies kann auch mit Hilfe von Konstanten angegeben werden, die im Tkinter-Modul enthalten sind. In diesem Fall hätten wir zum Beispiel tkinter verwenden können. E). Wir legen auch den gleichen horizontalen Rand fest, den wir für den Fortschrittsbalken verwendet haben.
Bei der Erstellung der Widgets haben wir self als erstes Argument ihrer Klassenkonstruktoren übergeben, um das von unserer Klasse repräsentierte Fenster als übergeordnetes Fenster festzulegen.
Wir haben noch keinen Callback für unseren Button definiert. Schauen wir uns zunächst an, wie unsere Anwendung aussieht. Um dies zu tun, müssen wir den Hauptsentinel an unseren Code anhängen, eine Instanz der WordpressDownloader-Klasse erstellen und die mainloop-Methode darauf aufrufen:
if __name__ == '__main__':
app = WordpressDownloader()
app.mainloop()
An dieser Stelle können wir unsere Skriptdatei ausführbar machen und starten. Angenommen, die Datei heißt app.py, in unserem aktuellen Arbeitsverzeichnis würden wir Folgendes ausführen:
chmod +x app.py
./app.py
Wir sollten folgendes Ergebnis erhalten:
Alles scheint gut. Lassen Sie uns nun unseren Button dazu bringen, etwas zu tun! Wie wir im grundlegenden tkinter-Tutorial gesehen haben, müssen wir, um einer Schaltfläche eine Aktion zuzuweisen, die Funktion, die wir als Callback verwenden möchten, als Wert des Befehlsparameters des Button-Klassenkonstruktors übergeben. In unserer Anwendungsklasse definieren wir die handle_download Methode, schreiben den Code, der den Download durchführt, und weisen dann die Methode als Button-Callback zu.
Um den Download durchzuführen, verwenden wir die urlopen-Funktion, die im Modul urllib.request enthalten ist. Importieren wir es:
from urllib.request import urlopen
So setzen wir die handle_download Methode um:
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)
Der Code in der handle_download Methode ist recht einfach. Wir geben eine get-Anfrage aus, um das neueste Wordpress-Release-Tarball-Archiv herunterzuladen, und wir öffnen/erstellen die Datei, die wir verwenden werden, um den Tarball lokal im wb-Modus (binary-write) zu speichern.
Um unseren Fortschrittsbalken zu aktualisieren, müssen wir die Menge der heruntergeladenen Daten in Prozent erhalten: Um dies zu tun, ermitteln wir zuerst die Gesamtgröße der Datei, indem wir den Wert des Content-Length-Headers lesen und in int umwandeln, dann legen wir fest, dass die Dateidaten in Blöcken von 1024 Bytes gelesen werden sollten. und behalten Sie die Anzahl der Blöcke bei, die wir mit der Variablen read_chunks lesen.
Innerhalb der unendlichen while-Schleife verwenden wir die read-Methode des Anforderungsobjekts, um die Datenmenge zu lesen, die wir mit chunk_size angegeben haben. Wenn die read-Methode einen leeren Wert zurückgibt, bedeutet dies, dass es keine Daten mehr zum Lesen gibt, daher unterbrechen wir die Schleife. Andernfalls aktualisieren wir die Anzahl der gelesenen Chunks, berechnen den Download-Prozentsatz und referenzieren ihn über die Variable read_percentage. Wir verwenden den berechneten Wert, um den Fortschrittsbalken zu aktualisieren, indem wir die Konfigurationsmethode aufrufen. Zum Schluss schreiben wir die Daten in die lokale Datei.
Wir können nun den Callback dem Button zuweisen:
self.button = Button(self, text='Download', command=self.handle_download)
Es sieht so aus, als ob alles funktionieren sollte, aber sobald wir den obigen Code ausführen und auf die Schaltfläche klicken, um den Download zu starten, stellen wir fest, dass es ein Problem gibt: Die GUI reagiert nicht mehr und der Fortschrittsbalken wird auf einmal aktualisiert, wenn der Download abgeschlossen ist. Warum passiert das?
Unsere Anwendung verhält sich so, da die handle_download Methode innerhalb des Hauptthreads ausgeführt wird und die Hauptschleife blockiert: Während des Downloads kann die Anwendung nicht auf Benutzeraktionen reagieren. Die Lösung für dieses Problem besteht darin, den Code in einem separaten Thread auszuführen. Mal sehen, wie es geht.
Verwenden eines separaten Threads zum Ausführen von Vorgängen mit langer Ausführungszeit
Was ist ein Thread? Ein Thread ist im Grunde eine Rechenaufgabe: Durch die Verwendung mehrerer Threads können wir bestimmte Teile eines Programms unabhängig voneinander ausführen lassen. Python macht es sehr einfach, über das Threading-Modul mit Threads zu arbeiten. Das allererste, was wir tun müssen, ist, die Thread-Klasse daraus zu importieren:
from threading import Thread
Um einen Codeblock in einem separaten Thread auszuführen, können wir entweder:
- Erstellen Sie eine Klasse, die die
Thread-Klasseerweitert und dierun-Methodeimplementiert - Geben Sie den Code an, den wir über den
target-ParameterdesThread-Objektkonstruktors ausführen möchten.
Um die Dinge besser zu organisieren, werden wir hier den ersten Ansatz verwenden. So ändern wir unseren Code. Als erstes erstellen wir eine Klasse, die Thread erweitert. Zuerst definieren wir in seinem Konstruktor eine Eigenschaft, die wir verwenden, um den Download-Prozentsatz zu verfolgen, dann implementieren wir die Run-Methode und verschieben den Code, der den Tarball-Download durchführt, darin:
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)
Nun sollten wir den Konstruktor unserer WordpressDownloader-Klasse so ändern, dass er eine Instanz von DownloadThread als Argument akzeptiert. Wir könnten auch eine Instanz von DownloadThread innerhalb des Konstruktors erstellen, aber indem wir es als Argument übergeben, deklarieren wir explizit, dass WordpressDownloader davon abhängt:
class WordpressDownloader(Tk):
def __init__(self, download_thread, *args, **kwargs):
super().__init__(*args, **kwargs)
self.download_thread = download_thread
[...]
Was wir jetzt tun wollen, ist, eine neue Methode zu erstellen, die verwendet wird, um den prozentualen Fortschritt zu verfolgen und den Wert des Fortschrittsbalken-Widgets zu aktualisieren. Wir können es update_progress_bar nennen:
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)
In der update_progress_bar Methode überprüfen wir mit der is_alive Methode, ob der Thread ausgeführt wird. Wenn der Thread ausgeführt wird, aktualisieren wir die Statusleiste mit dem Wert der read_percentage-Eigenschaft des Threadobjekts. Um den Download weiter zu überwachen, verwenden wir danach die Methode after der Klasse WordpressDownloader. Bei dieser Methode wird nach einer bestimmten Anzahl von Millisekunden ein Rückruf ausgeführt. In diesem Fall haben wir es verwendet, um die update_progress_bar Methode nach 100 Millisekunden erneut aufzurufen. Dies wird wiederholt, bis der Thread aktiv ist.
Schließlich können wir den Inhalt der handle_download Methode ändern, die aufgerufen wird, wenn der Benutzer auf die Schaltfläche "Herunterladen" klickt. Da der eigentliche Download in der run-Methode der DownloadThread-Klasse ausgeführt wird, müssen wir hier nur den Thread starten und die update_progress_bar Methode aufrufen, die wir im vorherigen Schritt definiert haben:
def handle_download(self):
self.download_thread.start()
self.update_progress_bar()
An dieser Stelle müssen wir ändern, wie das App-Objekt erstellt wird:
if __name__ == '__main__':
download_thread = DownloadThread()
app = WordpressDownloader(download_thread)
app.mainloop()
Wenn wir nun unser Skript neu starten und den Download starten, können wir sehen, dass die Schnittstelle während des Downloads nicht mehr blockiert wird:
Es gibt jedoch immer noch ein Problem. Um es zu "visualisieren", starten Sie das Skript und schließen Sie das Fenster der grafischen Oberfläche, sobald der Download begonnen, aber noch nicht abgeschlossen ist. Sehen Sie, dass etwas am Terminal hängt? Dies liegt daran, dass der Hauptthread zwar geschlossen wurde, der zum Ausführen des Downloads verwendete Thread jedoch weiterhin ausgeführt wird (Daten werden weiterhin heruntergeladen). Wie können wir dieses Problem lösen? Die Lösung besteht darin, "events" zu verwenden. Mal sehen, wie.
Verwenden von Ereignissen
Durch die Verwendung eines Event-Objekts können wir eine Kommunikation zwischen Threads herstellen. In unserem Fall zwischen dem Hauptthread und dem, den wir zum Ausführen des Downloads verwenden. Ein "event"-Objekt wird über die Event-Klasse initialisiert, die wir aus dem Threading-Modul importieren können:
from threading import Thread, Event
Wie funktioniert ein Ereignisobjekt? Ein Event-Objekt verfügt über ein Flag, das über die set-Methode auf True gesetzt und über die clear-Methode auf False zurückgesetzt werden kann. Sein Status kann über die Methode is_set überprüft werden. Die lange Aufgabe, die in der run-Funktion des Threads ausgeführt wird, den wir zum Ausführen des Downloads erstellt haben, sollte den Flagstatus überprüfen, bevor sie jede Iteration der while-Schleife ausführt. So ändern wir unseren Code. Zuerst erstellen wir ein Ereignis und binden es an eine Eigenschaft innerhalb des DownloadThread-Konstruktors:
class DownloadThread(Thread):
def __init__(self):
super().__init__()
self.read_percentage = 0
self.event = Event()
Jetzt sollten wir eine neue Methode in der DownloadThread-Klasse erstellen, mit der wir das Flag des Ereignisses auf False setzen können. Wir können diese Methode stop nennen, zum Beispiel:
def stop(self):
self.event.set()
Schließlich müssen wir eine zusätzliche Bedingung in der while-Schleife in der run-Methode hinzufügen. Die Schleife sollte unterbrochen werden, wenn keine Chunks mehr zu lesen sind oder wenn das Ereignisflag gesetzt ist:
def run(self):
[...]
while True:
chunk = request.read(chunk_size)
if not chunk or self.event.is_set():
break
[...]
Was wir jetzt tun müssen, ist, die stop-Methode des Threads aufzurufen, wenn das Anwendungsfenster geschlossen wird, also müssen wir dieses Ereignis abfangen.
Tkinter-Protokolle
Die Tkinter-Bibliothek bietet eine Möglichkeit, bestimmte Ereignisse, die mit der Anwendung passieren, mithilfe von Protokollen zu behandeln. In diesem Fall möchten wir eine Aktion ausführen, wenn der Benutzer auf die Schaltfläche klickt, um die grafische Oberfläche zu schließen. Um unser Ziel zu erreichen, müssen wir das WM_DELETE_WINDOW Ereignis "abfangen" und einen Rückruf ausführen, wenn es ausgelöst wird. Innerhalb des WordpressDownloader-Klassenkonstruktors fügen wir den folgenden Code hinzu:
self.protocol('WM_DELETE_WINDOW', self.on_window_delete)
Das erste Argument, das an die Protokollmethode übergeben wird, ist das Ereignis, das wir fangen möchten, das zweite ist der Name des Callbacks, der aufgerufen werden soll. In diesem Fall lautet der Callback: on_window_delete. Wir erstellen die Methode mit folgendem Inhalt:
def on_window_delete(self):
if self.download_thread.is_alive():
self.download_thread.stop()
self.download_thread.join()
self.destroy()
Wie Sie sich erinnern können, verweist die download_thread-Eigenschaft unserer WordpressDownloader-Klasse auf den Thread, den wir zum Ausführen des Downloads verwendet haben. Innerhalb der on_window_delete Methode prüfen wir, ob der Thread gestartet wurde. Wenn dies der Fall ist, rufen wir die stop-Methode auf, die wir zuvor gesehen haben, und dann die join-Methode, die von der Thread-Klasse geerbt wird. Letzteres blockiert den aufrufenden Thread (in diesem Fall den Hauptthread), bis der Thread, auf dem die Methode aufgerufen wird, beendet wird. Die Methode akzeptiert ein optionales Argument, bei dem es sich um eine Gleitkommazahl handeln muss, die die maximale Anzahl von Sekunden darstellt, die der aufrufende Thread auf den anderen wartet (in diesem Fall wird sie nicht verwendet). Schließlich rufen wir die destroy-Methode für unsere WordpressDownloader-Klasse auf, die das Fenster und alle Nachkommen-Widgets beendet.
Hier ist der vollständige Code, den wir in diesem Tutorial geschrieben haben:
#!/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()
Öffnen wir einen Terminal-Emulator und starten unser Python-Skript mit dem obigen Code. Wenn wir nun das Hauptfenster schließen, während der Download noch ausgeführt wird, meldet sich der Shell-Prompt zurück und akzeptiert neue Befehle.
Zusammenfassung
In diesem Tutorial haben wir eine vollständige grafische Anwendung mit Python und der Tkinter-Bibliothek unter Verwendung eines objektorientierten Ansatzes erstellt. Dabei haben wir gesehen, wie man Threads verwendet, um lang laufende Operationen auszuführen, ohne die Schnittstelle zu blockieren, wie man Ereignisse verwendet, um einen Thread mit einem anderen kommunizieren zu lassen, und schließlich, wie man Tkinter-Protokolle verwendet, um Aktionen auszuführen, wenn bestimmte Schnittstellenereignisse ausgelöst werden.


