Seite 5 von 10

Re: 'command' zwei Argumente übergeben?

Verfasst: Freitag 7. August 2015, 21:48
von Alfons Mittelmeyer
Gleich vier Parameter, das sieht mit lambda wohl nicht mehr gut aus. Und wenn man es dynamisch aus den Parametern erzeugen will, dann hilft wohl nur mehr Kompilierung zur Laufzeit, und genau das wollten wir ja vermeiden. Und das Senden war dann gemein. Kann das lambda die Werte speichern? Ein Objekt kann es problemlos.

Das mit den Callbacks stelle ich mir doch noch anders vor, denn da will ich doch alle Möglichkeiten anbieten:

Code: Alles auswählen

	def evcommand(self,function,parameters=None,wishWidget=False,wishEvent=False,wishSelf=False):
		widget = None
		if wishWidget: widget = self
		cmd = Callback(function,parameters,widget,wishEvent,wishSelf).execute
		self.config(command=lambda:send('execute_function',cmd))
	

	def EvEvent(self,eventkey,function,parameters=None,wishWidget=False,wishEvent=False,wishSelf=False):
		widget = None
		if wishWidget: widget = self
		cmd = Callback(function,parameters,widget,wishEvent,wishSelf).setEvent
		self.bind(eventkey,lambda event: send('execute_function',cmd(event)))
Ausprobiert habe ich es noch nicht, ob ich mich vertippt habe. Wenn Ihr es nicht in die Widgets einbauen wollt, müßt ihr eben statt self eine Referenz auf ein Widget angeben.

Und natürlich nichts mit send machen. Aber receive ist immer gut, auch wenn bei einem command ein execute reichen würde. Und ohne send braucht man auch kein lambda.

Ich bau das mal in mein Programm ein und mache dann ein schönes Beispiel. Das heißt, ich habe schon eines, aber dann eben mit normalen Funktionen und für normales tkinter umschreiben.
Ich denke dass da zur Abwechslung mal ein Message Callback recht interessant wäre.

Re: 'command' zwei Argumente übergeben?

Verfasst: Freitag 7. August 2015, 23:29
von Alfons Mittelmeyer
Also mit lambda geht es auch dynamisch. Mir ist da gerade eine Lösung eingefalllen. Wenn man 0 bis 10 Parameter hat und mit und ohne Event und möchte nicht immer die Ausdrücke auf der linken Seite vom Doppelpunkt angeben, dann könnte man es ja so machen, dass man sich elf lambda Ausdrücke ohne Event und elf mit Event macht, und dann entsprechend verzweigt.

Re: 'command' zwei Argumente übergeben?

Verfasst: Samstag 8. August 2015, 09:53
von Sirius3
@Alfons Mittelmeyer: wenn es zu kompliziert wird, kann man natürlich auch eine eigene Funktion statt eines Lambdas schreiben. Hier habe ich ja lambda nur gewählt, weil es kurz und einfach geht. Elf Parameter zu haben, ist immer unübersichtlich, egal wie man es schreibt. Da können eigene Klassen helfen, was unter Python aber auch kein Problem ist, da es die __call__-Methode gibt, so dass sich Instanzen wie eine Funktion verhalten. Aber das kann jeder Anwender so machen, wie es die Situation erfordert; er muß nur wissen, daß eine Callback-Funktion einen event-Parameter erwartet. Der ganze Boilerplate-Code verkompliziert nur die Anwendung, bietet keine Vorteile und machen Dir mehr Arbeit beim Warten des Codes.

Info Messages statt Methoden

Verfasst: Samstag 8. August 2015, 15:52
von Alfons Mittelmeyer
Sirius3 hat geschrieben:@Alfons Mittelmeyer: wenn es zu kompliziert wird, kann man natürlich auch eine eigene Funktion statt eines Lambdas schreiben. Hier habe ich ja lambda nur gewählt, weil es kurz und einfach geht. Elf Parameter zu haben, ist immer unübersichtlich, egal wie man es schreibt. Da können eigene Klassen helfen, was unter Python aber auch kein Problem ist, da es die __call__-Methode gibt, so dass sich Instanzen wie eine Funktion verhalten. Aber das kann jeder Anwender so machen, wie es die Situation erfordert; er muß nur wissen, daß eine Callback-Funktion einen event-Parameter erwartet. Der ganze Boilerplate-Code verkompliziert nur die Anwendung, bietet keine Vorteile und machen Dir mehr Arbeit beim Warten des Codes.
Natürlich war jetzt zehn Parameter übertrieben. Ich habe sie zwar zur Zeit noch in meinem Code, aber nur weil keine richtigen Funktionen mit Defaultwerten existierten und ich daher die Parameter auf diese Weise übergeben mußte und dann dabei auch abzählen mußte.

Allerdings ab das da stimmt?
Der ganze Boilerplate-Code verkompliziert nur die Anwendung, bietet keine Vorteile und machen Dir mehr Arbeit beim Warten des Codes.
Das müssen wir noch abwarten, also ich finde dass es im Gegenteil übersichtlich und daher auch sehr wartbar ist. Lasse mich natütlich auch gerne eines Besseren belehren.

Nützlich hat sich diese Callback Funktion auch bei Info Messages erwiesen. Aber selbstverständlich geht es da auch noch mit lambda. Heute ist mir allerdings noch etwas eingefallen, wo es dann ganz gewiss nicht mehr mit lambda geht. Aber lassen wir das vorerst mal beiseite. Etwas sehr Interessantes sind Info Messages.

Um was es dabei geht? Eigentlich auch nichts anderes, was man auch auf normale Weise implementieren kann, wenn man entsprechende Klassen implementiert. Nur kann man es durch die Info Messages ohne extra Klassen erreichen und ist gegen Programmabstürze abgesichert. Bei DynTkInter kann man die Nachinstallation solcher Methoden gar vornehmen, während das Programm läuft.

Betrachten wir uns einmmal folgende Beispiele:
  • Sohnemann.aufstehen()
    Blinklabel.blink()
