Newbie - Pygame / QT / ...

Hier werden alle anderen GUI-Toolkits sowie Spezial-Toolkits wie Spiele-Engines behandelt.
Antworten
Benutzeravatar
Hel
User
Beiträge: 8
Registriert: Montag 14. März 2016, 15:51

Hallo,

als absoluter Newbie und Senior (Newbie im Programmieren und Senior im Alter ;) ) würde ich bitte eure Unterstützung benötigen.

Mein Ziel:
Ein Programm das in einem Fenster an jeder Ecke einen farbigen Kreis zeichnet, bei diesen in einem bestimmten Intervall die Farbe wechselt.
Sobald nun ein Kreis mit roter Farbe am Bildschirm auftaucht muss eine Taste gedrückt werden (z.B. Returntaste).
Dann soll die Zeit zwischen auftauchen des roten Kreises und dem betätigen der Taste ausgewertet werden.

Klingt für euch höchstwahrscheinlich einfach für mich habe ich das Ziel wohl etwas zu hoch gesteckt.

Was schaffe ich nach etwas Recherche:
Die Farbauswahl mit random:

Code: Alles auswählen

BLACK = [0, 0, 0]
WHITE = [255, 255, 255]
RED = [255, 0, 0]
GREEN =[0,255,0]
YELLOW=[255,255,0]
FARBLISTE = ("RED", "GREEN", "YELLOW", "WHITE")
zufall = random.randint(0,3)
farbe = FARBLISTE[zufall]
Das Fenster mit den Kreisen in den Ecken => QT fürs Fenster und Qpainter() für die Kreise

Code: Alles auswählen

    def paintEvent(self, event):
        painter.begin(self)
        painter.setBrush(QtGui.QColor(farbe))
        painter.drawEllipse(10,10,180,180)
        painter.end()
Die Rückmeldung der Tastatur über pygame:

Code: Alles auswählen

while True:
    pressed = pygame.key.get_pressed()
    if pressed[pygame.K_RETURN]:
        RICHTIG = True                
        endzeit()
        dauer()
    if pressed[pygame.K_ESCAPE]:
        quit()
Jetzt wollte ich euch mal fragen:
Bin ich nur annähernd auf einer richtigen Spur?

Ich habe auch mit folgenden Funktionen versucht die Zeitmessung zu aktivieren, aber ich scheitere total am Ganzen :)

Code: Alles auswählen

def startzeit():
    global starttime    
    starttime = time.time()
    print("Startzeit: ", starttime)   
        
def endzeit():
    global endtime
    endtime = time.time()
    print("Endzeit: ", endtime)
    
def dauer():
    global usedtime   
    usedtime = endtime - starttime
    print("Dauer: ", usedtime)
Hinweise würden mir sehr helfen, was ich auf keinen Fall will ist eine fertige Lösung :) (ich will es ja selbst verstehen) also bitte bitte Hinweise.

Hier nochmals das ganze Chaos:

Code: Alles auswählen

import sys
from PyQt5 import QtCore, QtGui, uic
from PyQt5.QtWidgets import (QMainWindow, QTextEdit, QAction,  QLabel, 
    QComboBox, QApplication) 
import sqlite3
import time
import random
import pygame

painter = QtGui.QPainter()

# Farben definieren (RGB format)
BLACK = [0, 0, 0]
WHITE = [255, 255, 255]
RED = [255, 0, 0]
GREEN =[0,255,0]
YELLOW=[255,255,0]
 
FARBLISTE = ("RED", "GREEN", "YELLOW", "WHITE")
zufall = random.randint(0,3)
farbe = FARBLISTE[zufall]
print(farbe)

qtCreatorFile = "main.ui" # Enter file here.
 
Ui_MainWindow, QtBaseClass = uic.loadUiType(qtCreatorFile)

def startzeit():
    global starttime    
    starttime = time.time()
    print("Startzeit: ", starttime)   
        
def endzeit():
    global endtime
    endtime = time.time()
    print("Endzeit: ", endtime)
    
def dauer():
    global usedtime   
    usedtime = endtime - starttime
    print("Dauer: ", usedtime)

