Binding events to widget not working - python

I have a function, say foo and it's supposed to bind an event
def foo:
for x in widget_list:
widget.config(command = lambda: self.function(widget_list.index(x))
def function(index):
do something here....
print index
The widget list contains buttons, and whenever I click corresponding buttons, they're supposed to print to the IDLE their index, but what happens is that all the buttons I click end up printing the last index of widget_list. I'm guessing that while for iterates, the argument of the function also changes thus only the last index is preserved. Is there anyway to bind the previous indexes to the button?

The simplest fix is to make sure that your lambda is using the current value of x:
widget.config(command = lambda x=x: self.function(widget_list.index(x))
This forces x to be a default parameter to the function, and the default value will be the value of x at the time the lambda is defined.

Related

How to add two integers dynamically

I'm trying to add two integers dynamically with tkinter. My idea is to get the sum after typing in a integer.
My idea:
import tkinter as tk
root = tk.Tk()
root.geometry("600x100")
v = tk.IntVar()
w = tk.IntVar()
eingabefeld_0 = tk.Entry(master=root, textvariable = v)
eingabefeld_0.place(x=80, y=40)
eingabefeld_1 = tk.Entry(master=root, textvariable = w)
eingabefeld_1.place(x=320, y=40)
label = tk.Label(master=root, textvariable = v)
label.place(x=80, y=80)
label = tk.Label(master=root, textvariable = str(int(v.get())+int(w.get())))
label.place(x=320, y=80)
root.mainloop()
If start the program the result of the addition is not presented in the label. What is missing here?
You aren't using the textvariable option correct. The value passed to that option needs to be an instance of one of the tkinter variable objects (StringVar, IntVar, etc). You can't just pass an expression to it.
You need to write a function that calculates the result and sets the value of this variable. The function can then be called from a button, or you can set a trace on the variable and update it every time the value changes. You'll have to make sure to take care of the case where one of the values you're trying to add is not a valid integer.
For example, start by creating a new variable for the result, and using it in your label. In this case I recommend StringVar so that we can display something like "???" if we can't compute the value.
resultvar = tk.StringVar()
label = tk.Label(master=root, textvariable = resultvar)
Next, create a function that computes the result and stores it in this variable. You need to use try/except since it's possible for the user to delete everything in the entry widget in which case the call to .get() will fail.
def compute_result():
try:
result = v.get() + w.get()
resultvar.set(result)
except Exception as e:
resultvar.set("???")
Now you just need to add code that calls this function. You can do it several ways:
you can bind to the return key so that the result is updated when the user presses that key
you can bind to any key release so that the result is updated when the user presses and releases any key
you can add a button that the user has to click to update the value
you can add a trace to the input variables, which will cause a function to be called whenever the values change
To use a trace, call trace_variable telling it when to do the trace ("w" means whenever the value is written/changed), and what function to call. The trace will append some arguments to the function that we don't use, so we'll have to change the function to accept these arguments
def compute_result(*args):
try:
result = v.get() + w.get()
resultvar.set(result)
except Exception as e:
resultvar.set(f"???")
v.trace_add("write", compute_result)
w.trace_add("write", compute_result)
With that, whenever the value in the variable v or w changes, the function will be called and the result variable will be updated.

Loop changes button binding functions

sair_grid = self.root.ids["sairler_screen"].ids["sair_grid"]
for a in dler:
with open(a,"r",encoding="utf-8") as file1:
for b in file1:
btn = Button(text=b[:-5],on_press=lambda y:self.sairi(b))
sair_grid.add_widget(btn)
def sairi(self,x):
pass
No matter which button I clicked to calling the function, it is giving the same result. I want every button to take different b values and call the function with different arguments. All buttons created with different names but each of them calls with same last argument of the file. I am new in python and kivy so every help is really appreciated.
There is only one variable named b here and it changes its value until the last one remains last time the loop executes:
for b in file1:
btn = Button(text=b[:-5],on_press=lambda y:self.sairi(b))
sair_grid.add_widget(btn)
So, you are creating many lambdas, but they all use the same b from the closure.
A typical simple workaround for that problem is to bind the current value of b to a default value of the lambda argument:
for b in file1:
btn = Button(text=b[:-5],on_press=lambda y, b=b:self.sairi(b))
sair_grid.add_widget(btn)
Now each lambda you create has a locally bound b argument which has its own default value. The default value of b is now different for each lambda you create.

How is Tkinter handling lambda after binding event?

I am trying to write some code that will send the value of an Entry box to a function based on a binding. I can technically get the behavior I want in the code below, but I'm a) not sure why it works and b) am quite sure I am not doing this in the most pythonic way. I'm pretty sure I'm misunderstanding the event or lambda or both.
In the code below the Entry box called input_box1 when the binding triggers, the inp_var1.get() code only gets the default value, not anything that has been entered into the box. In other words, the test1 function will print...
Test1 Foo
... no matter what you type into the entry.
The binding on input_box2 works exactly as I expect. I type anything in there and click somewhere else and it prints the new entry. However I don't understand why my lambda doesn't want an event or why I need to repeat the inp_var2.get() call.
If anyone knows what's going under the hood I'd love to hear it! Here's the code:
from tkinter import *
from tkinter import ttk
def test1(event, i):
print('Test1', i)
def test2(event, i):
print('Test2', i)
root = Tk()
title_label = Label(root, text='This does not work!')
title_label.grid(column=0, row=0)
inp_var1 = StringVar(value='Foo')
input_box1 = Entry(root, textvariable=inp_var1)
input_box1.grid(column=0, row=1)
inp_var2 = StringVar(value='Bar')
input_box2 = Entry(root, textvariable=inp_var2)
input_box2.grid(column=0, row=2)
input_box1.bind('<FocusOut>', lambda event, i=inp_var1.get(): test1(event, i))
input_box2.bind('<FocusOut>', lambda i=inp_var2.get(): test2(i, inp_var2.get()))
root.mainloop()
This has very little to do with Tkinter itself. It's also not so much connected to lambda as it is to Python in general.
Take both of those out of the equation, and consider the following Python program:
def f(x=3, y=4):
print('x =', x, 'y =', y)
f(0, 0)
f(0)
f()
Assuming Python 3 (or from __future__ import print_function), when run, this prints:
x = 0 y = 0
x = 0 y = 4
x = 3 y = 4
That's because the first call to f passes 0, 0, so x and y are both bound to zero. The second call to f passes just 0 so x is bound to 0 and y is bound to its default value of 4. The third call passes nothing at all and x and y are both bound to their default values.
(So far, this should all be clear enough.)
Now let's fuss with this a bit. I'll continue to assume Python 3 so that input means what in Python 2 we have to use raw_input to achieve:
def f(x=input('enter default x: '), y=input('enter default y: ')):
print('x =', x, 'y =', y)
print('about to call f three times')
f(0, 0)
f(0)
f()
Before you run this sample program, think about what you expect it to do. Then run it—here's my result:
$ python3 t.py
enter default x: hello
enter default y: world
about to call f three times
x = 0 y = 0
x = 0 y = world
x = hello y = world
Why did this read the default values for x and y before we even called it the first time? Why didn't it read new default values for x and y on each call?
Think about that for a bit, then read on
Really, do that. Ask why the inputs happened at these odd times.
Now that you've thought about it...
It did do that, though. That's the key here. The default values for your arguments are captured at the time the def statement is executed. The def statement, which binds the name f to our function, is actually run when Python loads the file, after seeing the body of the function. The function itself is run later, after Python has gotten to the print call and then to the first f call.
A lambda in Python is just a sort of anonymous function definition. Instead of:
def square(x):
return x * x
we can write:
square = lambda x: x * x
The lambda expression defines a new function-like item, a lambda function—you can't use if/else type statements inside one of these, just expressions, and they automatically return the value of their expression. In this case our lambda function has one argument named x. The function returns x * x.
The outer assignment, square =, binds this lambda function to the name square, just as if we'd done def square(x) instead. So it's mostly just a syntactic trick: we can have a real function, like square, with arguments, or a limited lambda function that can only use expressions, like this anonymous function that we almost immediately bind to the name square.
The arguments part works the same either way though. Just as with f and input, if we bind x:
square = lambda x=3: x * x
or:
square = lambda x=int(input('choose a default for x now> ')): x * x
that happens just once, when the lambda expression itself executes. The function now has a variable x with a default value.
When, later, we call the function, we can provide a value for x, or let it default. If we don't provide a value, Python uses the default that it captured earlier, when we executed the line with lambda in it, rather than now, when we call the lambda function.
This all holds for Tkinter as well. You wrote:
input_box2.bind('<FocusOut>', lambda i=inp_var2.get(): test2(i, inp_var2.get()))
but that's pretty much the same as writing:
def f(i=inp_var2.get()):
test2(i, inp_var2.get())
input_box2.bind('<FocusOut>', f)
except that you don't have to come up with the function-name f here. The lambda variant of the function has no name, but it's still just a function. (For that matter, when we do square = lambda ..., the lambda function doesn't have a name. The name square is the name of a variable, not the name of the function. The variable is merely bound to the function, so that square(10) calls it.)
Anyway, later, when Tkinter has an event that matches <FocusOut>, Tkinter calls f ... or, if you used lambda, calls your unnamed lambda function. Tkinter provides one argument to that function; the one argument it provides is the event. So your default value for i in f above is irrelevant—you could do:
def f(i=None):
test2(i, inp_var2.get())
or:
def f(i='hello world'):
test2(i, inp_var2.get())
because when Tkinter calls f, it always provides an actual argument for i.

Python Tkinter button callback

I am trying to print out the button number when i click each buttons that are created by for loop. The following is what i have tried.
import Tkinter as tk
root=tk.Tk()
def myfunction(a):
print a
for i in range(10):
tk.Button(root,text='button'+str(i),command=lambda:myfunction(i)).place(x=10,y=(10+(25*i)))
root.mainloop()
But instead of printing out each button number, it actually giving me the last button number everytime. Is there anything i can do so that when i click button 1, it will print 1,2 for 2 ,and so on?
Easy fix is to initialize the lambda function with the current value of i each time the lambda function is created. This can be done using Python default values for another dummy variable j.
command = lambda j=i: myfunction(j)
Blender's answer is a clever solution but in case you get thrown off by the function abstraction, here is another possible way to do it. It really just creates a mapping, saved in buttons, from Button widgets to their proper numbers.
import Tkinter as tk
root = tk.Tk()
def myfunction(event):
print buttons[event.widget]
buttons = {}
for i in range(10):
b = tk.Button(root, text='button' + str(i))
buttons[b] = i # save button, index as key-value pair
b.bind("<Button-1>", myfunction)
b.place(x=10,y=(10+(25*i)))
root.mainloop()
The problem is that the Button command in tk ignores any parameters so insomething like mycommand(data) the data is ignored.
I use buttons quite a bit and decided to subclass a tk Button to include data. I called it a DataButton and added a data member ( in this case an index number). That way when clicked it passes back its data. ( index number) Now I use the DataButton whenever I want it to hold information like an index or even a message.
It's because i in your anonymous function refers to the counter variable, not to the value:
from __future__ import print_function
x = [lambda: print(i) for i in range(10)]
for f in x:
f()
This produces an output of 10 consecutive 9s.
To get around this, you'd have to use two lambdas and shadow i with a second function (which creates your first function):
from __future__ import print_function
x = [(lambda i: (lambda: print(i)))(i) for i in range(10)]
for f in x:
f()
Although at that point, you'd be better off just making a named function:
def my_command(i):
def inner_function():
return my_function(i)
return inner_function
And using it like this:
tk.Button(root, text='button' + str(i), command=my_command(i))

Tkinter OptionMenu add a command to multiple OptionMenus

Basically I have a series of OptionMenus that are created in a loop, but is currently empty:
option_menu = []
for ii in range(jj):
option_menu.append([])
for ll in range(kk):
option_menu[ii].append(OptionMenu(frame,tkinter_text_var[ii][ll],''))
Then elsewhere I use a checkbox to set the values along the lines of:
for ii in range(jj):
for ll in range(kk):
option_menu[ii][ll]["menu"].add_command(label = name_from_box.get(), command = lambda: tkinter_text_var[ii][ll].set(name_from_box.get()))
This works to populate all of the OptionMenus properly, but when I select a value in any of the OptionMenus, it only sets option_menu[jj][kk] (i.e. that last one made).
So what have I done wrong?
This is a very common question involving closures. Look at the following example:
alist = [lambda : x for x in range(10) ]
print (alist[2]()) #9
print (alist[4]()) #9
The'll all be 9. Why? Because each lambda function refers to the variable x. x gets changed at every iteration through the loop, but they all still refer to the same object.
One way around this is to use a default argument. Default arguments are evaluated when the function is created, not when it is called.
alist = [lambda y=x: y for x in range(10) ]
print (alist[2]()) #2
print (alist[4]()) #4
(another way to do the same thing involves functools.partial which you'll see sometimes ...)
I often like to say -- "take care with closures". They can be a little tricky.

Categories