Marcel
Marcel

Reputation: 719

tkinter creating buttons in for loop passing command arguments

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

Answers (4)

AKX
AKX

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

Joel
Joel

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:

  • Customize gridsize
  • Responsive resizing
  • Toggle active state

#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()

enter image description here enter image description here

enter image description here

Upvotes: 3

lukad
lukad

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

BrenBarn
BrenBarn

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

Related Questions