Problem mit bind event

Fragen zu Tkinter.
Antworten
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Hatte mich schon gewundert, warum mein Programm mitunter abschmiert, wenn ich commands und events nicht über meine Queue zur Ausführung bringe. Habe jetzt herausgefunden, woran es liegt.

Hier ist eine kleine Callback Funktion und die ist abgeschmiert:

Code: Alles auswählen

def entry_event(me,button=None):
    if button != None: button.setconfig('bg',me.get())
    setconfig(me.mydata,me.get())
    me['bg']='gray'
    informLater(300,me,'color',True)
Erste Zeile egal, kam da eh nicht zur Anwendung. Zweite Zeile ein durch try und except abgesichertes config. Und die nachfolgende Zeile funktionierte in einem speziellen Fall nicht mehr:

Code: Alles auswählen

me['bg']='gray'
Da fragt man sich schon, wie kann das sein. Hier handelt es sich um ein Entry Eingabefeld. Meine Widgets haben eine Membervariable mydata für extra Daten. Hier hatte ich abgelegt für welche config Option das sein sollte. Eine spezielle config Option meiner Widgets, die es normalerweise nicht gibt, heißt 'link'. Wenn man die setzt, wird ein Sccript nachgeladen oder sukzessive auch viele. Beim Nachladen dieser Scripts wurde richtigerweise das Entry Eingabefeld gelöscht. Die Callbackfunktion aber hat doch im ersten Parameter 'me' eine Referenz auf das Widget. Also hätte es doch noch existieren müssen.

Aber der bind Befehl kümmert sich nicht darum, hat das Widget beseitigt trotz der Referenz 'me' im ersten Parameter. Dadurch crashte me['bg']='gray'. Wenn ich allerdings den bind Befehl nicht den Callback Aufruf ausführen lasse, sondern den Callback zur Ausführung an eine Queue übergeben lasse, dann bleibt bei der Ausführung die Referenz auf das Widget erhalten und es wird deshalb auch nicht während des Callbacks gelöscht, sondern erst nachher.

Daher habe ich keine Probleme, wenn ich dann die Callbacks über eine Queue ausführen lasse. Mit dem bind Befehl dagegen komme ich in speziellen Situationen nicht besonders weit.

Also, wenn man 'bind' nimmt, ist es keine gute Idee, das Widget während des Callbacks zu beseitigen und dann noch auf es zugreifen wollen.

Die andere Frage wäre, macht das Programm dann diese Timer Message noch oder nicht mehr: informLater(300,me,'color',True)

Da hab ich keine Ahnung. Sichtbar ist es eh nicht mehr, und ob es dann noch existiert, entzieht sich meiner Kenntnis. Aber crashen tut es nicht, denn infom und informLater sind abgesichert gegen Nichtexistenz.

Also ohne zusätzliche Referenz wird das Widget beseitigt, ohne Rücksicht darauf, dass man gerade den Callback ausführt. Callback ausführen zählt also nicht als Referenz!
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Dieses Problem passiert normalerweise nicht bei Euch. Denn im Normalfall:
- habt Ihr eine Variable, welche die Referenz auf das Widget enthält
- löscht Ihr wahrscheinlich kaum Widgets
- und wenn, dann wohl kaum während des Callbacks
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

