lahmende Animation

Fragen zu Tkinter.
BlackJack

@Goswin: Das ist doch logisch, dass die Schleife die Animation "aufhält", denn wenn die Schleife läuft, dann läuft ja ganz offensichtlich nicht die Tkinter-Hauptschleife. Da GUI-Kram immer im gleichen Thread passieren muss, kann entweder nur Dein Code etwas tun, oder die GUI-Hauptschleife.
Benutzeravatar
Goswin
User
Beiträge: 366
Registriert: Freitag 8. Dezember 2006, 11:47
Wohnort: Ulm-Böfingen
Kontaktdaten:

BlackJack hat geschrieben:@Goswin: Das ist doch logisch, dass die Schleife die Animation "aufhält", denn wenn die Schleife läuft, dann läuft ja ganz offensichtlich nicht die Tkinter-Hauptschleife. Da GUI-Kram immer im gleichen Thread passieren muss, kann entweder nur Dein Code etwas tun, oder die GUI-Hauptschleife.
Ist ja toll, was für euch Insider alles logisch ist :cry:
Leider muss ich meine Schleife unterbrechen können, indem ich auf einen Button drücke. Andere GUIs können das, folglich muss es irgendeine Art geben, das zu machen. Sollte die undurchsichtige after()-Methode *befürcht* vielleicht die einzige Möglichkeit dazu sein?
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

Hallo Goswin

Es gibt sicher noch andere Möglichkeiten als mit der after()-Methode. Hier ein Lösungsansatz der nur auf after()-Methoden basiert:
http://paste.pocoo.org/show/179359/

Das ganze kann natürlich noch optimiert evtl. vereinfacht werden.

Gruss wuf :wink:
Zuletzt geändert von wuf am Mittwoch 17. Februar 2010, 18:29, insgesamt 1-mal geändert.
Take it easy Mates!
Benutzeravatar
Goswin
User
Beiträge: 366
Registriert: Freitag 8. Dezember 2006, 11:47
Wohnort: Ulm-Böfingen
Kontaktdaten:

:D :D :D Geschafft :D :D :D

Es geht auch OHNE die after()-Methode, einfach und direkt, im Anschluss folgt mein Code.

Aber ich weiß noch immer nicht genau, warum es jetzt funktioniert und vorher nicht. Wenn ich Blackjack richtig verstanden habe, dürfte der Code unten nämlich nicht funktionieren, weil die while-Schleife angeblich nicht unterbrochen werden kann. Unter anderem habe ich update_idletasks() durch update() ersetzt, aber ob so etwas die Schleife unterbricht, bin ich mir nicht sicher.

Falls das Ganze einen Pferdefuß hat, lasst es mich bitte wissen...

Code: Alles auswählen

#!/usr/bin/python3

import tkinter as t
from math import pi,cos
from time import sleep


class Oval(object):

   def __init__(self, canvas, cx, cy, rx, ry, color='black'):
      #
      self.canvas = canvas
      self.cx = cx
      self.cy = cy
      self.rx = rx
      self.ry = ry
      self.color = color
      #
      self.tt = 0
      self.coords = (
         self.cx-self.rx, self.cy-self.ry,
         self.cx+self.rx, self.cy+self.ry )
      self.graph_obj = self.canvas.create_oval(
         self.coords, fill=self.color)
      #
      self.run = False

   def animate_step(self):
      #
      self.tt += 1
      self.xx = self.rx*cos(self.tt*pi/128)
      self.coords = (
         self.cx - self.xx,
         self.cy - self.ry,
         self.cx + self.xx,
         self.cy + self.ry
         )
      #
      self.canvas.coords(self.graph_obj, self.coords)


class Bild(object):
 
   def __init__(self):
      #
      root = t.Tk()
      #
      self.canvas = t.Canvas(root,bg='white')
      self.canvas.pack()
      button = t.Button(root,text='start/stop',command=self.startstop)
      button.pack()
      #
      cx = 150; cy = 120; rx = 100; ry = 100
      self.oval = Oval(self.canvas, cx, cy, rx, ry)
      #
      cx = 150; cy = 120; rx = 80; ry =  80
      self.oval_red = Oval(self.canvas, cx, cy, rx, ry, 'red')
      #
      self.run = False

   def startstop(self):
      self.run = not self.run
      #
      while self.run:
         self.oval.animate_step()
         self.oval_red.animate_step()
         #
         self.canvas.update()
         sleep(0.01)


bild = Bild()
t.mainloop()
Python nervt. (Nicht immer, und natürlich nur mich, niemals andere)
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

@Goswin: Super! Gratuliere!