Wenn die entsprechende Methode implementiert ist, dann funktioniert das auch. Aber man muß extra dafür eine Klasse anlegen. Und dann hatte mich etwas gestört.
Nach Blinklabel.blink() hatte der Label wie gewünscht zum Blinken angefangen, was vollauf richtig war. Aber dann wollte er einfach nicht mehr aufhören zu blinken.
Dann hatte ich mir gedacht, wäre gut, wenn man das einfach ausschalten könnte. Da könnte man zwar eine Methode definieren wie Blinklabel.blink_stop(), aber dass man so eine Deaktivierung gleich für alles machen könnte, wäre auch nicht schlecht.

Sohnemann könnte eine durchzechte Nacht gehabt haben, muss noch schlafen und will jetzt auch nicht aufstehen.
Statt mit einer Klasse habe ich das dann durch eine Nachricht realisiert. Und das schaut dann so aus:

Code: Alles auswählen

# statt Sohnemann.aufstehen()
inform(Sohnemann,'aufstehen')
Allerdings habe ich jetzt noch keine Rückmeldungen implementiert. Wenn diese Reaktion auf 'aufstehen' deaktiviert ist, könnte man ja etwas ausgeben, wie etwa:
Hier Empfänger der Nachricht 'aufstehen': zur Zeit ist diese Funktion leider nicht verfügbar. Probieren Sie es bitte zu einem späteren Zeitpunkt noch einmal
Und dann gibt es noch mehr Fälle, die bei normaler Implementierung nicht abgefangen werden. Nach Sohnemann.aufstehen() erfolgt der Weltuntergang, das heisst Programmabsturz, weil man vergessen hart, diese Methode zu implementieren. Aber ein realitätsnahes Verhalten ist so ein Programmabsturz auch nicht. Ich habe jetzt noch keinen Text zu diesem Fall implementiert, vielleicht wäre so etwas richtig?
Hier Empfänger der Nachricht 'aufstehen': 'aufstehen' was soll denn das sein? Nie davon gehört.
Und dann kann natürlich auch sein, dass zwar die entsprechende Methode implementiert wurde, aber das betreffende Widget inzwischen gelöscht wurde, nicht mehr verfügbar ist und man daher eine ungültige Referenz hat oder dass man versehentlich eine Referenz auf etwas angibt, das überhaupt nicht über derartige Mehoden verfügt. Soll da gleich das Programm abstürzen? Bei diesen info Messages passiert das freilich nicht. Text habe ich mir aber auch noch keinen überlegt. Wie wäre es mit:
Hier Nachrichtenübermittlungsregistratur: Der Emfänger Ihrer Nachricht 'aufstehen' ist uns leider unbekannt. Habe Sie bitte Verständnis dafür dass wir Ihre Nachricht daher nicht weiterleiten können
Bei DynTkInter könntge man sogar den Namen des Empfängers ausfindig machen und angeben. Mit normalem tkinter geht das aber nicht.

Da ich Euch nichts aufdrängen möchte, ist die Frage, ob jemand Interesse daran hat? Das wäre jetzt mit Callbacks realisiert. Oder was Ihr zu Fehlertexten meint?

Re: 'command' zwei Argumente übergeben?

Verfasst: Samstag 8. August 2015, 17:25
von Alfons Mittelmeyer
@sirius3 Du hattest das geschrieben:
Der ganze Boilerplate-Code verkompliziert nur die Anwendung, bietet keine Vorteile und machen Dir mehr Arbeit beim Warten des Codes.
Das Prinzip guter Programnmierung ist das Verbergen von Implementierungsdetails. Und auf oberster Ebene soll man nur die Funktionen haben, die man braucht. Und das ist Parameterübergabe mit oder ohne Event. Man könnte natürlich auch den Event immer gleich mitgeben, dann hätte man weiniger Unterscheidungsfälle. Das Widget könnte man auch, wenn man es braucht, nochmals in die Parameter schreiben.

Aber wie auch immer, weder mein Callback noch Dein lambda haben auf oberster Ebene etwas zu suchen. Weil das wäre Boilerplate-Code. Nur Übergabe der Parameter und mit oder ohne Event. Weil diesen braucht man auch nicht immer. Wenn man die Entertaste in einem Entry Feld gedrückt hat, braucht mann den Event nicht. Wenn man aber die Mauskoordinaten wissen muss, dann schon. Ich weiss jetzt nicht, ob diese Reihenfolge sinnvoll ist: nach den Parametern: wishWidget, wishEvent,wishSelf oder sollte wishEvent vor wishWidget kommen? Außerdem kann man das ja auch ohne Berücksichtigung der Reihenfolge einfach wishEvent=True in Python angeben. Und dann habe ich Defaultmäßig nur Parameterübergabe eingestellt ohne Übergabe von widget, event usw. Sinnvoll oder nicht?

Was mir noch nicht gefällt sind die Namen: evcommand und EvEvent, denn das ev zu Beginn sollte ausdrücken, dass man es aus einem String compilieren kann, der dann mittels eval ausgeführt wird. Und das soll wegfalllen und diese Funktionen sollen deswegen auch umbenannt werden. Was wäre evtl. ein passender Name?

Ich habe schon genug Boilerplate-Code gesehen, bei dem Programmierer Implementierungsdetails in die oberste Ebene geschrieben haben, und ich dann bei Änderungen an 250 Stellen nachbessern mußte. Du mußt daher verstehen, dass ich so etwas bestimmt nicht tue. Wenn es Änderungen gibt, dann brauch ich nur evcommand zu ändern und EvEvent Aber muß nicht an vielen Stellen ändern. Und das ist wartbarer Code!

Ok, 0 bis fünf Parameter mit wishWidget und wishEvent sind 24 lambda Ausdrücke. Das könnte ich mit lambda realisieren. Aber da sträubt sich einges dagegen, besondern wenn ich dann an einen Fall denke, bei dem dann lambda nicht mehr geht: nämlich späterer Parameteraustausch.

Re: 'command' zwei Argumente übergeben?

