tkinter中如何在for循环中创建按钮并传递命令参数

67
我正在尝试在tkinter中使用for循环创建按钮。并且通过每次循环传递i计数值作为命令值的参数。因此,当从command值调用函数时,我可以知道按下了哪个按钮并相应地采取行动。问题是,假设长度为3,它将创建3个标题为“Game 1”到“Game 3”的按钮。但是,当按下任何按钮时,打印出的值总是2,即最后一次迭代。因此,似乎这些按钮是作为单独的实体被创建的,但是命令参数中的i值似乎都是相同的。以下是代码:
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)

有没有一种方法可以在每次迭代中获取当前的i值,并将其与特定的按钮关联起来?
此问题可以视为在循环中创建函数的特例。另请参阅lambda函数闭包捕获什么?,以获取更多技术概述。
此外,还可以参考如何将参数传递给Tkinter中的Button命令?,解决将参数传递到Button回调函数的一般问题。

如果BrenBarn的解决方案对您有用,那么您应该将其标记为已接受的答案。 - lukad
@martineau 您认为重复的问题是否回答了此问题? - Delrius Euphoria
1
@Delrius:啊,不好意思,我误将其标记为重复问题,并且是错误的问题。感谢您提醒我。 - martineau
另请参见https://dev59.com/YHjZa4cB1Zd3GeqPfpb1 - PM 2Ring
参见:https://dev59.com/9XE95IYBdhLWcg3wd9xK - Karl Knechtel
显示剩余3条评论
4个回答

158

将您的lambda更改为lambda i=i: self.open_this(i)

这看起来可能有点神奇,但实际上是这样的。当您使用该lambda来定义函数时,open_this调用不会获得您定义函数时变量i的值。相反,它创建了一个闭包,类似于一个便签,告诉自己“我应该在被调用时查找变量i的值此时”。当然,函数在循环结束后调用,所以此时的i将始终等于循环中的最后一个值。

使用i=i的技巧使您的函数在lambda定义时存储变量i的当前值,而不是等待以后查找变量i的值。


2
如果我们想要像open_this这样的函数传递两个参数,该怎么办? - Amen
13
如果两个参数都来自于某个外部循环,并且你希望像上面展示的那样“冻结”它们,那么你可以使用lambda x=x, y=y: self.open_this(x, y)。@Amen说:“这取决于你想让这些参数成为什么样子。如果两个参数都来自于某个外部循环,并且你想要以上面展示的方式“冻结”它们,那么你只需要使用lambda x=x, y=y: self.open_this(x, y)。” - BrenBarn
4
这个解释非常棒、简单明了且通俗易懂,应该是正确答案。 - Battleroid
1
@BrenBarn 刚刚我也遇到了同样的情况,真是太神奇了。我真的很奇怪你们是如何发现这些技巧的。非常感谢你。 - velpandian
1
@CoolCloud:这和我说的一样。你说“参数存储在函数中”,我说“函数存储值”。这是同一件事。 - BrenBarn
显示剩余8条评论

12

这就是Python中闭包的工作原理。我曾经也遇到过这个问题。你可以使用functools.partial来解决。

for i in range(3):
    self.button.append(Button(self, text='Game '+str(i+1), command=partial(self.open_this, i)))

看起来很简单,但如果你正在使用虚拟环境,可能会遇到问题,请参考第一个:self.button.append(Button(self, text='Game ' + str(i + 1), command=lambda x=i: self.open_this(x))) - Lucem
2
@Lucem "如果你正在使用虚拟环境,你可能会遇到问题"。没有什么好的理由,因为functools.partial是标准库的一部分,并且已经存在很长时间了。如果你在使用它时遇到困难,那么你应该尝试诊断问题并提出自己的问题。 - Karl Knechtel

3

只需像这样在 lambda 函数中附加您的按钮范围:

btn["command"] = lambda btn=btn: click(btn) 其中 click(btn) 是传递按钮本身的函数。 这将从按钮创建绑定作用域到函数本身。

特点:

  • 自定义网格大小
  • 响应式调整大小
  • 切换活动状态

#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


1

这是因为名称i的值发生了变化,并且没有被lambda所捕获。 (您可以通过在循环后添加i = 1234来尝试该理论并查看发生了什么。)

您需要编写一个函数,将i作为局部名称包装,然后在该函数中返回捕获ilambda

def make_button_click_command(i):
    return lambda: button_click(i)

# ...

btn = Button(..., command=make_button_click_command(i))

另一个选择是使用functools.partial,它实际上也能达到同样的效果:
command=functools.partial(button_click, i)

总的来说,您也可以通过仅使用range从0到10获取数字,并使用divmod在一个函数调用中获取行和列来简化事情:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接