Python GUI mit Kivy – “Instruction outside the main Kivy thread”

Im letzten Beitrag (und Video) haben wir das Zufügen von Inhalten zu unserer Kivy App in einen eigenen Thread gepackt.
Darauf hin habe ich von euch ein paar Meldungen bekommen.
Die Fehlermeldung “Cannot create graphics instruction outside the main Kivy thread” trat auf und die App ist einfach abgestürzt.

Youtube Kommentar mit dem Fehler
YouTube Kommentar

Das können wir natürlich nicht so stehen lassen.

Unsicher mit Git?
Lade jetzt dein Git CheatSheet herunter!

Kivy Serie

Warum habe ich den Fehler nicht?

Bei mir liefen Anaconda 3.9 und Kivy 2.0
Nachdem ich auf Standard Python 3.10 und Kivy 2.1 umgestiegen bin, kam es auch bei mir zu dem Fehler.

Anaconda muss für das eigene Repository teilweise Pakete anpassen. Ich vermute, hier kam es zu einer Änderung.
Oder der Versionssprung von Kivy 2.0 auf Kivy 2.1 hat eine Änderung eingeführt, die in den Fehler resultiert.
Ich habe mich nicht tiefer damit beschäftigt.

Woher kommt der Fehler?

Die Funktion query_stock_data() haben wir in einen eigenen Thread gepackt.
Sie läuft also unabhängig zu unserem Kivy Thread.
In der Funktion rufen wir wiederum add_ticker_row() auf und fügen damit neue Label zur Kivy App zu.
Und genau das gefällt Kivy nicht.

Damit greifen wir von “außen” auf den Kivy Thread zu und versuchen etwas an ihm zu ändern.
Kivy haut uns dann mit dem Cannot create graphics instruction outside the main Kivy thread auf die Finger.

Wie beheben wir das jetzt?

Die Lösung ist denkbar einfach: Wir müssen Kivy die Änderung vornehmen lassen

Kivy gibt uns mit der Klasse Clock eine Möglichkeit an die Hand.
Damit können wir von außen Funktionsaufrufe einplanen lassen.
Kivy kümmert sich dann darum, die Funktion einzuplanen und auszuführen.

Wir ersetzen also:

def query_stock_data(self):
    symbol = self.ticker_input.text.strip()
    stock = yfinance.Ticker(ticker=symbol)
    data = stock.info
    if 'regularMarketPrice' in data and data.get('regularMarketPrice') is not None:
        price = f'{data.get("regularMarketPrice"):.2f} {data.get("currency")}'
        self.add_ticker_row(data.get('symbol'), data.get('longName'), price)
    else:
        self.message.text = f'Für das angegebene Symbol {symbol} gab es kein Ergebnis.'

Mit einem Aufruf über Clock.schedule_once()

# an den Anfang der Datei zu den Importen
from functools import partial

def query_stock_data(self):
    symbol = self.ticker_input.text.strip()
    stock = yfinance.Ticker(ticker=symbol)
    data = stock.info
    if 'regularMarketPrice' in data and data.get('regularMarketPrice') is not None:
        price = f'{data.get("regularMarketPrice"):.2f} {data.get("currency")}'
        Clock.schedule_once(partial(self.add_ticker_row, data.get('symbol'), data.get('longName'), price), 0.5)
    else:
        self.message.text = f'Für das angegebene Symbol {symbol} gab es kein Ergebnis.'

Was passiert hier?

Die Funktion schedule_once() kennen wir schon aus dem Teil für weitere Ansichten.
Sie nimmt eine Referenz auf eine Funktion und einen timeout entgegen.
Damit gibst du an, welche Funktion du ausführen möchtest und wann sie ausgeführt werden soll.

Bei Funktionsreferenzen gibt es allerdings immer wieder das Problem, dass keine Parameter übergeben werden können.
Das haben wir hier auch. Unsere Funktion add_ticker_row() braucht die Parameter ticker, name, price.
Und für solche Fälle gibt uns Python das Modul functools mit der Funktion partial an die Hand.

Die Funktion partial benötigt als ersten Parameter die Funktionsreferenz. Alle weiteren Parameter werden dann als Parameter an diese Referenz übergeben.
Aus dem Paket baut uns partial dann wiederum eine Funktionsreferenz.

Klingt etwas kompliziert, aber im Grunde packt uns partial einfach nur die Funktionsreferenz mit allen benötigten Parametern zu einem hübschen Paket zusammen.
Und das Paket können wir dann wieder an schedule_once() übergeben.

Ein letzter Schritt

Ein letzter Schritt fehlt uns noch.
Clock.schedule_once() hat die Angewohnheit uns den timeout mit an die Funktion zu übergeben.
Das heißt so wie er jetzt geschrieben ist, fällt uns der Code wieder mit einer Exception auf die Nase:

TypeError: StockView.add_ticker_row() takes 4 positional arguments but 5 were given

Dagegen können wir nicht viel machen.
Als Lösung können wir einfach einen Sammelparameter zu unserer add_ticker_row() Funktion hinzufügen:

def add_ticker_row(self, ticker, name, price, *args):

Durch das *args werden jetzt alle weiteren Parameter einfach in einer Liste gesammelt.
Und schon ist auch der Error beseitigt.

Zusammenfassung

Wir dürfen den main Thread von Kivy nicht einfach von außen manipulieren.
Als Lösung liefert uns Kivy die Klasse Clock, mit der sich Aktionen durch Kivy einplanen und steuern lassen.
Die Funktion schedule_once in Verbindung mit functools.partial ermöglicht die Änderung von außen.

Ich hoffe, der Beitrag konnte dir erklären, warum es zu dem Problem kommt und wie du es lösen kannst.
Sollten trotzdem noch Fragen offen sein, schreib mir hier einfach einen Kommentar oder schau im Discord vorbei.
Ich freu mich auf dich 🙂

Wie kannst du am schnellsten Summen... x
Wie kannst du am schnellsten Summen bilden? Python Built-Ins | #Shorts

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht.

Scroll to Top
ANFORDERN
Anfordern
Bereit zum Ausdrucken | Mit Erklärungen
IMMER ZUR HAND
ANFORDERN
Anfordern
Bereit zum Ausdrucken | Mit Erklärungen
IMMER ZUR HAND
Trag dich schnell ein und bekomme Zugang zum Download!
Gib mir den Download!