Countdown in einem Label

Fragen zu Tkinter.
Benutzeravatar
__blackjack__
User
Beiträge: 14085
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Sam123: Du hast da einfach Glück das beide `Canvas`-Objekte die gleichen IDs für die Elemente liefern. Da sollte man ich aber nicht drauf verlassen. Ich würde das über Tags lösen, oder eine eigene Klasse für die Würfeldarstellung wo dann gleiche Attributnamen nicht zwingend gleiche Werte haben müssen.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Sam123
User
Beiträge: 35
Registriert: Freitag 20. November 2020, 12:25

@__blackjack__: Vielen Dank für deine Tips! Ich würde gern beide Methoden ausprobieren, mit Tags und eigener Klasse. Aber auch nach dem googeln usw. hab ich momentan gar keinen Plan, wie du das hier mit Tags meinst. Könntest du evtl. ein kleines Beispiel geben?
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Hast du auch mal probiert das zu googeln? Das ist der erste Treffer: https://tkdocs.com/tutorial/canvas.html Mit einer eigenen Sektion zu tags: https://tkdocs.com/tutorial/canvas.html#tags
Sam123
User
Beiträge: 35
Registriert: Freitag 20. November 2020, 12:25

Hallo zusammen,

ich hab folgenden einfachen Code, der mir einfach das Würfeln simulieren soll und nach Zufallsprinzip eins von sechs Würfelbildern mit den entsprechenden Augenzahlen in einem Label anzeigen soll. Und das als eine Endlosschleife. Wenn ich auf den Button drücke, wechselt das Bild nur einmal, die Funktionl läuft 988 mal durch, danach kommen dutzend Fehlermeldungen und als letztes wird angezeigt: RecursionError: maximum recursion depth exceeded while calling a Python object. Leider komme ich selber nicht weiter und wäre für Hilfe sehr dankbar.

Code: Alles auswählen

import tkinter
from functools import partial
import random


IMAGES = [
    "wuerfel_ein_auge.gif",
    "wuerfel_zwei_augen.gif",
    "wuerfel_drei_augen.gif",
    "wuerfel_vier_augen.gif",
    "wuerfel_fuenf_augen.gif",
    "wuerfel_sechs_augen.gif",
]


def next_wuerfel(hauptfenster, label, images):
    label["image"] = random.choice(images)
    hauptfenster.after(1000, next_wuerfel(hauptfenster, label, images))


def main():
    hauptfenster = tkinter.Tk()

    images = [tkinter.PhotoImage(file=image) for image in IMAGES]
    label_bild = tkinter.Label(hauptfenster, image=random.choice(images))
    label_bild.pack()
    tkinter.Button(hauptfenster, text = "Next", command = partial(next_wuerfel, hauptfenster, label_bild, images)).pack()

    hauptfenster.mainloop()

if __name__ == '__main__':
    main()
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du darfst ja auch keine Rekursion starten, sondern next_wuerfel nochmal schedulen. Wie schon im Button-Command einfach mit partial wieder eine Funktion mit all ihren Argumenten erzeugen, die dann ohne Argumente einfach von after aufgerufen wird. Genauso wie der Button.
Sirius3
User
Beiträge: 18289
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sam123: Du warst doch am 4. Januar schon deutlich weiter?
@__deets__: bei after braucht man kein partial, weil man `after` einfach die Parameter für die Funktion mitgeben kann.
Sirius3
User
Beiträge: 18289
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sam123: Du warst doch am 4. Januar schon deutlich weiter?
@__deets__: bei after braucht man kein partial, weil man `after` einfach die Parameter für die Funktion mitgeben kann.
Sam123
User
Beiträge: 35
Registriert: Freitag 20. November 2020, 12:25

Das untere Beispiel ist doch absolut gleichwertig, oder etwa nicht. Nur, dass ich dem Label statt Bilder Zahlen zuweise. Und da funtkioniert das doch.
Also irgendwie verstehe ich das nicht. Ich möchte einfach, dass wenn ich auf den Button klicke, mir die Bilder nach dem Zufallsprinzip ausgegeben werden, bis ich das Finster irgendwann schließe.

Code: Alles auswählen

import tkinter as tk
from functools import partial

def countdown(root, counter, label):
    counter -= 1
    label["text"] = counter
    root.after(1000, countdown, root, counter, label)

def main():
    root = tk.Tk()
    counter = 10
    textlabel = tk.Label(root, text = "10", font=("Arial", 36))
    textlabel.pack()
    tk.Button(root, text="Countdown", command=partial(countdown, root, counter, textlabel)).pack()
    root.mainloop()

if __name__ == '__main__':
    main()
Sam123
User
Beiträge: 35
Registriert: Freitag 20. November 2020, 12:25

@Sirius3: Von der Quelltextmenge und dem Ergebnis war ich weiter. Vom wirklichen Verständnis her leider nicht.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

