Iterating through buttons in tkinter [duplicate] - python

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 2 years ago.
I try to create buttons in tkinter using a loop.
def bulkbuttons():
i = 0
x = 0
buttons=[]
while x < 10:
buttons.append('button' + str(x))
x+=1
while i < 10:
buttons[i]=Button(window,text=str(i), width=12,borderwidth=1, relief='raised', bg='#134f5c', fg='#FFFFFF', command=(lambda: justprint(i)))
buttons[i].grid(row=i, column=2)
i += 1
The justprint function prints i.
def justprint(i):
print(i)
The buttons show the correct numbers. But the justprint function only prints the last number, 10, no matter which button I click.
What did I do wrong? I want to click a button and then use the number of the button as a parameter for some functions.

You can pass i as a default argument in the lambda function. Alternatively you can use the partial function from functools:
from functools import partial
def bulkbuttons():
# Put all formatting related parameters of the Button constructor in a dictionary
# Using ** will pass dict values to
formatting = {'width': 12,
'borderwidth': 1,
'relief': 'raised',
'bg': '#134f5c',
'fg': '#FFFFFF'}
for i in range(10):
buttons[i]=Button(window,
text=str(i),
**formating,
command=partial(justprint,i))
buttons[i].grid(row=i, column=2)
Notes:
Your first while loop can be expressed as this pretty and concise list comprehesion:
buttons = ['button' + str(x) for x in range(10)]
Try using this notation when possible, since they will save you typing time and is far more readable.
Your second while loop overwrites the list created in the first one, so there is no need to do the first while loop.
I took the liberty of placing all your formating related parameters for the Button constructor in a dictionary, you can pass them all at once passing **formating to the constructor. You can place these parameters in your global scope (outside functions) and define models for every type of button, saving you some time and making the code more readable.
If you have a fixed number of iteration is a loop, use for i in range(n) instead of a while, it will avoid some infinite loops when you forget the i+=1

Related

How to make variables and functions that are attached on every single widget in for-loop in python?

I want to create 2D minecraft test game. Got page with textures and so on... And then, I realized I need to create 25*25 map!
In this code below you will see, that I created 5 functions and 5 labels in for-loop and if you are good at this language you will see I created block breaking animation. I'm going to care about holding animation after(if you know how to create holding animation please let me know)...
Code you can test:
from tkinter import *
tk=Tk()
tk.title('Minecraft 2D')
tk.config(bg='lightblue')
tk.resizable(False, False)
app_width=1500
app_height=750
screen_width=tk.winfo_screenwidth()
screen_height=tk.winfo_screenheight()
x=(screen_width/2)-(app_width/2)
y=(screen_height/2)-(app_height/2)
tk.geometry(f'{app_width}x{app_height}+{int(x)}+{int(y)}')
DestroyCounter=0
for i in range(10, 15):
def Destroy1():
global DestroyCounter
if DestroyCounter==0:
DestroyCounter=1
GrassBlockSprite.config(bg='gray30')
elif DestroyCounter==1:
DestroyCounter=2
GrassBlockSprite.config(bg='gray26')
elif DestroyCounter==2:
DestroyCounter=3
GrassBlockSprite.config(bg='gray22')
elif DestroyCounter==3:
DestroyCounter=4
GrassBlockSprite.config(bg='gray18')
elif DestroyCounter==4:
DestroyCounter=5
GrassBlockSprite.place_forget()
GrassBlockSprite=Canvas(tk, width=48, height=48, bg='brown', bd=0)
GrassBlockSprite.place(relx=((i+0.2)/31.5), rely=0.305)
GrassBlockSprite.bind('<Button-1>', lambda e:Destroy1())
tk.mainloop()
There just last block accepts animation, not other ones.
How to make this animation for other blocks? How to make variables and functions that will not overlap such as a loop in a thread? Do I need to create threads? But how to create them for every block in for loop? You can answer with just these 5 blocks in code. You can fix code too if you want.
Consider this as a replacement:
animation = ['gray30','gray26','gray22','gray18']
DestroyCounter=[]
GrassBlockSprite=[]
def Destroy(n):
if DestroyCounter[n] < len(animation):
GrassBlockSprite[n].config(bg=animation[DestroyCounter[n]])
else:
GrassBlockSprite[n].place_forget()
DestroyCounter[n] += 1
for i in range(5):
gbs=Canvas(tk, width=48, height=48, bg='brown', bd=0)
gbs.place(relx=((i+10.2)/31.5), rely=0.305)
gbs.bind('<Button-1>', lambda e, i=i:Destroy(i))
GrassBlockSprite.append(gbs)
DestroyCounter.append(0)