#pygame.init()
#startzeit()

#while True:
#    pressed = pygame.key.get_pressed()
#    if pressed[pygame.K_RETURN]:
#        RICHTIG = True                
#        endzeit()
#        dauer()
#    if pressed[pygame.K_ESCAPE]:
#        quit()

        
class MyApp(QMainWindow, Ui_MainWindow):
    def __init__(self):
        QMainWindow.__init__(self)
        Ui_MainWindow.__init__(self)
        self.setupUi(self)  

    def paintEvent(self, event):
        painter.begin(self)
        painter.setBrush(QtGui.QColor(farbe))
        painter.drawEllipse(10,10,180,180)
        painter.end()
  
    def initUI(self):
        OKButton = self.OKButton
        OKButton.clicked.connect(endzeit())


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MyApp()
    window.show()
    sys.exit(app.exec_())
Danke euch.
Planlos :K aber motiviert :D
BlackJack

@Hel: Ich würde auf jeden Fall das mischen von GUI-Rahmenwerken sein lassen. Entweder Qt *oder* Pygame, nicht beides zusammen.

Statt `randint()` und hart kodierten Argumenten würde sich die `choice()`-Funktion aus dem `random`-Modul eher anbieten.

``global`` hat in einem sauberen Programm nichts zu suchen. Funktionen und Methoden sollten ausser auf Konstanten, nur auf Werte zugreifen die als Argument übergeben wurden und keine ”globalen” Werte setzen, sondern entweder objektorientierte Programmierung verwenden oder Rückgabewerte an den Aufrufer zurückgeben wenn sie ein Ergebnis haben was ausserhalb der Funktion oder Methode noch verwendet werden soll.
Benutzeravatar
Hel
User
Beiträge: 8
Registriert: Montag 14. März 2016, 15:51

Hallo BlackJack,

danke für den Tip mit Choice(), der wird gleich umgesetzt.

Das Mischen von qt und pygame enstand nur deswegen, weil ich bei qt keine Infos gefunden habe, wie ich auf den Tastendruck einer bestimmten Taste reagieren kann, was ja bei pygame sogar für mich einigermassen einfach umzusetzen ist. Und qt ist halt sehr komfortabel beim Oberflächen “basteln“.

“global“ ist nur entstanden, weil es anders nicht lief :oops:
Da muss ich mich wohl noch einlesen.
Wenn ich dich richtig verstehe müsste dann die “dauer“ mit den Argumenten endzeit und startzeit aufgerufen werden.
Planlos :K aber motiviert :D
BlackJack

@Hel: Tastendrücke werden bei Qt als Ereignisse (`QEvent`) verarbeitet. `QWidget` hat `keyPressEvent()`- und `keyReleaseEvent()`-Methoden die man überschreiben kann. Wenn man selbst keine Klasse ableitet, kann man mit `QObject.eventFilter()` auch die Ereignisse von beliebigen `QObject`-Exemplaren überwachen, filtern, und darauf reagieren.

`dauer()` wäre dann wahrscheinlich eher eine Methode in einer Stoppuhr-Klasse. Wir haben hier ja GUI-Programmierung, und damit ereignisbasiertes Vorgehen, man muss sich also zwischen Aufrufen von der GUI-Hauptschleife irgendwie Zustände merken ohne das alles irgendwo global abzulegen und das Programm damit schwer durchschaubar zu machen.

Was für Anforderungen gibt es denn sonst noch so an die GUI? Bunte Kreise und auf Tastendrücke reagieren geht auch mit Tkinter, und das gehört zur Standardbibliothek.
Benutzeravatar
Hel
User
Beiträge: 8
Registriert: Montag 14. März 2016, 15:51

Wir haben hier ja GUI-Programmierung, und damit ereignisbasiertes Vorgehen, man muss sich also zwischen Aufrufen von der GUI-Hauptschleife irgendwie Zustände merken ohne das alles irgendwo global abzulegen und das Programm damit schwer durchschaubar zu machen.
Ich glaube hier liegt mein Knoten :)

