Aktualisierung des Canvas, Buttons und andere Sorgen

Fragen zu Tkinter.
Antworten
Rezzi
User
Beiträge: 4
Registriert: Sonntag 2. Dezember 2012, 22:17

Hi,
ich versuche mich gerade daran, Tetris zu programmieren. Die grundlegenden Funktionen funktionieren bisher eigentlich auch ganz gut, nur mit tkinter arbeite ich jetzt zum ersten Mal und es gibt einige Dinge, die ich nicht nachvollziehen kann.
Das erste prob ist, dass mein canvas nicht aktualisiert wird, sobald ich eine while/for-schleife oder Rekursion einbaue. Ich habe schon gehört, dass man das bei grafischer Darstellung tunlichst vermeiden soll, aber ich weiß nicht, wie ich es umgehen soll.
Der zweite Punkt ist der, dass ich gerade versuche Buttons für die Steuerung(natürlich würde ich das lieber über die Tastatur machen, aber im Moment weiß ich noch nicht wie) zu basteln. Sie entstehen auch, aber sie bewirken nichts. Die Funktion, die die Buttons aufrufen, wird einmal automatisch ausgeführt und lässt sich dann nicht nochmal ausführen.
Aber bevor ich euch noch mehr verwirre, zeige ich euch am besten erstmal einen Code-Ausschnitt. Wichtig sind, denke ich, für euch nur die Funktion und Methode draw und die class Button, also nicht vor der Länge des Codes erschrecken.
Vielen Dank fürs anschauen

Code: Alles auswählen

import copy
import random
import time
from tkinter import *

master = Tk()

canvas_width = 100
canvas_height = 250
w = Canvas(master, 
           width=canvas_width,
           height=canvas_height)
w.pack()

fieldlist = []
for i in range(26):
    fieldlist.append([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])

class Tetriminos(object):

    def __init__(self, position, pivot, color):
        self.position = position
        self.pivot = pivot
        self.color = color

    def left_shift(self):
        backup = copy.copy(self.position) 
        for i in range(len(self.position)):
            change = str(int(self.position[i][0]) - 1) + self.position[i][1:]
            if "-" in change:  #checks that tetrimino is in screen
                self.position = backup
                return False
            for j in self.position:
                if "0" in self.position:
                    self.position = backup
                    return False
                elif fieldlist[int(j[1:]) - 1][int(j[0])-2] == 1: #checks for other tetriminos
                    self.position = backup
                    return False
            self.position[i] = change
        self.pivot[0] -=1

    def right_shift(self):
        backup = copy.copy(self.position)
        for i in range(len(self.position)):
            change = str(int(self.position[i][0]) + 1) + self.position[i][1:]
            if "0" in change:
                self.position = backup
                return False
            for j in self.position:
                if fieldlist[int(j[1:]) - 1][int(j[0])] == 1: #checks for other tetriminos
                    self.position = backup
                    return False
            self.position[i] = change
        self.pivot[0] += 1

    def rotate(self): #direction?
        for i in range(len(self.position)):
            a = int(self.position[i][0]) - self.pivot[0]
            b = int(self.position[i][1:]) - self.pivot[1]
            a, b = a*0 + b*(-1), a*1 + b*0
            a += self.pivot[0]
            b += self.pivot[1]
            if a >= 0 and b >= 0:
                self.position[i] = str(a) + str(b)
            else:
                pass
            
    def drop(self):
        pass

    def slow_drop(self):
        backup = copy.copy(self.position)
        for i in range(len(self.position)):
            change = self.position[i][0] + str(int(self.position[i][1:]) + 1)
            if int(change[1:]) >= 26:
                self.position = backup
                return False
            for j in self.position:    #checks for other tetriminos
                if fieldlist[int(j[1:]) - 1][int(j[0])] == 1:
                    self.position = backup
                    return False
            else:
                self.position[i] = change
        self.pivot[1] += 1
        return True

    def draw(self):
        for i in range(len(self.position)):
            a = int(self.position[i][0])*10
            b = (int(self.position[i][1:])-1)*10 #Das Koordinatensystem ist gedreht!!
            c = (int(self.position[i][0])+1)*10
            d = (int(self.position[i][1:]))*10
        
            w.create_rectangle(a, b, c, d, fill=self.color)
            w.create_rectangle(a, b, c, d, fill=self.color)
            w.create_rectangle(a, b, c, d, fill=self.color)
            w.create_rectangle(a, b, c, d, fill=self.color)
        

