Methode von Objekt B aus Objekt A aufrufen funktioniert nicht

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

@__blackjack__: Danke. Genau das habe ich gesucht.

Nach einem Klick auf "OK" im Ergebnisfenster erscheinen viele Fehlermeldungen in der Konsole:

Code: Alles auswählen

Exception in Tkinter callback
Traceback (most recent call last):
  File "/usr/lib/python3.6/tkinter/__init__.py", line 1702, in __call__
    return self.func(*args)
  File "/usr/lib/python3.6/tkinter/__init__.py", line 746, in callit
    func(*args)
  File "/home/ata/source/test.py", line 51, in change_bg_color
    self.canvas.config(bg=self.color)
  File "/usr/lib/python3.6/tkinter/__init__.py", line 1482, in configure
    return self._configure('configure', cnf, kw)
  File "/usr/lib/python3.6/tkinter/__init__.py", line 1473, in _configure
    self.tk.call(_flatten((self._w, cmd)) + self._options(cnf))
_tkinter.TclError: invalid command name ".!toplevel.!canvas"
Zeile 51 funktioniert eigentlich. Trotzdem kommen diese Fehlermeldungen. Mit auskommentierten Zeile 51 erscheinen die Fehlermeldungen nicht mehr, aber der Farbwechsel funktioniert dann leider auch nicht mehr.

Außerdem schließt sich nach dem Klick auf "OK" ebenfalls das Hauptfenster, obwohl es eigentlich offen bleiben sollte.

Gruß
Atalanttore
Benutzeravatar
__blackjack__
User
Beiträge: 13007
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Atalanttore: Das sieht so aus als wenn Du versuchen würdest die Farbe auf dem zusammen mit dem Fenster zerstörten Canvas zu ändern. Nach `destroy()` kannst Du keines der Objekte mehr benutzen die da zerstört wurden, denn auf Tk-Seite gibt es die dann ja nicht mehr.

Die Methode wird ja wegen einem `after()`-Aufruf aufgerufen. Da müsstest Du vor dem zerstören dafür sorgen, dass das nach dem zerstören nicht mehr versucht wird. Schau Dir mal `after_cancel()` an.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

Ich habe nun `after_cancel()` im Code ergänzt. Richtig verstanden habe ich Erklärungen zu `after_cancel()` zwar nicht, aber zumindest die Fehlermeldungen nach einem Klick auf "OK" im Ergebnisfenster sind jetzt anders. :roll:

Code: Alles auswählen

Exception in Tkinter callback
Traceback (most recent call last):
  File "/usr/lib/python3.6/tkinter/__init__.py", line 1702, in __call__
    return self.func(*args)
  File "/usr/lib/python3.6/tkinter/__init__.py", line 746, in callit
    func(*args)
  File "/home/ata/source/test/test.py", line 39, in change_bg_color
    self.game_window.after_cancel(self.change_bg_color())
  File "/home/ata/source/test/test.py", line 39, in change_bg_color
    self.game_window.after_cancel(self.change_bg_color())
  File "/home/ata/source/test/test.py", line 39, in change_bg_color
    self.game_window.after_cancel(self.change_bg_color())
  [Previous line repeated 986 more times]
RecursionError: maximum recursion depth exceeded
Einen RecursionError hatte ich noch.
Warum wird die Methode `self.game_window.after_cancel(self.change_bg_color())` so oft aufgerufen?


Aktueller Code:

Code: Alles auswählen

from tkinter import Tk, Canvas, Toplevel, Frame, Label, Button, Listbox
from random import choice
import time
from statistics import mean



DESIGNATED_COLOR = "red"
SWITCH_TIME = 1000