Ziel:
Schlussendlich sollte man in einem Fenster mehrere solcher kleinen "Unterprogramme" auswählen können, die dann Bildschirm füllend ausgeführt werden. Die ganzen Ereignisse innerhalb der Unterprogramme (also Tastendrucke und die Zeiten dazu) sollen in einer DB abgelegt werden (sqllite wäre hier meine Wahl).
Die Ergebnisse sollen dann (mit mehreren Optionen, also statistische Auswertungen) grafisch dargestellt werden.

Tkinter habe ich noch nicht in Betracht gezogen (qt ist wohl Marketingseitig besser im Netz vorhanden ;) )

Welche "Systeme" ich schlussendlich verwende ist mir egal, ich will mal in die Materie eintauchen und zumindest einigermassen verstehen was ich mache, was zur Zeit nicht unbedingt der Fall ist.
Planlos :K aber motiviert :D
Benutzeravatar
Hel
User
Beiträge: 8
Registriert: Montag 14. März 2016, 15:51

Hallo,

hier mal meine tkinter Versuche:

Code: Alles auswählen

from tkinter import *
import random
import time

# Farben definieren (RGB format)
BLACK = [0, 0, 0]
RED = [255, 0, 0]
GREEN =[0,255,0]
YELLOW=[255,255,0]
WHITE = [255, 255, 255]

FARBLISTE = ("BLACK", "RED", "GREEN", "YELLOW", "WHITE")


class start():
    starttime= time.time()

class ende():
    endtime= time.time()

#class dauer(ende,start):
#    dauer = ende - start
#    print(dauer)

class startButton(Button):
    def __init__(self, parent):
        Button.__init__(self, parent)
        self['text'] = 'Starten Sie hier'
        # Command to close the window (the destory method)
        self['command'] = parent.destroy
        self.pack(side=BOTTOM)

class Test_FK(Frame):
  
    def __init__(self, parent):
        Frame.__init__(self, parent)   
         
        self.parent = parent        
        self.initUI()
        
        self.e1 = Entry(parent)
        self.e1.bind("<Return>", self.zeit_neustart)   

    def zeit_neustart():
        self['command'] = parent.destroy

     
    def initUI(self):
      
        self.parent.title("Test Farbige Kreise")
      
        self.pack(fill=BOTH, expand=1)
        
        canvas = Canvas(self)

        def farbiger_kreis(x,y,x1,y1): 
            farbe = random.choice(FARBLISTE)
            canvas.create_oval(x, y, x1, y1,  outline=farbe,
                fill=farbe, width=2) 

        #Kreis links oben        
        farbiger_kreis(10,10,80,80)     
        #Kreis rechts oben        
        farbiger_kreis(320,10,390,80) 
        #Kreis links unten
        farbiger_kreis(10,320,80,390) 
        #Kreis rechts unten
        farbiger_kreis(320,320,390,390) 
        
        canvas.pack(fill=BOTH, expand=1)

def menu():
    root = Tk()
    sb = startButton(root)
    #root.geometry("400x400+200+200")
    root.mainloop()
    
def main():
    root = Tk()
    ex = Test_FK(root)
    root.geometry("400x400+200+200")
    root.mainloop()
 
menu()
zeit=0

while zeit < 10:
    start()
    main()
    ende()
    print("Startzeit:",start)
    print("Endzeit: ", ende)
    zeit=zeit+1
print("Ende")
Die Zeitmessung ist noch ferne Zukunft :)
Ich vermute auch meine Aufrufe der Klassen sind nicht besonders zielführend :)

Bin wie immer über (fasst) jede Rückmeldung froh :)
Planlos :K aber motiviert :D
BlackJack

@Hel: Arbeite doch mal ein Grundlagentutorial durch. In der Python-Dokumentation gibt es zum Beispiel ein Tutorial.

Sternchen-Importe sollte man nicht machen, das wird ganz schnell unübersichtlich wenn man nicht mehr weiss und auch nicht einfach herausfinden kann wo ein Name definiert wurde. Ausserdem besteht die Gefahr von Namenskollisionen.

Bezüglich Namenskonventionen lohnt ein Blick in den Style Guide for Python Code.
Benutzeravatar
Hel
User
Beiträge: 8
Registriert: Montag 14. März 2016, 15:51

