Declare buttons in a loop in Tkinter (Python3) - python

I have never created a GUI before and I decided to try to create one in Python3, using tkinter.
I would like to create a 5x5 "matrix" of buttons that change color and text when pressed. After some googling, I figure out how to do it for a single button and in principle I could just copy-paste and create the 25 buttons I need. However I'd like to find a more elegant way to do it inside a loop. My problem is that I need to give different names to each button, but I don't know how to do that.
I hope the question is clear enough. Thank you in advance for any help!!

Here is a very simple example on how to do so, by making a list of all the 25 color and then using the conventional matrix looping and assigning items to the buttons, like:
from tkinter import *
root = Tk()
colors = ['Red','Orange','Yellow','Green','Blue','Purple','Brown','Magenta',
'Tan','Cyan','Olive','Maroon','Navy','Aquamarine','Turquoise','Silver',
'Lime','Teal','Indigo','Violet','Pink','Black','White','Gray','crimson']
colors = list(reversed(colors)) # Reversing list bc pop returns last item
def color_changer(btn,color):
btn.config(fg=color) # Change the color of the corresponding button
for i in range(5): # Number of rows
for j in range(5): # Number of column
color = colors.pop() # First color
btn = Button(root,text=color,fg='black',width=25)
btn.grid(row=i,column=j) # Place the widget
btn['command'] = lambda btn=btn,color=color: color_changer(btn,color) # Assign a command
root.mainloop()
There is a caveat here, you should define exactly 25 colors, else, you should use try to catch the IndexError that comes up and ignore it.
How does the function work?:
You are using lambda to create a new nameless function that takes in btn and color and passes that btn and color to the color_changer(). This way we can store corresponding btn and color, unlike if you would normally assign it like lambda: color_changer(btn,color), it is just going to pass the last popped item. This is usually how we assign commands for buttons inside a loop.
Alternative(to lambda):
You can also use a nested function(avoiding lambdas). So the function would be like:
def nester(btn,color):
def color_changer():
btn.config(fg=color)
return color_changer
and the command would be like:
btn['command'] = nester(btn,color)
This is similar to what functools.partial does.

Related

Python/TKinter - How to generate a list of selected widgets by name

I've re-written the question for clarity. I'm open to anything- complete redesign if needed.
I am having a problem and trying to start from the ground up since I can't find any solutions that work.
I have a file with a 75 columns, and each column is a feature. I would like the user to be able to select which features to include in the analysis.
Question 1. What is the best widget to use for this?
First, I tried using a listbox, but it wouldn't let me select non-consecutive items except on the default state which required selecting each individual item (there are 75 items so this would be a lot of click).
Next, I tried using checkboxes. I think this is the best solution, but I'm having problems implementing. Here's what the UI looks like:
I am trying to associate this with a list of 'clicked' boxes that I can then pass to my back-end to remove unwanted variables since the rest of the application is pretty data intensive.
The select all and deselect all buttons work fine; my problem is with the individual selections.
Is this the right way to accomplish this goal at all? If so, how might this be accomplished? TIA- I started using tkinter yesterday so I know very little.
Here is how I'm generating the below (simplified)
Code that creates the button:
import tkinter as tk
settings.data_included_cols = ['button1'] #This is the list of clicked buttons
checkbox_buttons=dict()
checkbox_variables=dict()
button_names=['button1', 'button2', 'button3']
i=0
for i in range(len(button_names)):
checkbox_variables[i]=1
checkbox_button[i] = tk.Checkbutton(frame, text=button_names[i],
variable=checkbox_variables[i],
command=checkbox_click)
Command Code (checkbox_click)- I don't know what goes here but nothing I've tried so far has worked. Originally I was calling it as: command=lambda: checkbox_click(i), and it tried to work like the below:
def checkbox_click(i):
if i in settings.data_included_cols:
settings.data_included_cols.remove(button_name[i])
This doesn't work because 'i' is not associated with the button, it's associated with the loop so it will always be whatever the final value is (in this case 3 which maps to button3).
Any thoughts?
Checkboxes do not need to have a command specified.
They do however need a StringVar or IntVar associated with them, so you can query the values.
So I would build your code like this:
names = {
"id", "member_id", "loan_amnt"
}
values = {}
boxes = {}
for name in names:
values[name] = tk.Intvar(value=1)
boxes[name] = ttk.Checkbox(root, text=name, variable=values[name])
Once the user has made submitted their choice, you can query the dict of values to see which options were selected.
The problem is that although you assign each check-button different "feature" as you create them, when one of the buttons is clicked, the program will execute the function
lambda: feature_list(feature)
where it will try to get the value of "feature", and will find that it equal to the last one in the list.
The seemingly only solution is to assign each check-button a variable, such as something like this:
import tkinter as tk
from tkinter import ttk
def click():
temp = []
for i in range(3):
if value[i].get() == 1:
temp.append(feature[i])
print(temp)
if __name__ == "__main__":
root = tk.Tk()
feature = ["one", "two", "three"]
check = {}
value = {}
for i in range(3):
value[i] = tk.IntVar(value=0)
check[i] = ttk.Checkbutton(root, text=feature[i], variable=value[i], onvalue=1, offvalue=0, command=click)
check[i].grid(row=0, column=i)
root.mainloop()
then when you select some buttons, for example "one" and "three", then it will print
['one', 'three']
You should not be stingy for creating Tk variables, since the space consumed by a check-button should be much bigger than that consumed by a variable. Also, creating variables to get the state of widgets is the most "standard" and "Pythonic" way which you should always use.