class Game:
    def __init__(self, main_window):
        self.main_window = main_window
        self.colors = ["green", "red", "blue", "yellow", "black"]
        self.color = None
        self.reaction_times = []
        self.false_positives = 0
        self.color_missed = 0
        self.key_too_often_pressed = 0
        self.stop_test = False
        self.color_locked = False


    def start_game(self):
        self.game_window = Toplevel(self.main_window)
        self.game_window.attributes("-fullscreen", True)
        self.canvas = Canvas(self.game_window,
                             width=self.game_window.winfo_screenwidth(),
                             height=self.game_window.winfo_screenheight(),
                             highlightthickness=0)
        self.canvas.pack()
        self.change_bg_color()


    def change_bg_color(self, color_locked = False):
        if self.stop_test:
            self.game_window.after_cancel(self.change_bg_color()) # Zeile 39 ist hier.
            self.game_window.destroy()


        if not self.color_locked and self.color == DESIGNATED_COLOR:
            self.color_missed += 1

        self.color_locked = color_locked
        current_color = self.color

        while self.color == current_color:
            self.color = choice(self.colors)

        self.canvas.config(bg=self.color)
        self.start_time = time.monotonic() # Ereignis Timer
        self.game_window.after(SWITCH_TIME, self.change_bg_color)


    def key_pressed(self):
        if self.color == DESIGNATED_COLOR:
            if self.color_locked:
                self.key_too_often_pressed += 1
            else:
                reaction_time = time.monotonic() - self.start_time
                self.reaction_times.append(reaction_time)
                self.color_locked = True
        else:
            self.false_positives += 1


    def show_results(self, event=None):
        self.stop_test = True
        results_window = Toplevel(self.main_window)
        results_frame = Frame(results_window)
        results_window.title("Ergebnisse")

        if self.reaction_times:
            # mean() auf Liste ohne Einträge führt zu einem Fehler

            desc_mean_reaction_time = Label(results_frame, text="Gemittelte Reaktionszeit:")
            desc_reaction_times = Label(results_frame, text="Reaktionszeit pro richtiger Farbe:")

            result_mean_reaction_time = Label(results_frame, text=mean(self.reaction_times))
            result_reaction_times = Listbox(results_frame)

            for item in self.reaction_times:
                result_reaction_times.insert('END', item)

            desc_mean_reaction_time.grid(row=0, column=0)
            desc_reaction_times.grid(row=1, column=0)

            result_mean_reaction_time.grid(row=0, column=1)
            result_reaction_times.grid(row=1, column=1)

        desc_too_often = Label(results_frame, text="Bei richtiger Farbe zu oft gedrückt:")
        desc_color_missed = Label(results_frame, text="Bei richtiger Farbe nicht gedrückt:")
        desc_false_positives = Label(results_frame, text="Bei falscher Farbe gedrückt:")

        result_too_often = Label(results_frame, text=self.key_too_often_pressed)
        result_color_missed = Label(results_frame, text=self.color_missed)
        result_false_positives = Label(results_frame, text=self.false_positives)

        desc_too_often.grid(row=2, column=0)
        desc_color_missed.grid(row=3, column=0)
        desc_false_positives.grid(row=4, column=0)

        result_too_often.grid(row=2, column=1)
        result_color_missed.grid(row=3, column=1)
        result_false_positives.grid(row=4, column=1)

        results_frame.grid(row=0, column=0)

        ok_button = Button(results_window, text="OK", padx=10, pady=10, command=results_window.quit)
        ok_button.grid(row=1, column=0)


def main():
    main_window = Tk()

    game = Game(main_window)

    main_window.bind_all("<Escape>", lambda e: game.show_results())
    main_window.bind_all("<space>", lambda e: game.key_pressed()) # Ereignis Tastendruck
    main_window.title("Spiel")
    main_window.geometry('640x480')

    start_button = Button(main_window, text="Start", padx=10, pady=10, command=game.start_game)
    start_button.pack()
    close_button = Button(main_window, text="Schließen", padx=10, pady=10, command=main_window.quit)
    close_button.pack()

    main_window.mainloop()


if __name__ == '__main__':
    main()