So... ein bisserl zusammengeräumt und "brav" an die PEP 0008 gehalten :)

Grundlagen (ein bisserl) gemacht.
Irgendwie schaffe ich es nicht mit einem Tastendruck meine Farbe der Kreise zu ändern.

Sollte nicht mit
self.bind('<Key>', farbe_aendern)
die farbe_aendern aufgerufen werden, sobald eine Taste gedrückt wird?

Und in der def würde ich dann mit
canvas.itemconfig(fk1, outline=farbe, fill=farbe)
die Farbe ändern.

Da hänge ich leider wieder.

Danke für die Hilfe.

P.S. das Thema ist hier glaube ich nun falsch - sollte vielleicht in tkinter gewechselt werden oder off topic ;)

Code: Alles auswählen

from tkinter import Tk, Canvas, Frame, BOTH, TOP
import random

# Farben definieren (RGB format)
BLACK = [0, 0, 0]
RED = [255, 0, 0]
GREEN = [0, 255, 0]
YELLOW = [255, 255, 0]
WHITE = [255, 255, 255]
FARBLISTE = ("BLACK", "RED", "GREEN", "YELLOW", "WHITE")

class FarbigeKreise(Frame):
    """Zeichnet die Kreise in den 4 Ecken"""
    def __init__(self, parent):
        Frame.__init__(self, parent)
        self.parent = parent
        self.init_ui()

    def init_ui(self):
        """Kreise zeichnen und deren Farbe bei Tastendruck ändern"""
        self.parent.title("Test Farbige Kreise")
        self.pack(fill=BOTH, expand=1)

        canvas = Canvas(self)
        def farbiger_kreis(x, y, x1, y1):
            """Zeichne einen farbigen Kreis"""
            farbe = random.choice(FARBLISTE)
            canvas.create_oval(x, y, x1, y1, outline=farbe, fill=farbe, width=2)

        canvas.pack(fill=BOTH, expand=1)

        #Kreis rechts oben
        fk1 = farbiger_kreis(10, 10, 80, 80)
        #Kreis links unten
        fk2 = farbiger_kreis(320, 10, 390, 80)
        #Kreis rechts unten
        fk3 = farbiger_kreis(10, 320, 80, 390)
        #Kreis links unten
        fk4 = farbiger_kreis(320, 320, 390, 390)

        def farbe_aendern(event):
            """Ändert die Farbe der Kreise"""
            if event.char in ('e', 's', 'r'):
                print("Bestimmte Taste gedrückt")
                return
            farbe = random.choice(FARBLISTE)
            canvas.itemconfig(fk1, outline=farbe, fill=farbe)
            canvas.itemconfig(fk2, outline=farbe, fill=farbe)
            canvas.itemconfig(fk3, outline=farbe, fill=farbe)
            canvas.itemconfig(fk4, outline=farbe, fill=farbe)
            print("Farbe geändert")
            canvas.pack(fill=BOTH, expand=1)

        self.bind('<Key>', farbe_aendern)

def main():
    """"""
    root = Tk()
    fk = FarbigeKreise(root)
    fk.pack(side=TOP)
    root.geometry("400x440+200+200")
    root.mainloop()

if __name__ == '__main__':
    main()
    print("Ende")
Planlos :K aber motiviert :D
BlackJack

@Hel: `fk1` bis `fk4` haben den Wert `None` weil die Funktion die den Kreis zeichnet nichts explizit zurück gibt und damit wird `None` zurückgegeben und an diese Namen gebunden. Die sind zudem aus zwei Gründen schlecht: `fk` sagt so gut wie nichts aus, da muss man erst einmal suchen wo die definiert werden um zu raten das das wohl die Abkürzung für `farbiger_kreis` ist, und zum anderen ist durchnummerieren von Namen ein „code smell“. In den meisten Fällen, und so auch hier, möchte man da eigentlich eine Datenstruktur statt einzelner Namen verwenden. Meistens eine Liste. So auch in diesem Fall.

