Tkinter GUI einfrieren verhindern

Fragen zu Tkinter.
Antworten
Welpe
User
Beiträge: 26
Registriert: Mittwoch 30. Dezember 2020, 10:39

Moin,

ich möchte in Tkinter mit einem Button eine Funktion aufrufen. Während diese aufgerufene Funktion läuft, ist meine GUI eingefroren. Um das zu verhindern habe ich den Funktionsaufruf in einen Thread gepackt. Das funktioniert wunderbar, allerdings nur ein Mal. Drücke ich den Button ein zweites Mal, bekomme ich die Fehlermeldung: RuntimeError: threads can only be started once.
Was muß ich tun um den Button auch mehrmals drücken zu können?

button1 -> GUI für 5 Sekunden eingefroren
button2 -> GUI läuft weiter und ich kann wärend die 5 Sekunden laufen den button3 klicken, allerdings nach Ablauf der 5 Sekunden kein zweites Mal

Ich weiß, Code auf Modulebene und kein pep8 aber ist halt nur ein Beispiel.

Code: Alles auswählen

from tkinter import Tk, Label, Button
from threading import Thread
import time
from random import randint

root = Tk()
root.geometry('500x400')


def fuenf_sekunden():
    label.config(text='5 Sekunden laufen...')
    time.sleep(5)
    label.config(text='Ich habe fertig!')


def zufallszahl():
    random_label.config(text=f'Zufällige Zahl {randint(1, 100)}')


label = Label(root, text='Hello World')
label.pack(pady=20)

button1 = Button(root, text='5 Sekunden ohne', command=fuenf_sekunden)
button1.pack(pady=20)

button2 = Button(root, text='5 Sekunden mit', command=Thread(target=fuenf_sekunden).start)
button2.pack(pady=20)

button3 = Button(root, text='Zufällige Zahl ausgeben', command=zufallszahl)
button3.pack(pady=20)

random_label = Label(root, text='')
random_label.pack(pady=20)
  
root.mainloop()
Benutzeravatar
__blackjack__
User
Beiträge: 12984
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Welpe: Der Code mit dem Thread hat noch ein anderes Problem: Der verändert die GUI, das sollte man aber nur aus dem Thread heraus machen in dem die GUI-Hauptschleife läuft. Das kann funktionieren, das kann aber auch die GUI/den Tcl-Interpreter in einen inkonsistenten Zustand bringen.

Am einfachsten wäre es wenn Du `after()` verwenden könntest. Also in diesem Beispiel geht das auf jeden Fall, denn da wird ja nur gewartet.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Welpe
User
Beiträge: 26
Registriert: Mittwoch 30. Dezember 2020, 10:39

Das mit dem Zustand des Interpreters wusste ich nicht, wieder etwas gelernt.
Ich habe mich ein wenig belesen über die after() Funktion. Sie ersetzt quasi das Time Modul, richtig?
Es geht mir nicht um das warten in der Funktion an sich, sondern das ich während des Wartens nichts anderes machen kann, da die GUI in der Wartezeit eingefroren ist. Das sleep() bzw after() sollte nur simulieren, das die Funktion eine Weile brauch um ihre Aufgabe zu erledigen. Um auf das Beispiel zurückzukommen, ich möchte halt den Button "5 Sekunden ohne" drücken und danach den Button "Zufällige Zahl ausgeben", was aber nicht möglich ist solange die fuenf_sekunden() Funktion läuft.

In meinem Programm läd die fuenf_sekunden() Funktion Informationen in Form von Text mit Bildern und das kann unterschiedlich lange dauern. In der Zeit kann ich aber nichts anderes machen und das stört mich halt. Mit Thread kann ich das umgehen allerdings nur einmal, denn beim erneuten Aufruf der fuenf_sekunden() bekomme ich die oben genannte Fehlermeldung. Kann man mein Problem verstehen?
Sirius3
User
Beiträge: 17703
Registriert: Sonntag 21. Oktober 2012, 17:20

Du erzeugst einmal beim Aufbau der GUI einen Thread und den kann man nur einmal starten. Du musst bei jedem Tastendruck den Thead starten. Der darf aber dann nicht auf die GUI zugreifen. Du musst also mindestens noch mit after prüfen, ob der Thead noch läuft.
Benutzeravatar
__blackjack__
User
Beiträge: 12984
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Welpe: Die Meldung ist ja recht eindeutig: Man kann `Thread`-Objekte nur einmal starten. Du darfst halt nicht *ein* `Thread`-Objekt erstellen und auf dem immer wieder `start()` aufrufen, sondern musst jedes mal einen neuen Thread erstellen.

Kein Code auf Modulebene ist hier ein No-Go und keine Frage davon ob das nur ein Beispiel ist, weil man hier um eine Klasse nicht wirklich sinnvoll herum kommt.

Code: Alles auswählen

#!/usr/bin/env python3
import time
import tkinter as tk
from random import randint
from threading import Event, Thread


class MainWindow(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)

        self.fuenf_sekunden_fertig_event = Event()

        self.label = tk.Label(self, text="Hello World")
        self.label.pack(pady=20)

        self.button = tk.Button(
            self, text="5 Sekunden", command=self.start_fuenf_sekunden
        )
        self.button.pack(pady=20)

        tk.Button(
            self, text="Zufällige Zahl ausgeben", command=self.zufallszahl
        ).pack(pady=20)

        self.random_label = tk.Label(self)
        self.random_label.pack(pady=20)

    def start_fuenf_sekunden(self):
        self.fuenf_sekunden_fertig_event.clear()
        Thread(target=self.fuenf_sekunden, daemon=True).start()
        self.button.config(state=tk.DISABLED)
        self.label.config(text="5 Sekunden laufen...")
        self.warte_auf_ende()

    def fuenf_sekunden(self):
        time.sleep(5)
        self.fuenf_sekunden_fertig_event.set()

    def warte_auf_ende(self):
        if self.fuenf_sekunden_fertig_event.is_set():
            self.label.config(text="Ich habe fertig!")
            self.button.config(state=tk.NORMAL)
        else:
            self.after(500, self.warte_auf_ende)

    def zufallszahl(self):
        self.random_label.config(text=f"Zufällige Zahl {randint(1, 100)}")


def main():
    window = MainWindow()
    window.mainloop()


if __name__ == "__main__":
    main()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Welpe
User
Beiträge: 26
Registriert: Mittwoch 30. Dezember 2020, 10:39

Ich danke euch für die Hilfe und Geduld. Das mit dem 'Thread'-Objekt habe ich jetzt verstanden. Den Code mit der Klasse schau ich mir in Ruhe an denn den richtigen Durchblick habe ich bei OOP leider noch nicht.
Antworten