Tutorial 8 - Glatt machen

Wenn Sie keine wirklich schnelle Internetverbindung haben, werden Sie vielleicht feststellen, dass die grafische Benutzeroberfläche Ihrer Anwendung beim Drücken der Schaltfläche kurzzeitig blockiert wird. Das liegt daran, dass die Webanforderung, die wir gestellt haben, synchron ist. Wenn unsere Anwendung die Webanforderung stellt, wartet sie, bis die API eine Antwort zurückgibt, bevor sie fortfährt. Während sie wartet, erlaubt sie der Anwendung nicht, neu zu zeichnen - und das Ergebnis ist, dass die Anwendung blockiert.

GUI-Ereignis-Schleifen

Um zu verstehen, warum das so ist, müssen wir uns mit den Details der Funktionsweise einer GUI-Anwendung befassen. Die Einzelheiten variieren je nach Plattform, aber die grundlegenden Konzepte sind dieselben, unabhängig von der Plattform oder der GUI-Umgebung, die Sie verwenden.

Eine GUI-Anwendung besteht im Grunde aus einer einzigen Schleife, die etwa so aussieht:

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

Diese Schleife wird Ereignisschleife genannt. (Dies sind keine tatsächlichen Methodennamen - es ist eine Veranschaulichung dessen, was im „Pseudocode“ vor sich geht).

Wenn Sie auf eine Schaltfläche klicken, eine Bildlaufleiste ziehen oder eine Taste drücken, erzeugen Sie ein „Ereignis“. Dieses „Ereignis“ wird in eine Warteschlange gestellt, und die Anwendung wird die Warteschlange von Ereignissen verarbeiten, wenn sie das nächste Mal die Gelegenheit dazu hat. Der Benutzercode, der als Reaktion auf das Ereignis ausgelöst wird, wird Ereignishandler genannt. Diese Event-Handler werden als Teil des process_events() Aufrufs aufgerufen.

Sobald eine Anwendung alle verfügbaren Ereignisse verarbeitet hat, wird sie die grafische Benutzeroberfläche neu zeichnen(). Dabei werden alle Änderungen berücksichtigt, die Ereignisse an der Anzeige der Anwendung verursacht haben, sowie alles andere, was im Betriebssystem vor sich geht - zum Beispiel können die Fenster einer anderen Anwendung einen Teil des Fensters unserer Anwendung verdecken oder sichtbar machen, und die Neuzeichnung unserer Anwendung muss den Teil des Fensters widerspiegeln, der gerade sichtbar ist.

Ein wichtiges Detail: Während eine Anwendung ein Ereignis verarbeitet, kann sie nicht neu zeichnen und kann sie keine anderen Ereignisse verarbeiten.

Das bedeutet, dass jede in einem Event-Handler enthaltene Benutzerlogik schnell abgeschlossen werden muss. Jede Verzögerung bei der Fertigstellung des Event-Handlers wird vom Benutzer als Verlangsamung (oder Stopp) der GUI-Aktualisierungen wahrgenommen. Wenn diese Verzögerung lang genug ist, kann Ihr Betriebssystem dies als Problem melden - die macOS-„Beachball“- und Windows-„Spinner“-Symbole sind das Betriebssystem, das Ihnen mitteilt, dass Ihre Anwendung in einem Event-Handler zu lange braucht.

Einfache Operationen wie „Aktualisieren eines Etiketts“ oder „Neuberechnung der Gesamtsumme der Eingaben“ sind leicht und schnell zu erledigen. Es gibt jedoch eine Reihe von Vorgängen, die nicht schnell erledigt werden können. Wenn Sie eine komplexe mathematische Berechnung durchführen, alle Dateien in einem Dateisystem indizieren oder eine große Netzwerkanforderung ausführen, können Sie das nicht „einfach schnell erledigen“ - die Vorgänge sind von Natur aus langsam.

Wie können wir also langlebige Operationen in einer GUI-Anwendung durchführen?

Asynchrone Programmierung