@Alfons Mittelmeyer: das Tk-Widget ist halt unabhängig vom Python-Objekt. Wenn Du das Tk-Widget löschst, dann ist es weg und die Tk-Referenz darauf, die im Python-Objekt steht nicht mehr gültig. Das Python-Objekt hat aber noch Referenzen. Das kann man dadurch lösen, dass man nur schwache Referenzen dort benutzt, wo man keine direkte Kontrolle über das Tk-Widget hat.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Sirius3 hat geschrieben:@Alfons Mittelmeyer: das Tk-Widget ist halt unabhängig vom Python-Objekt. Wenn Du das Tk-Widget löschst, dann ist es weg und die Tk-Referenz darauf, die im Python-Objekt steht nicht mehr gültig. Das Python-Objekt hat aber noch Referenzen. Das kann man dadurch lösen, dass man nur schwache Referenzen dort benutzt, wo man keine direkte Kontrolle über das Tk-Widget hat.
Wenn man destroy macht, dann ist das Widget noch nicht weg. Es ist zwar nicht mehr sichtbar, aber widget['bg']='gray' geht noch. Bei mir war es so dass zur Zeit des Callbacks gar keine Referenz mehr da war. Meine abgeänderte destroy Anweisung beseitigt nämlich die Referenzen. Ich habe keine Referenz auf das Widget durch eine Variable sondern durch einen Eintrag in eine Namensliste. Die Destroy Anweisung löscht diesen Eintrag. Dann existiern Message Callbacks. Die Destroy Anweisung für einen Container Inhalt löscht auch diese Message Callbacks. Dann gibt es Message Callbacks für das Widget selber. Auch diese werden gelöscht. Nach meinem destroy gibt es keinerlei Referenzen mehr.

Eine letzte Referenz habe ich nur durch die Queue, wenn ich nämlich den Callback über die Queue aufrufe. Und eine letzte Galgenfrist von 300 ms bekam das widget noch über die informLater Anweisung im Callback. Das wird aber nicht mehr ausgeführt, da diese Callbacks bereits gelöscht wurden.

Also, wenn das Widget gelöscht wird (destroy) und gar keine Referenz mehr auf das widget da ist, erst dann bekommt man Probleme wenn man noch im Callback für das Widget steckt und auf es zugreifen will. Die Referenz in einem bind oder command zählt nämlich nicht, weil beim Widget löschen diese Bindung beseitigt wird.

Die Tk-Referenz ist also noch gültig, solange es für das Widget noch mindests eine sonstige Python Referenz gibt. Gültig noch nach destroy(), ungültig erst bei __destroy__()

Sehr spezieller Spezialfall bei mir. Die einzige Referenz für die Callbackfunktion war nämlich auch dieser bind. Ob die Callbackfunktion dann während der Ausführung noch gültig ist, keine Ahnung. Im physikalischen Speicher war sie jedenfalls noch.

Frage: wird in diesem Falle eine Funktion bereits während ihrer Ausführung ungültig, oder erst danach? (return oder Ende?)
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Alfons Mittelmeyer:
Was meinst Du mit "während des Callbacks"? Da man GUI-Manipulationen nur im GUI-Thread machen sollte, gibt es kein "während". Das Objekt ist entweder vorher schon abgeräumt - dann hast Du eine ungültige Referenz (z.B. dangling pointer in C). Oder danach - dann ist es Dir egal, weil der Callback normal funktioniert. Offensichtlich wird in `.send` nicht streng referenziert, warum das so ist, muss Du die Programmierer der Bibliothek fragen. Es kommt wahrscheinlich daher, dass Tk und Python Referenzen unterschiedlich handhaben, diesen Twist findet man auch bei anderen nicht originären Pythonbibliotheken wie den Qt-bindings.
Für Dich heisst das, Du solltest auch die callback-bindings löschen, wenn Du ein Objekt verwirfst.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@jerch Man kann auch während des Callbacks das Widget löschen, an welches der Callback gebunden ist. Da man im mainloop steckt, kann man nur während Callbacks etwas tun. Natürlich hatte ich das nicht mit voller Absicht getan, sondern etwas getan, wobei eben dieses Widget gelöscht wurde.

Ich weiss ja nicht, wie es bei Deiner tkinter Version ist. Aber bei mir geht das dann nicht mehr: me.['bg'] = 'gray'

Code: Alles auswählen

import tkinter as tk

root = tk.Tk()

liste = []

liste.append(tk.Entry())
liste[0].pack()

