fbpx

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

Inhalt
    Add a header to begin generating the table of contents

    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.

    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 🙂

    Ingo Janßen

    Ingo Janßen

    Lerne nicht einfach programmieren. Löse Probleme und automatisiere Aufgaben!

    Das könnte dich auch interessieren

    Nach oben scrollen
    Newsletter Popup Form

    Keine Inhalte mehr verpassen?

    Melde dich direkt für den "Code-Kompass" an und erhalte nützliche Tipps und Informationen direkt in deinen Posteingang.