Gruss wuf :wink:
Take it easy Mates!
Benutzeravatar
numerix
User
Beiträge: 2696
Registriert: Montag 11. Juni 2007, 15:09

Goswin hat geschrieben:Es geht auch OHNE die after()-Methode, einfach und direkt, im Anschluss folgt mein Code.

Aber ich weiß noch immer nicht genau, warum es jetzt funktioniert und vorher nicht. Wenn ich Blackjack richtig verstanden habe, dürfte der Code unten nämlich nicht funktionieren, weil die while-Schleife angeblich nicht unterbrochen werden kann. Unter anderem habe ich update_idletasks() durch update() ersetzt, aber ob so etwas die Schleife unterbricht, bin ich mir nicht sicher.

Falls das Ganze einen Pferdefuß hat, lasst es mich bitte wissen...
Dein Code funktioniert, weil die Schleife den update()-Aufruf enthält.
Ansonsten würde deine Anwendung durch die Schleife komplett blockiert.
Sofern dein Programm nicht mehr machen soll, als diese Animation, ist damit alles in Ordnung. Wenn nicht, wäre das der Pferdefuß. Willst du, dass die Animation quasi im Hintergrund abläuft und das Programm noch andere Aufgaben erledigt, dann landest du wieder bei after() ....

Aber abgesehen davon: Es wäre gewiss nicht das Schlechteste, wenn du deine Aversionen gegen after() abbauen könntest. Wenn du das einmal verstanden hast, wirst du after() auch gerne einsetzen ...
BlackJack

@Goswin: Jetzt machst Du mit dem `update()` den Job den sonst die Tkinter-Hauptschleife machen würde. Also auch hier läuft wieder nur die eine *oder* die andere der beiden Schleifen.

Der Pferdefuss ist, dass das nicht skaliert. Zwei Animations-Schleifen "gleichzeitig" gehen nicht.
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

......Hier noch ein Hinweis:

w.update()

Code: Alles auswählen

This method forces the updating of the display. It should be used only if you know what you're
doing, since it can lead to unpredictable behavior or looping. It should never be called from an event
callback or a function that is called from an event callback.
Take it easy Mates!
Benutzeravatar
Goswin
User
Beiträge: 366
Registriert: Freitag 8. Dezember 2006, 11:47
Wohnort: Ulm-Böfingen
Kontaktdaten:

numerix hat geschrieben:Es wäre gewiss nicht das Schlechteste, wenn du deine Aversionen gegen after() abbauen könntest. Wenn du das einmal verstanden hast, wirst du after() auch gerne einsetzen ...
Na, dann will ichs mal versuchen; Unklarheiten habe ich haufenweise, da ich noch nie Threads programmiert habe!

(1)
widget.after(1000,tuwas) bedeutet offenbar _nicht_, dass nach 1_Sekunde <tuwas> ausgeführt wird, sondern wohl eher:
  • Sobald der Tk-Mainloop-Thread der after()-Methode Rechenzeit zuweist, wird die Uhr aufgerufen, um den Zeitpunkt <jetzt> zu erhalten.
    Dann wird im Mainloop-Thread für den Zeitpunkt <jetzt>+1_Sekunde ein Termin eingetragen mit dem Auftrag <tuwas> aufzurufen.
    Sobald das Mainloop seinem Terminkalender Zeit zuweist, (was lange nach der vorgesehenen Zeit sein kann) wird wieder die Uhr aufgerufen und mit dem Terminzeitpunkt verglichen.
    Und wenn die Uhrzeit nach dem Terminzeitpunkt liegt, wird (hoffentlich unmittelbar und synchron) die Methode <tuwas> ausgeführt.
Könnt ihr mir so etwas bestätigen?

(2)
widget.after(1000,tuwas) ist immer an ein Widget gebunden. Welches Widget soll das sein und welche Bedeutung hat es? Ist es völlig egal, mit welchem Widget ich after(1000,tuwas) aufrufe? Das Tk-Mainloop muss ja sowieso alle Widgets befragen.
BlackJack

@Goswin: Das ist jetzt auch noch keine Programmierung mit Threads. Also eigentlich noch ein wenig unkomplizierter, denn ein paar hässliche Sachen, die einem mit Threads passieren können, treten bei `after()` nicht auf.

