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.
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich finde es etwas viel verlangt, das ich mir hier Code aus mehreren Posts zusammen mit vorgeschlagenen Aenderungen selbst zusammen basteln soll, um dann am Ende hoffentlich mit etwas dazustehen, dass dem entspricht, was du da gemacht hast. Und dabei dann auch noch davon auszugehen, dass weder ich noch du etwas falsch verstanden haben.

Wenn du nicht bereit bist, Code mit dazugehoerigem Fehler zu posten, kann ich dir nicht helfen. Und ob andere das koennen, wage ich zu bezweifeln. Das gehoert schon zu deinen Aufgaben, wenn du dir Hilfe wuenschst.
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

__deets__ hat geschrieben: Sonntag 26. August 2018, 20:22 Und dabei dann auch noch davon auszugehen, dass weder ich noch du etwas falsch verstanden haben.
Ich gehe eigentlich davon aus, dass ich etwas falsch verstanden und programmiert habe.


Der Code sieht momentan so aus:

Code: Alles auswählen

from tkinter import *
from random import choice
import time
from statistics import mean



DESIGNATED_COLOR = "red"
SWITCH_TIME = 1000



class Game:
    def __init__(self, game_window): # 3. Formaler Parameter 'game_window' enthält Referenz auf Tk-Instanz 'root'
        self.game_window = game_window # 4. Objektvariable 'self.game_window' bekommt Referenz auf Tk-Instanz 'root'
        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.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.quit()

        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):
        self.stop_test = True
        results_window = Toplevel(self.game_window) # 5. Toplevel bekommt Referenz auf die Tk-Instanz 'root'
        results_window.title("Ergebnisse")

        if self.reaction_times:
            # mean() auf Liste ohne Einträge führt zu einem Fehler
            label_reaction_times = Label(self, text="Gemittelte Reaktionszeit: {}".format(mean(self.reaction_times)))

            label_for_listbox = Label(self, text="Reaktionszeit pro richtiger Farbe:")
            listbox_reaction_times = Listbox(self)
            for item in self.reaction_times:
                listbox_reaction_times.insert(item)

            label_reaction_times.grid(row=0, column=0)
            label_for_listbox.grid(row=1, column=0)
            listbox_reaction_times.grid(row=1, column=1)

        label_too_often = Label(self, text="Bei richtiger Farbe zu oft gedrückt: {}".format(self.key_too_often_pressed))
        label_color_missed = Label(self, text="Bei richtiger Farbe nicht gedrückt: {}".format(self.color_missed))
        label_false_positives = Label(self, text="Bei falscher Farbe gedrückt: {}".format(self.false_positives))
        ok_button = Button(self, text="OK", command=results_window.quit)

        label_too_often.grid(row=2, column=0)
        label_color_missed.grid(row=3, column=0)
        label_false_positives.grid(row=4, column=0)
        ok_button.grid(row=6, column=0)


def main():
    root = Tk() # 1. Tk-Instanz 'root' wird instanziert
    root.bind("<Escape>", lambda e: game.show_results())

    game = Game(root) # 2. Referenz auf Tk-Instanz 'root' wird übergeben

    root.bind("<space>", lambda e: game.key_pressed()) # Ereignis Tastendruck
    root.title("Spiel")
    root.geometry('640x480')

    start_button = Button(root, text="Start", command=game.start_game)  # !!!
    start_button.pack()
    close_button = Button(root, text="Schließen", command=root.quit)
    close_button.pack()

    root.mainloop()


if __name__ == '__main__':
    main()

Drückt man während der Programmausführung auf die ESC-Taste kommen folgende Fehler:

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 "/home/ata/source/test.py", line 95, in <lambda>
    root.bind("<Escape>", lambda e: game.show_results())
  File "/home/ata/source/test.py", line 82, in show_results
    label_too_often = Label(self, text="Bei richtiger Farbe zu oft gedrückt: {}".format(self.key_too_often_pressed))
  File "/usr/lib/python3.6/tkinter/__init__.py", line 2763, in __init__
    Widget.__init__(self, master, 'label', cnf, kw)
  File "/usr/lib/python3.6/tkinter/__init__.py", line 2289, in __init__
    BaseWidget._setup(self, master, cnf)
  File "/usr/lib/python3.6/tkinter/__init__.py", line 2259, in _setup
    self.tk = master.tk