How to create widget list in python?

I want to add buttons to list, I know there is widget list and I already read is somewhere but I forgot where. I can't find any helpful posts on this question.
I want to change button's background in for loop, when some action happens, like mouse hovering.
I'm using python 3.10.4
Thanks for any help!
Step one: create an empty list
widget_list = []
Step two: add widgets to the list
for i in range(10):
button = tk.Button(root, text=f"Button #{i+1}", command=reset)
widget_list.append(button)
Step three: loop over the list to change the background:
for widget in widget_list:
widget.configure(background="red")
If the code in these steps are in separate functions, you'll need to declare widget_list as global variable or an instance variable.

receiving selected choice on Tkinter listbox

I'm trying to build a listbox using Tkinter and receive the selected option by clicking it.
import Tkinter as tk
from Tkinter import *
root = tk.Tk()
lst=Listbox(root, height=30, width=50)
lst.insert(1, "hy")
lst.insert(2, "hello")
lst.insert(3, "hey")
lst.pack()
sel = lst.curselection()
print sel
root.mainloop()
However, when I run the code it prints me an empty tuple before I pressed any choise.
Does someone know how to get the selected choise after I press one and not right after I run it?
Thanks a lot :)
You are getting the selection about a millisecond after creating the widget, well before the user has a chance to see the UI much less interact with it.
GUI programs are event based, meaning that things happen in response to events. Events are things like clicking buttons, inserting data into input widgets, and selecting items from listboxes.
You need to do one of two things: create a button or other widget which will get the selected item, or configure it so that a function is called whenever an item is selected.
No matter which solution you use, you will need a function that ultimately calls the curselection method of the listbox to get a list of indices. You can then call the get method to get the selected item or items.
Here's a function definition that will print the selected item, or print "no selection" if nothing is selected. So that it can be resused without modification. we'll define it to take the listbox as an argument.
Note: this example assumes the widget only supports a single select, to keep it simple:
def print_selection(listbox):
selection = listbox.curselection()
if selection:
print(f"selected item: {listbox.get(selection[0])}")
else:
print("nothing is selected")
Using a button
To call this from a button is straight-forward. We just create a button after we create the listbox, and use the command attribute to call the function. Since the function we wrote earlier needs a parameter, we'll use lambda to create a temporary function for the button.
button = tk.Button(root, text="Print Selected Item", command=lambda: print_selection(lst))
button.pack()
Calling the function when the selection is made
To call the function whenever the user changes the selection, we can bind a function to the <<ListboxSelect>> event. We'll create a separate function for this, and then pull the widget from the event object that is automatically passed to the function.
def print_callback(event):
print_selection(event.widget)
lst.bind("<<ListboxSelect>>", print_callback)
First of all, the reason you are getting an empty tuple is because you have executed the statements:
sel = lst.curselection()
print(sel)
before you have executed the root.mainloop()
Secondly, your setup for listbox fails to include a StringVar variable to hold your list.
Once the variable has been defined, you should be able to use the .insert statements to add your list items one at a time, or you can initialize the StringVar variable using a .set('hy', 'hello', 'hey') command.
To provide a return of a selected variable, you must incorporate an event handler to determine the list position selected onclick or some other triggering method.
For a pretty clear explanation of these characteristics check here

How to change label (image form) with next button python