Verfasst: Samstag 8. August 2015, 18:06
von Sirius3
Du hast den Kern meiner Ausführungen noch nicht erkannt: wishSelf wird unnötig, weil es ja keine Callback-Klasse mehr gibt, wishWidget wird dadurch ersetzt, dass der lambda-Ausdruck explizit entweder ein Widget übergibt oder eben nicht. wishEvent ist genauso unnötig, da das der einzig verbleibende Parameter ist, den der Lambdaschreiber einfach ignorieren kann, wenn er ihn nicht braucht. "Explicit is better than implicit" hat irgendwann mal jemand geschrieben; hier: durch Deine wish-Flags änderst Du implizit die Funktionssignatur.

Re: 'command' zwei Argumente übergeben?

Verfasst: Samstag 8. August 2015, 18:39
von Alfons Mittelmeyer
Sirius3 hat geschrieben:Du hast den Kern meiner Ausführungen noch nicht erkannt: wishSelf wird unnötig, weil es ja keine Callback-Klasse mehr gibt, wishWidget wird dadurch ersetzt, dass der lambda-Ausdruck explizit entweder ein Widget übergibt oder eben nicht. wishEvent ist genauso unnötig, da das der einzig verbleibende Parameter ist, den der Lambdaschreiber einfach ignorieren kann, wenn er ihn nicht braucht. "Explicit is better than implicit" hat irgendwann mal jemand geschrieben; hier: durch Deine wish-Flags änderst Du implizit die Funktionssignatur.
Hatte ich ja auch schon geschrieben, dass man das widget nicht braucht, weil man es auch als Parameter übergeben kann und das meself, das wäre nur in ganz speziellen Fällen, die kaum vorkommen nützlich, etwa:

Eine Funktion hat Parameter bekommen, braucht aber keine oder nur wenig davon. Der Zweck wäre die Parameter an eine andere Funktion weiterzugeben. Das müßte man dann nicht explizit tun. Oder der andere Spezialfall, dass eine Funktion mit geringer Änderung auch für zwei Zecke einsetzbar wäre. Also wären das kaum vorkommende Spezialfälle und meself wäre also verzichtbar. Bleibt also nur der event, oder nicht?

OK, dann was sagst Du hier dazu?

Code: Alles auswählen

def function_blink(mewidget,message_blinkon,blinkon_color,blinkon_time=500,blinkoff_time=500):
	if message_blinkon:
		mewidget['fg'] = blinkon_color
		informLater(blinkon_time,mewidget,'blink',not message_blinkon)
	else:
		mewidget['fg'] = mewidget['bg']
		informLater(blinkoff_time,mewidget,'blink',not message_blinkon)
Ein Label soll blinken. Wenn die message message_blinkon == True empfangen wird (die message ist dasselbe wie event) setzt er seine Farbe entsprechend dem Parameter blinkon_color.
Das ist der Parameter, der über die Rückruffunktion definiert ist. Zum Blinken wird die message invertiert und dann nach der blinkon_time die Funktion wieder aufgerufen.
Was bei message_blinkon == False geschieht, sollte ebenfallls verständlich sein.

Das Blinken kann dann extern wieder abgeschaltet werden, indem man diese Funktion deaktiviert.

Der Label hatte vielleicht rot geblinkt. Danach kommt man auf die Idee ihn grün blinken zu lassen. Beim Callback könnte man einfach den Parameter ['red'] durch ['green'] ersetzen.
Bei lambda geht das nicht . OK andere Lösungen gibt es natürlich auch, wie die Farbe in der Message mit zu schicken, wie [True,'red'] und dann müsste die Funktion darin jeweils das erste Element austauschen.

Ich fände es aber geschickter, wenn man den Parameter austauschen könnte. Und hier zeigt sich dann, dass das widget extra auch Sinn macht. Denn die Funktion hat nur die Farbe als Parameter, wenn das widget extra geht. Und die Farbe ist dann auch leicht austauschbar.

Re: 'command' zwei Argumente übergeben?

Verfasst: Samstag 8. August 2015, 19:20
von Alfons Mittelmeyer
@sirius3 Also insgesamt, habe ich mir die Lösung so gedacht:

Code: Alles auswählen

# callback function for info message 'blink_start'
def blink_start(mewidget,message_blinkcolor):
	mewidget.getReactionCallback('blink').setPar([message_blinkcolor])
	mewidget.getReactionCallback('stop_blink').setPar([mewidget['fg']])
	mewidget.activateReaction('blink',True)
	inform(mewidget,'blink',True)

# callback function for info message 'blink'
def blink(mewidget,message_blinkon,blinkon_color,blinkon_time=500,blinkoff_time=500):
	if message_blinkon:
		mewidget['fg'] = blinkon_color
		informLater(blinkon_time,mewidget,'blink',not message_blinkon)
	else:
		mewidget['fg'] = mewidget['bg']
		informLater(blinkoff_time,mewidget,'blink',not message_blinkon)
			
# callback function for info message 'blink_stop'
def blink_stop(mewidget,message_none,color_before):
	mewidget.activateReaction('blink',False)
	mewidget['fg'] = color_before
Also bitte sag mir, ob Du eine bessere Lösung weißt.

Re: 'command' zwei Argumente übergeben?

Verfasst: Samstag 8. August 2015, 21:28
von Alfons Mittelmeyer
Ich muss zugeben, das kann man auch noch besser schreiben. Statt 'not message_blinkon' kann man auch direkt True oder False nehmen:

Code: Alles auswählen

# callback function for info message 'blink'
def blink(mewidget,message_blinkon,blinkon_color,blinkon_time=500,blinkoff_time=500):
	if message_blinkon:
		mewidget['fg'] = blinkon_color
		informLater(blinkon_time,mewidget,'blink',False)
	else:
		mewidget['fg'] = mewidget['bg']
		informLater(blinkoff_time,mewidget,'blink',True)
Vielleicht dieses Beispiel mal zeigen?

Re: 'command' zwei Argumente übergeben?

