Python3 tkinter:小部件行的删除在第一次迭代时起作用,但在后续迭代时不起作用

时间:2017-03-20 13:47:39

标签: python python-3.x button tkinter

我使用tkinter条目小部件创建了一个由行和列组成的小部件。我希望在每一行旁边放一个“ - ”按钮,它只会删除该行。

我的对象名为DisplayTable,它是tk.Frame的一个实例,它由一系列条目组成,每行包含一个按钮,其上有一个-,应该允许轻松删除该行。

我会尝试跳过代码中更详细的部分,以便我能够达到目的。 DisplayTable对象在self.rows中创建行列表__init__。随着行的添加,此列表将增长。例如,具有三行和两列的表格如下所示:

[
    [<tkinter.Entry object>, <tkinter.Entry object>, <tkinter.Button object>],
    [<tkinter.Entry object>, <tkinter.Entry object>, <tkinter.Button object>],
    [<tkinter.Entry object>, <tkinter.Entry object>, <tkinter.Button object>]
]

结果图: Resulting graphic

DisplayTable.add_row()中,我有这段代码片段(删除了很​​多额外的代码以达到目的):

def add_row(self):
    row = []
    for i in range(self.columns):
        cell = tk.Entry(self)
        cell.grid(row=offset, column=i)
        row.append(cell)        

    row_to_delete = len(self.rows)
    btn = tk.Button(self, image=self.minus, command=lambda: self.delete_row(row_to_delete))
    btn.grid(row=offset, column=self.columns)
    row.append(btn)

    self.rows.append(row)

上面的代码片段非常适合在数组末尾添加行。它适用于行删除一次。

def delete_row(self, row_number):
    for widget in self.rows[row_number]:
        widget.destroy()
    self.rows.pop(row_number)

    # re-assign the delete row buttons
    for i, row in enumerate(self.rows):
        button = row[-1]
        button.configure(command=lambda: self.delete_row(i))

我对delete_row的意图是重新分配删除按钮,因为self.rows的行索引刚刚为每行更改。执行此函数后,我得到的行为是始终删除最后row_number。例如,如果有10行,则始终删除索引为9的行。

使用更直接的引用替换删除按钮的重新分配似乎不会改变行为:

def delete_row(self, row_number):
    for widget in self.rows[row_number]:
        widget.destroy()
    self.rows.pop(row_number)

    # re-assign the delete row buttons
    for i, row in enumerate(self.rows):
        self.rows[i][-1].configure(command=lambda: self.delete_row(i))

使用print命令,我已经验证变量i在每一轮中肯定是不同的,并且每次引用的button对象都是不同的按钮,因此{{ 1}}分配也应该是不同的。不知何故,似乎只有最后一次循环才会粘到所有按钮上,而不仅仅是它所针对的按钮。

2 个答案:

答案 0 :(得分:1)

for i, row in enumerate(self.rows):
    button = row[-1]
    button.configure(command=lambda: self.delete_row(i))

单击该按钮时,将删除该时刻变量i 的值指定的行。由于此循环将在您可能单击按钮之前完成,i将始终引用最后一行。这是循环中lambdas的常见问题,解决方案是捕获默认参数中所有已使用变量的值:command=lambda i=i: self.delete_row(i))

我认为更好的方法是将row变量传递给delete_row()而不是索引 - 这样,删除行时不需要更改任何内容。您可以迭代for widget in row:以删除要删除的小部件,并通过self.rows.remove(row)删除行本身。

答案 1 :(得分:1)

jasonharper已经解释了问题的原因:lambda中的变量在创建时并未“冻结”,它们是动态的。

有关详细信息,请参阅SO Python Wiki中的Why do my lambda functions or nested functions created in a loop all use the last loop value when called?

这是一种避免这个问题的方法。我们可以在回调函数中使用默认参数。定义函数时会计算默认值,而不是在调用函数时计算默认值(函数是lambda还是完整def函数无关紧要)。

我们可以使用行对象本身作为delete_row方法的参数,而不是存储行索引号,这需要在行删除后进行杂乱的重新赋值。

import tkinter as tk

class Grid(tk.Frame):
    def __init__(self, root):
        super().__init__(root)
        self.pack()

        self.rows = []
        self.numcols = 3
        self.numrows = 4
        for _ in range(self.numrows):
            self.add_row()

    def add_row(self):
        rownum = len(self.rows)
        row = []
        for i in range(self.numcols):
            cell = tk.Entry(self)
            cell.grid(row=rownum, column=i)
            row.append(cell)

        btn = tk.Button(self, text='-', command=lambda r=row: self.delete_row(r))
        btn.grid(row=rownum, column=self.numcols)
        row.append(btn)

        self.rows.append(row)

    def delete_row(self, row):
        for widget in row:
            widget.destroy()
        self.rows.remove(row)


root = tk.Tk()
gui = Grid(root)
root.mainloop()