Gruß
Atalanttore
Benutzeravatar
__blackjack__
User
Beiträge: 13007
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Atalanttore: Wenn `self.stop_test` wahr ist wird `self.change_bg_color()` aufgerufen, das wenn `self.stop_test` wahr ist `self.change_bg_color()` aufruft, das wenn `self.stop_test` wahr ist `self.change_bg_color()` aufruft, das wenn `self.stop_test` wahr ist `self.change_bg_color()` aufruft, das wenn `self.stop_test` wahr ist `self.change_bg_color()` aufruft, das wenn `self.stop_test` wahr ist `self.change_bg_color()` aufruft, … Du siehst das Problem?

`after_cancel()` erwartet als Argument die ID die beim vom `after()`-Aufruf zurückgegeben wurde. *In* der Rückruffunktion die mit dem `after()` angefordert wurde, ist das aber nicht mehr sinnvoll, denn man kann den Aufruf ja nicht mehr verhindern während er bereits läuft. Die ID ist dann ja nicht mehr gültig. Wenn es funktioniert würde das Zerstören des Fensters in der Rückruffunktion auch reichen, denn zu dem Zeitpunkt kann man sich ja sicher sein, dass kein weiterer Rückruf kommt wenn man nicht erneut einen mit `after()` anfordert. (Das muss man dann verhindern.)
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

@__blackjack__: `after_cancel()` in der Methode `change_bg_color()` war eine schlechte Idee. Das führt dann zu einer Endlosschleife, sobald mittels ESC-Taste `self.stop_test` auf wahr gesetzt wird.

`self.game_window.after_cancel(<ID>)` würde ich jetzt eher am Anfang der Methode `show_results()` platzieren, aber wie kommt man an die ID, die beim Aufruf von `self.game_window.after(SWITCH_TIME, self.change_bg_color)` zurückgegeben wird? Ist mit ID der Wert gemeint, den die Funktion `id()` zurückliefert?

Gruß
Atalanttore
Sirius3
User
Beiträge: 17712
Registriert: Sonntag 21. Oktober 2012, 17:20

`id` ist der Rückgabewert der Funktion `after`. Und man kommt da dran, wenn man sich den Wert beim Aufruf von `after` in einer Variable merkt.
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

@__blackjack__: Okay, jetzt funktioniert `after_cancel()` so wie es soll. Danke für die Erklärung.

Die nächste Macke, die ich dem Programm gerne austreiben würde, ist, dass sich das Programm nach einem Klick auf "OK" im Ergebnisfenster (`results_window`) komplett beendet.

Dieser "OK"-Button wird im Programm mit folgendem Code erzeugt:

Code: Alles auswählen

ok_button = Button(results_window, text="OK", padx=10, pady=10, command=results_window.quit)
Nach meinem Verständnis müsste sich beim Aufruf von `results_window.quit` nur das Ergebnisfenster (`results_window`) schließen und nicht das komplette Programm, aber offensichtlich liege ich da etwas falsch. Wie macht man das richtig?

Aktueller Code:

Code: Alles auswählen

from tkinter import Tk, Canvas, Toplevel, Frame, Label, Button, Listbox
from random import choice
import time
from statistics import mean



DESIGNATED_COLOR = "red"
SWITCH_TIME = 1000



