Passing value to button click event - python-3.x

The code below creates 10 buttons. When user clicks the button, we need to know which button was clicked.
import tkinter as t
def click_button(number):
print("you clicked %d" % number)
root = t.Tk()
for number in range(0, 10):
btn = t.Button(root, text=number, padx=15, command=lambda: click_button(number))
btn.grid(row=0, column=number)
root.mainloop()
When I click any button, I always get 9. Why is that?
Is there a better/alternative way to address this problem?

It appears that the closure over number is by reference given a simple loop test, however I'm not sure this is correct behavior.
This works using partial which I have no doubt captures the value of number at the time of constructing the partial:
import functools as fc
import tkinter as t
def click_button(number):
print("you clicked %d" % number)
root = t.Tk()
for number in range(0, 10):
btn = t.Button(
root,
text=number,
padx=15,
command=fc.partial(click_button, number))
btn.grid(row=20, column=number)
root.mainloop()

You need to pass number as a lambda parameter.
Try:
command=lambda param = number : click_button(param)
Some clarification about the way lambda function "captures" local variable can be found here:
What do (lambda) function closures capture?
and here:
https://docs.python.org/3/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result
In a nutshell, by passing number as a parameter to the lambda, you ensure a copy of number will be performed for iterated number value. If you do not pass it as parameter, all lambda functions "number" point to the same variable ( loop number) which is evaluated only when the lambda function is called. At the time it is called number value is "9". This is very different from C++ where the scope of a "for loop" variable is the loop itself. In python the variable of the loop exists as long as someone (here the lambdas) references it.

Related

Run function when an item is selected from a dropdown

