Binding method with multiple arguments to <<ListboxSelect>> - python-3.x

Trying to make a method execute on the selection of an item in a tkinter listbox. I can bind some function to is using .bind("<<ListboxSelect>>", method), but if i try to give any arguments to it, like method(argumentone, argumenttwo), the method no longer executes on the selection of an item from the listbox. It is however executed at startup when Mainloop starts. Furthermore, giving arguments to a method is problematic, as i cant pass the event argument already passed to any bound method, as it is not assigned to any variable beforehand. I'd like to be able to pass the event and one other variable to some method ideally. Any help on achieving this would be very welcome. Some example code of what I'm trying to achieve
def onSelection(event, foo):
widget = event.widget
print(widget.curselection())
print(foo)
sam = "sam"
chars = Listbox(main)
chars.insert(someList)
chars.bind("<<ListboxSelect>>", onSelection(evt, sam))
Note that this code doesn't work. evt isn't defined.

When you specify a function in bind() that ends in parenthesis it just runs the function without bind generating an event. Instead you have to give bind a name. You can accomplish this with lambda or functools.partial. I'll use lambda in my example.
Bind is generating an event when triggered, so lambda will have to take it as input. Then call onSelection() with desired arguments.
from tkinter import *
main = Tk()
def onSelection(event, foo):
widget = event.widget
print(widget.curselection())
print(foo)
someList = ['Stilton', 'Brie', 'Edam', 'Cheddar', 'Ilchester']
chars = Listbox(main)
chars.pack(padx=10, pady=10)
for item in someList:
chars.insert("end", item)
sam = "sam"
chars.bind("<<ListboxSelect>>", lambda event: onSelection(event, sam))
# Take care of event created by bind ----^ ^
# Pass the event as well as argument to callback function ----|
main.mainloop()

Related

Bind variable number of PyQt toggles to same number of functions [duplicate]

I'm connecting multiple signal/slots using a for loop in PyQt. The code is bellow:
# Connect Scan Callbacks
for button in ['phase', 'etalon', 'mirror', 'gain']:
getattr(self.ui, '{}_scan_button' .format(button)).clicked.connect(
lambda: self.scan_callback(button))
What I expect:
Connect button phase_scan_button clicked signal to the scan_callback slot and send the string phase as a parameter to the slot. The same for etalon, mirror and gain.
What I'm getting:
For some reason my functions is always passing the string gain as parameter for all the buttons. Not sure if I'm being stupid (likely) or it is a bug.
For reference, the slot method:
def scan_callback(self, scan):
print(scan) # Here I always get 'gain'
if self.scanner.isWorking:
self.scanner.isWorking = False
self.scan_thread.terminate()
self.scan_thread.wait()
else:
self.scanner.isWorking = True
self.scan_thread.start()
getattr(self.ui, '{}_scan_button' .format(
scan)).setText('Stop Scan')
getattr(self, '_signal{}Scan' .format(scan)).emit()
My preferred way of iterating over several widgets in pyqt is storing them as objects in lists.
myButtons = [self.ui.phase_scan_button, self.ui.etalon_scan_button,
self.ui.mirror_scan_button, self.ui.gain_scan_button]
for button in myButtons:
button.clicked.connect(lambda _, b=button: self.scan_callback(scan=b))
If you need the strings "phase", "etalon", "mirror" and "gain" separately, you can either store them in another list, or create a dictionary like
myButtons_dict = {"phase": self.ui.phase_scan_button,
"etalon": self.ui.etalon_scan_button,
"mirror": self.ui.mirror_scan_button,
"gain": self.ui.gain_scan_button}
for button in myButtons_dict:
myButtons_dict[button].clicked.connect(lambda: _, b=button self.scan_callback(scan=b))
Note, how I use the lambda expression with solid variables that are then passed into the function self.scan_callback. This way, the value of button is stored for good.
Your lambdas do not store the value of button when it is defined. The code describing the lambda function is parsed and compiled but not executed until you actually call the lambda.
Whenever any of the buttons is clicked, the current value of variable button is used. At the end of the loop, button contains "gain" and this causes the behaviour you see.
Try this:
funcs = []
for button in ['phase', 'etalon', 'mirror', 'gain']:
funcs.append( lambda : print(button))
for fn in funcs:
fn()
The output is:
gain
gain
gain
gain
Extending the example, as a proof that the lambdas don't store the value of button note that if button stops existing, you'll have an error:
del button
for fn in funcs:
fn()
which has output
funcs.append( lambda : print(button))
NameError: name 'button' is not defined
As noted here : Connecting slots and signals in PyQt4 in a loop
Using functools.partial is a nice workaround for this problem.
Have been struggling with same problem as OP for a day.

