Probleme mit der Zeitverzögerung bei Canvas

Fragen zu Tkinter.
JFrey
User
Beiträge: 6
Registriert: Mittwoch 24. September 2008, 07:48

Mittwoch 24. September 2008, 08:16

Hi Leute, das ist hier mein erster Post im Python-Forum und ich hoffe wirklich, dass ihr mir weiterhelfen könnt, ich bin nämlich ziemlich am verzweifeln...

Also: im Informatikunterricht in der Schule (12. Klasse) schreiben wir ein kleines Tic-Tac-Toe Spiel mit einer Tkinter Oberfläche.
Wenn man dieses Spiel startet, soll am Anfang ein Hauptmenu geöffnet werden, in dem eine Minuiatur-Vorschau des Spieles von alleine spielt.
Das heist, es sollen mithilfe von Zeitverzögerungen Canvas Objekte auf eine Leinwand "gemalt" werden.

Mein Problem besteht nun darin, dass wenn ich einfach time.sleep(3) vor die Zeichenfunktion in meinem Skript setzte, der die 3 Sekunden mit dem Programmstart wartet und erst startet, wenn alle time.sleep() Funktionen abgearbeitet sind, dann aber taucht das komplett gemalte Canvas auf.
Das ganze habe ich auch schon mit der after(ms=2000, func=self.zeichne_kreis()) versucht und das gleiche Problem wie mit time.sleep() gehabt.

Hier findet ihr die relevanten Stellen in meinem Quelltext:

Code: Alles auswählen

    def vorschau(self):
        self.fenster.after(ms=2000, func=self.zeichne_kreis(1,'yellow'))
        self.fenster.after(ms=200, func=self.zeichne_kreis(2,'yellow'))
bzw. mit time():

Code: Alles auswählen

import time

    def vorschau(self):
        time.sleep(3)
        self.zeichne_kreis(1,'yellow')
        time.sleep(3)
        self.zeichne_kreis(2,'yellow')
Wenn ihr wollt, kann ich auch gern den ganzen Quelltext posten, dachte mir aber, dass es dann ggf. etwas unübersichtlich wird...

Wäre echt klasse, wenn ihr mir helfen könntet...
Leonidas
Administrator
Beiträge: 16024
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Mittwoch 24. September 2008, 09:09

Hallo JFrey, willkommen im Forum,

Du musst ``after()`` eine Referenz auf eine Funktion mitgeben. So wie du das machst, rufst du die Funktion ja auf und zwar *sofort* und übergibst den Rückgabewert der Funktion an ``after``.
My god, it's full of CARs! | Leonidasvoice vs Modvoice
BlackJack

Mittwoch 24. September 2008, 10:29

Und bei der Variante mit `time.sleep()` müsstest Du nach dem zeichnen die `update()`-Methode aufrufen, damit die Operationen auch *sofort* ausgeführt werden.
Benutzeravatar
numerix
User
Beiträge: 2696
Registriert: Montag 11. Juni 2007, 15:09

Mittwoch 24. September 2008, 12:41

Wenn es dir auch darum geht, dass die von dir vorgegebene Verzögerung einigermaßen genau eingehalten wird, würde ich bei der sleep()-Variante mit eingestreutem update() bleiben.

Für die after()-Methode gilt, dass man sich lediglich darauf verlassen kann, das die entsprechende Funktion nicht VOR der angegebenen Zeit aufgerufen wird. Es kann aber auch länger dauern.
JFrey
User
Beiträge: 6
Registriert: Mittwoch 24. September 2008, 07:48

Mittwoch 24. September 2008, 21:20

Hey! Erstmal vielen Dank für die schnelle und professionelle Hilfe, leider konnte mein Problem jedoch nicht ganz behoben werden...

Hab das mit update() versucht, hatte dann aber das Problem, dass das Programm für die sleep()-Zeit komplett ausgelastet war und man keine Buttons mehr drücken konnte...
Diese ganze Def soll ja eine Animation werden, die die Grundfunktionen des Spiels zeigt und im Hauptmenu im Hintergrund abläuft. Das heist, dass man am besten noch Buttons etc. bedienen soll, wärend die "Animation" läuft...

Wenn das jetzt gar nicht mit time.sleep oder after() funktionieren sollte, könnte ich dann nicht ein animiertes .gif erstellen und einfach abspielen oder kann man mit TKinter generell keine gifs abspielen?