Verfasst: Samstag 8. August 2015, 23:52
von Alfons Mittelmeyer
Jetzt ist mir doch noch ein Fehler unterlaufen. Ich hatte nicht bedacht, dass wenn der Label blinkt, jemand nochmals blink_start aufrufen könnte. Dann bekämen wir nicht die original Vordergrundfarbe, sondern lediglich die, welche der Label durch das Blinken gerade hat. Da fehlt bei blink_start zu Beginn:

Code: Alles auswählen

informImmediate(mewidget,'blink_stop') # important: if the label already blinks, we wouldn't get the original fg color
'informImmediate' wird sofort ausgeführt, genau wie ein Funktionsaufruf und danach hat der Label wieder die original Farbe.

Und dann brauchen wir auch nicht über einen Zugriff auf den Callback für blink_stop color_before zu verändern. Denn das können wir gleich bei der Definition der Funktion als Parameter angeben. Der Code sieht dann so aus:

Code: Alles auswählen

# functions for the Callbacks ===================================================

# callback function for info message 'blink_start'
def blink_start(mewidget,message_blinkcolor):
	informImmediate(mewidget,'blink_stop') # important: if the label already blinks, we wouldn't get the original fg color
	getReactionCallback(mewidget,'_blink').setPar([message_blinkcolor])
	mewidget.activateReaction('_blink',True)
	inform(mewidget,'_blink',True)

# callback function for info message '_blink'
def _blink(mewidget,message_blinkon,blinkon_color,blinkon_time=500,blinkoff_time=500):
	if message_blinkon:
		mewidget['fg'] = blinkon_color
		informLater(blinkon_time,mewidget,'_blink',False)
	else:
		mewidget['fg'] = mewidget['bg']
		informLater(blinkoff_time,mewidget,'_blink',True)
			
# callback function for info message 'blink_stop'
def blink_stop(mewidget,message_none,color_before=Blinklabel['fg']):
	mewidget.activateReaction('_blink',False)
	mewidget['fg'] = color_before


# Definining the Callbacks =========================================================
Blinklabel.registerReaction('blink_start',blink_start) # the function don't have parameters and the widget and the message are default for info messages
Blinklabel.registerReaction('_blink',_blink) # we don't need to set parameters, because the color will be set by blink_start
Blinklabel.registerReaction('blink_stop',blink_stop) # we don't need to set parameters, because color_before is already set

# example how to let the Blinklabel blink
inform(Blinklabel,'blink_start','red')
inform(Blinklabel,'blink_stop')
Eigentglich ganz easy, oder?

Aber wenn wir in blink_start an blink_stop sowieso nicht die Origalfarbe übergeben brauchen, dann können wir auf diese Zeile auch verzichten:

Code: Alles auswählen

informImmediate(mewidget,'blink_stop') # important: if the label already blinks, we wouldn't get the original fg color
Damit haben wir dann für blink_start:

Code: Alles auswählen

# callback function for info message 'blink_start'
def blink_start(mewidget,message_blinkcolor):
        getReactionCallback(mewidget,'_blink').setPar([message_blinkcolor])
        mewidget.activateReaction('_blink',True)
        inform(mewidget,'_blink',True)
Aber wenn man jetzt bei der Funktion _blink die Blinkzeiten umstellen würde, zB. blinkon_time auf 600 ms und blinkoff_time auf 400 ms, was man ja einfachen machen könnte durch:

Code: Alles auswählen

getReactionCallback(mewidget,'_blink').setPar(['red',600,400])
Dann wäre es richtig, in blink_start zu implementieren:

Code: Alles auswählen

mewidget.getReactionCallback('_blink').parameters[0]=message_blinkcolor
Allerdings erst, nachdem man dafür gesorgt hat, dass die Funktion auch mindestens einen Parameter hat, also bei der Callback Definition.

Re: 'command' zwei Argumente übergeben?

Verfasst: Sonntag 9. August 2015, 09:09
von Sirius3
Wenn man veränderbare Werte hat, dann baut man sich nicht irgendeine Callback-Struktur, wo dann implizit irgendwie die Parameter an die eigentliche Funktion übergeben werden, sondern man schreibt explizit eine Klasse. Da kann man dann die Werte, die Aktion und die Methoden zum Starten und Stoppen gleich zusammenfassen. Hier ein Beispiel in reinem Tk und damit auch lauffähig zum Testen:

Code: Alles auswählen

import Tkinter as tk

class Blinker(object):
    def __init__(self, widget, blinkon_color,blinkon_time=500,blinkoff_time=500):
        self.widget = widget
        self.colors = [self.widget['fg'], blinkon_color]
        self.times = [blinkoff_time, blinkon_time]
        self.after_id = None
        self.step = 0

    def start(self):
        if self.after_id is None:
            self.after_id = self.widget.after(0, self._blink)

    def stop(self):
        if self.after_id is not None:
            self.widget.after_cancel(self.after_id)
            self.widget['fg'] = self.colors[0]
            self.after_id = None

    def _blink(self):
        self.step += 1
        self.widget['fg'] = self.colors[self.step % len(self.colors)]
        self.after_id = self.widget.after(self.times[self.step % len(self.times)], self._blink)

def main():
    root = tk.Tk()
    label = tk.Label(root, text="Important Message")
    label.pack()
    bl = Blinker(label, 'red')
    tk.Button(root, text="Start", command=bl.start).pack()
    tk.Button(root, text="Stop", command=bl.stop).pack()
    def setcolor():
        bl.colors[1] = "green"
    tk.Button(root, text="Green", command=setcolor).pack()
    root.mainloop()

if __name__ == '__main__':
    main()

Warum reitest Du auf lambda herum?

Verfasst: Sonntag 9. August 2015, 20:46
von Alfons Mittelmeyer
@sirius3 Was ich nicht verstehe, ist, dass Du so darauf herumreitest, dass man lambda nehmen soll. Lambda ist nichts anderes als eine namenlose Krücke von einer Funktion, weil es nur für einen einzigen Ausdruck zu gebrauchen ist. Und man kann sich sein lambda zu jeder Zeit selber machen, etwa wie:

Code: Alles auswählen

