tkinter create clickable labels in for loop [duplicate] - python

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 1 year ago.
I want to create multiple clickable labels in a for-loop. The labels are structured grid-like with a row and col attribute. If I click the label, the row and col of the clicked label should be printed with the print_it() function.
The problem is if I click any label, the output is always the last defined row/col (2,2) in this case. How can I fix it, so the correct row/col gets printed?
Here is a code example to reproduce:
from tkinter import *
def print_it(row, col):
print(row, col)
root = Tk()
sizex = 700
sizey = 400
posx = 0
posy = 0
root.wm_geometry("%dx%d+%d+%d" % (sizex, sizey, posx, posy))
canvas_area = Canvas(root)
canvas_area.grid(row=1, column=1)
labels = []
for row in range(3):
for col in range(3):
labels.append(Label(canvas_area, text=f"Row, Col: {row},{col}", bd=0))
labels[-1].grid(row=row, column=col)
labels[-1].bind(
f"<Button-1>",
lambda e: print_it(row, col),
)
root.mainloop()

I actually found another post which let me solve this problem: Python Tkinter: Bind function to list of variables in a for-loop
The correct definition of the bind function is this:
labels[-1].bind(
"<Button-1>",
lambda event, row=row, col=col: print_it(row, col),
)

Related

tkinter Button command doesn't work properly in iteration [duplicate]

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 1 year ago.
I am making GUI and have some problems on the button command which is used to clear the input value .When i input a value into the 1st entry and try to click the 1st button , the value is not erased which should be cleared, Same as the 2nd button.But for the 3rd button ,it can clear the 3rd input value ,and the 1st,2nd button can also erase the value on 3rd entry.I want the 1st entry to 1st button and can erase data .What's wrong with my code .How should i modify it .Your help is very gratefully appreciated.
Here is my code:
from tkinter import *
root = Tk()
screenwidth = root.winfo_screenwidth()
screenheight = root.winfo_screenheight()
root.title('PN List Box')
root.geometry("500x300+%d+%d"%((screenwidth-400)/2,(screenheight-230)/2-100))
mycolor = '#%02x%02x%02x' % (101, 119, 141)
root.configure(bg=mycolor)
cv = Canvas(root,bg=mycolor)
dict1={}
dict2={}
list1=[]
label_rely=0
for a in range(3):
label_rely+=0.1
list1.append(StringVar())
dict1[a]=Entry(root,textvariable=list1[a]).place(relx=0.27, rely=0.33 + label_rely)
dict2[a]= Button(root,text='clear',command=lambda :list1[a].set('')).place(relx=0.86,rely=0.32+label_rely)
root.mainloop()
sys.exit()
This is a tricky little corner of Python. Remember that your lambda function isn't evaluated until the lambda is actually executed. By the time the lambda is executed, a has the value 2, for all three callbacks. What you need to do is "capture" the loop value, by passing it as a default parameter to the function:
dict2[a]= Button(root,text='clear',command=lambda a=a :list1[a].set('')).place(relx=0.86,rely=0.32+label_rely)

Tkinter List of Variable Sizes