@ Leonidas: Was soll das heißen?
Du musst ``after()`` eine Referenz auf eine Funktion mitgeben.
da ist leider mein erst ca. 1 jähriges Wissen über python am Ende... wie müsste jetzt genau die after() Funktion aussehen?

Ich denke, dass ich am besten mal den ganzen Quelltext anfüge, damit ihr mitbekommt, was das Programm eigentl. soll...

Code: Alles auswählen

#!/usr/bin/python
# -*- coding: cp1252 -*-

import time
from Tkinter import *
from tkMessageBox import *

class Start:

    def __init__(self):
        self.fenster=Tk()
        self.fenster.title('Scharfenberger Tic Tac Toe')
        self.fenster.geometry('232x106+100+100')
        self.but_frame=Frame(self.fenster)
        self.but_frame.pack(side='left', anchor=N, pady=11, padx=10)
        self.but_start=Button(self.but_frame, text='Programm starten', command=self.starten, width=16)
        self.but_start.pack()
        self.but_anleitung=Button(self.but_frame, text='Anleitung', command=self.anleitung, width=16)
        self.but_anleitung.pack(pady=6)
        self.but_beenden=Button(self.but_frame, text='Programm beenden', command=self.beenden, width=16)
        self.but_beenden.pack()
        self.c_frame=Frame(self.fenster, width=120, height=120)
        self.c_frame.pack(side='right', anchor=N, pady=6, padx=6)
        self.canvas=Canvas(self.c_frame)
        self.canvas.pack()

    def position_auslesen(self,nr):
       if nr == 7:
          return [3,1]
       else:
          hilf = nr/4
          return [hilf+1,nr%4+hilf]

    def zeichne_quadrat(self,z,sp):
        self.quadrat=self.canvas.create_rectangle(30*(sp-1)+2,30*(z-1)+2,30*(sp-1)+31,30*(z-1)+31, outline='#949494', fill='#e0e0e0')

    def zeichne_kreis(self,nr,steinfarbe):
       var=self.position_auslesen(nr)
       self.kreis=self.canvas.create_oval(30*var[1]-24+1,30*var[0]-24,30*var[1]-3,30*var[0]-3, width=1, outline='#626262', fill=steinfarbe)

    def loesche_kreis(self,nr):
       self.geloeschter_kreis=self.c.create_oval(150*sp-140,150*z-140,150*sp-10,150*z-10, width=2, outline='#e0e0e0', fill='#e0e0e0')

    def zeichne_spielfeld(self):
        for i in range(9):
          if i == 6:
             self.zeichne_quadrat(3,1)
          else:
            index = (i+1)/4
            self.zeichne_quadrat(index+1,(i+1)%4+index)
 
    def starten(self):
        print 'Juhu'

    def anleitung(self):
        print 'Blubbl'

    def beenden(self):
        self.janein=askyesno('Wirklich beenden?', 'Möchten Sie das Programm wirklich beenden?')
        if self.janein == 1:
            self.fenster.destroy()

    def vorschau(self):
        self.fenster.update()
        time.sleep(2)
        self.zeichne_kreis(1,'yellow')
        self.fenster.update()
        time.sleep(1)
        self.zeichne_kreis(2,'yellow')
        self.fenster.update()

        

s=Start()
s.zeichne_spielfeld()
s.vorschau()
s.fenster.mainloop()
Benutzeravatar
numerix
User
Beiträge: 2696
Registriert: Montag 11. Juni 2007, 15:09

Mittwoch 24. September 2008, 21:46

Animierte GIFS funktionieren in Tkinter leider nicht.

Du kannst aber z.B. statt eines langen sleep() eine while-Schleife verwenden, die lauter kurze sleep()-Aufrufe enthält und entweder so lange läuft, bis die Gesamtzeit abgelaufen ist, oder bis z.B. durch einen Button-Klick ein Event eingetreten ist. Dazu muss allerdings zwischen den einzelnen Aufrufen der kurzen sleeps jeweils ein update() eingefügt werden, damit die event-Verarbeitung auch funktioniert.

Alternativ könntest du eine auf time.time() (Linux) oder time.clock() (Windows) basierende eigene Funktion schreiben, in der das Lauschen auf ein Event schon eingebaut ist.
Benutzeravatar
kaytec
User
Beiträge: 545
Registriert: Dienstag 13. Februar 2007, 21:57