`FARBLISTE` ist keine Liste sondern ein Tupel. Ein Grund warum man Grunddatentypen nicht in Namen schreiben sollte. Irgendwann ändert man den Typ mal, weil man einen anderen oder spezielleren braucht, und dann muss man überall den Namen ändern, oder hat falsche, irreführende Namen im Programm.

Die Konstanten mit den RGB-Werten werden nicht verwendet, die kann man löschen.

Die Fenstergeometrie sollte man nicht setzen müssen. Das Fenster ist so gross wie die Elemente darin. Das heisst man sollte dem Canvas eine grösse geben, so dass dort alles was man zeichnet hinein passt.

Das Layout von einem Widget darf man nur einmal machen — Du rufst `pack()` für das bereits angezeigte `Canvas` jedes mal beim Farbwechsel auf. Und es sollte nicht von dem Objekt selbst gemacht werden. Das macht kein bereits existierendes Widget so, weil einem das die Freiheit nimmt ein Widget beliebig in die GUI zu integrieren.

`parent` muss man sich a) nicht merken, weil Widgets eine Methode haben mit der man das Elternwidget abfragen kann, und b) ist das nur zufällig ein Objekt bei dem man den Titel setzen kann. Wenn man den `Frame` beispielsweise in einen anderen Frame steckt, dann kracht es an der Stelle.

Das verschachteln von Funktionen ist nicht objektorientiert, was in Python die offensichtlichere Lösung wäre statt Closures zu schreiben. Die Klasse wird damit sogar total sinnfrei weil in aller Regel eine Klasse die nur aus der `__init__()` besteht auch einfacher als Funktion ausgedrückt werden kann. Da Du irgendwann die Zeit noch als Zustand dazu nehmen willst, braucht man hier wirklich eine Klasse.

Man sollte Code- und Datenwiederholungen vermeiden. Wenn man die Kreise beim erstellen zufällig einfärben muss und auch später noch mal alle zufällig einfärben muss, dann kann man dieses Einfärben herausziehen und den Code dafür nur einmal schreiben.

Ich lande dann ungefähr bei so etwas:

Code: Alles auswählen

import random
import tkinter as tk

FARBEN = ['BLACK', 'RED', 'GREEN', 'YELLOW', 'WHITE']


class FarbigeKreise(tk.Canvas):
    """Zeichnet die Kreise in den 4 Ecken"""

    def __init__(self, parent):
        tk.Canvas.__init__(self, parent, width=400, height=400)

        self.kreis_ids = [
            self.zeichne_kreis(x, y, 70)
            for x, y in [(10, 10), (320, 10), (10, 320), (320, 320)]
        ]
        self.aendere_farben()
        self.winfo_toplevel().bind('<Key>', self.on_key)

    def zeichne_kreis(self, x, y, size):
        return self.create_oval(x, y, x + size, y + size, width=0)

    def aendere_farben(self):
        for kreis_id in self.kreis_ids:
            self.itemconfig(kreis_id, fill=random.choice(FARBEN))

    def on_key(self, event):
        if event.char in ['e', 's', 'r']:
            print('Bestimmte Taste gedrückt.')
        else:
            self.aendere_farben()
            print('Farben geändert.')


def main():
    root = tk.Tk()
    root.title('Test Farbige Kreise')
    farbiger_kreise = FarbigeKreise(root)
    farbiger_kreise.pack(side=tk.TOP)
    root.mainloop()
    print('Ende')


if __name__ == '__main__':
    main()
Benutzeravatar
Hel
User
Beiträge: 8
Registriert: Montag 14. März 2016, 15:51

Danke für die Hilfe!
`FARBLISTE` ist keine Liste sondern ein Tupel. Ein Grund warum man Grunddatentypen nicht in Namen schreiben sollte. Irgendwann ändert man den Typ mal, weil man einen anderen oder spezielleren braucht, und dann muss man überall den Namen ändern, oder hat falsche, irreführende Namen im Programm.

Die Konstanten mit den RGB-Werten werden nicht verwendet, die kann man löschen.
Das mit der Liste ist aufgrund meiner nicht programmiersprachenkonformen Sprache entstanden :)
In Python sind die Farben anscheinend vorab definiert, deswegen keine RGB Werte?

