Python tkinter button callback unexpected behaviour [duplicate] - python

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.

Related

How do I make a button that allows me to send two variables into the same function in Tkinter?

def openCipher():
cipher = Toplevel()
cipher.title("decryptt - CIPHER")
cipherLabel = Label(cipher, text="cipher").pack()
cipherEntry = Entry(cipher, width=20, borderwidth=5) #separating pack now allows you to use get() on this
cipherEntry.pack()
cipherChoices = [
("Binary","bcipher"),
("Caesar","ccipher"),
("Hexadecimal","hcipher"),
("Atbash","acipher"),
("Letter-to-Number","lcipher")
]
cipherType = StringVar()
cipherType.set("Binary")
for text, cipherChoice in cipherChoices:
Radiobutton(cipher, text=text, variable=cipherType, value=cipherChoice).pack()
cipherButton = Button(cipher, text="Cipher", padx=10, pady=5, command=lambda:[ciphering(cipherEntry.get()), ciphering(cipherChoice.get())]).pack() #lambda allows you to pass arguments to functions
quitButton = Button(cipher, text="Exit Cipher", padx=10, pady=5, command=cipher.destroy).pack()
# This is the function that is suppose to split the input from cipherEntry into individual characters in an array.
def ciphering(entry,choice):
ciphering = Toplevel() #needed to add new label to
cipherLabeling = Label(ciphering, text = "You have inputted " + entry).pack() #couldn’t add a list to string like that, nor use get() on a list, changed to just use the string
seperatedWord = list(entry)
cipherLabeling = Label(ciphering, text = seperatedWord[2]).pack()
seperatedWordLength = len(seperatedWord)
cipherLabeling = Label(ciphering, text = seperatedWordLength).pack()
selection = Label(ciphering, text = choice).pack()
Above is part of the code I have for my ciphering app I am making in Tkinter. Took out the less important parts.
Basically, what is being created in OpenCipher() functions is an entry box that is named cipherEntry. Then there are radio buttons with different names of different ciphers and the value and variable of each radio button is the same as each other for that radio button. Then there is another button that takes whatever cipherEntry is and brings it to another window using the ciphering() function.
What I need to know is how do I also get whatever the value and/or variable of whatever radio button they have selected to that window using the same button they pressed to get to that window ( cipherButton ). Because I want to then use their selection and input to know what cipher type they want their input to be changed to. I already have the function for it sorted.
I have tried using cipherType, cipherChoice, cipherChoices but have no idea how to get them both in there. With the current code above. It works as if there was no second command. It totally disregards whatever selection I put in and the 'selection' label widget doesn't display their choice. I have also made each variable a global to see if that did anything but no luck.
I would really appreciate any assistance :)
First of all, the code should give an error because def ciphering(entry,choice) expects two positional arguments to be passed at the same time. Even after fixing that, it should give another error because cipherChoice is a string(from the list of tuples) and does not have a get attribute.
The thing to focus on here is:
command=lambda: [ciphering(cipherEntry.get()), ciphering(cipherChoice.get())]
When you say something like lambda: [func1(arg1),func1(arg2)] you are set to executing the function func1 and again func1 one after the other(so twice). What you want is to pass multiple arguments to the same function just using a normal lambda without any list, like:
command=lambda: ciphering(cipherEntry.get(), cipherType.get())
Also notice how I changed cipherChoice.get() to cipherType.get(), it is because cipherChoice is a string and also does not have a get attribute, but the value of the radiobutton should be acquired from the associated tkinter variable(StringVar) only. So you have to use cipherType.get()

Label appearing conditionally

I am trying to make a label appear if the condition of my entry (textbox) is met. Unfortunately I cannot see anything when I am pressing the button on the testing. Here is what I have:
from tkinter import *
main= Tk()
firstname=Entry(main).place(x=30, y=50)
def register():
if any(i.isdigit() for i in firstname.get())== True:
print (Label(main,text='no numbers please').place(x=30, y=180))
else:
print(Label(main, text='pass').place(x=40, y=170))
register=Button(main,text='REGISTER', command= lambda :register).place(x=300, y=200)
There are at least three problems with your code. The first is in how you define the button's command:
register=Button(main,text='REGISTER', command= lambda :register)
When you do command=lambda: register, you're telling the button "when you're clicked run the code register". register all by itself does nothing. Since register is (supposed to be) a function, you need to call it like register() inside the lambda.
Since you aren't passing any values to the function, the lambda is completely unnecessary. Instead, just directly reference the function: command=register without the parenthesis.
The second problem is that you've used the name register to be two different things: a function and a reference to a widget. Because of the ordering of the code, command=register or command=lambda: register() will try to call the button rather than the function.
The third problem is a very, very common mistake. In python, when you do x = y().z(), x is given the value of z(). Thus, register = Button(...).pack(...) returns the value of pack(...) and pack (and grid and place) always returns None.
Therefore, you've set register to None, and when you try to call it you get NoneType object is not callable.
In addition to fixing the command, you need to pick a different name for either the function or the button. And you should not be calling place (or pack or grid) in-line with creating the widget. They should be separate steps.
So, putting that all together, you need to define firstname like this so that firstname is not None:
firstname=Entry(main)
firstname.place(x=30, y=50)
And then you need to define the button like this:
register_button = Button(main,text='REGISTER', command= register)
register_button.place(x=300, y=200)

Generate buttons in Tkinter with a different name each time

I want to generate a button for each .py document in a file, and each button give a different argument to the function
for name in files:
if name[-3:]=='.py':
a=name[:-3]
button=Button(barre, text=a, command=python_file(a))
button.pack()
def python_file(name):
text = Text(barre)
text.insert(name)
os.popen("python3 "+a+".py","r")
It basicly write the name of the python file and execute. But, I need to give a different name to each button, just like : button1, button2 (...).
Also when I run my code, It execute the function each time thee is a new python file.
Thx for helping !!
Change the command argument in your button to:
button=Button(barre, text=a, command=lambda a=a: python_file(a))
The argument passing to your function is the one which is the last value in the for loop. To pass the current value, you need to specify it through the lambda function.
Also, change the os.popen method like this to fully execute the code inside the file.
c = os.popen("python3 "+a+".py","r")
print(c.read())

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 Button Commands in For Loop [duplicate]

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

Categories