Reputation: 719
I am trying to create buttons in tkinter within a for
loop. And with each loop pass the i
count value out as an argument in the command value. So when the function is called from the command
value I can tell which button was pressed and act accordingly.
The problem is, say the length is 3, it will create 3 buttons with titles Game 1 through Game 3 but when any of the buttons are pressed the printed value is always 2
, the last iteration. So it appears the buttons are being made as separate entities, but the i
value in the command arguments seem to be all the same. Here is the code:
def createGameURLs(self):
self.button = []
for i in range(3):
self.button.append(Button(self, text='Game '+str(i+1),
command=lambda: self.open_this(i)))
self.button[i].grid(column=4, row=i+1, sticky=W)
def open_this(self, myNum):
print(myNum)
Is there a way to get the current i
value, each iteration, to stick with that particular button?
This problem can be considered a special case of Creating functions in a loop. There's also What do lambda function closures capture?, for a more technical overview.
See also How to pass arguments to a Button command in Tkinter? for the general problem of passing arguments to Button callbacks.
Upvotes: 71
Views: 65691
Reputation: 168834
It's because the value for the name i
changes and isn't captured by lambda:
. (You can try that theory out by adding i = 1234
after the loop and seeing what happens.)
You'll need to write a function to wrap that i
as a local name, then return a lambda in that function that captures i
.
def make_button_click_command(i):
return lambda: button_click(i)
# ...
btn = Button(..., command=make_button_click_command(i))
Another option is functools.partial
, which does effectively the same thing:
command=functools.partial(button_click, i)
All in all, you can also simplify things a bit by using just range
to get numbers from 0 to 10 and divmod
to get the row and column in one function call:
from tkinter import Tk, Button
def button_click(i):
print(i)
def make_button_click_command(i):
return lambda: button_click(i)
root = Tk()
for i in range(10):
value = (i + 1) % 10
row, col = divmod(i, 3)
btn = Button(root, text=value, padx=40, pady=20, command=make_button_click_command(value))
btn.grid(row=row + 1, column=col)
root.mainloop()
Upvotes: 1
Reputation: 6173
Simply attach your buttons scope within a lambda function like this:
btn["command"] = lambda btn=btn: click(btn)
where click(btn)
is the function that passes in the button itself.
This will create a binding scope from the button to the function itself.
Features:
#Python2
#from Tkinter import *
#import Tkinter as tkinter
#Python3
from tkinter import *
import tkinter
root = Tk()
frame=Frame(root)
Grid.rowconfigure(root, 0, weight=1)
Grid.columnconfigure(root, 0, weight=1)
frame.grid(row=0, column=0, sticky=N+S+E+W)
grid=Frame(frame)
grid.grid(sticky=N+S+E+W, column=0, row=7, columnspan=2)
Grid.rowconfigure(frame, 7, weight=1)
Grid.columnconfigure(frame, 0, weight=1)
active="red"
default_color="white"
def main(height=5,width=5):
for x in range(width):
for y in range(height):
btn = tkinter.Button(frame, bg=default_color)
btn.grid(column=x, row=y, sticky=N+S+E+W)
btn["command"] = lambda btn=btn: click(btn)
for x in range(width):
Grid.columnconfigure(frame, x, weight=1)
for y in range(height):
Grid.rowconfigure(frame, y, weight=1)
return frame
def click(button):
if(button["bg"] == active):
button["bg"] = default_color
else:
button["bg"] = active
w= main(10,10)
tkinter.mainloop()
Upvotes: 3
Reputation: 17843
This is how closures work in python. I ran into this problem myself once.
You could use functools.partial
for this.
for i in range(3):
self.button.append(Button(self, text='Game '+str(i+1), command=partial(self.open_this, i)))
Upvotes: 12
Reputation: 251355
Change your lambda to lambda i=i: self.open_this(i)
.
This may look magical, but here's what's happening. When you use that lambda to define your function, the open_this call doesn't get the value of the variable i at the time you define the function. Instead, it makes a closure, which is sort of like a note to itself saying "I should look for what the value of the variable i is at the time that I am called". Of course, the function is called after the loop is over, so at that time i will always be equal to the last value from the loop.
Using the i=i
trick causes your function to store the current value of i at the time your lambda is defined, instead of waiting to look up the value of i later.
Upvotes: 170