Und hier die Fragen zu deinem Code:

Code: Alles auswählen

import tkinter as tk

Ist das nicht so wie from tkinter import *, bzw. warum muss ich hier nicht die einzelnen Argumente aufrufen?

Code: Alles auswählen

self.kreis_ids = [
            self.zeichne_kreis(x, y, 70)
            for x, y in [(10, 10), (320, 10), (10, 320), (320, 320)]
        ]
             self.aendere_farben()
Damit erzeugst du eine Liste von Kreisen indem du diese Funktion aufrufst.

Code: Alles auswählen

def zeichne_kreis(self, x, y, size):
        return self.create_oval(x, y, x + size, y + size, width=0)
Und danach füllst du die Kreise mit der gleichen Funktion mit der du sie nachher änderst.

Code: Alles auswählen

self.aendere_farben()
Cool :)

Code: Alles auswählen

        self.winfo_toplevel().bind('<Key>', self.on_key)
Was ist winfo_toplevel()?
Damit bindest du jedes Keyboard Input mit der Funktion on_key

Code: Alles auswählen

def aendere_farben(self):
        for kreis_id in self.kreis_ids:
            self.itemconfig(kreis_id, fill=random.choice(FARBEN))
Und hier änderst du dann die Farbe aller Kreise in der Liste der Kreise.
Aufgerufen durch eine Keyboardtaste.

Sieht hier so "einfach" aus, wäre aber selbst wohl nie darauf gekommen.

Danke nochmals!
Planlos :K aber motiviert :D
BlackJack

@Hel: Tk kennt Namen für Farben. Das sind aber Zeichenketten und keine Python-Bezeichner die an Werte gebunden sind die eine Farbe beschreiben.

Beim Sternchen-Import aus Tkinter holst Du *alle* Namen aus dem Modul in das importierende Modul. Das sind so um die 190 Stück. Bei ``import tkinter as tk`` wird nur das Modul unter dem Namen `tk` im importierenden Modul bekannt gemacht, das ist also nur *ein* Name den man damit definiert. Und *über* dieses Objekt kommt man dann an den Inhalt heran, denn die Namen in einem Modul sind Attribute des Modulobjekts. Man greift dann ja nicht auf ``Canvas`` zu sondern auf ``tk.Canvas``.

`winfo_toplevel()` ist eine Methode auf `Widget`\s die das Widget zurück liefert welches das Fenster darstellt in dem das `Widget` angezeigt wird. Also entweder das `Tk`-Exemplar oder ein `Toplevel`-Exemplar. Damit die Bindung des Ereignisses sich auf das gesamte Fenster bezieht und nicht nur auf ein Widget *in* dem Fenster was diese Ereignisse nur verarbeitet wenn es den Fokus hat.
Benutzeravatar
Hel
User
Beiträge: 8
Registriert: Montag 14. März 2016, 15:51

Danke für die Erklärungen!

Habe noch eine Anpassung gemacht und stehe wieder vor dem Problem, dass die Liste farbe_verwendet in der Funktion on_key nicht erkannt wird :(

Code: Alles auswählen

    def aendere_farben(self):
        """Befülle bestehende Kreise mit Farbe"""
        farbe_verwendet = []
        for kreis_id in self.kreis_ids:
            farbe = random.choice(FARBEN)
            self.itemconfig(kreis_id, fill=farbe)
            farbe_verwendet.append(farbe)
            print(farbe)
            print(farbe_verwendet)
            if 'RED' in farbe_verwendet:
                print("Rot ist enthalten")
            else:
                print("Rot ist nicht enthalten")

    def on_key(self, event):
        """Wartet auf Keyboard Input"""
        if 'RED' not in farbe_verwendet:
            sleep(5)
            self.aendere_farben()
        if 'RED' in farbe_verwendet and event.keysym in ['Return']:
            #Info mitnehmen, dass richtig gedrückt wurde
            self.aendere_farben()
        else:
            #Info mitnehmen, dass richtig gedrückt wurde
            sleep(5)
            self.aendere_farben()
Planlos :K aber motiviert :D
BlackJack

@Hel: Natürlich ist das nicht einfach so bekannt nur weil der Name gleich ist. Das ist ja gerade der Sinn von lokalen Namen, dass man da nicht wissen muss was die anderen zig Funktionen und Methoden in einem Programm für Namen verwenden und man sich bei jeder Funktion und Methode Namen ausdenken müsste die nirgend wo anders verwendet werden. Wenn dem so wäre, hätte man ernsthafte Probleme grössere Programme zu schreiben ohne wahnsinnig zu werden wegen der ganzen Namen die man sich merken und ausdenken muss. :-)

