Tkinter Button Commands in For Loop [duplicate] - python

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 6 years ago.
I am using python 3 with tkinter and I am having issues with a command I want to execute from a button. A variable number of buttons are generated, one for each visitor, and I am trying to call the function signOut from the button press whilst passing the relevent item (visitor) from the list to it.
I realise that the issue is with the for loop as by the time the button is pressed, i will == the last item in the list. How can I make it specific to the actual visitor. I can't seem to think of the solution. Any advice is appreciated.
buttonDictionary = {}
for i in range(0,len(currentVisitors)):
buttonDictionary[i] = Button(bottomFrame, text=currentVisitors[i], command=lambda: signOut(topFrame, bottomFrame, currentVisitors[i]))
buttonDictionary[i].pack()

It is my understanding that e.g. i in a lambda within a loop like this is referring to the variable i itself, not the value of the variable on each iteration, so that when the command callback is called, it will use the value of i at that moment, which as you noticed is the value on the last iteration.
One way to solve this is with a partial. partial will, in effect "freeze" the arguments at their current state in the loop and use those when calling the callback.
Try using a partial instead of a lambda like this:
from functools import partial
buttonDictionary = {}
for i in range(0,len(currentVisitors)):
buttonDictionary[i] = Button(bottomFrame, text=currentVisitors[i], command=partial(signOut, topFrame, bottomFrame, currentVisitors[i]))
buttonDictionary[i].pack()
Another way I have seen this done, but haven't tried, is to assign i to a new variable in your lambda each time:
command=lambda i=i: signOut(topFrame, bottomFrame, currentVisitors[i])
I have gotten burned bad more than once when I first started with Python by using lambdas in loops (including a case very similar to this trying to assign a callback to dynamically generated buttons in a loop). I even created a snippet that would expand to think_about_it('are you sure you want to use a lambda?') whenever I typed lambda just to remind me of the pain I caused myself with that...

Related

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

Why is only the last element of a list being accessed? [duplicate]

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 5 years ago.
I am attempting to make a turn-based combat system, and I ran into a problem while trying to get buttons to access elements of a list relatively (the button in a list of buttons accesses an element of the same index in another list). However when clicked, each button only accesses the last element of the list. Here is simplified sample code that exhibits the same problem:
from tkinter import *
root = Tk()
my_list = ['index 0', 'index 1', 'index 2']
buttons = []
for i in range(len(my_list)):
buttons.append(Button(root, text='Button '+str(i), command=lambda: print(my_list[i])))
buttons[i].pack()
I understand that since I am doing this in a loop, the last element is probably being accessed by all buttons because it is the most recent value of i, but I can't figure out another way to accomplish my goal. To be more specific and draw from the example code above, my_list represents a list of actions that a character can take. Each button in buttons corresponds to one of those actions (clicking the button sets the action you take at the end of the turn). Normally, I could probably assign the actions more explicitly, but since there are multiple fighters I opted to use a loop and do it implicitly since my_list changes frequently to reflect the actions of each different fighter.
I am not very clear on why this is happening, and I would also like to know how I can accomplish my goal of allowing each button to access its corresponding element in my_list since it probably cannot be done in this way. Thank you for reading and let me know if I can clarify anything for you.
Use functools.partial instead of lambda:
from functools import partial
# ...
buttons.append(Button(root, text='Button '+str(i), command=partial(print, my_list[i])))

Capture variable from for-loop for using later in QPushButton [duplicate]