root.after(1000, countdown, root, counter, label)


Ist NICHT was du oben in deinem nicht laufenden Beispiel gemacht hast. Was da der Unterschied ist, muss dir nachdrücklich bewusst werden.
Sam123
User
Beiträge: 35
Registriert: Freitag 20. November 2020, 12:25

Hallo zusammen,
ich hab das kleine Programm unten geschrieben. Und es funktioniert auch.

Code: Alles auswählen

import tkinter
import random


def start_wuerfeln():
    global running
    running = True
    wuerfeln()


def wuerfeln():
    global running
    if (running == True):
        zufallszahl_wuerfel_1 = random.randint(1, 6)
        label_wuerfel_1["text"] = " " + str(zufallszahl_wuerfel_1) + " "
        hauptfenster.after(500, wuerfeln)


def stop_wuerfeln():
    global running
    running = False


running = False
hauptfenster = tkinter.Tk()
label_wuerfel_1 = tkinter.Label(hauptfenster, text=" 0 ", bg="yellow", font="Arial 44", borderwidth=5, relief="groove")
label_wuerfel_1.pack()
tkinter.Button(hauptfenster, text="Start", command=start_wuerfeln).pack()
tkinter.Button(hauptfenster, text="Stop", command=stop_wuerfeln).pack()
hauptfenster.mainloop()

Nun wollte ich das Programm mit einer main()-Funktion versehen, s.u. Es läuft auch, aber irgendwie nicht richtig. Denn beim Stop-Drücken würfelt er trotzdem weiter. Weiss jemand, was der Fehler ist? (Ich weiss, globale Variablen usw sind böse. Alles nur zu Übungszwecken.).

Code: Alles auswählen

import tkinter
import random
from functools import partial


def start_wuerfeln(hauptfenster, label_wuerfel_1):
    running = True
    wuerfeln(running, hauptfenster, label_wuerfel_1)


def wuerfeln(running, hauptfenster, label_wuerfel_1):
    print(running)
    if (running == True):
        zufallszahl_wuerfel_1 = random.randint(1, 6)
        label_wuerfel_1["text"] = " " + str(zufallszahl_wuerfel_1) + " "
        hauptfenster.after(500, wuerfeln, running, hauptfenster, label_wuerfel_1)


def stop_wuerfeln(hauptfenster, label_wuerfel_1):
    running = False
    wuerfeln(running, hauptfenster, label_wuerfel_1)


def main():
    hauptfenster = tkinter.Tk()
    label_wuerfel_1 = tkinter.Label(hauptfenster, text=" 0 ", bg="yellow", font="Arial 44", borderwidth=5, relief="groove")
    label_wuerfel_1.pack()
    tkinter.Button(hauptfenster, text="Start", command=partial(start_wuerfeln, hauptfenster, label_wuerfel_1)).pack()
    tkinter.Button(hauptfenster, text="Stop", command=partial(stop_wuerfeln, hauptfenster, label_wuerfel_1)).pack()
    hauptfenster.mainloop()


if __name__ == '__main__':
    main()

Benutzeravatar
__blackjack__
User
Beiträge: 14085
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

In `stop_wuerfeln()` rufst Du `wuerfeln()` auf was einfach nichts macht, weil `running` den Wert `False` hat. Dieser Aufruf hat keinen Einfluss auf die anderen Aufrufe, denn das `running` ist ein anderes. Das ist eine lokaler Name der nichts mit den anderen Aufrufen zu tun hat.

Du brauchst eine Klasse um Dir den Zustand zu merken. Auch nicht unbedingt als Flag, sondern besser als `None` oder die ID des nächsten Ereignisses das mit `after()` geplant wurde. Denn damit und `after_cancel()` kann man das abbrechen/wiederrufen.

Namen sollte man nicht nummerieren. Ist hier auch total überflüssig eine 1 an die Namen zu hängen. Es gibt ja nicht mal eine andere Zahl.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Sirius3
User
Beiträge: 18289
Registriert: Sonntag 21. Oktober 2012, 17:20

Dafür brauchst Du eine Klasse:

Code: Alles auswählen

import tkinter
import random


class Hauptfenster(tkinter.Tk):
    def __init__(self):
        tkinter.Tk.__init__(self)
        self.wuerfel_id = None
        self.wuerfel_wert = tkinter.IntVar(self, 0)
        tkinter.Label(self, textvariable=self.wuerfel_wert,
            bg="yellow", font="Arial 44", borderwidth=5,
            relief="groove").pack()
        tkinter.Button(self, text="Start", command=self.start_wuerfeln).pack()
        tkinter.Button(self, text="Stop", command=self.stop_wuerfeln).pack()

    def start_wuerfeln(self):
        self.wuerfel_id = self.after(0, self.wuerfeln)

    def stop_wuerfeln(self):
        if self.wuerfel_id is not None:
            self.after_cancel(self.wuerfel_id)
            self.wuerfel_id = None

    def wuerfeln(self):
        self.wuerfel_wert.set(random.randint(1, 6))
        self.wuerfel_id = self.after(500, self.wuerfeln)


