Reputation: 2645
I have created a widget that consists of rows and columns using tkinter entry widgets. I wish to place a '-' button next to each row which will simply delete that row.
My object is called DisplayTable
, which is an instance of tk.Frame
and it consists of rows of entries, each row of which contains a button with a -
on it that should allow easy removal of that row.
I'll try to skip the more verbose portions of the code so that I can get to the point. The DisplayTable
object creates a list of rows, self.rows
within __init__
. As rows are added this list will grow. For instance, a table that has three rows and two columns would look like:
[
[<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>]
]
In DisplayTable.add_row()
, I have this code snippet (removed a lot of extra code to get to the point):
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)
The above code snippet works perfectly for adding rows to the end of the array. It works properly for row deletion one time.
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))
My intent in delete_row is to re-assign the delete buttons since the row index of self.rows
just changed for each row. After executing this function, the behavior that I get is that the last row_number
is always deleted. For instance, if there are 10 rows, the row with index 9
is always deleted.
Replacing the re-assignment of the delete buttons with a more direct reference doesn't appear to change the behavior:
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))
Using print
commands, I have verified that the variable i
is definitely different on each round and that the button
object referenced is a different button each time around, so the command
assigned should also be different. Somehow, it appears that only the last time around the loop is sticking to all o the buttons rather than just the one it was intended for.
Upvotes: 2
Views: 581
Reputation: 55469
jasonharper has explained the cause of your problem: variables in lambda
s aren't "frozen" at the time of creation, they're dynamic.
For further info, please see Why do my lambda functions or nested functions created in a loop all use the last loop value when called? in the SO Python Wiki.
Here's a way to avoid that problem. We can use a default argument in the callback function. Default values are evaluated when the function is defined, not when it's called (it doesn't matter whether the function is a lambda
or a full def
function).
Rather than storing the row index number, which requires the messy re-assignment after row deletion, we can use the row object itself as the argument to the delete_row
method.
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()
Upvotes: 1
Reputation: 9597
for i, row in enumerate(self.rows):
button = row[-1]
button.configure(command=lambda: self.delete_row(i))
When the button is clicked, you delete the row specified by the value of the variable i
at that moment in time. Since this loop will have long since completed before you can possibly click a button, i
will always refer to the last row. This is a common problem with lambdas in a loop, and the solution is to capture the values of all used variables in default parameters: command=lambda i=i: self.delete_row(i))
I think a better approach would be to pass your row
variable to delete_row(), rather than an index - that way, nothing needs to change when a row is deleted. You can iterate for widget in row:
to get the widgets to delete, and get rid of the row itself via self.rows.remove(row)
.
Upvotes: 1