Werte betreten Funktionen und Methoden als Argumente. Du musst also dafür sorgen das der Wert an das Objekt gebunden wird, denn was `on_key()` als Argumente übergeben bekommt hast Du ja nicht in der Hand. Das ist das Objekt auf dem die Methode aufgerufen wurde und das Objekt, welches das Ereignis beschreibt.

`sleep()` darfst Du hier nicht verwenden. Wenn Dein Code läuft, dann läuft die GUI-Hauptschleife nicht, und wenn die nicht läuft, friert solange die GUI ein. Solche Rückruffunktionen dürfen immer nur kurz etwas machen und müssen dann die Kontrolle wieder an die aufrufende GUI-Hauptschleife zurück geben. Wenn man in der GUI etwas zeitverzögert machen möchte, dann kann man die `after()`-Methode auf Widgets verwenden um eine Verzögerung und eine Funktion oder Methode anzugeben die nach der Verzögerung aufgerufen werden soll.

Das ändern der Farben passiert in *jedem* Zweig am Ende, ist also unabhängig davon was in den Bedingungen steht. Dann muss man das nicht in jeden Zweig schreiben, sondern kann das auch *einmal* am Ende schreiben. Ansonsten unterscheiden sich die Fälle nur durch die Wartezeit, also kann man das auch vereinheitlichen und in den Zweigen einfach nur die Wartezeit festlegen die man danach verwendet. Der mittlere Fall hat dann eine Wartezeit von 0.

Dann bekommt man allerdings das Problem, das man anderweitig dafür sorgen muss das der Effekt von `sleep()` erreicht wird, nämlich das der Benutzer während der fünf Sekunden keine Eingaben mehr machen kann. Das könnte man sich in einem Flag merken, welches von der `on_key()`-Methode geprüft und gegebenfalls gesetzt wird, und von der `farbe_aendern()` zurückgesetzt wird.

``event.keysym in ['Return']`` ist eine umständliche Art ``event.keysym == 'Return'`` zu schreiben.

Code: Alles auswählen

import random
import tkinter as tk

COLORS = ['BLACK', 'RED', 'GREEN', 'YELLOW', 'WHITE']


def get_random_colors(count):
    return [random.choice(COLORS) for _ in range(count)]


class FarbigeKreise(tk.Canvas):
    """Zeichnet die Kreise in den 4 Ecken"""

    def __init__(self, parent):
        tk.Canvas.__init__(self, parent, width=400, height=400)
        self.eingaben_blockiert = False
        self.verwendete_farben = list()
        self.kreis_ids = [
            self.zeichne_kreis(x, y, 70)
            for x, y in [(10, 10), (320, 10), (10, 320), (320, 320)]
        ]
        self.aendere_farben()
        self.winfo_toplevel().bind('<Key>', self.on_key)

    def zeichne_kreis(self, x, y, size):
        return self.create_oval(x, y, x + size, y + size, width=0)

    def aendere_farben(self):
        self.verwendete_farben = get_random_colors(4)
        for kreis_id, farbe in zip(self.kreis_ids, self.verwendete_farben):
            self.itemconfig(kreis_id, fill=farbe)
        self.eingaben_blockiert = False

    def on_key(self, event):
        if not self.eingaben_blockiert:
            delay = 5000
            if 'RED' in self.verwendete_farben and event.keysym == 'Return':
                delay = 0
            self.eingaben_blockiert = True
            self.after(delay, self.aendere_farben)


def main():
    root = tk.Tk()
    root.title('Test Farbige Kreise')
    farbiger_kreise = FarbigeKreise(root)
    farbiger_kreise.pack(side=tk.TOP)
    root.mainloop()