def callback(event,me):
    me.destroy()
    me['bg'] ='gray'	

liste[0].bind("<Return>",lambda event, me = liste[0]: callback(event,me))

liste.pop()

root.mainloop()
Allerdings bin ich jetzt auch nicht schlauer. Denn wenn ich liste.pop() nicht mache, geht me['bg'] ='gray' auch nicht.
Und trotzdem ging das dann in meinem grossen Programm noch - jetzt komme ich aber nicht darauf, woran das gelegen haben könnte.

Muss aber mit meinem Message Sytem und meinem proxy irgendwie zusammengehangen haben. Müsste nur noch darauf kommen, was dann unterschiedlich gewesen sein könnte.
Ob im Falle von send dann auch der Aufruf der Funktion, die das destroy beinhaltete nochmal über die Queue lief und dann erst hinterher statt fand? Aber warum das?

Also bin noch nicht darauf gekommen. Müsste mal nachsehen, welche Proxy Konfiguration ich hatte. Bei einer wird nämlich der erste send sofort wie ein Funktionsaufruf ausgeführt und wenn während der Ausführung nochmals ein send kommt, dann kommt das in die Queue. Im einen Falle wäre dann das destroy als erstes send, also wie ein Funktionsaufruf ausgeführt worden, im zweiten Falle aber erst hinterher über die Queue.

Das müßte ich mir einmal genauer ansehen.
Zuletzt geändert von Alfons Mittelmeyer am Freitag 4. September 2015, 08:24, insgesamt 2-mal geändert.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Alfons Mittelmeyer:
Klar das das nicht funktionieren kann, es wirft einen TclError. Du musst Dir vor Augen halten, dass Tkinter den Tcl-Interpreter innerhalb des Python-Interpreter betreibt. Mit destroy sagst Du dem Tcl-Interpreter, dass er das Widget freigeben soll, und willst dann die Farbe ändern. Daraufhin wirft Tcl einen Fehler, welcher von Python durchgereicht wird. Was Du da versuchst, ist analog zu folgendem in Python:

Code: Alles auswählen

obj = SuperDooperClass()
del obj
obj.do_sumthing()
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@jerch Ist klar dass ich das so nicht geschrieben hatte, sondern es war so etwas wie:

Code: Alles auswählen

def callback(event,me):
    send('LOAD_SOMETHING_NEW')
    me['bg'] ='gray'
Und da ich den proxy umgestelt hatte auf event_generate statt direktem Funktionsaufruf, hatte ich gedacht, ich könnte den bind auch umstellen auf direkte Ausführung. Aber wenn das nicht nur ein send war, sondern ein Funktionsaufruf, der länger dauerte und einen send beinhaltete, dann schlägt event_generate wohl auch mal durch. Aber das darf ja auch nicht sein, dass während eines Events noch ein anderes ausgeführt wird. Mir kommt es fast vor, wie wenn das bei mir unter Ubuntu passiert. Es scheint mir, dass ich nur sicher vor event während event bin, wenn ich auch die GUI callbacks über die Queue laufen lasse. Aber diese Untersuchung habe ich noch nicht abgeschlossen.

Jedenfalls sollte ich in dem einen Fall implementieren:

Code: Alles auswählen

if widget_exist(me):  me['bg'] ='gray'
Dann kann nichts passieren

Also sicher vor Durchschlagen von GUI events bin ich mit:

Code: Alles auswählen

liste[0].bind("<Return>",lambda event, me = liste[0]: send('execute_function',lambda:callback(event,me)))
Jetzt weiss ich auch nicht mehr, was los war. Habe meinen neuen Proxy genommen, diesen auf event_generate umgestellt und jetzt funktioniert es. Vielleicht hatte ich beim alten noch etwas mit direktem Funktionsaufruf drin. Ach ja, ich hatte noch keine HighPriority Queue und führte undo_receiveAll verbunden mit destroy direkt aus. Jetzt geht es über die HighPriority Queue.