def mylambda(parfunction=myfunction,par1=mypar1,par2=mypar2): parfunction(par1,par2)
Blöd nur, dass man dafür zuvor noch eine Variable nehmen muß, etwa so:

Code: Alles auswählen

def mylambdafunction(parfunction=myfunction,par1=mypar1,par2=mypar2): parfunction(par1,par2)
mylambda = mylambdafunction
Das ist, was lambda tut und das kann man jeder Zeit auch ohne lambda machen. Aber weder das noch lambda sind für die Übergabe von Parametern bei einer Rückruffunktion schön.
Also noch ein Weg, wie man beliebig viele Parameter an eine Funktion übergibt: man definiert sich eine Funktion, die genau das tut!

Re: 'command' zwei Argumente übergeben?

Verfasst: Sonntag 9. August 2015, 22:57
von Alfons Mittelmeyer
@sirius3 Dass man mit Klassen viel machen kann, weiss ich schon. Aber eine übersichtliche Programmstruktur hast Du nicht.
Meine Vorstellung ist: Zuerst definiert man die Widgets. Dann definiert man die Callbacks dazu. Und dass eine einheitlice Callbackstruktur übersichtlicher ist als irgendwelche lambdas sollte auch einleuchtend sein. Und hier sieht man, dass man über die Callbacks sogar die Parameter von Funktionen austauschen kannn. Dazu habe ich jetzt mal zwei Module gemacht für Python3.

Modul1 callbacks.py:

Code: Alles auswählen

from tkinter import *
from tkinter import colorchooser
import action

VAR={}

root = action.init(Tk())

root.title('Callbacks')
root.geometry('492x363+191+125')

VAR["Blinklabel"] = action.Label(root,text="""Let me blink""",pady='10',padx='20',font='TkDefaultFont 37',bd='5',relief='solid')
VAR["Blinklabel"].place(y='26',x='68')

VAR["Frame"] = LabelFrame(text="""Blinkcontrol""",height='180',width='350')
VAR["Frame"].place(y='140',x='70')

Label(VAR["Frame"],text="""blinkoff_time""").place(y='80',x='100',anchor='e')
Label(VAR["Frame"],text="""blinkon_time""").place(y='50',x='100',anchor='e')
Label(VAR["Frame"],text="""blink_color""").place(y='20',x='100',anchor='e')
Label(VAR["Frame"],text="""blink""").place(y='110',x='100',anchor='e')

VAR["ColorEntry"] = action.Entry(VAR["Frame"])
VAR["ColorEntry"].place(width='150',y='20',x='110',anchor='w')
VAR["Help"] = Button(VAR["Frame"],text="""?""")
VAR["Help"].place(y='20',x='270',anchor='w')

VAR["on_scale"] = Scale(VAR["Frame"],orient='horizontal',from_='50.0',to='1000.0',showvalue='0',length='192',resolution='50.0')
VAR["on_scale"].place(y='50',x='110',anchor='w')
VAR["off_scale"] = Scale(VAR["Frame"],orient='horizontal',from_='50.0',to='1000.0',showvalue='0',length='192',resolution='50.0')
VAR["off_scale"].place(y='71',x='109')

VAR["blink_on"] = Button(VAR["Frame"],text="""Start""",bg='green')
VAR["blink_on"].place(y='110',x='235',anchor='e')
VAR["blink_off"] = Button(VAR["Frame"],text="""Stop""",bg='#ff5e00')
VAR["blink_off"].place(y='110',x='303',anchor='e')

### CODE ===================================================

VAR["on_scale"].set(500)
VAR["off_scale"].set(500)


# blink Callbacks for the Blinklabel =================
 
# info message 'blink_start'
def function(mewidget):
    mewidget.activateAction('blink',True)
    action.inform(mewidget,'blink',True)
VAR["Blinklabel"].do_action('blink_start',function,None,True)

# ================================================================================================
# We can change the parameters blinkon_color,blinkon_time,blinkoff_time for this function
# ================================================================================================
# info message 'blink'
def function(mewidget,message_blinkon,blinkon_color,blinkon_time,blinkoff_time):
	if message_blinkon:
		    mewidget['fg'] = blinkon_color
		    action.informLater(blinkon_time,mewidget,'blink',False)
	else:
		    mewidget['fg'] = mewidget['bg']
		    action.informLater(blinkoff_time,mewidget,'blink',True)
VAR["Blinklabel"].do_action('blink',function,('red',500,500),True,True)
                       
# info message 'blink_stop'
def function(mewidget,color_before):
	mewidget.activateAction('blink',False)
	mewidget['fg'] = color_before
VAR["Blinklabel"].do_action('blink_stop',function,VAR["Blinklabel"]['fg'],True)


# start and stop buttons =================

def function(blinklabel): action.inform(blinklabel,'blink_start')
action.do_command(VAR["blink_on"],function,VAR["Blinklabel"])

def function(blinklabel): action.inform(blinklabel,'blink_stop')
action.do_command(VAR["blink_off"],function,VAR["Blinklabel"])

# scales ====================================

VAR["Blink_Callback"] = VAR["Blinklabel"].getActionCallback('blink')

# ================================================================================================
# Here we see, that we can change the parameters of a function, which is a function of a Callback
# ================================================================================================

def function(mewidget,callback): callback.parameters[1] = mewidget.get() # changing blinkon_time
action.do_command(VAR["on_scale"],function,VAR["Blink_Callback"],True)

def function(mewidget,callback): callback.parameters[2] = mewidget.get() # changing blinkoff_time
action.do_command(VAR["off_scale"],function,VAR["Blink_Callback"],True)

# entry ====================================


# ================================================================================================
# Here we see, that we can change the parameters of a function, which is a function of a Callback
# ================================================================================================

def function(mewidget,helpbutton,callback):
	try: helpbutton['bg'] = mewidget.get()
	except: pass
	callback.parameters[0] = helpbutton['bg'] # changing blinkon_color
	mewidget['bg'] = 'gray'
	action.informLater(300,mewidget,'white')
action.do_event(VAR['ColorEntry'],'<Return>',function,(VAR["Help"],VAR["Blink_Callback"]),True)