class Game:
    def __init__(self, main_window):
        self.main_window = main_window
        self.colors = ["green", "red", "blue", "yellow", "black"]
        self.color = None
        self.reaction_times = []
        self.false_positives = 0
        self.color_missed = 0
        self.key_too_often_pressed = 0
        self.stop_test = False
        self.color_locked = False
        self.id_value_change_bg_color = 0


    def start_game(self):
        self.game_window = Toplevel(self.main_window)
        self.game_window.attributes("-fullscreen", True)
        self.canvas = Canvas(self.game_window,
                             width=self.game_window.winfo_screenwidth(),
                             height=self.game_window.winfo_screenheight(),
                             highlightthickness=0)
        self.canvas.pack()
        self.change_bg_color()


    def change_bg_color(self, color_locked = False):
        if self.stop_test:
            self.game_window.destroy()


        if not self.color_locked and self.color == DESIGNATED_COLOR:
            self.color_missed += 1

        self.color_locked = color_locked
        current_color = self.color

        while self.color == current_color:
            self.color = choice(self.colors)

        self.canvas.config(bg=self.color)
        self.start_time = time.monotonic() # Ereignis Timer
        self.id_value_change_bg_color = self.game_window.after(SWITCH_TIME, self.change_bg_color)


    def key_pressed(self):
        if self.color == DESIGNATED_COLOR:
            if self.color_locked:
                self.key_too_often_pressed += 1
            else:
                reaction_time = time.monotonic() - self.start_time
                self.reaction_times.append(reaction_time)
                self.color_locked = True
        else:
            self.false_positives += 1


    def show_results(self, event=None):
        self.game_window.after_cancel(self.id_value_change_bg_color)
        self.stop_test = True
        results_window = Toplevel(self.main_window)
        results_frame = Frame(results_window)
        results_window.title("Ergebnisse")

        if self.reaction_times:
            # mean() auf Liste ohne Einträge führt zu einem Fehler

            desc_mean_reaction_time = Label(results_frame, text="Gemittelte Reaktionszeit:")
            desc_reaction_times = Label(results_frame, text="Reaktionszeit pro richtiger Farbe:")

            result_mean_reaction_time = Label(results_frame, text=mean(self.reaction_times))
            result_reaction_times = Listbox(results_frame)

            for item in self.reaction_times:
                result_reaction_times.insert('end', item)

            desc_mean_reaction_time.grid(row=0, column=0)
            desc_reaction_times.grid(row=1, column=0)

            result_mean_reaction_time.grid(row=0, column=1)
            result_reaction_times.grid(row=1, column=1)

        desc_too_often = Label(results_frame, text="Bei richtiger Farbe zu oft gedrückt:")
        desc_color_missed = Label(results_frame, text="Bei richtiger Farbe nicht gedrückt:")
        desc_false_positives = Label(results_frame, text="Bei falscher Farbe gedrückt:")

        result_too_often = Label(results_frame, text=self.key_too_often_pressed)
        result_color_missed = Label(results_frame, text=self.color_missed)
        result_false_positives = Label(results_frame, text=self.false_positives)

        desc_too_often.grid(row=2, column=0)
        desc_color_missed.grid(row=3, column=0)
        desc_false_positives.grid(row=4, column=0)

        result_too_often.grid(row=2, column=1)
        result_color_missed.grid(row=3, column=1)
        result_false_positives.grid(row=4, column=1)

        results_frame.grid(row=0, column=0)

        ok_button = Button(results_window, text="OK", padx=10, pady=10, command=results_window.quit)
        ok_button.grid(row=1, column=0)


def main():
    main_window = Tk()

    game = Game(main_window)

    main_window.bind_all("<Escape>", lambda e: game.show_results())
    main_window.bind_all("<space>", lambda e: game.key_pressed()) # Ereignis Tastendruck
    main_window.title("Spiel")
    main_window.geometry('640x480')

    start_button = Button(main_window, text="Start", padx=10, pady=10, command=game.start_game)
    start_button.pack()
    close_button = Button(main_window, text="Schließen", padx=10, pady=10, command=main_window.quit)
    close_button.pack()

    main_window.mainloop()


if __name__ == '__main__':
    main()

Gruß
Atalanttore
Sirius3
User
Beiträge: 17712
Registriert: Sonntag 21. Oktober 2012, 17:20

Wie kommst Du zu Deinem Verständnis, anstatt einfach in die Hilfe zu schauen:
Quit the Tcl interpreter. All widgets will be destroyed.
Du suchst `destroy`.
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

@Sirius3: Kurze Zwischenfrage: Welche Hilfe verwendest du für Tkinter?

`results_window.destroy` funktioniert schon besser. Allerdings bleibt das `game_window` weiterhin im Vollbildmodus geöffnet, obwohl es durch `self.game_window.destroy()` in Zeile 40 doch eigentlich beendet werden sollte, wenn `self.stop_test` nach einem Druck auf die ESC-Taste wahr wird.

Gruß
Atalanttore
Antworten