Bokeh: Handling callbacks for unknown (at compile-time) number of buttons - python

I've created multiple buttons at runtime and stored them in a list.
keys = []
keys.append(Button(label="-- Parent --"))
for key in node_obj.children.keys():
keys.append(Button(label=key))
Note that the number of children of node_obj may vary, so the number of buttons is not always the same. I'm trying to create callbacks for all the buttons and did it like this:
def test_fn(button):
print(button.label)
for button in keys:
button.on_click(lambda : test_fn(button))
but it always prints the label of the last button in the list. How can I modify it such that the label of the button that was clicked is printed?

This is a result of the way Python works. When the lambda is actually executed it uses the value of button from the outer scope—which is the last value of the loop. You will need to use the standard library functools.partial function to "bake in" each different button ahead of time:
from functools import partial
def test_fn(button):
print(button.label)
for button in keys:
button.on_click(partial(test_fn, button=button))

Related

Declare buttons in a loop in Tkinter (Python3)

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.

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

Tkinter - Selecting an item from a Treeview using single click instead of double click (Callback on Treeview item selection)

When you want to select an item in a Treeview, you usually use the double-click:
def print_element(event):
print(my_treeview.selection()[0])
my_treeview.bind("<Double-1>", print_element)
Today I tried to do the same but using a single click instead:
my_treeview.bind("<Button-1>", print_element)
But it wouldn't work. The output was just an empty tuple.
I started to search online for an explanation... why is it not working?
EDIT:
My goal was actually to do something every time a treeview item was selected.
I proposed a solution myself using the identify() function of Tkinter
Another user proposed to use the Tkinter callback <ButtonRelease-1> which is much more appropriate
Finally, a third user focused his answer on using the Tkinter callback <<TreeviewSelect>>, which is for sure the best option
The reason it doesn't work the way you expect is because your custom single-click binding happens before the default behavior. So, when your single-click is processed, that happens before an item is selected. The second time you click, your function will print the previously selected item.
If you want to have a function called when an item is selected, you should bind to <<TreeviewSelect>>, which will fire immediately after the user selects an item with a single click or via the keyboard.
The default behavior of a treeview supports selecting multiple items at once, so the following code will print out the text of all of the selected items as a list, even if only a single item is selected. You can, of course, modify this to only print out the first selected item if you so desire.
def print_element(event):
tree = event.widget
selection = [tree.item(item)["text"] for item in tree.selection()]
print("selected items:", selection)
tree.bind("<<TreeviewSelect>>", print_element)
It is because the selection is not set yet when the <Button-1> (it is the same as <ButtonPress-1>, i.e. when the mouse button 1 is pressed and not released) event callback is called.
You should bind on <ButtonRelease-1> or <<TreeviewSelect>> instead as the selection is set when the event callback is being executed.
Why it doesn't work
When you click an item in a treeview, that item is still not in a SELECTED status in the moment the callback is activated. You are changing the status in that very moment.
Using a double-click, the first click change the status, and at the second click you are activating your callback, so the status has already been changed.
How can it work
Kudos to this website
In short,
def print_element(event):
print(my_treeview.identify('item', e.x, e.y))
my_treeview.bind("<Button-1>", print_element)
This time, print_element() will check the coordinates of the mouse, and will discover the selected item check what is under the mouse. Nice and clean!

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).

Partial parameters are duplicated

I am creating on_press callbacks for button behavior objects inside a loop and for some reason all of the parameters for the partial stay the same as the parameters for the last partial created.
shortened example of creation:
(button is a class that implements button behaviour)
for button in list:
button.on_press=partial(my_func, button, button.arg1, button.arg2)
the problem is that the arguments arg1 and arg2 stay the same as the last iteration of the loop instead of being updated. so when I press the buttons (doesn't matter which one) the callback is called as if I pressed the last button.
why does this happen / how can I fix this?

Categories