def function(mewidget,helpbutton):
	mewidget.delete(0,END)
	mewidget.insert(0,str(helpbutton['bg']))
	mewidget['bg'] = 'white'
VAR['ColorEntry'].do_action('white',function,VAR["Help"],True)

# ================================================================================================
# Here we see, that we can change the parameters of a function, which is a function of a Callback
# ================================================================================================

def function(mewidget,callback,entry):
	color = colorchooser.askcolor(parent=root,initialcolor=mewidget['bg'],title="Choose color")[1]
	if color != None:
		callback.parameters[0] = color # changing blinkon_color
		mewidget['bg'] = color
		entry.delete(0,END)
		entry.insert(0,color)
action.do_command(VAR['Help'],function,(VAR["Blink_Callback"],VAR['ColorEntry']),True)

### ========================================================

del function
VAR.clear()
del VAR
root.mainloop()
Und dazu wird das Modul mit den Callbacks benötigt, nämlich action.py:

Code: Alles auswählen

import tkinter as tk

Application = None
ACTORS = {}


# please call init in your module, for example:
#
#  root = action.init(Tk())
#

def init(root):
	global Application
	global ACTORS
	Application = root
	ACTORS = {}
	return root

def dummyfunction(par):pass


class Callback:
	def __init__(self,widget,function,parameters=None,wishWidget=False,wishEvent=False,wishSelf = False):
		self.widget = widget
		self.event = None
		self.wishWidget = wishWidget
		self.wishEvent = wishEvent
		self.wishSelf = wishSelf
		self.mydata = None # may be used for many purposes. Accessible via self

		# special case for functions =========
		self.isFunction = False
		if type(function) is type(dummyfunction):
			self.isFunction = True
			self.function = function
			self.parameters = []
			if type(parameters) is tuple:
				for e in parameters: self.parameters.append(e)
			elif type(parameters) is list: self.parameters = parameters
			elif parameters != None: self.parameters = [parameters]
		else: # other cases ============
			print("Callback: please feel free to implement, what you need")

	# for execution later =======
		
	def execute(self):
		# special case for functions =========
		if self.isFunction:
			par = []
			if self.wishWidget: par = [self.widget]
			if self.wishEvent: par.append(self.event)
			if self.wishSelf: par.append(self)
			par += self.parameters
			return self.function(*par)
		else: # other cases ============
			print("Callback: please feel free to implement, what you need")
		
	def setEvent(self,event = None):
		self.event = event
		return self.execute
		
	# for execution immediate =======

	def receive(self,event = None): return self.setEvent(event)()

	# for using the Callback as funcion =======

	def call(self,*args): 
		if self.isFunction: return self.function(*args) # a function cannot be copied, but a Callback can. Using different mydata, the functions can behave different.
		else: print("Please, call only functions.")

# if you don't like to use the Callback class, please feel free to implement this in another way, e.g. by using lambda
def do_command(widget,function,parameters=None,wishWidget=False,wishEvent=False,wishSelf=False):
	cmd = Callback(widget,function,parameters,wishWidget,wishEvent,wishSelf).receive
	widget.config(command = cmd)

def do_event(widget,eventkey,function,parameters=None,wishWidget=False,wishEvent=False,wishSelf=False):
	cmd = Callback(widget,function,parameters,wishWidget,wishEvent,wishSelf).receive
	widget.bind(eventkey,cmd)

class InformAndReact:

	def __init__(self):
		self.actions = {}

	def do_action(self,actionid,function,parameters=None,wishWidget=False,wishMessage=False,wishSelf=False):
		ACTORS[self] = self
		self.actions[actionid] = [True,Callback(self,function,parameters,wishWidget,wishMessage,wishSelf)]

	def activateAction(self,actionid,flag):
		if actionid in self.actions: self.actions[actionid][0] = flag

	def undo_action(self,actionid):
		self.actions.pop(actionid,None)
		if len(self.actions) == 0: ACTORS.pop(self,None)

	def destroyActions(self):
		self.actions.clear()
		ACTORS.pop(self,None)

	def getActionCallback(self,actionid): return self.actions[actionid][1]

	def destroy(self):
		self.destroyActions()
		self.tkClass.destroy(self)
		
		
class Label(InformAndReact,tk.Label):

	def __init__(self,master,**kwargs):
		self.tkClass = tk.Label
		tk.Label.__init__(self,master,**kwargs)
		InformAndReact.__init__(self)


class Entry(InformAndReact,tk.Entry):

	def __init__(self,master,**kwargs):
		self.tkClass = tk.Entry
		tk.Entry.__init__(self,master,**kwargs)
		InformAndReact.__init__(self)


def informImmediate(widget,actionid,msg=None):
	if widget in ACTORS:
		if actionid in widget.actions:
			if widget.actions[actionid][0] == True: # if action is activated
				widget.actions[actionid][1].receive(msg) # receive message


# sending via queue currently not available, so:
def inform(widget,actionid,msg=None):
	informImmediate(widget,actionid,msg)

def _informLater(parameters):
	inform(*parameters)

def informLater(ms,widget,actionid,msg=None):
	Application.after(ms,_informLater,(widget,actionid,msg))

Re: 'command' zwei Argumente übergeben?

Verfasst: Montag 10. August 2015, 05:47
von Sirius3
@Alfons Mittelmeyer: Wir haben hundert Leute auf der Straße gefragt, wer hat die übersichtlichere Programmstruktur. Die Top-Antwort ist ...?

Natürlich kann man meine 40 Zeilen nicht mit Deinen 274 Zeilen vergleichen, weil Du noch ein paar Extra-Gimmicks eingebaut hast. Natürlich kann man dein MainFrame in eine Klasse packen und darin das Erzeugen der GUI-Elemente von der Verknüpfung mit den Commands trennen, das ist wohl Geschmackssache, ob man das will. Aber das was Du geschrieben hast, will niemand außer Dir.

Warum nennst Du jede Funktion function? Damit hast Du ja Deine anonymen Funktionen, über die Du immer meckerst. Und Du willst allen Ernstes behaupten es wäre übersichtlicher "callback.parameters[1]" zu schreiben als "blink.color"?

