tkinter-widget Spinbox: ButtonRelease-Event kommt zu früh / after funktioniert nicht

  • Servus,

    ich bin gerade dabei, meinen >> Mediaplayer für Python3 << zu verbessern/erweitern.

    mein Problem:
    Auch jetzt kann man schon in der derzeit auf Github aktuellen Version 0.1 (Github-Repository >> hier <<) einstellen, mit welchem Alpha-Wert (Transparenz) ein Video dargestellt werden soll. Dieser Wert wird über das tkinter-Widget Spinbox eingestellt. Jetzt wird der Alpha-Wert beim Aufruf von omxplayer.bin als Kommandozeilenparameter übergeben und kann während des Abspielens nicht verändert werden. Zwischenzeitliche Änderungen wirken sich also erst bei einem Neustart von omxplayer.bin aus, d.h. beim Start des nächsten Videos.
    Eine geplante Verbesserung soll sein, dass ein geänderter Alpha-Wert über ein dbus-Kommando "sofort" an omxplayer.bin gesendet wird und das Video sofort in seiner Transparenz geändert wird. Die in meinem Programm verwendete Python-Klasse >> github.com/willprice/python-omxplayer-wrapper << gibt eine entsprechende Funktion set_alpha(alpha) her und das funktioniert auch.

    Für die Einstellung des Alpha-Wertes (0 - 255 in 15er-Schritten) verwende ich das tkinter-Widget Spinbox, das eigentlich für so etwas vorgesehen ist. Die Instanz des Widgets wird so erstellt:
    [code=php]self.spinAlpha = tkinter.Spinbox(self.frPlayerbuttons, width = 4, from_ = 0, to = 255, increment = 15, font = self.fileFont) # width = Anzahl der dargestellten Zeichen
    [/php]
    und so werden die Ereignishandler gebunden:
    [code=php]self.spinAlpha.bind('<KeyRelease>', self.spinAlpha_KeyRelease) # 2016-11-20 schlizbaeda V0.2: Ereignis für Änderung des Alpha-Wertes (Transparenz)
    self.spinAlpha.bind('<ButtonRelease-1>', self.spinAlpha_Click) # 2016-11-20 schlizbaeda V0.2: Ereignis für Änderung des Alpha-Wertes (Transparenz)
    [/php]

    Zunächst wurde der erste Ereignishandler für das Tastaturereignis erstellt:
    [code=php]
    def spinAlpha_KeyRelease(self, event):
    # 2016-11-20 schlizbaeda V0.2: Ereignishandler für Änderung des Alpha-Wertes (Transparenz) über die Tastatur (Loslassen): Zu diesem Zeitpunkt ist der Textinhalt des Steuerelementes bereits aktualisiert!
    alpha = self.spinAlpha.get()
    if alpha == '': alpha = '0'
    try:
    alpha = int(alpha)
    except:
    alpha = self.gl_alphaDefault
    if alpha > 255:
    alpha = 255
    elif alpha < 0:
    alpha = 0
    # Das Setzen des Alpha-Wertes (Transparenz) ist nur bei Videos möglich. Audiodateien werfen eine Exception --> daher try/except
    try:
    self.gl_omxplayer.set_alpha(alpha) # Alpha-Wert (Transparenz) über dbus ändern
    except:
    pass
    [/php]
    Hier wird der aktuelle Wert des Spinbox-Widgets ausgelesen und per dbus an omxplayer.bin gesendet. Es ist der Wert, der NACH der Änderung im Widget steht. Wenn z.B. mit "Cursor-down" von 201 auf 195 geändert wird, liest self.spinAlpha.get() wie erwartet, den Wert 195, den man als Anwender an dieser Stelle intuitiv erwartet.

    Nicht jedoch beim Ereignishandler für <ButtonRelease-1>:
    [code=php]
    def spinAlpha_Click(self, event):
    # 2016-11-20 schlizbaeda V0.2: Ereignishandler für Änderung des Alpha-Wertes (Transparenz) über ein Klick-Ereignis: Zu diesem Zeitpunkt ist der Textinhalt des Steuerelementes NOCH NICHT aktualisiert!

    ## Alle im folgenden auskommentierten Anweisungen beißen nicht wie gewünscht an:
    ## Der Wert der über self.spinAlpha.get() ausgelesen wird, wird offenbar erst
    ## NACH der vollständigen Abarbeitung des Ereignishandlers aktualisiert,
    ## d.h. während der Abarbeitung erhält man noch den alten Wert!
    #self.YAMuPlayGUI.update() # DoEvents
    #time.sleep(1.5)
    #self.YAMuPlayGUI.after(2000, self.spinAlpha_KeyRelease(event)) # funzt nicht!
    self.spinAlpha_KeyRelease(event)
    print(self.spinAlpha.get()) # DEBUG!
    [/php]
    Hier wird der Wert des Spinbox-Widgets ausgelesen, der UNMITTELBAR VOR der Änderung im Widget steht. Wenn z.B. mit "Cursor-down" von 201 auf 195 geändert wird, liest self.spinAlpha.get() den Wert 210 und unmittelbar danach wird der Wert erst aktualisiert. Die Folge ist, dass die Transparenz des Videos dann nicht dem angezeigten Wert im Widget entspricht!

    Die ganzen Befehle, mit denen ich herumprobiert habe, sind auskommentiert, da sie keine Änderung bewirken.

    Mir ist schon klar, dass die Ereignisverwaltung einer GUI beim Auftreten eines Ereignisses irgendwelche internen Sachen abarbeiten muss und erste den KOMPLETTEN Ereignishandler abarbeiten muss, bevor sie wieder in die Mainloop zurückkehrt. Beim Button-Ereignis, selbst in der ButtonRelease-Variante wird vermutlich zuerst der gebundene Ereignishandler abgearbeitet und erst danach das Unterereignis für die kleinen Pfeil-Buttons im Spinbox-Widget. So erklärt sich für mich die verspätete Aktualisierung des Wertes.
    Beim KeyRelease-Ereignis ist es offenbar andersherum und der im Ereignishandler ermittelte Wert ist "richtig".

    Was ich an dieser Stelle komisch finde und nicht verstehe:
    Warum beißt hier self.YAMuPlayGUI.after(2000, self.spinAlpha_KeyRelease(event)) nicht an? Die Wirkung ist die gleiche wie bei time.sleep(1.5): Es verlängert nur die Ausführungszeit des Ereignishandlers. Wegen der langen Testzeiten sieht man in der GUI genau, dass die Werte im Spinbox-Widget erst danach aktualisiert werden. Ich dachte immer, after dient in Python/tkinter dazu, der mainloop mitzuteilen, den angegebenen Befehl erst einige Millisekunden später aus der mainloop heraus auszuführen. Dann müsste ja der Ereignishandler "sofort" mit dem nächsten Befehl fortfaren und noch vor dem Kommando aus der after-Anweisung beendet sein (inkl. aller internen Ereignishandler für die Pfeilbuttons etc).
    Oder liege ich hier völlig falsch und verstehe die richtige Arbeitsweise von after nicht?

  • tkinter-widget Spinbox: ButtonRelease-Event kommt zu früh / after funktioniert nicht? Schau mal ob du hier fündig wirst!

  • Hm, muss vorab gestehen das ich da nicht allzu sehr durchsteige... Was ich aber bisher glaube zu wissen ist:

    python-omxplayer-wrapper macht ja im Prinzip nichts anderes als eine /tmp/omxplayerdbus.* Datei oder so anzusprechen. (quelle)
    Sofern deine GUI es also tatsächlich sendet und der Wrapper es auch weitergibt, sollte zumindest dieser Part funktionieren - es sei denn OMXPlayer selbst unterstützt sowas nicht (bug?)

    Bei dem bind('<KeyRelease>' Gedöns bin ich etwas verwirrt... Du prüfst zZt nicht welcher Key gedrückt wurde?
    Gemäß dieses Scripts: http://wiki.roxxs.org/index.php/Pyth…d_event#Tkinter
    ...kann man prüfen welche Taste gedrückt wurde - du reagierst derzeit aber einfach nur auf "egal welche"... Oder sehe ich das falsch?

    Sämtliche Aufrufe/Aktionen wie "time.sleep" blockieren die GUI, in der Zeit kann also nichts "geschehen". Solltest du also vermeiden.

    Zum "after": Dein aktuelles Problem besteht nur darin das der Wert in deiner GUI zu spät angezeigt wird, oder?

    Die mainloop ist quasi eine while Schleife die ziemlich schnell rotiert. Bei jeder Rotation wird geprüft ob die in "after" von dir definierte Zeit vorüber ist und erst dann die definierte Funktion aufgerufen. Wenn aber die mainloop durch irgendwas aufgehalten/blockiert wird verzögert sich das ganze - es läuft also nur ein Thread, nicht mehrere. Deshalb solltest du jetzt aber trotzdem nicht mehrere Threads nutzen, dann ignorierst du nur mögliche Probleme ;)
    Ein zu kleiner Zeitwert für "after" kann zB auch ausbremsen, je nachdem was dort geschieht. Es ist also besser einen sinnvollen Wert zu verwenden als einfach nur "10" oder so.

    Wenn ich das jetzt mit einem kleinen Script nachstelle kann ich glaube sehen was dein Problem sein könnte:
    [code=php]
    from Tkinter import *

    def spinAlpha_KeyRelease(event):
    print spinAlpha.get()

    def spinAlpha_Click(event):
    spinAlpha_KeyRelease(event)
    print spinAlpha.get()

    master = Tk()
    master.geometry("50x50")
    spinAlpha = Spinbox(master, width=4, from_=0, to=255, increment=15)
    spinAlpha.bind('<KeyRelease>', spinAlpha_KeyRelease)
    spinAlpha.bind('<ButtonRelease-1>', spinAlpha_Click)
    spinAlpha.pack()
    mainloop()
    [/php]
    Wenn ich das nun ausführe wird mir immer ein Wert von zuvor angezeigt, also ein mal auf den Pfeil nach oben geklickt wird in der GUI "15" angezeigt aber in der Konsole seh ich 2x "0". Klick dann noch mal auf den oben Pfeil wird in der GUI "30" angezeigt in der Konsole steht "15"... Wohlgemerkt sehe ich 2 Ausgaben in der Konsole, was an dem doppelten Aufruf in spinAlpha_Click() liegt - das solltest du da also wieder rauswerfen.

    Wenn ich das jetzt aber umbaue, so wie es glaub ich eher vorgesehen ist, dann passt es:
    [code=php]
    from Tkinter import *

    def spinAlpha_cmd():
    print spinAlpha.get()

    master = Tk()
    master.geometry("50x50")
    spinAlpha = Spinbox(master, width=4, from_=0, to=255, increment=15, command=spinAlpha_cmd)
    spinAlpha.pack()
    mainloop()
    [/php]

    Klick ich da auf einer der Pfeile stimmt der Wert in der Konsole absolut mit dem in der GUI überein.


    Falls das bei Dir trotzdem nicht klappt könntest du es noch mit textvariable probieren.


  • Hm, muss vorab gestehen das ich da nicht allzu sehr durchsteige...


    Ja genau! Wie ist denn das erst, wenn Du da richtig durchsteigst? ;)


    python-omxplayer-wrapper macht ja im Prinzip nichts anderes als eine /tmp/omxplayerdbus.* Datei oder so anzusprechen. (quelle)
    Sofern deine GUI es also tatsächlich sendet und der Wrapper es auch weitergibt, sollte zumindest dieser Part funktionieren - es sei denn OMXPlayer selbst unterstützt sowas nicht (bug?)


    funzt wie gewünscht, das ist ja das "Geile". In der omxplayer-Kommunikation gibt es (zum jetztigen Stand) keine Probleme. Allerdings liefert das Script Fehler, wenn man die Transparenz einer Audiodatei ändern will, aber das betrachte ich im Augenblick als ein absolut sekundäres "Problem".


    Bei dem bind('<KeyRelease>' Gedöns bin ich etwas verwirrt... Du prüfst zZt nicht welcher Key gedrückt wurde?
    Gemäß dieses Scripts: http://wiki.roxxs.org/index.php/Pyth…d_event#Tkinter
    ...kann man prüfen welche Taste gedrückt wurde - du reagierst derzeit aber einfach nur auf "egal welche"... Oder sehe ich das falsch?


    nein, das siehst Du richtig. Wenn man den Zahlenwert per Tastatur ändert, wird die Transparenz somit "in Echtzeit" angepasst. Mehr brauche ich da im Augenblick nicht...


    Sämtliche Aufrufe/Aktionen wie "time.sleep" blockieren die GUI, in der Zeit kann also nichts "geschehen". Solltest du also vermeiden.


    Das ist mir bewusst. Ich habe diese sleep-Funktion als "Debugmittel" eingebaut und dadurch erkannt, dass (zumindest in meinem Script) sich an dieser Stelle ein sleep und ein after für den Betrachter "nach außen hin" gleich verhalten.


    Zum "after": Dein aktuelles Problem besteht nur darin das der Wert in deiner GUI zu spät angezeigt wird, oder?


    richtig. ich verstehe nur nicht, warum after hier keinen "Pseudothread" erstellt, sondern anscheinend (wie sleep) den Ereignishandler verlängert. Es sieht so aus, als ob im Ereignishandler die after-Geschichte direkt abgearbeitet wird. Aber das kann auch ein rein visuelles Phänomen sein, so tief kann ich da nicht reindebuggen.
    Ich kenne das aus der Windows- bzw. .NET-Welt, da holt einen so etwas bisweilen bei jedem zweiten Ereignishandler ein. Da habe ich auch schon die übelsten Workarounds geschaffen. Ist aber auch (wie hier) vom Steuerelement abhängig. Es gibt aber (teilweise) mehr Ereignisse, die sich diesbezüglich zeitlich deutlich unterscheiden und verschiedenes Verhalten zeigen. Oder zumindest kenne ich da mehr...


    Ohne jetzt Deinen Vorschlag getestet zu haben, da habe ich erst heute Abend Zeit, aber das glaube ich sofort. Von dieser Sorte allgemeiner Ereignishandler, die man im "Konstruktor" mit command einbindet, habe ich auch schon gehört, aber sie (wie so vieles) erfolgreich verdrängt! Sobald ich es getestet habe, werde ich berichten.

    Aber vorab schon mal ein fettes :danke_ATDE:

    Gruß Peter
    Automatisch zusammengefügt:
    so, ich habe es jetzt "noch schnell" ausprobiert. Es funktioniert mit weniger Programmcode wie gewünscht und sogar noch besser :thumbs1:

    Aber jetzt ab in die Arbeit (es lebe die Gleitzeit)!

  • Ich war mir einfach unsicher weil ich schon etwas müde war ;)

    Vielleicht wär für deine GUI ja auch ein Slider / Schieberegler via Scale eine brauchbare alternative, den könnte man via Touchscreen denk ich besser bedienen.

    [code=php]
    from Tkinter import Tk, Scale, HORIZONTAL

    def spinAlpha_change(value):
    print value

    master = Tk()
    master.geometry("400x50")
    spinAlpha = Scale(master=master, from_=0, to=255, width=20, length=300, resolution=15, orient=HORIZONTAL, command=spinAlpha_change)
    spinAlpha.pack()
    master.mainloop()
    [/php]

    spinAlpha_change entspricht der spinAlpha_cmd mit dem einzigen Unterschied dass dieser Funktion direkt der aktuelle Wert übergeben wird. Man kann aber trotzdem spinAlpha.get() anwenden. Beachte aber dass die Funktion auch direkt beim ausführen dieses Scripts aufgerufen wird und auch für jeden einzelnen Schritt also nicht nur dann wenn losgelassen wird sondern wirklich 0 bis 255 in 15er Schritten - keine Ahnung ob das ein Problem wäre


  • spinAlpha_change entspricht der spinAlpha_cmd mit dem einzigen Unterschied dass dieser Funktion direkt der aktuelle Wert übergeben wird. Man kann aber trotzdem spinAlpha.get() anwenden. Beachte aber dass die Funktion auch direkt beim ausführen dieses Scripts aufgerufen wird und auch für jeden einzelnen Schritt also nicht nur dann wenn losgelassen wird sondern wirklich 0 bis 255 in 15er Schritten - keine Ahnung ob das ein Problem wäre

    Das Resultat aus command, dass in 15er-Schritten durchgelaufen wird, ist für meine Anwendung sogar gut, da man bei einem längeren Klick ein automatisches Durchlaufen der Transparenz erhält (cool).

    Das mit dem Slider ist noch eine Idee, die ich prüfen werde...


  • Was ich an dieser Stelle komisch finde und nicht verstehe:
    Warum beißt hier self.YAMuPlayGUI.after(2000, self.spinAlpha_KeyRelease(event)) nicht an? Die Wirkung ist die gleiche wie bei time.sleep(1.5): Es verlängert nur die Ausführungszeit des Ereignishandlers. Wegen der langen Testzeiten sieht man in der GUI genau, dass die Werte im Spinbox-Widget erst danach aktualisiert werden. Ich dachte immer, after dient in Python/tkinter dazu, der mainloop mitzuteilen, den angegebenen Befehl erst einige Millisekunden später aus der mainloop heraus auszuführen. Dann müsste ja der Ereignishandler "sofort" mit dem nächsten Befehl fortfaren und noch vor dem Kommando aus der after-Anweisung beendet sein (inkl. aller internen Ereignishandler für die Pfeilbuttons etc).
    Oder liege ich hier völlig falsch und verstehe die richtige Arbeitsweise von after nicht?

    Ohne auf all das andere einzugehen - wenn das, was du da schreibst, stimmt, dann bist du dem wohl üblichsten Fehler in der Geschichte der Callbacks aufgesessen, und wenn ich auch nur einen Euro haette fuer jedes mal, wenn ich darauf hingewiesen habe... nun, fuer nen Ferrari reicht's nicht, aber viel Freibier fuer mehrere Jahre schon.

    Du rufst den callback hier schon gleich *auf*, und was auch immer spinAlpha... zurückgibt wird als callback gesetzt. Wahrscheinlich None.

    Stattdessen musst du

    Code
    self.widget.after(2000, self.spinThing)

    schreiben - OHNE KLAMMERN!!!!!!!! Denn nur dann übergibst du ein callable. Was uebrigens auch impliziert, dass du kein Event haben kannst, ein Timer hat (in Tk zumindest) kein Event).


  • Du rufst den callback hier schon gleich *auf*, und was auch immer spinAlpha... zurückgibt wird als callback gesetzt. Wahrscheinlich None.

    Stattdessen musst du

    Code
    self.widget.after(2000, self.spinThing)

    schreiben - OHNE KLAMMERN!!!!!!!! Denn nur dann übergibst du ein callable. Was uebrigens auch impliziert, dass du kein Event haben kannst, ein Timer hat (in Tk zumindest) kein Event).

    Da ist der Hund begraben! Das ist eben Python mit seiner mir noch nicht 100% geläufigen Syntax, wenngleich prinzipiell leicht erlernbar.
    Jetzt ist mir auch klar, warum es zu den von mir beobachteten Phänomenen kam...

    Merke:
    Funktionsname mit Klammern ist IMMER ein Funktionsaufruf
    Funktionsname ohne Klammern ist ein Funktionszeiger (bzw. im .NET-Jargon ein "Delegate"), das was man u.a. für after benötigt

Participate now!

Don’t have an account yet? Register yourself now and be a part of our community!