Coding a general function to: Disable a tkinter widget with a checkbutton

I'm trying to write a function to disable a widget in a tkinter program depending on the value of a checkbutton. I want this function to be general: That is, I can pass it the widget and associated check variable and it will disable the widget (if the variable is checked the right way).
Here is an abstracted version of my code
import tkinter
class App:
def __init__(self,root):
widg = tkinter.Scale(root,from_=0,to=100)
checkvar = tkinter.IntVar()
checker = tkinter.Checkbutton(root,variable=checkvar,command=self.check(var,checkvar))
widg.grid()
checker.grid()
widg.configure(state=tkinter.DISABLED)
widg.configure(state=tkinter.NORMAL)
def check(self,widget,var):
if var.get()==1:
widget.configure(state=tkinter.DISABLED)
elif var.get()==0:
widget.configure(state=tkinter.NORMAL)
m = tkinter.Tk()
f=App(m)
It is intended to function such that clicking the checkbutton triggers the callback - check - with the parameters of the widget and the check variable. Then it will evaluate whether the widget should be on or off and change its state accordingly. There are no errors but the state doesn't change. What am I missing here?
Thanks
The command argument simply takes the uncalled function so passing arguments to it takes some workaround.
So it expects self.check rather than self.check() since the widget will call the function later.
I've found that using partial is a workaround for passing the arguments.
import tkinter
from functools import partial
class App:
def __init__(self,root):
widg = tkinter.Scale(root,from_=0,to=100)
checkvar = tkinter.IntVar()
checker = tkinter.Checkbutton(root,variable=checkvar,command=partial(self.check, widg, checkvar))
widg.grid()
checker.grid()
widg.configure(state=tkinter.DISABLED)
widg.configure(state=tkinter.NORMAL)
def check(self,widget,var):
if var.get()==1:
widget.configure(state=tkinter.DISABLED)
elif var.get()==0:
widget.configure(state=tkinter.NORMAL)
m = tkinter.Tk()
f=App(m)
m.mainloop()
Credit to my friend JB for helping me with this.
As user Axe319 suggested, the problem is just that tkinter doesn't expect to pass any parameters. It is possible to work around this by using a lambda function. The line that defines the checker variable, in my original post line 8, can be rewritten as such:
self.checker = tkinter.Checkbutton(root,variable=checkvar,command=lambda:self.check(self.widg,checkvar))
and it will work as intended.

Declaring variables in a python tkinter button on click command

i am trying to figure out a problem i'm having with declaring a variable upon a button click. For instance i wish to assign to the variable f the value True. I seem to be having trouble with the syntax or method of using the Button function to declare the variable. Do i initialize the variable beforehand? im not quite sure
here is my code:
import tkinter
from tkinter import *
root=Tk()
b1=Button(root,text="test", command=lambda: f=True)
b1.pack()
root.mainloop()
In Python, assignment is a statement, and thus cannot be done with simple lambdas, which can only contain expressions (function calls, variables, attributes, ... not statements, returns1, breaks, ...). To do what you're wanting to do, you must define a normal function as so:
f = False
def onclick():
global f
f = True
This will access the global namescape's f, (use nonlocal if you're within another function) and use that variable within the function. By assigning to this you will change its value in the outer scope.
Note that you must have f defined in the outer scope before the function can reassign it.
To use it, set the command to onclick as such:
b1=Button(root,text="test", command=onclick)
A lambda isn't necessary as you're passing the function object
Note that the function definition must occur before it is passed as the command argument to the new Button
1: By this I mean returning from an outer function, not returns within the lambda, as the entire lambda expression itself is returned.