tetrimino1 = Tetriminos(["302", "402", "502", "602"], [4, 2], "green")

tetrimino2 = Tetriminos(["402", "502", "401", "501"], [5, 1], "grey")

tetrimino3 = Tetriminos(["302", "402", "502", "401"], [4.5, 1.5], "yellow")

tetrimino4 = Tetriminos(["302", "402", "502", "301"], [4.5, 1.5], "blue")

tetrimino5 = Tetriminos(["302", "402", "502", "501"], [4.5, 1.5], "red")

tetrimino6 = Tetriminos(["302", "402", "401", "501"], [4, 1], "white")

tetrimino7 = Tetriminos(["402", "502", "301", "401"], [5, 1], "orange")

tetrimino_list = [tetrimino1, tetrimino2, tetrimino3, tetrimino4,tetrimino5,
                  tetrimino6, tetrimino7]

settled_tetriminos = []

def draw(current):
    w.delete(ALL)
    current.draw()
    for t in settled_tetriminos:
        t.draw()
    #time.sleep(0.5)
    current.slow_drop()
    #print(current.position)
    if current.slow_drop() == False:  #a tetrimino is settled
        w.delete(ALL)
        current.draw()
        for t in settled_tetriminos:
            t.draw()
        settled_tetriminos.append(current) 
        return False
    w.delete(ALL)
    current.draw()
    for t in settled_tetriminos:
        t.draw() 
    w.after(100, draw(current))

for i in range(3):
    current1 = random.choice(tetrimino_list)
    global current2
    current2 = copy.deepcopy(current1)
    draw(current2)

class App:
    def __init__(self, master):
        frame = Frame(master)
        frame.pack()
        self.button = Button(frame, 
                         text="Left", fg="red",
                         command=self.left())
        self.button.pack(side=LEFT)
        self.button2 = Button(frame,
                         text="Right", fg="red",
                         command=self.right())
        self.button2.pack(side=RIGHT)
    def left(self):
        print("dsdsd")
        current2.left_shift()
        current2.draw()
    def right(self):
        print("ffff")
        current2.right_shift()
        current2.draw()

app = App(master)

master.mainloop()
PS: rufe ich draw so auf, funktioniert es. aber eben nur mit einem Stein:

Code: Alles auswählen

def draw():
    w.delete(ALL)
    current.draw()
    for t in settled_tetriminos:
        t.draw()
    time.sleep(0.1)
    current.slow_drop()
    print(current.position)
    if int(current.position[1][1:]) == 25:
        w.delete(ALL)
        current.draw()
        for t in settled_tetriminos:
            t.draw()
        settled_tetriminos.append(current)
        return False
    w.delete(ALL)
    current.draw()
    for t in settled_tetriminos:
        t.draw() 
    w.after(100, draw)

current = random.choice(tetrimino_list)
draw()
Zuletzt geändert von Anonymous am Freitag 11. Januar 2013, 16:54, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Code-Tags gesetzt.
Rezzi
User
Beiträge: 4
Registriert: Sonntag 2. Dezember 2012, 22:17

PPS: wenn ich die Zeile print(current2.position) in der draw-Funktion das # entferne, printet er nur jede 2. Zeile. irgendeine Idee warum?
BlackJack

@Rezzi: Bei GUIs darfst Du keine länger laufenden Schleifen haben. Denn entweder läuft die GUI, oder Dein Code der vom GUI-Code aufgerufen wird. Aktualisierung der GUI und Verarbeitung der Benutzereingaben läuft immer dann wenn Dein Code nicht läuft. Und wenn Dein Code ständig oder zumindest lange läuft, dann ist die GUI eben so lange „tot”. GUI-Programmierung ist ereingnisbasiert. Du schreibst kurze Code-Stückchen die bei bestimmten Ereignissen von der GUI aufgerufen werden und darauf kurz reagieren und dann gleich die Kontrolle wieder an die GUI zurück geben müssen. Wenn etwas zeitverzögert, vielleicht sogar regelmässig, passieren soll, dann gibt es dafür die `after()`-Methode. Die verwendest Du ja auch schon. Allerdings genau wie `command` bei `Button` falsch.