Re: 'command' zwei Argumente übergeben?

Verfasst: Montag 10. August 2015, 10:27
von Alfons Mittelmeyer
Sirius3 hat geschrieben:Aber das was Du geschrieben hast, will niemand außer Dir.
Ob man jetzt die Info Messages will oder braucht? Brauchen wird man sie in den meisten Fällen nicht. Es kann aber in bestimmten Fällen kompliziert werden, wenn man es anders macht. Methoden zu einer Klasse dynamisch hinzufügen oder löschen geht zum Beispiel nicht.

Aber es gibt Fälle, die sich durch so etwas leicht lösen lasse, wie etwa die Simulation einer digitalelektronischen Schaltung, indem man den Signalausgang des einen Elements mit den Eingängen anderer Elemente verknüpft. Bei dieser Info Message ist die Mehrfachverknüpfung nicht implementiert. Das braucht es aber auch nicht, weil ich nämlich noch eine Callback Art für diesen Zweck habe, die dann als Konnektor dient, nämlich empfangene Signale an mehrere Empfänger weiterzuleiten.

Allerdings die Parameterübergabe für GUI Callbacks ist sicher etwas, was mancher gut brauchen kann.

Und das da, hatte ich bisher auch noch nie gebraucht. Mir war nur eingefallen, dass man das mit den Calllbacks anstelllen kann, nämlich die Callback Parameter nachher noch ändern.

Code: Alles auswählen

def function(mewidget,callback): callback.parameters[1] = mewidget.get() # changing blinkon_time
action.do_command(VAR["on_scale"],function,VAR["Blink_Callback"],True)
Sirius3 hat geschrieben:Warum nennst Du jede Funktion function? Damit hast Du ja Deine anonymen Funktionen, über die Du immer meckerst. Und Du willst allen Ernstes behaupten es wäre übersichtlicher "callback.parameters[1]" zu schreiben als "blink.color"?
Wenn ich alle Funktionen function nenne, kann ich mir eine Programmzeile sparen. Ich könnte natürlich auch das schreiben:

Code: Alles auswählen

    def change_blinkon_time(mewidget,callback): callback.parameters[1] = mewidget.get() # changing blinkon_time
    action.do_command(VAR["on_scale"],function,VAR["Blink_Callback"],True)
    del change_blinkon_time
Aber warum zuerst einen Namen ausdenken, den man dann doch gleich wieder löschen will? Und wenn ich alle Funktionen function nenne, erspare ich mir zusätzliche del-Anweisungen

Und dass ich über anonyme Funktionen gemeckert hatte? Nein genau das hatte ich ja gesucht. Und hätte mir ein lambda gewünscht, das nicht nur einen einzigen Ausdruck kann. Mit lambda könnte man auch schreiben:

Code: Alles auswählen

   # statt mit einer eine extra Funktion (function)
    def function(mewidget,callback): callback.parameters[1] = mewidget.get() # changing blinkon_time
    action.do_command(VAR["on_scale"],function,VAR["Blink_Callback"],True)

   # ginge das mit lambda auch so:
   action.do_command(VAR["on_scale"],lambda: Par()[1] = Me().get(),VAR["Blink_Callback"],True)
Allerdings habe ich diese Möglichkeit für Funktionen und damit auch lambda disabled. Aber praktisch wäre es schon, bei nur einem Ausdruck auch ganz auf eine extra Funktion verzichten zu können. Oder was meinst Du? Man bräuchte gar nicht callback.parameters[1] zu schreiben, wenn ich Par()[1] für Funktionen wieder enablen würde.

Also alles ist nicht disabled:

Code: Alles auswählen

# das ist disabled
action.do_command(VAR["on_scale"],lambda: Par()[1] = Me().get(),VAR["Blink_Callback"],True)

# das geht aber noch, denn sonst würde ja nichts mehr funktionieren
action.do_command(VAR["on_scale"],"Par()[1] = Me().get()",VAR["Blink_Callback"],True)

# ich tue ja gerne was Ihr wollt, aber dass ich wegen so einem Pipifax das schreiben soll, sehe ich langsam nicht mehr ein
def change_blinkon_time(mewidget,callback): callback.parameters[1] = mewidget.get() # changing blinkon_time
action.do_command(VAR["on_scale"],function,VAR["Blink_Callback"],True)
del change_blinkon_time
Ach so in diesem lambda und String Fall braucht man auch das True nicht, denn dieses ist nur dafür da, wie es einer Funktion übergeben werden soll

Am Geschicktesten wäre wohl lambda auch mit einem Flag zu berücksichtigen, denn dann könnte man schreiben:

Code: Alles auswählen

action.do_command(VAR["on_scale"],lambda: Par()[1] = Me().get(),VAR["Blink_Callback"],isLambda=True)
Oder doch lassen, wie bisher?

Code: Alles auswählen

action.do_command(VAR["on_scale"],lambda me,par : par[1] = me.get(),VAR["Blink_Callback"],True)
Das dürfte dann die optimale Schreibweise sein.

Sorry, es waren nicht die eigenen Parameter, sondern ein anderer Callback und davon die Parameter, also:

Code: Alles auswählen

action.do_command(VAR["on_scale"],lambda me, callback : callback.parameters[1] = me.get(),VAR["Blink_Callback"],True)
Ja lassen wir alles wie es so ist. Bei solchen Einzeilern kann man auch auf eine extra Funktion verzichten.

Re: 'command' zwei Argumente übergeben?

Verfasst: Montag 10. August 2015, 11:52
von BlackJack
@Alfons Mittelmeyer: Es geht nicht nur um Info-Messages, sondern um Deinen *gesamten* Code. Das will niemand ausser Dir. Wenn man so etwas will, dann will man nicht in Python programmieren, denn das bietet schon Klassen. Die mit so einem Konstrukt zu umgehen ist nicht in Python programmieren.