I am currently a novice in python and I'm trying to make a label switch from one image to another by clicking a next button. Here's my code:
from tkinter import *
def next1():
global slide
slide=1
if slide==1:
bglabel.config(image=bg1)
elif slide==2:
bglabel.config(image=bg2)
slide+=1
window.update()
window=Tk()
window.geometry("1500x750+0+0")
bg1=PhotoImage(file="backslide1.png")
bg2=PhotoImage(file="backslide2.png")
nextbutton=PhotoImage(file="next.png")
bglabel=Label(window, image=bg1)
bglabel.place(x=600,y=200)
nextbutton1=Button(window, image=nextbutton, bd=0, command=next1())
window.bind('<Button-1>', next1())
I sat for a good hour or so trying to tamper with the slide variable (trying to declare it before def, removing global, changing value, changing where slide+=1 is, etc) but one of two things always happens; either it's stuck on bg1 with the button clicking but doing nothing, or jumping straight to bg2. I've also tried splitting next1 into two different def's, one for variable tracking, one for switching bglabel, but still the same output. Please help.
(Also, will that window.bind be trouble as I continue to add buttons? If so please let me know how to do it correctly.)
As you mentioned, one 'error' that occurs is that the image immediately jumps to image bg2. This is the line causing that:
nextbutton1=Button(window, image=nextbutton, bd=0, command=next1())
More specifically, where you declare the command associated with the button:
command=next1()
With the enclosed brackets, you're calling the function next1 i.e. as soon as the button is created, run the specified function.
To solve this, just remove the pair of brackets:
nextbutton1=Button(window, image=nextbutton, bd=0, command=next1)
The same goes for your key binding. This way, the button/key now has a reference to the function - it knows what function to run and will run it when the specified action is performed.
More about the key binding...
When you use bind to assign a key to run a function, whatever function that is to be run needs to be made aware as such. Currently, the next function you are trying to bind is given no indication that it can be called using a keyboard button event. To fix that, we set a default parameter in next specifying the event:
def next1(event=None):
#rest of function code here
window.bind('<Button-1>', lambda event: next(event))
Setting a default parameter, event=None, basically means if no value forevent was passed to the function from whatever called it, set it to None by default (in that sense, you can choose to set it to whatever by default). Using lambda for the key bind in this way allows us to pass parameters to functions. We specify what parameter(s) we want to pass to the function and then specify the function, with the parameter(s) enclosed in brackets.
You need to provide the function, not the result of the function. So no parenthesis. Like this:
nextbutton1=Button(window, image=nextbutton, bd=0, command=next1)
Also remove the window.bind line, and your loop logic is broken. "slide" is always 1 since you set that in the function. Are you trying to cycle between the 2 images with every click? If so use itertools.cycle:
from tkinter import *
from itertools import cycle
def next1():
bglabel.config(image=next(bgimages))
window=Tk()
window.geometry("1500x750+0+0")
bg1=PhotoImage(file="backslide1.png")
bg2=PhotoImage(file="backslide2.png")
bgimages = cycle([bg1, bg2])
nextbutton=PhotoImage(file="next.png")
bglabel=Label(window)
bglabel.place(x=600,y=200)
next1() # set the first image
nextbutton1=Button(window, image=nextbutton, bd=0, command=next1)
nextbutton1.pack()
window.mainloop()
(totally untested since i don't have your images).

Tkinter: Update frequency of Listbox

Is it possible to control the update frequency of the Listbox widget? Right now I do a lot of insert and delete operations at a high frequency and the Listbox doesn’t refresh very well. Maybe there is a way to override some draw function of the Listbox to fix this issue?
I am not able to find a way to disable visual updates of your listbox so I had to build a work around. If someone knows if you can disable the visual update of listbox please let me know.
My workaround will involve a list and 2 functions.
My first function will take the data that is going to be added to the listbox and instead add it to a list. This function simply simulates new values being added faster than what we want to update for a good visual on the method. You can adapt this code to yours to see how it will work with your inserts.
My second function will run once a second and take all the new values of this list and add them to the listbox by index.
This is a simple example but it should be a good starting point for you.
import tkinter as tk
root = tk.Tk()
add_tracker = 1
new_lb_items = []
lb = tk.Listbox(root)
lb.pack()
def add_to_listbox():
global add_tracker, new_lb_items, root
new_lb_items.append([add_tracker, "Number {}".format(add_tracker)])
add_tracker += 1
root.after(250, add_to_listbox)
def update_listbox_display():
global lb, new_lb_items, root
for item in new_lb_items:
lb.insert(item[0], item[1])
new_lb_items = [] # resets the list so only new values are added next time.
root.after(1000, update_listbox_display)
add_to_listbox()
update_listbox_display()
root.mainloop()

Categories