def main():
    hauptfenster = Hauptfenster()
    hauptfenster.mainloop()


if __name__ == '__main__':
    main()
Sam123
User
Beiträge: 35
Registriert: Freitag 20. November 2020, 12:25

Vielen Dank für euere Antworten!
Ich bin jedoch erstmal eine Stufe runtergegangen und hab versucht, das einfache Beispiel für die Konsole zu schreiben s.u.
In der GUI kann ich eine Funktion immer wieder aufrufen während sie läuft. Hier funktioniert das nicht, die Funktion start_wuerfeln() blockiert dann das Programm und die weitere Ausführung. Gibt es eine Möglichkeit die Funktion zu unterbrechen, wenn ich z.B. eine betimmte Taste drücke und dann weiter laufen lassen mit einer anderen Taste?

Code: Alles auswählen

import random
from time import sleep


def start_wuerfeln(running):
    running = True
    wuerfeln(running)


def wuerfeln(running):
    if (running == True):
        zufallszahl = random.randint(1,6)
        print(zufallszahl)
        sleep(0.5)
        wuerfeln(running)


def stop_wuerfeln():
    running = False
    wuerfeln(running)


def main():
    running = False
    start_wuerfeln(running)
    sleep(5)
    stop_wuerfeln()


if __name__ == '__main__':
    main()
Benutzeravatar
sparrow
User
Beiträge: 4540
Registriert: Freitag 17. April 2009, 10:28

@Sam123: Vollzieh mal nach, was dein Programm tut.

In start_wuerfeln bindest du True an den Namen running. running übergibst du an wuefeln. In wuerfeln prruefst du, ob running wahr ist (ist es, wie wir gerade festgetellt haben), dann gibst du eine Zufallszahl aus, legst das Programm eine halbe Sekunde schlafen und rufst dann wieder wuerfeln auf.
Das erneute Aufrufen von wuerfeln wäre auch in einer GUI ein falscher rekursiver Aufruf. Warum sollte sich wuerfeln selbst aufrufen?
Und da auch bei dem rekursiven Aufruf die Umstände nicht geändert haben, kommst du wieder zu der Bedingung, ob running wahr ist ... und da das wahr ist geht es wieder den selben Gang.

Vielleicht kannst du kurz beschreiben, was genau du erreichen willst, dann kann man dir auch einen möglichen aufbau an die Hand geben. Dein Versuch lässt da viele Interpretationsmöglichkeiten.
Sam123
User
Beiträge: 35
Registriert: Freitag 20. November 2020, 12:25

ich denke, auf dem Level ist mir schon klar, was das Programm macht. Warum eigentlich falscher rekursiver Ausdruck? In den Beispielen davor (z.B. vom 3. Februar, s.u.) hat sich die Funktion doch auch immer wieder selbst aufgerufen, mit after. Ist es keine Rekursion?

Code: Alles auswählen

def wuerfeln(self):
        self.wuerfel_wert.set(random.randint(1, 6))
        self.wuerfel_id = self.after(500, self.wuerfeln)

Wenn ich das mit einer while-Schleife mache, dann habe ich keine Rekursion. Aber kann ich die Funktion wuerfeln() irgendwie stoppen, wenn sie erstmal läuft, running = False übergeben? Und dann evtl. wieder starten, wie bei der GUI mit den Buttons "Start" und "Stop".
Ich will später ein umfangreicheres Würfelspiel programmieren. Aber jetzt versuche ich erstmal, Python ein bischen besser zu vestehen :)

Code: Alles auswählen

import random
from time import sleep


def start_wuerfeln(running):
    running = True
    wuerfeln(running)


def wuerfeln(running):
    while(running == True):
        zufallszahl = random.randint(1,6)
        print("*****")
        print("* " + str(zufallszahl)  +" *")
        print("*****\n")
        sleep(0.5)


def stop_wuerfeln():
    running = False
    wuerfeln(running)


def main():
    running = False
    start_wuerfeln(running)
    stop_wuerfeln()


if __name__ == '__main__':
    main()
Sirius3
User
Beiträge: 18289
Registriert: Sonntag 21. Oktober 2012, 17:20

Nein, das ist keine Rekursion, die Ausführung wird immer wieder an den Mainloop zurückgegeben.
Ein Konsolenprogramm sieht normalerweise keine solche Interaktion vor. Da braucht man dann wieder Zusatzpakete für Text-UIs und dann bist Du bei einer GUI besser aufgehoben.
Antworten