if __name__ == '__main__':
    main()
Benutzeravatar
Hel
User
Beiträge: 8
Registriert: Montag 14. März 2016, 15:51

Sehr verständlich erklärt. Danke.

sleep() - OK habe ich verstanden.

Die Fälle unterscheiden sich hier noch nicht, aber später:
Fall 1:
Nichtdrücken bei richtiger Farbe => Nach bestimmter Zeit Farbe wechseln und (irgendwohin) zurückschreiben , dass nicht gedrückt wurde
Fall 2:
Falsche Taste zur Farbe => Nach bestimmter Zeit Farbe wechseln und (irgendwohin) zurückschreiben , dass falsch gedrückt wurde. Aber trotzdem noch "warten" ob die richtige Taste gedrückt wird, bis die Zeit gesamt vorüber ist.
Fall 3:
Richtige Taste bei richtiger Farbe: Farben aktualisieren und das ganze von vorne :) (irgendwohin zurückschreiben, dass richtig in innerhalb der Reaktionszeit gedrückt wurde)

`event.keysym in ['Return']`habe ich verwendet, weil es ja nicht nur die Returntaste werden soll, sondern auch noch zwei andere, (aber nicht alle anderen). Dann hätte ich wieder

Code: Alles auswählen

`event.keysym in ['Return', 's', 'a']`
oder?
Planlos :K aber motiviert :D
BlackJack

@Hel: Ich konnte halt nur von dem Code ausgehen der da steht, ohne zu wissen was der später mal machen soll. :-)

Wenn mal weiter gewartet werden soll und mal nicht, dann reicht ein Wahrheitswert als Flag wohl nicht aus und dann will man die Eingabe ja auch nicht blockieren. Das ist eher eine Situation in der man einen Timer startet der am Ende eine Farbänderung auslöst, der aber unterbrechbar sein muss wenn der Benutzer in der Zeit die richtige Taste drückt. Dann müsste man sich die ID merken die `after()` zurück gibt und mit der man mit `after_cancel()` den Timer wieder stoppen kann.

Code: Alles auswählen

import random
import tkinter as tk

COLORS = ['BLACK', 'RED', 'GREEN', 'YELLOW', 'WHITE']


def get_random_colors(count):
    return [random.choice(COLORS) for _ in range(count)]


class ColoredCircles(tk.Canvas):
    """Zeichnet die Kreise in den 4 Ecken"""

    def __init__(self, parent):
        tk.Canvas.__init__(self, parent, width=400, height=400)
        self.color_changer_id = None
        self.verwendete_farben = list()
        self.kreis_ids = [
            self.zeichne_kreis(x, y, 70)
            for x, y in [(10, 10), (320, 10), (10, 320), (320, 320)]
        ]
        self.aendere_farben()
        self.winfo_toplevel().bind('<Key>', self.on_key)

    def zeichne_kreis(self, x, y, size):
        return self.create_oval(x, y, x + size, y + size, width=0)

    def aendere_farben(self):
        if self.color_changer_id is not None:
            print('Farbwechsel wegen Zeitueberschreitung.')
            if 'RED' in self.verwendete_farben:
                print('Richtige Farbe aber kein Tastendruck.')
        self.verwendete_farben = get_random_colors(len(self.kreis_ids))
        for kreis_id, farbe in zip(self.kreis_ids, self.verwendete_farben):
            self.itemconfig(kreis_id, fill=farbe)
        self.color_changer_id = self.after(5000, self.aendere_farben)

    def on_key(self, event):
        if 'RED' in self.verwendete_farben:
            if event.keysym in ['Return', 's', 'a']:
                print('Richtige Farbe und Taste.')
                self.after_cancel(self.color_changer_id)
                self.color_changer_id = None
                self.aendere_farben()
        else:
            print('Falsche Farbe')


def main():
    root = tk.Tk()
    root.title('Test Farbige Kreise')
    colored_circles = ColoredCircles(root)
    colored_circles.pack(side=tk.TOP)
    root.mainloop()


if __name__ == '__main__':
    main()
Antworten