Natürlich kann man Klassen dynamisch Methoden hinzufügen oder löschen. Das sind Objekte wie jedes andere auch. Nur will man das in aller Regel nicht, weil dann nicht mehr klar ist welches Objekt zu welchem Zeitpunkt welche Methoden hat, oder eben auch nicht. Das ist unübersichtlich und fehleranfällig.

Warum man sich Funktionsnamen ausdenken sollte? Weil Namen für den Leser wichtige Informationen transportieren. Und man muss den auch nicht löschen. Wer weiss was Du da wieder für Fantasien von der Speicherverwaltung hast, daber das Löschen des Namens löscht in diesem Falle wirklich nur den Namen, denn der Wert wurde ja als Argument übergeben und wird erst freigegeben wenn er nicht mehr benötigt wird. Da die Namen aber nicht auf Modulebene stehen (sonst machst Du was falsch) braucht man die nicht explizit löschen, denn sie verschwinden am Ende des Funktionsablaufs automatisch.

Weder ``Par()[1]`` noch ``callback.parameters[1]`` ist besser als ``blink.color``. Nochmal: Namen sind wichtig weil sie dem Leser vermitteln was die Werte dahinter im Kontext des Programms bedeuten. Bei ``blink.color`` steckt deutlich mehr sinnvolle Information in den Namen als bei den generischen Namen die Du im Angebot hast, die sonst etwas bedeuten könnten, und wo niemand darauf kommen würde das es um etwas farbiges, blinkendes geht. Der Leser muss sich doch an dem Code orientieren können, und zwar ohne das gesamte Programm ständig im Kopf haben zu müssen, oder Bedeutungen über den Code zurückverfolgen zu müssen weil die Namen alle so furchtbar generisch und nichtssagend sind und interne Strukturen bezeichnen die den Leser eigentlich gar nicht interessieren.

Re: 'command' zwei Argumente übergeben?

Verfasst: Montag 10. August 2015, 12:32
von Alfons Mittelmeyer
@BlackJack Also über "callback.parameters[1]" wollen wir nicht streiten. Ich hatte es sonst nirgends benutzt. Es war nur ein Beispiel, dass man auf die Parameter eines solchen Callbacks zugreifen kann. Bei lambda sind die Werte auch enthalten, man kommt aber von aussen nicht an diese heran.

Und wegen Funktionsnamen. Da würde mich interessieren, wie diese dann von selber verschwinden. Ein unimport gibt es schließlich bei Python nicht. Wenn Du mir sagen könntest, wie ich den von mir benutzten Funktionsnamen Speicher wieder frei bekomme? Die referenzierten Funktionen freilich verschwinden von selbst, wenn das Widget, mit dem sie als Callback verbunden sind, gelöscht wird.

Also, wie verschwinden die Namen? Und ich möchte auch keinen Namen löschen, den bereits ein anderer verwendet und braucht. Statt function könnte ich ja eine Klasse MyNames verwenden und könnte dann MyNames.blink schreiben. Aber die Klasse MyNames bekäme ich auch nicht aus dem Speicher. Aber das wäre zumindest überschaubar. Man müßte dann MyNames immer wieder erweitern, wenn man neue Namen braucht. Ich denke dass das mit function besser ist. Man kann ja einen Kommentar dahinter schreiben.

Damit will ich keinesfalls sagen, dass irgend jemand so programmieren soll. Nur ich habe den Ehrgeiz für meinen GuiCreator: wenn jemand das Toplevel Window schließt, dann soll nichts übrig bleiben davon. Auch nicht die Function function, denn dafür gibt es del. Aber vielleicht benützt einer in seiner Applikation schon die Function function und dann könnte der GuiCreator zum Problem werden. Wenn man ihn reinimportiert, dann wahrscheinlich nicht, da es ja ein eigenes Modul ist. Aber wenn man das Fenster aus Versehen geschlossen hat und würde den GuiCreator nochmals haben wollen, dann nützt ein import nichts. Denn import macht es nicht zum zweiten mal. Mit imp.reload habe ich es nicht geschafft, aber vielleicht hatte ich da auch etwas falsch gemacht. Mit File kompilieren und eval aber geht es, aber dann wäre man auf der Hauptebene.

Vielleicht sollte ich mir einen Funktionsnamen wie gsfAgfBx0 ausdenken.

Re: 'command' zwei Argumente übergeben?

Verfasst: Montag 10. August 2015, 12:53
von BlackJack
@Alfons Mittelmeyer: Lokale Namen verschwinden nach ablauf der Funktion automatisch. Der Stapelrahmen für den Funktionsaufruf wird freigegeben und damit auch die lokalen Namen (die nicht Bestandteil eines „closures” sind). Aber wenn Du jetzt tatsächlich schon explizit den Speicher von *Namen* freigeben willst weil Dir der Speicher so kostbar und teuer ist, verlassen wir nun endgültig den Boden des rationalen Handelns. :roll: Deine manuellen Speicherverwaltungsversuche sind insgesamt fehlgeleiteter Enthusiasmus. Du bist immer noch den Beweis schuldig das damit überhaupt etwas gespart wird, und nicht einfach nur mehr Unübersichtlichkeit, mehr Rechenzeit, und am Ende auch noch mehr Speicher verbraucht wird, also würde man normales, idiomatisches Python schreiben.

Nur noch mal wegen Deinem letzten Satz: ``del`` löscht keine Objekte, wie beispielsweise Funktionen, sondern Namen (und Verweise in Containerdatentypen).

Re: 'command' zwei Argumente übergeben?

Verfasst: Montag 10. August 2015, 13:01
von Alfons Mittelmeyer
@BlackJack Stimmt, hast Du vollkommen recht. Man kann es ja auch so machen:

Code: Alles auswählen

def function():
   Button() ...
   Label() ...

   def blink()
   def irgendwas()

function()
del function
Und dann braucht man das VAR['name'] auch nicht

Re: 'command' zwei Argumente übergeben?

Verfasst: Montag 10. August 2015, 13:11
von BlackJack
@Alfons Mittelmeyer: Okay, mit der letzten Programmzeile bin ich hier raus. Das macht keinen Sinn. :K