Mittwoch 24. September 2008, 22:27

Hallo JFrey !

Koenntest es auch so loesen !

http://paste.pocoo.org/show/86130/

Den Kreis kannst du auch mit einem tag loeschen und musst ihn nicht uebermalen.

gruss frank
Leonidas
Administrator
Beiträge: 16024
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Mittwoch 24. September 2008, 22:35

JFrey hat geschrieben:@ Leonidas: Was soll das heißen?
Du musst ``after()`` eine Referenz auf eine Funktion mitgeben.
da ist leider mein erst ca. 1 jähriges Wissen über python am Ende... wie müsste jetzt genau die after() Funktion aussehen?
Ich schätze so:

Code: Alles auswählen

after(2000, self.zeichne_kreis, 1,' yellow')
Aber da ich TKinter nicht nutze, kann ich es nicht mit Bestimmtheit sagen. Aber so macht es PyGTK. Vielleicht nutzt man bei Tkinter auch lambdas an der Stelle, da musst du einfach mal in eine Referenz schauen.
My god, it's full of CARs! | Leonidasvoice vs Modvoice
Benutzeravatar
kaytec
User
Beiträge: 545
Registriert: Dienstag 13. Februar 2007, 21:57

Donnerstag 25. September 2008, 04:38

Hallo JFrey !

Code: Alles auswählen

self.canvas.after(1000, lambda : self.zeichne_kreis(1, "yellow"))
So muesste es mit einem lamdba aussehen.

gruss frank
lunar

Donnerstag 25. September 2008, 11:52

numerix hat geschrieben:Wenn es dir auch darum geht, dass die von dir vorgegebene Verzögerung einigermaßen genau eingehalten wird, würde ich bei der sleep()-Variante mit eingestreutem update() bleiben.

Für die after()-Methode gilt, dass man sich lediglich darauf verlassen kann, das die entsprechende Funktion nicht VOR der angegebenen Zeit aufgerufen wird. Es kann aber auch länger dauern.
Damit garantiert die after() Methode auf jeden Fall schon mal mehr als time.sleep(). Dazu sagt die Doku nämlich folgendes:
The actual suspension time may be less than that requested because any caught signal will terminate the sleep() following execution of that signal's catching routine. Also, the suspension time may be longer than requested by an arbitrary amount because of the scheduling of other activity in the system.
Genau genommen garantiert sleep also gar nichts, sondern ist eigentlich nur einem Empfehlung an das System, den eigenen Prozess mal bitte so lange schlafen zu legen.
Benutzeravatar
numerix
User
Beiträge: 2696
Registriert: Montag 11. Juni 2007, 15:09

Donnerstag 25. September 2008, 14:57

lunar hat geschrieben:Genau genommen garantiert sleep also gar nichts, sondern ist eigentlich nur einem Empfehlung an das System, den eigenen Prozess mal bitte so lange schlafen zu legen.
Ich hatte so etwas in Erinnerung, war mir aber nicht mehr sicher. Das war dann wohl auch der Grund, warum ich mir angewöhnt habe, mit der time() Funktion bzw. der clock() Funktion (je nach Betriebssystem) zu arbeiten, weil ich damit die besten Erfahrungen gemacht habe.
JFrey
User
Beiträge: 6
Registriert: Mittwoch 24. September 2008, 07:48

Freitag 26. September 2008, 14:47

Wow, echt cool, wie schnell man hier Hilfe bekommt!

So, ich hab mal mit euren Ratschlägen weiter gebastelt und muss sagen, dass ich eure Hilfsbereitschaft echt löblich finde...

Vor allem das "überarbeitete" Script von kaytec ist klasse, weil es genau so funktioniert, wie es später seinen soll: rechts läuft die Canvas-Animation ab und links kann der Benutzer jederzeit die Buttons klicken und das eigentliche Spiel starte... echt klasse!
Der einzige Haken besteht darin (vermutlich habe ich mich einfach nur sehr schlecht ausgedrückt :roll: ), dass eigentlich ein vorgegebenes Spiel ablaufen soll (ich hab einfach mal mit meiner Schwester die Tic-Tac-Toe Variante, die später im richtigen Programm gespielt werden kann, gespielt und die Spielzüge aufgeschrieben) und zwischen den einzelnen Spielzügen die Pausen von ca. 3 Sekunden eingefügt werden sollen.
Das Script von kaytec funktioniert mit after() wunderbar, blos wenn ich das mit einzelnen Befehlen (siehe Code weiter unten) mache, passiert das, was auch schon am Anfang mein Problem war...
Realisiere ich es mit self.canvas.after(1000, lambda : self.zeichne_kreis(1, "yellow")) und hab nur einen Befehl, funktioniert alles wunderbar, hab ich aber mehrere after() Befehle (mit lambda) passiert abermals das Problem von oben...