How to use self and event in a class function?

No matter how I try it I keep getting the error that I need to send an argument to the event.
and yes before you say to go look at the documentation. I have looked at it but it doesn't help with what I'm doing because it is never inside a class.
from tkinter import *
class App(Tk):
def __init__(self):
Tk.__init__(self)
self.callback()
self.initUI()
self.mainloop()
def callback(self, event):
print("made it")
def initUI(self):
canvas = Canvas(self, height = 300, width = 300)
canvas.create_rectangle(1.5,1.5,40,40, tag = "obj1")
canvas.tag_bind("obj1", '<Button-1>', callback) #where I assume the problem is happening
A = App()
EDIT: callback keeps saying it needs another argument but I don't know what it needs
You have defined your callback to take two arguments: self (meaning, it's a method on an object) and event. When you call it directly with self.callback(), self gets automatically passed, but you aren't passing in an event. That is why you get the error saying it needs another argument. It expects two, but is only getting one.
It's unclear what you are attempting to do by directly calling your callback, but a quick fix is to make the event attribute optional so you can call the callback directly or via a binding. Of course, this will only work if your binding doesn't actually use the event parameter. If it does, when calling it without an event you can expect it to fail.
Here's an example of making the event parameter optional:
def callback(self, event=None):
print("made it")
That solves one problem, but you also have to change how your binding is defined or your code will crash when it starts up. You need to prepend self to the callback:
canvas.tag_bind("obj1", '<Button-1>', self.callback)
When callback is called, self again is automatically passed as the first argument. Tkinter will automatically add the event parameter.
Pedantically speaking, you should not be calling mainloop() inside of __init__. That prevents the App object from fully being constructed. The proper way to do it is to call mainloop outside of the constructor:
A = App()
A.mainloop()

Passing StringVar object from Entry to Label within functions in tkinter

Hi I've been struggling to get this to work, each time i change something I receive another error. I've been trying to create an entry box with a function and then get the variable from the entry box into a label, created by a button press. When I tried to do this often this error came up.
TypeError: get() missing 1 required positional argument: 'self'
I then put self in in the method brackets.
command = lambda: x.myFunc(self.my_variable.get(self))
Then another error, which I'm not sure how to sort out.
AttributeError: 'My_Class' object has no attribute '_tk'
Here's the full code, I'm new to classes and self, so any corrections are welcome.
from tkinter import *
import time
class My_Class:
def start(self):
self.root=Tk()
self.my_variable=StringVar
self.entry_box=Entry(self.root, textvariable=self.my_variable)
self.entry_box.pack()
self.button=Button(self.root,text="Pass variable now",
command=lambda:x.myFunc(self.my_variable.get(self)))
self.button.pack()
def myFunc(self,my_variable):
self.lab=Label(self.root,text=self.my_variable)
self.lab.pack()
x=My_Class()
x.start()
This is the correct way to create a StringVar object:
text = StringVar() # note additional ()
Can you explain me what x is in the following statement:
lambda: x.myFunc(self.my_variable.get(self))
x is not visible inside the class, because it's declared outside the class.
myFunc is not indented correctly: you should indent it like the __init__ method.
I really recommend you to watch some tutorials on OOP before proceeding. You are basically trying to guess how OOP works.
If you make myFunc A method if the class (which you might be trying to do; it's hard to know because your indentation is wrong), you don't have to pass anything to myFunc. That function has access to everything in the class, so it can get what it needs, when it needs it. That lets you eliminate the use of lambda, which helps reduce complexity.
Also, you normally don't need a StringVar at all, it's just one more thing to keep track of. However, if you really need the label and entry to show exactly the same data, have them share the same textvariable and the text is updated automatically without you having to call a function, or get the value from the widget, or set the value n the label.
Here's an example without using StringVar:
class My_Class:
def start(self):
...
self.entry_box = Entry(self.root)
self.button = Button(..., command = self.myFunc)
...
def myFunc(self):
s = self.entry_box.get()
self.lab = Label(..., text = s)
...

Resources