AttributeError: 'Game' object has no attribute 'tk'

Gruß
Atalanttore
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Und schon bricht Klarheit aus. Schau dir doch mal genau an, WAS du da als erstes Argument uebergibst. Und was das sein sollte.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Atakanttore: ich zitiere mich gerne selbst: In ›show_results‹ benutzt Du die Game-Instanz an Stellen, wo eine Tk-Instanz erwartet wird. Das wichtige daran ist der Plural bei `Stellen`.
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

Hi Atalanttore

Habe dein Skript ein wenig manipuliert (nicht optimiert):

Code: Alles auswählen

from tkinter import *
from random import choice
import time
from statistics import mean



DESIGNATED_COLOR = "red"
SWITCH_TIME = 1000



class Game:
    def __init__(self, game_window): # 3. Formaler Parameter 'game_window' enthält Referenz auf Tk-Instanz 'root'
        self.game_window = game_window # 4. Objektvariable 'self.game_window' bekommt Referenz auf Tk-Instanz 'root'
        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.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.quit()
            return

        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.game_window) # 5. Toplevel bekommt Referenz auf die Tk-Instanz 'root'
        results_window.title("Ergebnisse")

        if self.reaction_times:
            # mean() auf Liste ohne Einträge führt zu einem Fehler
            label_reaction_times = Label(self, text="Gemittelte Reaktionszeit: {}".format(mean(self.reaction_times)))

            #label_for_listbox = Label(self, text="Reaktionszeit pro richtiger Farbe:")
            #listbox_reaction_times = Listbox(self)
            label_for_listbox = Label(results_window, text="Reaktionszeit pro richtiger Farbe:")
            listbox_reaction_times = Listbox(results_window)

            for item in self.reaction_times:
                listbox_reaction_times.insert(item)

            label_reaction_times.grid(row=0, column=0)
            label_for_listbox.grid(row=1, column=0)
            listbox_reaction_times.grid(row=1, column=1)

        #label_too_often = Label(self, text="Bei richtiger Farbe zu oft gedrückt: {}".format(self.key_too_often_pressed))
        #label_color_missed = Label(self, text="Bei richtiger Farbe nicht gedrückt: {}".format(self.color_missed))
        #label_false_positives = Label(self, text="Bei falscher Farbe gedrückt: {}".format(self.false_positives))
        #ok_button = Button(self, text="OK", command=results_window.quit)

        label_too_often = Label(results_window, text="Bei richtiger Farbe zu oft gedrückt: {}".format(self.key_too_often_pressed))
        label_color_missed = Label(results_window, text="Bei richtiger Farbe nicht gedrückt: {}".format(self.color_missed))
        label_false_positives = Label(results_window, text="Bei falscher Farbe gedrückt: {}".format(self.false_positives))
        ok_button = Button(results_window, text="OK", command=results_window.quit)

        label_too_often.grid(row=2, column=0)
        label_color_missed.grid(row=3, column=0)
        label_false_positives.grid(row=4, column=0)
        ok_button.grid(row=6, column=0)


def main():
    root = Tk() # 1. Tk-Instanz 'root' wird instanziert
    #root.bind("<Escape>", lambda e: game.show_results())

    game = Game(root) # 2. Referenz auf Tk-Instanz 'root' wird übergeben
    root.bind("<Escape>", lambda e: game.show_results())
    root.bind("<space>", lambda e: game.key_pressed()) # Ereignis Tastendruck
    root.title("Spiel")
    root.geometry('640x480')

    start_button = Button(root, text="Start", command=game.start_game)  # !!!
    start_button.pack()
    close_button = Button(root, text="Schließen", command=root.quit)
    close_button.pack()

    root.mainloop()


if __name__ == '__main__':
    main()
Gruss wuf ;-)
Take it easy Mates!
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