Damit klar wird, wie die Animation überhaupt aussehen soll:

Code: Alles auswählen

    def vorschau(self):
        #warten
        self.zeichne_kreis(5, 'red')
        #warten
        self.zeichne_kreis(2, 'blue')
        #warten
        self.zeichne_kreis(6, 'red')
        #warten
        self.zeichne_kreis(4, 'blue')
        #warten
        self.zeichne_kreis(9, 'red')
        #warten
        self.zeichne_kreis(3, 'blue')
        #warten
        self.zeichne_kreis(1, 'red')
        self.loesche_kreis(5)
        #warten
        self.zeichne_kreis(5, 'blue')
        self.loesche_kreis(2)
        #warten
        self.zeichne_kreis(8, 'red')
        self.loesche_kreis(6)
        #warten
        self.zeichne_kreis(6, 'blue')
        self.loesche_kreis(4)
        #warten
        self.zeichne_kreis(7, 'red')
        self.loesche_kreis(9)
        #warten
        self.zeichne_kreis(4, 'blue')
        self.loesche_kreis(3)
        #warten und ggf. die Animatiom wiederholen...
Benutzeravatar
kaytec
User
Beiträge: 545
Registriert: Dienstag 13. Februar 2007, 21:57

Freitag 26. September 2008, 17:46

Hallo JFrey !

Code: Alles auswählen

 def vorschau(self):
        if self.vorschau_index < len(self.vorschau_liste):
            var = self.position_auslesen(self.vorschau_liste[self.vorschau_index])
            self.kreis=self.canvas.create_oval(30*var[1]-24+1,30
                *var[0]-24,30*var[1] -3,30*var[0]-3, width=1, 
                    outline='#626262', fill="yellow", tag="kreis")
            self.vorschau_index += 1
            self.canvas.after(1000, self.vorschau)
Die Vorschauliste muss so aussehen und die machst du bei __init__ rein :

Code: Alles auswählen

def __init__(self):
        self.vorschau_liste = [4, 5, 7, 6, 1, 8, 9, 2, 3]

gruss frank
JFrey
User
Beiträge: 6
Registriert: Mittwoch 24. September 2008, 07:48

Freitag 26. September 2008, 20:26

Cooool!!! Ich habs!!!!
Vielen Dank an alle, die mir so schnell geschrieben haben, vor allem an kaytec (er hatte zum Schluss die Zündene Idee :wink: )

Nur 2 Sachen und es ist PERFEKT:
Warum zeigt der mit zum Schluss immer

Code: Alles auswählen

line 84, in vorschau
    self.canvas.after(self.zeit[self.vorschau_index], self.vorschau)
IndexError: list index out of range
aus?

und: Wie bekomm ich das hin, dass der das unendlich oft hintereinander ausführt? Hab mich schon an der while-Schleife versucht, bin aber leider gescheitert...

ansonsten ist hier der aktuelle Quelltext:

Code: Alles auswählen

#!/usr/bin/python
# -*- coding: cp1252 -*-

from Tkinter import *
from tkMessageBox import *

