Factory for callback methods - Python TKinter
Writing a test application to emulate PIO strings, I have a very simple Python / Tk GUI application. Use number keys 1 to 8 to simulate PIO pins 1 to 8. Press down = PIO High, release Key = PIO. It's not a problem for me. I kind of went down a rabbit hole trying to use a factory to create key call forwarding functions.
Here's some stripped down code:
#!usr/bin/env python
"""
Python + Tk GUI interface to simulate a 8 Pio lines.
"""
from Tkinter import *
def cb_factory(numberic_key):
"""
Return a call back function for a specific keyboard numeric key (0-9)
"""
def cb( self, event, key=numberic_key ):
bit_val = 1<<numberic_key-1
if int(event.type) == 2 and not (bit_val & self.bitfield):
self.bitfield |= bit_val
self.message("Key %d Down" % key)
elif int(event.type) == 3 and (bit_val & self.bitfield):
self.bitfield &= (~bit_val & 0xFF)
self.message("Key %d Up" % key)
else:
# Key repeat
return
print hex(self.bitfield)
self.display_bitfield()
return cb
class App( Frame ):
"""
Main TK App class
"""
cb1 = cb_factory(1)
cb2 = cb_factory(2)
cb3 = cb_factory(3)
cb4 = cb_factory(4)
cb5 = cb_factory(5)
cb6 = cb_factory(6)
cb7 = cb_factory(7)
cb8 = cb_factory(8)
def __init__(self, parent):
"Init"
self.parent = parent
self.bitfield = 0x00
Frame.__init__(self, parent)
self.messages = StringVar()
self.messages.set("Initialised")
Label( parent, bd=1,
relief=SUNKEN,
anchor=W,
textvariable=self.messages,
text="Testing" ).pack(fill=X)
self.bf_label = StringVar()
self.bf_label.set("0 0 0 0 0 0 0 0")
Label( parent, bd=1,
relief=SUNKEN,
anchor=W,
textvariable=self.bf_label,
text="Testing" ).pack(fill=X)
# This Doesn't work! Get a traceback saying 'cb' expected 2 arguements
# but only got 1?
#
# for x in xrange(1,9):
# cb = self.cb_factory(x)
# self.parent.bind("<KeyPress-%d>" % x, cb)
# self.parent.bind("<KeyRelease-%d>" % x, cb)
self.parent.bind("<KeyPress-1>", self.cb1)
self.parent.bind("<KeyRelease-1>", self.cb1)
self.parent.bind("<KeyPress-2>", self.cb2)
self.parent.bind("<KeyRelease-2>", self.cb2)
self.parent.bind("<KeyPress-3>", self.cb3)
self.parent.bind("<KeyRelease-3>", self.cb3)
self.parent.bind("<KeyPress-4>", self.cb4)
self.parent.bind("<KeyRelease-4>", self.cb4)
self.parent.bind("<KeyPress-5>", self.cb5)
self.parent.bind("<KeyRelease-5>", self.cb5)
self.parent.bind("<KeyPress-6>", self.cb6)
self.parent.bind("<KeyRelease-6>", self.cb6)
self.parent.bind("<KeyPress-7>", self.cb7)
self.parent.bind("<KeyRelease-7>", self.cb7)
self.parent.bind("<KeyPress-8>", self.cb8)
self.parent.bind("<KeyRelease-8>", self.cb8)
def display_bitfield(self):
"""
Display the PIO lines (1 for on, 0 for off)
"""
bin_lst = []
for x in xrange(8):
bit = 1 << x
if bit & self.bitfield:
bin_lst.append("1")
else:
bin_lst.append("0")
bin_lst.reverse()
bin_str = " ".join( bin_lst )
self.bf_label.set( bin_str )
def message( self, msg_txt ):
"set"
self.messages.set( msg_txt )
def cb_factory(self, numberic_key ):
"""
Return a call back function for a specific keyboard numeric key (0-9)
"""
def cb( self, event, key=numberic_key ):
bit_val = 1<<numberic_key-1
if int(event.type) == 2:
self.bitfield |= bit_val
self.message("Key %d Down" % key)
else:
self.bitfield &= (~bit_val & 0xFF)
self.message("Key %d Up" % key)
print hex(self.bitfield)
self.display_bitfield()
return cb
##########################################################################
if __name__ == "__main__":
root = Tk()
root.title("PIO Test")
theApp = App( root )
root.mainloop()
I finally got some kind of factory method working for the callback, but I don't find it very satisfying.
So my question is, can you have a factory class method that will generate class methods the way I tried (see the commented code and the cb_factory () application class class)?
NOTES: Yes, I know this app allows you to hold 4 keys at the same time, but that's good enough for my purposes.
a source to share
In response to your follow-up question.
I'm not sure which part you are not understanding, but I am assuming that you are not quite grasping how event callbacks work? If so, it's pretty easy. Tk starts up in a loop looking for events (key presses, mouseclicks, etc.). When you bind a callback / function to an event, you simply tell it to call your function and pass the event object as an argument. You can then query the event object for more details on what actually happened. You are currently creating separate callback functions and binding them to 18 key events (down and release keys 1-9). I think you can rewrite this to just use cb as a method of your class, because the event object will almost certainly contain the keycode.
class:
def __init__(self):
for x in xrange(8):
self.parent.bind("<KeyPress-%d>" % x, self.keyaction)
self.parent.bind("<KeyRelease-%d>" % x, self.keyaction)
def keyaction(self, event):
key = event.keycode # attribute may have another name, I haven't checked tk docs
... do stuff ...
Since we are now using self .keyaction as a callback, it should receive itself as its first argument. It gets its keycode from the event object. Now no value needs to be "inlined" into the function when the function is created, so the need to create different callbacks for each key will be removed and the code is easier to understand.
a source to share
Here is the corrected code based on SpliFF's answer. I find it much more aesthetically pleasing, but it worries me that I don't understand how it works. So, for additional credit, can anyone explain how this works?
#!usr/bin/env python
"""
Python + Tk GUI interface to simulate a 8 Pio lines.
"""
from Tkinter import *
from pio_handler import *
class App( Frame ):
"""
Main TK App class
"""
def __init__(self, parent):
"Init"
self.parent = parent
self.bitfield = 0x00
Frame.__init__(self, parent)
self.messages = StringVar()
self.messages.set("Initialised")
Label( parent, bd=1,
relief=SUNKEN,
anchor=W,
textvariable=self.messages,
text="Testing" ).pack(fill=X)
self.bf_label = StringVar()
self.bf_label.set("0 0 0 0 0 0 0 0")
Label( parent, bd=1,
relief=SUNKEN,
anchor=W,
textvariable=self.bf_label,
text="Testing" ).pack(fill=X)
# This is the clever bit!
# Use a factory to assign a callback function for keys 1 to 8
for x in xrange(1,9):
cb = self.cb_factory(x)
self.parent.bind("<KeyPress-%d>" % x, cb)
self.parent.bind("<KeyRelease-%d>" % x, cb)
def display_bitfield(self):
"""
Display the PIO lines (1 for on, 0 for off)
"""
bin_lst = []
for x in xrange(8):
bit = 1 << x
if bit & self.bitfield:
bin_lst.append("1")
else:
bin_lst.append("0")
bin_lst.reverse()
bin_str = " ".join( bin_lst )
self.bf_label.set( bin_str )
def message( self, msg_txt ):
"set"
self.messages.set( msg_txt )
def cb_factory(self, numeric_key ):
"""
Return a call back function for a specific keyboard numeric key (0-9)
"""
def cb( event, key=numeric_key ):
bit_val = 1<<numeric_key-1
if int(event.type) == 2:
self.bitfield |= bit_val
self.message("Key %d Down" % key)
else:
self.bitfield &= (~bit_val & 0xFF)
self.message("Key %d Up" % key)
print hex(self.bitfield)
self.display_bitfield()
return cb
##########################################################################
if __name__ == "__main__":
root = Tk()
root.title("PIO Test")
theApp = App( root )
root.mainloop()
a source to share