Why I can't loop a variable inside a list as a *args? [duplicate]

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 6 months ago.
(As the 'homework' tag indicates, this is part of a big project in Computer Science.)
I am writing a Jeopardy! simulation in Python with tkinter, and I'm having a big problem regarding the use of the lambda function in buttons. Assume root = Tk() and categories is a list.
# Variable to keep the buttons
root._buttons = {}
# Display headers on top of page
for i in range(5):
# Get category name for display in main window
name = categories[i]
b = Label(root, text=fill(name.upper(), 10), width=18, height=3,\
bg="darkblue", fg="white", font=("Helvetica bold", "", 11))
b.grid(row=0, column=i)
# Create list of buttons in that variable (root._buttons)
btnlist = [None]*5
# Display individual questions
for j in range(5):
# Make a button for the question
b = Button(root, text="$" + str(200 * (j+1)), width=8, height=1,
bg="darkblue", fg="orange", font=("Impact", "", 30))
b.cat = name
b.value = 200 * (j + 1)
b.sel = lambda: select(b.cat, b.value)
# Add callback event to button
print(b.cat, b.value, b.sel)
b.config(command=b.sel)
# Add button to window
b.grid(row=j+1, column=i)
# Append to list
btnlist[j] = b
root._buttons[categories[i]] = btnlist
For all of the code, see my little Code Viewer (under construction!)
It's at lambda: select(b.cat, b.value) where the problem seems to occur, because when I click any button on the board, it always goes to the one last button on the board. I've tried other approaches, unfortunately all using lambda, and I have not seen any approach that does not involve lambda.
Change
lambda: select(b.cat, b.value)
to
lambda b = b: select(b.cat, b.value)
In your original code, b is not a local variable of the lambda; it is found in enclosing scope. Once the for-loop is completed, b retains it last value. That is why the lambda functions all use the last button.
If you define the lambda to take one argument with a default value, the default value is determined (and fixed) at the time the lambda is defined. Now b is a local variable of the lambda, and when the lambda is called with no arguments, Python sets b to the default value which happily is set to various different buttons as desired.
It would let you be more expressive if you replaced the lambda expression with a function factory. (presuming that you're going to call this multiple times). That way you can do assignments, add more complicated logic, etc later on without having to deal with the limitations of lambda.
For example:
def button_factory(b):
def bsel():
""" button associated with question"""
return select(b.cat, b.value)
return bsel
Given an input b, button_factory returns a function callable with () that returns exactly what you want. The only difference is that you can do assignments, etc.
Even though it may take up more lines of code initially, it gives you greater flexibility later on. (for example, you could attach a counter to bsel and be able to count how many times a particular question was selected, etc).
It also aids introspection, as you could make each docstring clearly identify which question it is associated with, etc.

How do I make a tkinter button in an list of buttons return its index? [duplicate]

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 4 years ago.
I am new to coding in general (Python is my first language) and I ran into this problem while learning tkinter. My code is as follows:
from tkinter import *
window = Tk()
buttons = []
def position(pos):
print(pos)
for i in range(7):
buttons.append(Button(window, width = 10, height = 5, bg = "red", command = position(i)).grid(row = 0, column = i))
window.mainloop()
This does not work. I want to print the index of the button when that button is clicked. I have tried a few methods to accomplish this, but with no success. The buttons do not necessarily have to be in a list, however the first button must return 0, the second return 1, the third 2 etc. What is the simplest way to do this?
See this:
from tkinter import *
root = Tk()
files = [] #creates list to replace your actual inputs for troubleshooting purposes
btn = [] #creates list to store the buttons ins
for i in range(50): #this just popultes a list as a replacement for your actual inputs for troubleshooting purposes
files.append("Button"+str(i))
for i in range(len(files)): #this says for *counter* in *however many elements there are in the list files*
#the below line creates a button and stores it in an array we can call later, it will print the value of it's own text by referencing itself from the list that the buttons are stored in
btn.append(Button(root, text=files[i], command=lambda c=i: print(btn[c].cget("text"))))
btn[i].pack() #this packs the buttons
root.mainloop()
Taken from: How can I get the button id when it is clicked?

Assigning functions to buttons created in a for loop in Tkinter [duplicate]

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 5 years ago.
I have a for loop which creates an amount of buttons (in tkinter) based on the length of my list, compkeys. When I make each button, it is given a previously made function which takes one input. I am trying to make the input of the function specific to the iteration of the for loop. For example, the first button that is created in the loop should have the first item in the list comp keys as input in its function.
However, each button is only receiving an input of the final value of x, instead of the value of x depending on how many times the loop has repeated. Thank you for any and all help:)
import tkinter
compkeys = [2017onsc, 2017onwat]
for x in range(len(compKeys)):
compButton = Button(root, text = compKeys[x], command=lambda: compBuDef(compKeys[x]))
compButton.place(x=x * 100 + 200, y=300)
You must pass the parameter through the lambda function:
for x in range(len(compKeys)):
compButton = Button(root, text=compKeys[x], command=lambda z=compKeys[x]: compBuDef(z))
compButton.place(x=x*100+200, y=300)
or better, iterating over elements:
for idx, ckey in enumerate(compKeys):
compButton = Button(root, text=ckey, command=lambda z=ckey: compBuDef(z))
compButton.place(x=idx*100+200, y=300)