zu (1): Die `after()`-Methode wird *sofort* aufgerufen, also <jetzt> wird auch gleich bestimmt. Und nach ungefähr der angegebenen Zeit wird ein Ereignis in die Event-Queue von der Hauptschleife gesteckt, das dafür sorgt, dass `tuwas` aufgerufen wird. Wie das Ereignis in die Queue kommt, ist ein Implementierungsdetail, das kann von Plattform zu Plattform unterschiedlich gelöst werden. Es kann Verzögerungen geben, einmal bedingt durch die Art wie die Zeit ermittelt wird, wann das Ergeignis zum Aufruf fällig ist, und dann natürlich auch durch den ganzen Code der in der Mainloop abgearbeitet werden muss, inklusive von Rückruffunktionen die Du als Benutzer von `Tkinter` schreibst. Wenn Du einen Button mit einer Funktion verbindest, die sagen wir mal 3 Sekunden läuft, und innerhalb dieser 3 Sekunden das Ereignis für `tuwas` fällig ist, dann wird `tuwas` frühestens aufgerufen nachdem Dein Code für den Button-Druck die Kontrolle wieder an die Mainloop abgegeben hat. Weil ja keine verschiedenen Threads benutzt werden und nichts wirklich nebenläufig passiert.
dahaze
User
Beiträge: 75
Registriert: Freitag 13. März 2009, 10:57
Wohnort: im Schwabenland

Hallo Goswin!

Die Erklärung von dir unter (1) ist generell richtig.
Der Tkinter.mainloop() macht eigentlich nichts Anderes, als in einer Endlosschleife die Events der darauf aufbauenden Widgets abzufragen bzw. die Widgets zu aktualisieren. Ein Event, z.B. das Klicken auf einen Button, "unterbricht" diese Schleife und deshalb "taucht" deine Applikation in diese daran gebundene Methode ab.
Mit der Methode "update()" zwingst du dennoch deine GUI die "darunter" liegenden Widgets zu aktualisieren - sozusagen während der durch den Klick ausgelösten Methode - einen "mainloop()" bzw. eine Aktualisierung durchzuführen.
Dies kann allerdings ins Auge gehen wenn während diesem "Zwischen-Mainloop" wiederrum eine Update-Methode gerufen wird und das Ganze in einer Endlosschleife endet. Deshalb die Warnung zur Verwendung von update(), wie sie Wuf schon gepostet hat.^^ :wink:

Aus dem eingebauten Hilfetext zur after-Methode:

Code: Alles auswählen

>>> import Tkinter
>>> root = Tkinter.Tk()
>>> help(root.after)
Help on method after in module Tkinter:

after(self, ms, func=None, *args) method of Tkinter.Tk instance
    Call function once after given time.
    
    MS specifies the time in milliseconds. FUNC gives the
    function which shall be called. Additional parameters
    are given as parameters to the function call.  Return
    identifier to cancel scheduling with after_cancel.
Genau genommen wird die Methode "tuwas" erst aufgerufen wenn:
1) die eingestellte Zeit ("given time") abgelaufen ist
2) der mainloop oder eine update-Methode das Widget aktualisiert hat, an das die "after-Methode" gebunden ist.

Ein bisschen was zum Testen:

Code: Alles auswählen

import Tkinter
import time

def go_btn():
  textbox.insert('end', 'START')
  for zaehler in range(5):
    time.sleep(2)
    knopf2.after(1000,tuwas)
    textbox.insert('end', '\n'+str(zaehler))    
    # 1)
    # Ist diese Zeile aktiv, wird nach jedem Schleifendurchlauf geprueft ob die
    # Bedingungen fuer die after-Methode bereits erfuellt sind
    root.update()
  # 2)
  # Ist diese Zeile aktiv, wird der Button an dem die after Methode gebunden ist
  # zerstoert bevor der "mainloop" oder "update" eine Gelegenheit hat die Methode
  # auszuloesen. Resultat -> "tuwas" wird nie aufgerufen
  #knopf2.destroy()

  # 3)
  # Ist diese Zeile aktiv, werden jetzt alle after-Methoden aufgerufen deren 
  # "Zeitvorgabe" bereits abgelaufen ist.
  #root.update()

  textbox.insert('end', '\n\nENDE')


def tuwas():
  textbox.insert('end','\nund jetzt kommt die Fktn "tuwas"')

root = Tkinter.Tk()
knopf1 = Tkinter.Button(root, text='GO', command=go_btn)
knopf1.grid(row=1, column=0)
knopf2 = Tkinter.Button(root, text='GO2')
knopf2.grid(row=1, column=1)
textbox = Tkinter.Text(root)
textbox.grid(row=0, column=0, columnspan=2)

root.mainloop()
In dem geposteten Beispiel-Code natürlich immer nur eine der 3 Zeilen aktivieren um den beschriebenen Effekt zu erzeugen. :D
EDIT: Hab den Beispiel-Quelltext nochmal leicht geändert.

Gruß,
Simon
Antworten