I'm trying to make a menu of buttons using Tkinter. I have a randomly generated list of files with their attributes as tuples (Name, Size, Link), and need to arrange them in a grid. I can't set each button to the grid since the number of buttons changes. Here is how I tried to make it:
def selected_button(file):
print(f"File: {file[0]}, {file[1]}, Link: {file[2]}")
for file in files:
fileButton = Button(root, text= file[0],command= lambda: selected_button(file).grid()`
Problems:
The variable "file" that gets passed on at the button click is always the last generated button's.
The buttons arrange themselves as a long list in one column - I need a nice square/rectangle.
If I left out any info, comment and I'll answer promptly.
Problem #1
To get the lambda to store the current value in the iteration it needs to be called out in the lambda function such as command= lambda f = file: selected_button(f).
Problem #2
The method I would usually use for making a grid of buttons is pick a width you want, possibly 3 wide, then increment the column until it reaches that point. Once that width is reached, reset the column and increment the row.
import tkinter as tk
# Testing
files = []
for x in range(12):
files.append((f"Test{x}", f"stuff{x}", f"other{x}"))
# /Testing
def selected_button(file):
print(f"File: {file[0]}, {file[1]}, Link: {file[2]}")
root = tk.Tk()
r, c = (0,0) # Set the row and column to 0.
c_limit = 3 # Set a limit for how wide the buttons should go.
for file in files:
tk.Button(root,
text= file[0],
width = 20,
# This will store what is currently in file to f
# then assign it to the button command.
command= lambda f=file: selected_button(f)
).grid(row = r, column = c)
c += 1 # increment the column by 1
if c == c_limit:
c = 0 # reset the column to 0
r += 1 # increment the row by 1
root.mainloop()

Tkinter button not getting updated value [duplicate]

This question already has answers here:
Creating functions (or lambdas) in a loop (or comprehension)
(6 answers)
How can I identify buttons, created in a loop?
(6 answers)
Closed 4 years ago.
I'm having a problem where I create a for loop of buttons and I want each one of other to have different column, row and text.
I noticed only the text and the column (because i used lambda on it) are actually the updated values given to the function.
(btw the row and column are for something else, not the button - in a different function).
This is my code, I can't seem to understand how to get the most updated value for a button since all of my buttons are getting the values of the first button except for the text name and their column
for order in self.orders:
if counter2 >= 2 and counter2 % 2 == 0:
x += 1
print x, counter2
bt = Button(window, command=lambda i=counter2: self.display_order(self.orders[order], window, x, i))
bt.configure(text='order ' + str(counter2+1), fg='black', bg='steel blue', width=20)
bt.grid(sticky='W', row=0, column=counter2, columnspan=1) # increase row number for every button
counter2 += 1
x has also a problem with late binding. You've done it right for counter2. Use the following change (by adding x=x as additional lambda argument):
bt = Button(window, command=lambda i=counter2, x=x: self.display_order(self.orders[order], window, x, i))

What is the pythonic way to display a grid?

I am learning Python and I want to display buttons in a grid. The below code produces exactly what I want, but the code for displaying the buttons by incrementing x and y does not seem very Pythonic. (I come from a procedural language background) Is there a better way? Thanks for any help.
from tkinter import *
from tkinter import ttk
root = Tk()
numberButtonsFrame = ttk.Frame(root)
numberButtonsFrame.pack()
button=[0]
for i in range(1,10):
button.append (ttk.Button(numberButtonsFrame, text = i))
x=0
y=0
for i in range(1,10):
button[i].grid(row=x,column=y)
y=y+1
if y>2:
x=x+1
y=0
root.mainloop()
When you're dealing with a grid, the solution is very often to use nested loops:
for row in in range(nrows):
for col in range(ncolumns):
buttons[row][col].grid(row=row, column=col) # You could also calculate a linear index if that's what you want
Single loop
As I noted in a comment (along with another poster), there is a way of calculating the row and column based on i (and vice versa).
row, col = divmod(i, ncolumns)
You could do this at the same time as you create each button. You could also simplify the button creation with a list comprehension.
buttons = [ttk.Button(numberButtonsFrame, text = i) for i in range(1,10)]
I assume you added the 0 at the start of your button(s) list to shift the indices: you don't have to do that. Just add 1 to i in your calculations instead.
Lastly, I would recommend using well-named variables rather than literals (eg. ncolumns). And buttonsinstead of button for the list. I'll conclude with an example (// is floor division - the div in divmod):
for i, button in enumerate(buttons):
button.grid(row=i//ncolumns, column=i%ncolumns)
Use the divmod() function to calculate each row and column from the index.
buttons_per_row = 3
for i in range(9):
button = ttk.Button(numberButtonsFrame, text = i+1)
row, col = divmod(i, buttons_per_row)
button.grid(row=row, column=col)
A different method...
Using two nested loops like others have done, you can calculate the text from the row and column simply by:
(r * 3) + c + 1
Obviously, this will return an int so str() will have to be applied -leading to a more succinct solution of:
from tkinter import *
from tkinter import ttk
root = Tk()
numberButtonsFrame = ttk.Frame(root)
numberButtonsFrame.pack()
for r in range(3):
for c in range(3):
ttk.Button(numberButtonsFrame, text=(r * 3) + c + 1).grid(row=r, column=c)
root.mainloop()

How do you use the same function as the command for multiple buttons created in a loop?

I am creating a tic-tac-toe board of buttons. Each button, when clicked, should switch to an X or an O on an alternating basis. I have created the buttons inside for loops, and would, if possible, like to avoid initializing them each separately.
I have tried passing the button to the function by using lambda, and I have tried eliminating the problem of parameters in commands by calling the buttons self.bttn (as this is all taking place within my modified Frame class). However, in both cases, I come across the problem that, regardless of which of the 9 buttons I click on, the final button created by the for loops responds.
def __build_board(self):
""" Creates tic-tac-toe board of 9 blank buttons. """
for row_num in range(3):
for col_num in range(3):
self.bttn = Button(self, text = " ", command = self.__change_button)
self.bttn.grid(row = row_num + 1, column = col_num, sticky = W)
That's the way that I tried it with self.bttn. self.__change_button merely switches the text within self.bttn to an X or O.
And this is the way I tried it with lambda...
def __build_board(self):
""" Creates tic-tac-toe board of 9 blank buttons. """
for row_num in range(3):
for col_num in range(3):
bttn = Button(self, text = " ", command = lambda: self.__change_button(bttn))
bttn.grid(row = row_num + 1, column = col_num, sticky = W)
I think I understand why these ways of going about it do not work. Seemingly, I have commanded all the buttons to change the text of the final button, but I'm at a loss as to how I should get them to each change their own text the way I want. Any help would be greatly appreciated.
This is a tricky problem...
the reason is that the lambda will be tied to the bttn variable, not to its value and therefore all the buttons will act on the last button (because that will be the value of the variable after the loop).
To see this consider:
x = 1
y = lambda : x
print(y()) # will print 1
x = 2
print(y()) # will print 2; lambda captures variables, not values
A possible work-around is:
def __build_board(self):
""" Creates tic-tac-toe board of 9 blank buttons. """
for row_num in range(3):
for col_num in range(3):
def mkbutton():
b = Button(self, text = " ",
command = lambda: self.__change_button(b))
return b
bttn = mkbutton()
bttn.grid(row = row_num + 1, column = col_num, sticky = W)
this way each lambda will be tied to its own separate variable b.
Normally this kind of issue can be solved more easily with the frequent pattern:
x = lambda y=y: ...
where the value y is copied as default for an optional parameter of the lambda.
In your case however this approach is not possible because at the time the lambda is evaluated the Button instance doesn't exist yet (so you cannot store it as a default parameter value in the lambda itself).

Categories

Resources