Tkinter lambda function [duplicate]

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 6 months ago.
(As the 'homework' tag indicates, this is part of a big project in Computer Science.)
I am writing a Jeopardy! simulation in Python with tkinter, and I'm having a big problem regarding the use of the lambda function in buttons. Assume root = Tk() and categories is a list.
# Variable to keep the buttons
root._buttons = {}
# Display headers on top of page
for i in range(5):
# Get category name for display in main window
name = categories[i]
b = Label(root, text=fill(name.upper(), 10), width=18, height=3,\
bg="darkblue", fg="white", font=("Helvetica bold", "", 11))
b.grid(row=0, column=i)
# Create list of buttons in that variable (root._buttons)
btnlist = [None]*5
# Display individual questions
for j in range(5):
# Make a button for the question
b = Button(root, text="$" + str(200 * (j+1)), width=8, height=1,
bg="darkblue", fg="orange", font=("Impact", "", 30))
b.cat = name
b.value = 200 * (j + 1)
b.sel = lambda: select(b.cat, b.value)
# Add callback event to button
print(b.cat, b.value, b.sel)
b.config(command=b.sel)
# Add button to window
b.grid(row=j+1, column=i)
# Append to list
btnlist[j] = b
root._buttons[categories[i]] = btnlist
For all of the code, see my little Code Viewer (under construction!)
It's at lambda: select(b.cat, b.value) where the problem seems to occur, because when I click any button on the board, it always goes to the one last button on the board. I've tried other approaches, unfortunately all using lambda, and I have not seen any approach that does not involve lambda.
Change
lambda: select(b.cat, b.value)
to
lambda b = b: select(b.cat, b.value)
In your original code, b is not a local variable of the lambda; it is found in enclosing scope. Once the for-loop is completed, b retains it last value. That is why the lambda functions all use the last button.
If you define the lambda to take one argument with a default value, the default value is determined (and fixed) at the time the lambda is defined. Now b is a local variable of the lambda, and when the lambda is called with no arguments, Python sets b to the default value which happily is set to various different buttons as desired.
It would let you be more expressive if you replaced the lambda expression with a function factory. (presuming that you're going to call this multiple times). That way you can do assignments, add more complicated logic, etc later on without having to deal with the limitations of lambda.
For example:
def button_factory(b):
def bsel():
""" button associated with question"""
return select(b.cat, b.value)
return bsel
Given an input b, button_factory returns a function callable with () that returns exactly what you want. The only difference is that you can do assignments, etc.
Even though it may take up more lines of code initially, it gives you greater flexibility later on. (for example, you could attach a counter to bsel and be able to count how many times a particular question was selected, etc).
It also aids introspection, as you could make each docstring clearly identify which question it is associated with, etc.

Categories