@wuf: Was bewirkt der Default-Parameter `event=None` in der Methode `show_results()`?

Im folgenden Code habe ich das `results_window` etwas überarbeitet:

Code: Alles auswählen

from tkinter import *
from random import choice
import time
from statistics import mean



DESIGNATED_COLOR = "red"
SWITCH_TIME = 1000



class Game:
    def __init__(self, game_window): # 3. Formaler Parameter 'game_window' enthält Referenz auf Tk-Instanz 'root'
        self.game_window = game_window # 4. Objektvariable 'self.game_window' bekommt Referenz auf Tk-Instanz 'root'
        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.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.quit()
            return

        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.game_window) # 5. Toplevel bekommt Referenz auf die Tk-Instanz 'root'
        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", command=results_window.quit)
        ok_button.grid(row=1, columnspan=2)


def main():
    root = Tk() # 1. Tk-Instanz 'root' wird instanziert
    #root.bind("<Escape>", lambda e: game.show_results())

    game = Game(root) # 2. Referenz auf Tk-Instanz 'root' wird übergeben
    root.bind("<Escape>", lambda e: game.show_results())
    root.bind("<space>", lambda e: game.key_pressed()) # Ereignis Tastendruck
    root.title("Spiel")
    root.geometry('640x480')

    start_button = Button(root, text="Start", command=game.start_game)  # !!!
    start_button.pack()
    close_button = Button(root, text="Schließen", command=root.quit)
    close_button.pack()

    root.mainloop()


if __name__ == '__main__':
    main()
Warum ist der Parameter `end` in folgender Anweisung aus dem obigen Code nötig (ohne funktioniert es nicht)?

Code: Alles auswählen

            for item in self.reaction_times:
                result_reaction_times.insert('end', item)
Gruß
Atalanttore
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Atalanttore: Das Default-Argument bewirkt das man die Methode auch ohne Argument aufrufen kann. Wird hier soweit ich das überblicken kann nicht benötigt, kann aber nützlich sein wenn man eine Methode zum Beispiel über ein Tastenkürzel *und* eine Schaltfläche verfügbar macht. Denn die Schaltfläche erwartet ja ein aufrufbares Objekt das kein Argument erwartet.

Der Parameter 'end' ist nötig weil die Methode halt so geschrieben ist das als erstes Argument gesagt werden muss *wo* eingefügt werden soll. Und wenn es am Ende sein soll, dann ist der Wert dafür 'end'. Wobei es das auch als Konstante im `tkinter`-Modul gibt. Welches man eher nicht per *-Import verwenden sollte, denn die Konstante `END` kommt mit ca. 190 Freunden, also anderen Namen in das importierende Modul.

Zur `Listbox` siehe auch: http://effbot.org/tkinterbook/listbox.htm

Ich hab's nur kurz überflogen, aber kann es sein dass das ``columnspan=2`` bei `ok_button` keinen Sinn macht? In dem Elternwidget gibt es doch sonst gar nichts in der zweiten Spalte, oder?
“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__ hat geschrieben: Sonntag 2. September 2018, 21:31 Der Parameter 'end' ist nötig weil die Methode halt so geschrieben ist das als erstes Argument gesagt werden muss *wo* eingefügt werden soll. Und wenn es am Ende sein soll, dann ist der Wert dafür 'end'. Wobei es das auch als Konstante im `tkinter`-Modul gibt. Welches man eher nicht per *-Import verwenden sollte, denn die Konstante `END` kommt mit ca. 190 Freunden, also anderen Namen in das importierende Modul.
Ist die Konstante `END` kein einfacher String?
Was meinst du mit "anderen Namen in das importierende Modul"?

__blackjack__ hat geschrieben: Sonntag 2. September 2018, 21:31 Zur `Listbox` siehe auch: http://effbot.org/tkinterbook/listbox.htm

Ich hab's nur kurz überflogen, aber kann es sein dass das ``columnspan=2`` bei `ok_button` keinen Sinn macht? In dem Elternwidget gibt es doch sonst gar nichts in der zweiten Spalte, oder?
Stimmt. Das war noch ein Relikt vom Umbau.