class Start:

    def __init__(self):
        g_1 = '#626262'
        g_2 = '#e0e0e0'
        r = 'red'
        b = 'blue'
        gr = 'green'
        self.vorschau_liste = [5, 2, 6, 4, 9, 3, 1, 5, 5, 2, 8, 6, 6, 4, 7, 9, 4, 3, 4, 5, 6, 4, 5, 6, 4, 5, 6, 4, 5, 6, 1, 4, 7, 5, 8, 6]
        self.vorschau_index = 0
        self.zeit = [1500, 1500, 1500, 1500, 1500, 1500, 1500, 0, 1500, 0, 1500, 0, 1500, 0, 1500, 0, 1500, 0, 1000, 0, 0, 1000, 0, 0, 1000, 0, 0, 1000, 0, 0, 1000, 1000, 1000, 1000, 1000, 1000]
        self.aussenfarbe = [g_1, g_1, g_1, g_1, g_1, g_1, g_1, g_2, g_1, g_2, g_1, g_2, g_1, g_2, g_1, g_2, g_1, g_2, g_1, g_1, g_1, g_1, g_1, g_1, g_1, g_1, g_1, g_1, g_1, g_1, g_2, g_2, g_2, g_2, g_2, g_2]
        self.fuellfarbe = [r, b, r, b, r, b, r, g_2 , b, g_2 ,r, g_2, b, g_2, r, g_2, b, g_2, gr, gr, gr, 'yellow', 'yellow', 'yellow', gr, gr, gr, b ,b ,b, g_2, g_2, g_2, g_2, g_2, g_2]
        self.fenster=Tk()
        self.fenster.title('Scharfenberger Tic Tac Toe')
        self.fenster.geometry('232x106+100+100')
        self.but_frame=Frame(self.fenster)
        self.but_frame.pack(side='left', anchor=N, pady=11, padx=10)
        self.but_start=Button(self.but_frame, text='Programm starten', command=self.starten, width=16)
        self.but_start.pack()
        self.but_anleitung=Button(self.but_frame, text='Anleitung', command=self.anleitung, width=16)
        self.but_anleitung.pack(pady=6)
        self.but_beenden=Button(self.but_frame, text='Programm beenden', command=self.beenden, width=16)
        self.but_beenden.pack()
        self.c_frame=Frame(self.fenster, width=120, height=120)
        self.c_frame.pack(side='right', anchor=N, pady=6, padx=6)
        self.canvas=Canvas(self.c_frame)
        self.canvas.pack()
        self.zeichne_spielfeld()
        self.vorschau()
        self.fenster.mainloop()

    def position_auslesen(self,nr):
       if nr == 7:
          return [3,1]
       else:
          hilf = nr/4
          return [hilf+1,nr%4+hilf]

    def zeichne_quadrat(self,z,sp):
        self.quadrat=self.canvas.create_rectangle(30*(sp-1)+2,30*(z-1)+2,30*(sp-1)+31,30*(z-1)+31, outline='#949494', fill='#e0e0e0')

    def zeichne_kreis(self,nr,steinfarbe):
       var=self.position_auslesen(nr)
       self.kreis=self.canvas.create_oval(30*var[1]-24+1,30*var[0]-24,30*var[1]-3,30*var[0]-3, width=1, outline='#626262', fill=steinfarbe)

    def loesche_kreis(self,nr):
       var=self.position_auslesen(nr)
       self.geloeschter_kreis=self.canvas.create_oval(30*var[1]-24+1,30*var[0]-24,30*var[1]-3,30*var[0]-3, width=1, outline='#e0e0e0', fill='#e0e0e0')

    def zeichne_spielfeld(self):
        for i in range(9):
          if i == 6:
             self.zeichne_quadrat(3,1)
          else:
            index = (i+1)/4
            self.zeichne_quadrat(index+1,(i+1)%4+index)
 
    def starten(self):
        print 'Juhu'

    def anleitung(self):
        print 'Blubbl'

    def beenden(self):
        self.janein=askyesno('Wirklich beenden?', 'Möchten Sie das Programm wirklich beenden?')
        if self.janein == 1:
            self.fenster.destroy()

    def vorschau(self):
            if self.vorschau_index < 36:
                var = self.position_auslesen(self.vorschau_liste[self.vorschau_index])
                self.kreis=self.canvas.create_oval(30*var[1]-24+1,30
                    *var[0]-24,30*var[1] -3,30*var[0]-3, width=1,
                    outline=self.aussenfarbe[self.vorschau_index], fill=self.fuellfarbe[self.vorschau_index], tag="kreis")
                self.vorschau_index += 1
                self.canvas.after(self.zeit[self.vorschau_index], self.vorschau)


def main():
    Start()
    
if __name__ == '__main__':
    main()
Benutzeravatar
kaytec
User
Beiträge: 545
Registriert: Dienstag 13. Februar 2007, 21:57

Freitag 26. September 2008, 20:32

Hallo JFrey !

Eine Deiner Listen wird evt. zu zu klein sein. Du koenntest aber auch alles in eine Liste speichern und so wuerde sich das Problem loesen.

Code: Alles auswählen

self.vorschau_liste = [[1000, 1, "red"], [1200, 4, "yellow"], usw,..]]
gruss frank
Antworten