Was wir brauchen, ist eine Möglichkeit, einer Anwendung in der Mitte eines langlebigen Event-Handlers mitzuteilen, dass es in Ordnung ist, die Kontrolle vorübergehend an die Event-Schleife zurückzugeben, solange wir dort fortfahren können, wo wir aufgehört haben. Es liegt an der Anwendung, zu bestimmen, wann diese Freigabe erfolgen kann; aber wenn die Anwendung die Kontrolle regelmäßig an die Ereignisschleife freigibt, können wir einen lang laufenden Event-Handler haben und eine reaktionsfähige Benutzeroberfläche beibehalten.

Wir können dies mit Hilfe der asynchronen Programmierung erreichen. Asynchrone Programmierung ist eine Art, ein Programm zu beschreiben, das es dem Interpreter erlaubt, mehrere Funktionen gleichzeitig auszuführen und die Ressourcen zwischen allen gleichzeitig laufenden Funktionen zu teilen.

Asynchrone Funktionen (so genannte Co-Routinen) müssen ausdrücklich als asynchron deklariert werden. Sie müssen auch intern erklären, wann die Möglichkeit besteht, den Kontext zu einer anderen Co-Routine zu wechseln.

In Python wird die asynchrone Programmierung mit den Schlüsselwörtern async und await, und dem Modul asyncio in der Standardbibliothek implementiert. Mit dem Schlüsselwort async können wir eine Funktion als asynchrone Co-Routine deklarieren. Mit dem Schlüsselwort await kann man angeben, wann die Möglichkeit besteht, den Kontext zu einer anderen Co-Routine zu wechseln. Das Modul asyncio bietet einige andere nützliche Werkzeuge und Primitive für asynchrone Programmierung.

Asynchronität des Tutorials

Um unser Tutorial asynchron zu machen, ändern Sie den „say_hello()“-Ereignishandler so, dass er wie folgt aussieht:

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"],
    )

In diesem Code gibt es nur 4 Änderungen gegenüber der vorherigen Version:

  1. Die Methode ist als async def definiert, anstatt nur als def. Dies sagt Python, dass die Methode eine asynchrone Co-Routine ist.

  2. Der Client, der erzeugt wird, ist ein asynchroner AsyncClient(), anstatt eines synchronen Client(). Dies teilt httpx mit, dass es im asynchronen Modus und nicht im synchronen Modus arbeiten soll.

  3. Der zur Erstellung des Clients verwendete Kontextmanager ist als async markiert. Dies sagt Python, dass es eine Möglichkeit gibt, die Kontrolle abzugeben, wenn der Kontextmanager betreten und verlassen wird.

  4. Der get-Aufruf wird mit einem await-Schlüsselwort versehen. Dies weist die Anwendung an, dass sie die Kontrolle an die Ereignisschleife abgeben kann, während wir auf die Antwort des Netzwerks warten.

Toga erlaubt es Ihnen, reguläre Methoden oder asynchrone Co-Routinen als Handler zu verwenden; Toga verwaltet alles hinter den Kulissen, um sicherzustellen, dass der Handler wie gewünscht aufgerufen oder erwartet wird.

Wenn Sie diese Änderungen speichern und die Anwendung erneut ausführen (entweder mit briefcase dev im Entwicklungsmodus oder indem Sie die gepackte Anwendung aktualisieren und erneut ausführen), wird es keine offensichtlichen Änderungen an der Anwendung geben. Wenn Sie jedoch auf die Schaltfläche klicken, um den Dialog auszulösen, können Sie eine Reihe von subtilen Verbesserungen feststellen:

  • Die Schaltfläche kehrt in einen „nicht angeklickten“ Zustand zurück, anstatt in einem „angeklickten“ Zustand zu verharren.

  • Das „Beachball“/“Spinner“-Symbol wird nicht angezeigt

  • Wenn Sie das Anwendungsfenster verschieben/vergrößern, während Sie darauf warten, dass der Dialog erscheint, wird das Fenster neu gezeichnet.

  • Wenn Sie versuchen, ein Anwendungsmenü zu öffnen, wird das Menü sofort angezeigt.

Nächste Schritte

Wir haben jetzt eine Anwendung, die glatt und reaktionsschnell ist, auch wenn sie auf eine langsame API wartet. Aber wie können wir sicherstellen, dass die Anwendung auch weiterhin funktioniert, wenn wir sie weiter entwickeln? Wie können wir unsere Anwendung testen? Schauen Sie sich Tutorial 9 an, um das herauszufinden…