Bisher werden die beiden Buttons des Hauptmenüs, also "Start" und "Schließen", nach einem Klick auf "Start" nach oben verschoben. Eigentlich sollte das Hauptmenü nach einem Klick auf "Start" komplett vom Spiel überdeckt werden. Erst nach dem Spiel, also nachdem man es mit ESC abgebrochen hat, sollte es unter dem Ergebnisfenster (`results_window`), das man mit einem Klick auf "OK" schließen kann, wieder zum Vorschein kommen.
Gibt es dafür eine Methode zum Überlagern von Fenstern?


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

@Atalanttore: Ja `tkinter.END` ist auch an die Zeichenkette 'end' gebunden. Dann weiss der Leser aber gleich das 'end' nicht irgendetwas ist was beliebig ausgewählt werden kann, sondern eine Konstante die eine Bedeutung hat und nach der man in der Dokumentation suchen kann. In diesem konkreten Fall findet man die Konstante nicht über den Index, aber bei den meisten anderen Modulen funktioniert das. Zudem bekommt man schneller eine Rückmeldung wenn man sich vertippt. Wenn man die Konstante falsch schreibt, kann das eine statische Analyse in der Regel schon feststellen und man kann in IDEs schon vor dem Ausführen des Codes einen Hinweis darauf bekommen.

Nach einem ``from tkinter import *`` hast Du alle Namen aus dem `tkinter`-Modul in deinem Modul definiert. Und das sind viele. In Python 3 sind es ein paar weniger als in Python 2, aber immer noch etwas mehr als 130 Namen:

Code: Alles auswählen

In [5]: len([name for name in dir(tkinter) if not name.startswith('_')])
Out[5]: 136
Und das sind nicht nur Namen die im `tkinter`-Modul (neu) definiert werden, sondern auch alle Namen die das `tkinter`-Modul selbst importiert.
“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__ : Danke für die ausführliche Erklärung.

Kennt jemand auch den Grund für das Problem mit den sich nach oben verschiebenden Buttons (siehe letzter Absatz in Beitrag)?

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

Naja, Du verwendest ja das Fenster mit den Schaltflächen um da auch noch ein `Canvas` mit rein zu basteln. Nirgendwo im Code steht das die Schaltflächen entfernt werden sollen. Du könntest das Spiel tatsächlich in ein eigenes Fenster packen, welches dann beim vergrössern auf Vollbild dann das Hauptfenster verdeckt.
“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

Okay. Dem Spiel habe ich mit der Methode `Toplevel()` ein eigenes Fenster verpasst. Nach einem Klick auf "Start" werden die Schaltflächen nun nicht mehr nach oben verschoben. Leider reagiert damit ein laufendes Spiel nicht mehr auf die ESC-Taste zum Abbrechen.

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.quit()
            return

        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("<Escape>", lambda e: game.show_results())
    main_window.bind("<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: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Atalanttore: Tastatureingaben gehen an das Fenster das aktuell den Fokus hat. Wenn das Hauptfenster nicht den Fokus hat, bekommt es auch nicht mit wenn die Escape-Taste gedrückt wurde. Du musst also entweder für jedes Fenster die Tasten binden auf die sie reagieren sollen, oder es global für alle Fenster Deiner Anwendung machen (`bind_all()`).
“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__: Das Tastatureingaben nur an das aktive Fenster gehen wusste ich noch nicht. Wieder was gelernt. 8)

Nachdem das Problem mit den Tastatureingaben durch `bind_all()` behoben ist, stehe ich jetzt vor dem nächsten Problem.
Nach einem Druck auf die ESC-Taste sollte sich eigentlich das Vollbild-Spielfenster (`game_window`) vollständig beenden, aber stattdessen wird es nur angehalten und das Ergebnisfenster (`results_window`) erscheint auf dem Spielfenster. Weder mit `self.game_window.quit()`, `self.canvas.quit()` oder `return` lässt sich das Spielfenster schließen.

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

@Atalanttore: Du suchst die `destroy()`-Methode.
“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__: 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: 13004
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: 13004
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
Antworten