This question already has answers here:
Using lambda expression to connect slots in pyqt
(4 answers)
Closed 5 years ago.
Disclaimer: I've read other questions like this already (eg. this one) but didn't find a working solution for me yet (or I just don't understand them :))
When I create a lambda inside a for loop accessing data from the scope of the block I get a pylint warning (cell-var-from-loop) because of the way Python captures work. E.g:
for key, value in data.items():
button = QtGui.QPushButton('show data')
button.clicked.connect(lambda: show_data(value))
table_widget.setCellWidget(1, 1, button)
There are more questions like this but I still don't now how I systematically solve this issue. I tried to provide default values to the lambda like suggested here:
for key, value in data.items():
button = QtGui.QPushButton('show data')
button.clicked.connect(lambda v=value: show_data(v))
table_widget.setCellWidget(1, 1, button)
But when I do it like this weird things happen - while value ought to be a string in my example show_data is being called with a bool.
Am I doing something totally wrong? Should this approach work?
The clicked signal sends a checked parameter. So try:
button.clicked.connect(lambda checked, v=value: show_data(v))

Python 3.5.1, Tkinter: Functions execute on start instead of button click [duplicate]

This question already has answers here:
Why is my Button's command executed immediately when I create the Button, and not when I click it? [duplicate]
(5 answers)
Closed 6 months ago.
So, I'm creating a client manager software for a local club.
I'm using Python 3.5.1 and Tkinter.
Used a Notebook to nest my Frames.
On my first frame I made the form to add new clients (labels and textboxes) and an "add" button at the end.
Problem is that it executes the function associated with the button insted of onclick, and the button actually does nothing on click.
Been searching everywhere and it seems a rare problem.
Help?
From what I could decipher, as stated in comments your not setting the command properly.
If you have a function you need to set my_button = tk.Button(..., command = my_function)
If your function takes a keyword argument then you need to pass the function like so
my_button = tk.Button(...., command = lambda: function(argument))
I would try using lambda: before the command.
For instance, replace readFile(file) with lambda: readFile(file).
This will ensure an anonymous ("lambda") function with no parameters is passed, which upon execution will run the intended code. Otherwise, the function is executed once when the behavior is set, then the returned value is simply re-evaluated every time rather than the appropriate function being called.

Python tkinter button callback unexpected behaviour [duplicate]

This question already has answers here:
Creating functions (or lambdas) in a loop (or comprehension)
(6 answers)
Closed 6 months ago.
I have made a simple "program launcher" in Python. I have a tab delimited text file, with, at the moment, just:
notepad c:\windows\notepad.exe
write c:\windows\write.exe
The program reads the textfile and creates an array of objects. Each object has a name property (e.g. notepad) and a route property (e.g. C:\windows\notepad.exe). Then, for each object, a button should be made with the correct name on the button, and clicking the button should execute the correct program using the route.
The program very nearly works. Indeed, the array of objects is formed correctly, because the for loop correctly prints out two different program names, and two different routes. The problem is that both buttons, although labeled correctly, launch the write program ! I believe the problem is arising somewhere in the callback, but my Python knowledge just isn't developed enough to solve this! As you can see from my code below, I have tried an "inline" callback, and with a "runprog" function defined. They both give the same outcome.
Your help would be appreciated.
import Tkinter as tk
import subprocess
class MyClass:
def __init__(self, thename,theroute):
self.thename=thename
self.theroute=theroute
myprogs = []
myfile = open('progs.txt', 'r')
for line in myfile:
segmentedLine = line.split("\t")
myprogs.append(MyClass(segmentedLine[0],segmentedLine[1]))
myfile.close()
def runprog(progroute):
print(progroute)
subprocess.call([progroute])
root = tk.Tk()
button_list=[]
for prog in myprogs:
print(prog.thename)
print(prog.theroute)
button_list.append(tk.Button(root, text=prog.thename, bg='red', command=lambda: runprog(prog.theroute)))
# button_list.append(tk.Button(root, text=prog.thename, bg='red', command= lambda: subprocess.call(prog.theroute)))
# show buttons
for button in button_list:
button.pack(side='left', padx=10)
root.mainloop()
Change your command to look like this:
tk.Button(..., command=lambda route=prog.theroute: runprog(route))
Notice how the lambda has a keyword argument where you set the default value to the route you want to associate with this button. By giving the keyword arg a default value, you are "binding" this value to this specific lambda.
Another option is to use functools.partial, which many people find a little less intimidating than lambda. With this, your button would look like this:
import functools
...
tk.Button(..., command=functools.partial(runprog,route)
A third option is to move the "runprog" function to the class instead of in the main part of your program. In that case the problem becomes much simpler because each button is tied specifically to a unique object.
tk.Button(..., command=prog.runprog)
Just change this line:
button_list.append(tk.Button(root, text=prog.thename, bg='red', command=lambda: runprog(prog.theroute)))
to:
button_list.append(tk.Button(root, text=prog.thename, bg='red',
command= (lambda route:(lambda: runprog(route))) (prog.theroute)))
Reasoning: when you create a lambda function (or any other function within a function), it does have access (in Python 2, read-only access) to the variables in the outer function scope. However, it does access the "live" variable in that scope - when the lambda is called, the value retrieved from "prog" will be whatever "prog" means at that time, which in this case will be the last "prog" on your list (since the user will only click a button long after the whole interface is built)
This change introduces an intermediary scope - another function body into which the current "prog" value is passed - and prog.theroute is assigned to the "route" variable in the moment the expression is run. That is done once for each program in the list. The inner lambda which is the actual callback does use the "route" variable in the intermediate scope - which holds a different value for each pass of the loop.

Categories