I have a dropdown in tkinter, that i have populated with some items.
OPTIONS = [0,1,2,3,4,5,6,7]
clicked = tk.StringVar()
clicked.set(OPTIONS[0]) # default value
drop = tk.OptionMenu(frame2, clicked, *OPTIONS)
drop.place(relx = 0.65, rely=0.25, relwidth=0.08, relheight=0.6)
However, when a user selects a value, i want other things to happen as well.
Like returning the value to a global variable, or making the state of a button normal, so it's visible again.
How can i run a function, when an item is selected, or when a different item is selected?
EDIT:
Following the suggestions of TheLizzard, i changed my code to this:
# this function is triggered, when a value is selected from the dropdown
def dropdown_selection():
global dropdown_value
dropdown_value = clicked.get()
print("You changed the selection. The new selection is %s." % dropdown_value)
button_single['state'] = 'normal'
OPTIONS = list(range(8))
clicked = tk.StringVar(master=frame2)
clicked.set(OPTIONS[0])
clicked.trace("w", dropdown_selection)
drop = tk.OptionMenu(frame2, clicked, *OPTIONS)
drop.place(relx = 0.65, rely=0.25, relwidth=0.08, relheight=0.6)
However, i get this error:
TypeError: dropdown_selection() takes 0 positional arguments but 3 were given
Try this:
import tkinter as tk
def changed(*args):
print("You changed the selection. The new selection is %s." % clicked.get())
root = tk.Tk()
OPTIONS = list(range(8))
clicked = tk.StringVar(master=root) # Always pass the `master` keyword argument
clicked.set(OPTIONS[0]) # default value
clicked.trace("w", changed)
drop = tk.OptionMenu(root, clicked, *OPTIONS)
drop.pack()
root.mainloop()
In tkinter you can add callbacks to variables like StringVar using <tkinter variable>.trace(mode, callback) for more info read this.
Also always pass in the master keyword argument to all tkinter widgets/variables. You did this with the OptionMenu (it's the first argument usually). But you didn't do it for the StringVar. If you always pass the master keyword argument, you can save yourself a headache.
Edit:
When tkinter calls the callback when the variable is changed it passes some arguments (I don't think they are useful) so make sure that the callback accepts them. Instead of having def callback() use def callback(*args).

Tkinter Entry validatation

I am writing a program in Python 3.6 using Tkinter where a customer has multiple(11) entry fields. I want these entry fields to only accept integers and also be able to define the maximum amount of characters.
I already have a function that does this. But this function only works for one entry field. I have tried entering variables with calling the function so it changes another entry field for example. I was not able to do this.
This is the function I have that works with 1 entry field.
def limitMuntgeld(self, *args):
value = self.invoerM.get()
if len(value) > 5:
self.invoerM.set(value[:5])
if value.lower() in "abcdefghijklmnopqrstuvwxyz-=[];/":
self.invoerM.set(value[:0])
This is the example entry field code that works with the function
self.invoerMuntgeld = Entry(self, font=('Arial', 14), textvariable=self.invoerM)
This is combined with a trace on the entry field posted below.
self.invoerM = StringVar()
self.invoerM.trace('w', self.limitMuntgeld)
I have also tried it with vcmd and validatecommand. However, no good results.
My endresult would be one function working with all entry fields. If anyone has any suggestions, I am all ears!
The proper way to do entry validation is with the validatecommand option rather than using trace. With the validation feature built into the widget you don't need a reference to the widget itself (though you can use it if you want).
When the validatecommand is run, you can have it pass in what the new value will be if the input is valid. You only need to check this value and then return True or False, without having to know which widget it applies to.
For example:
import tkinter as tk
def validate_input(new_value):
valid = new_value .isdigit() and len(new_value) <= 5
return valid
root = tk.Tk()
validate = root.register(validate_input)
for i in range(10):
entry = tk.Entry(root, validate="key", validatecommand=(validate, "%P"))
entry.pack(side="top", fill="x")
root.mainloop()
For information about what %P represents, and what else can be used as arguments to the command, see this question: Interactively validating Entry widget content in tkinter

How to use a variable inside of a function without declaring it as a global variable

I have a 2 part question (if that's not allowed, I really only need to first part answered)
I have the following sample code
import tkinter as tk
window = tk.Tk()
def countIncrease():
count +=1
t1.insert(tk.END,count)
count = 0
t1=tk.Text(window,height=3,width=30)
t1.grid(row=0,column=0,columnspan=3)
b1=tk.Button(window,text="+",height=3,width=10,command=countIncrease)
b1.grid(row=1,column=0)
window.mainloop()
and if I execute this code, I get the error UnboundLocalError: local variable 'count' referenced before assignment
I know that I could simply fix this by adding global count to the function
After I do that, when I press the button, the output is 1, and repeated presses produce 12, 123, 1234, 12345 and so on.
My first (and main) question is that I know it is bad practice to make variables global. What would be the proper way of making this work without making count a global variable?
My second question is how do I make the screen "refresh" so it is only showing the up to date variable, ie instead of 123 its just 3.
You should restructure your code to use a class and make count a class variable if you don't want to use a global variable. And to 'refresh' the screen/tkinter text, you need to delete the content before inserting new one.
Here is one way you can address the two issues:
import tkinter as tk
class app():
def __init__(self, parent):
self.count = 0
self.t1=tk.Text(parent, height=3,width=30)
self.t1.grid(row=0,column=0,columnspan=3)
self.b1=tk.Button(parent,text="+",height=3,width=10,command=self.countIncrease)
self.b1.grid(row=1,column=0)
def countIncrease(self):
self.count +=1
self.t1.delete('1.0', tk.END) #refresh/delete content of t1
self.t1.insert(tk.END,self.count)
window = tk.Tk()
app(window) # Create an instance of app
window.mainloop()

tkinker optionmenu not showing chosen result

import tkinter
window = tkinter.Tk()
def abc(event):
ans=0
numberss=['7','8','9']
omenu2['menu'].delete(0, 'end')
for number in numberss:
omenu2['menu'].add_command(label=numberss[ans], command=efg)
ans=ans+1
def efg(event=None):
print('yee')
numbers = ['1','2', '3']
number=['4','5','6']
var = tkinter.StringVar(window)
var1 = tkinter.StringVar(window)
omenu = tkinter.OptionMenu(window, var, *numbers, command = abc)
omenu.grid(row=1)
omenu2 = tkinter.OptionMenu(window, var1, *number, command = efg)
omenu2.grid(row=2)
after you have entered the first option menu, it will update the second one. when you enter data into the second one, it runs the command, but doesn't show you what you entered. i do not want to include a button, and i know that the command works and not on the second
i found some code that changed the options of the second menu, however when i ran this, the command wouldn't work as it was changed to tkinter.setit (i would also like to know what is does. i do not currently understand it)
omenu2['menu'].add_command(label=numberss[ans], command=tkinter._setit(var1, number))
this has been taken from a larger piece of code, and has thrown the same error
You should set your StringVar(var1) new value.
def abc(event):
numberss=['7','8','9']
omenu2['menu'].delete(0, 'end')
for number in numberss:
omenu2['menu'].add_command(label=number, command=lambda val=number: efg(val))
def efg(val, event=None):
print('yee')
var1.set(val)
You are using for loop so you don't need ans(at least not in this code) since it iterates over items themselves.

Getting return value from tkinter button when clicked

I need a tkinter Button to assign a value to a variable, but I can't figure out how. I can't just put the assignment in the button callback function, because that would be local within the callback function and would be lost. How can I get a value back from the button in my main function?
Here is the code:
def newfile():
def create_file(entry):
file=open(entry.get(0),'w')
return file
chdir(askdirectory())
name=Tk()
name.title("Name the File?")
prompt=Label(name, text="Enter name for new file:")
prompt.grid(row=0)
e=Entry(name)
e.grid(row=1)
e.insert(0, "Untitled")
create=Button(name, text="Create")
#Code I want the button to execute: current=create_file(e), name.destroy()
create.grid(row=2, column=3)
name.mainloop()
return current
Does anyone know?
Also, I need to be able to retrieve current from the return of newfile().
If you use nonlocal current, you should be able to directly set the current variable within the create_file function, as long as current has already been defined, it should work. Remember to put the function call connected to the buttons command argument, in a lambda function, so you can give it the argument. In the future, though, really do follow the comments, the whole code could be reorganised to make it seem more sensible...
def newfile():
current = None
def create_file(entry):
nonlocal current
current = open(entry.get(),'w')
e.master.destroy()
chdir(askdirectory())
name=Tk()
name.title("Name the File?")
prompt=Label(name, text="Enter name for new file:")
prompt.grid(row=0)
e=Entry(name)
e.grid(row=1)
e.insert(0, "Untitled")
create=Button(name, text="Create", command = lambda: create_file(e))
create.grid(row=2, column=3)
name.mainloop()
return current
What I would do is create a class, in this class define name and current as class variables (self.name and self.current) so I could modify them in a class function without problem.

Resources