Die Funktionen bei den Schaltflächen werden nicht „automatisch” aufgerufen. *Du* rufst die da selbst ganz explizit auf! Lass das einfach. `command` soll ja die *Funktion* übergeben werden und nicht der Rückgabewert von einem *Aufruf* der Funktion. Genau letzteres machst Du da aber.

Der Sternchenimport von `tkinter` ist unsauber. Man holt sich da hunderte Namen in den Modulnamensraum, von denen nur ein Bruchteil tatsächlich genutzt wird.

Auf Modulebene sollte kein Code stehen der nicht nur Konstanten, Funktionen, Klassen, und so weiter definiert. Man sollte Module importieren können, ohne dass es als Programm sofort losläuft. Sonst kann man Code nicht wiederverwenden, oder einzelne Funktionen oder Klassen zur Fehlersuche isoliert testen und ähnliches. Dazu gibt es folgendes Idiom:

Code: Alles auswählen

if __name__ == '__main__':
    main()
In der `main()`-Funktion befindet sich dann der Code der das Modul als Programm ausführt.

Bei `fieldlist` würde sich eine „list comprehension” anbieten.

Die Sicherungskopien mit `copy.copy()` sind IMHO falsch herum gedacht. Es wäre sicherer eine Kopie zu erstellen und die zu verändern und am Ende zuzuweisen, wenn keine Abbruchbedingung gefunden wurde, als an mehreren Stellen in der Methode darauf achten zu müssen die Sicherungskopie wieder herzustellen. Dann könnte man auch mit deutlich weniger literalen `False`-Werten auskommen. Die teilweise aber sowieso komisch sind. Eine Methode die irgendwo `False` zurück gibt, sollte an anderer Stelle potentiell `True` zurückgeben und kein implizites `None`.

Man könnte den neuen, geänderten Wert auch „pythonischer” erstellen in dem man über die alte Position iteriert und eine neue Liste aufbaut, ohne den Umweg über Indizes und das ``for i in range(len(sequence)):``-Muster was in Python in 99% der Fälle ein „code smell” ist.

Die Datenstruktur eines Spielsteins ist gruselig. Statt ständig Zeichenketten zu zerlegen und zusammen zu setzen, und in Zahlen und wieder zurück zu wandeln, sollte man sich da einen anständigen Datentyp für schreiben. Dann kann man sich die Funktionsweise des Codes vielleicht auch einfacher vorstellen.

Warum werden in `draw()` vier Rechtecke mit den selben Argumenten erstellt?

Die durchnummerierten `tetrimino*`\s müssten nicht sein. Die kann man auch *gleich* als Liste schreiben, ohne die Objekte an unnötige Namen zu binden.

Funktionen sollte nur auf Werte zugreifen die als Argumente übergeben wurden, und nicht einfach globale Datenstrukturen verändern. Dadurch werden Programme unübersichtlich, fehleranfällig, schwerer zu testen, und Fehler sind schwerer zu isolieren und zu finden. Ausnahme sind Konstanten, die konventionell dadurch gekennzeichnet werden, dass der Name komplett in Grossbuchstaben geschrieben wird. ``global`` macht ander Stelle wo Du es geschrieben hast keinen Sinn, weil es dort absolut keinen Effekt hat. Das Schlüsselwort kommt in sauberen Programm in der Regel sowieso nicht vor.
Rezzi
User
Beiträge: 4
Registriert: Sonntag 2. Dezember 2012, 22:17

Vielen Dank, das war sehr hilfreich. Ich hab einiges umgesetzt und einige Fehler sind behoben. Die buttons funktionieren jetzt zB sauber. Ich habe eine Funktion main() zum Erstellen eines neuen Steinchens, die, wenn draw False zurückgibt wieder main mit w.after aufruft. Ich bastle aber noch ein bisschen an deinen Ratschlägen, ehe ich hier wieder Code poste.
Antworten