Mit direktem Funktionsaufruf habe ich aber noch nichts entdeckt. Muss man wohl weiter beobachten, ob unter Ubuntu doch Events während Events durchschlagen. Jedenfalls ist das Verhalten zur Zeit noch nicht erklärbar.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Fehler gefunden. Hatte das global vergessen:

Code: Alles auswählen

def mainloop():
    global _mainloop_started
    cdApp()
    _mainloop_started = True
    _Application.mainloop()
Also lief der jeweils erste send über direkten Funktionsaufruf. Und event_generate hatte ich dann gar nicht.
Das heißt, wenn ich nicht event_generate nehme und die Callbacks direkt aufrufe, kommt es hier zum Crash. Wenn ich event_generate als Trigger nehme, kommt es nicht zum Crash.
Wenn ich nicht event_generate nehme und somit den ersten send als Funktionsaufruf ausführe, kommt es nicht zum Crash, wenn ich die Callbacks über send ausführe. Weil davon weiter aufgerufene sends gehen dann über die Queue.

Man muss solche Dinge eben manchmal untersuchen, um zu verstehen, was da passiert.

Am sichersten ist es bei GUI Callbacks über send zu bleiben. Dann ist es völlig egal, ob man dann event_generate nimmt oder nicht.

Oder man berücksichtigt alle Fälle, bei denen die Widgets ansonsten gelöscht werden, während man im Callback steckt - aber diese Lösung gefällt mir nicht, denn sie sollen erst nachher über die Queue gelöscht werden.

Es war also doch kein Fehlverhalten von tkinter unter Ubuntu. Fall gelöst!

Diese Fälle treten bei mir öfters auf. Wenn ich etwa im Navigationsmodul auf einen Button drücke, um ein anderes Widget zu wählen, muss dann die Auswahl neu aufgebaut werden, wobei die alte gelöscht wird. Und das geschieht während des Callbacks, bzw. nachher, wenn es über die Queue läuft.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Alfons Mittelmeyer:
Ich versteh von dem, was Du schreibst, fast nur Bahnhof, weil mir der Kontext, sprich Dein Code dazu fehlt. Koch das doch mal auf ein minimales Bsp. runter, was das Problem illustriert, dann können wir Dir vllt. auch helfen. Die Sache mit global und die lambda-in-lambda-in-lambda-Ausdrücke riechen nach code smell. Gerade mit lambdas kann man sich ordentlich ins Knie schiessen, wenn man die Bezeichner nicht ans lambda bindet (sprich die Ausprägung mitnimmt).
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@jerch Das betraf diesen Teil meines Codes im Proxy. Hab es im anderen Thread erläutert:

Code: Alles auswählen

    def do_work(self,*args):
        if self.running: return
        self.running = True
        while self.work(): pass
        self.running = False
Wenn ich die GUI Callbacks direkt durchführe, dann wird mein erster Send Befehl unmittelbar ausgeführt, denn running steht auf False. Dadurch lösche ich das Widget, während ich mich im Callback befinde. Wenn ich aber die GUI Callbacks über send implementiere, dann wird dieser Callback über do_work ausgeführt und setzt running auf True. Wenn ich dann während des Callbacks den Send zum Löschen absetze, dann landet diese Message in der Queue und wird erst ausgeführt nach Beendigung des GUI Callbacks. Und das ist der Grund, warum dann das Widget erst nach der Beendigung des Callbacks gelöscht wurde und ich dann in diesem Falle keinen Crash bekam.

Helfen brauchst Du mir dabei nicht. Denn jetzt weiss ich, was da passierte. Wenn ich aber einen anderen Trigger nehme wie after und den auf self.work oder auch auf do_work setze, dann wird sowieso alles hintereinander ausgeführt und dann spielt es keine Rolle, ob ich die GUI Callbacks direkt ausführe oder über send.
Antworten