Tkinter lambda function [duplicate] - python

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 6 months ago.
(As the 'homework' tag indicates, this is part of a big project in Computer Science.)
I am writing a Jeopardy! simulation in Python with tkinter, and I'm having a big problem regarding the use of the lambda function in buttons. Assume root = Tk() and categories is a list.
# Variable to keep the buttons
root._buttons = {}
# Display headers on top of page
for i in range(5):
# Get category name for display in main window
name = categories[i]
b = Label(root, text=fill(name.upper(), 10), width=18, height=3,\
bg="darkblue", fg="white", font=("Helvetica bold", "", 11))
b.grid(row=0, column=i)
# Create list of buttons in that variable (root._buttons)
btnlist = [None]*5
# Display individual questions
for j in range(5):
# Make a button for the question
b = Button(root, text="$" + str(200 * (j+1)), width=8, height=1,
bg="darkblue", fg="orange", font=("Impact", "", 30))
b.cat = name
b.value = 200 * (j + 1)
b.sel = lambda: select(b.cat, b.value)
# Add callback event to button
print(b.cat, b.value, b.sel)
b.config(command=b.sel)
# Add button to window
b.grid(row=j+1, column=i)
# Append to list
btnlist[j] = b
root._buttons[categories[i]] = btnlist
For all of the code, see my little Code Viewer (under construction!)
It's at lambda: select(b.cat, b.value) where the problem seems to occur, because when I click any button on the board, it always goes to the one last button on the board. I've tried other approaches, unfortunately all using lambda, and I have not seen any approach that does not involve lambda.

Change
lambda: select(b.cat, b.value)
to
lambda b = b: select(b.cat, b.value)
In your original code, b is not a local variable of the lambda; it is found in enclosing scope. Once the for-loop is completed, b retains it last value. That is why the lambda functions all use the last button.
If you define the lambda to take one argument with a default value, the default value is determined (and fixed) at the time the lambda is defined. Now b is a local variable of the lambda, and when the lambda is called with no arguments, Python sets b to the default value which happily is set to various different buttons as desired.

It would let you be more expressive if you replaced the lambda expression with a function factory. (presuming that you're going to call this multiple times). That way you can do assignments, add more complicated logic, etc later on without having to deal with the limitations of lambda.
For example:
def button_factory(b):
def bsel():
""" button associated with question"""
return select(b.cat, b.value)
return bsel
Given an input b, button_factory returns a function callable with () that returns exactly what you want. The only difference is that you can do assignments, etc.
Even though it may take up more lines of code initially, it gives you greater flexibility later on. (for example, you could attach a counter to bsel and be able to count how many times a particular question was selected, etc).
It also aids introspection, as you could make each docstring clearly identify which question it is associated with, etc.

Related

Iterating through buttons in tkinter [duplicate]

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 2 years ago.
I try to create buttons in tkinter using a loop.
def bulkbuttons():
i = 0
x = 0
buttons=[]
while x < 10:
buttons.append('button' + str(x))
x+=1
while i < 10:
buttons[i]=Button(window,text=str(i), width=12,borderwidth=1, relief='raised', bg='#134f5c', fg='#FFFFFF', command=(lambda: justprint(i)))
buttons[i].grid(row=i, column=2)
i += 1
The justprint function prints i.
def justprint(i):
print(i)
The buttons show the correct numbers. But the justprint function only prints the last number, 10, no matter which button I click.
What did I do wrong? I want to click a button and then use the number of the button as a parameter for some functions.
You can pass i as a default argument in the lambda function. Alternatively you can use the partial function from functools:
from functools import partial
def bulkbuttons():
# Put all formatting related parameters of the Button constructor in a dictionary
# Using ** will pass dict values to
formatting = {'width': 12,
'borderwidth': 1,
'relief': 'raised',
'bg': '#134f5c',
'fg': '#FFFFFF'}
for i in range(10):
buttons[i]=Button(window,
text=str(i),
**formating,
command=partial(justprint,i))
buttons[i].grid(row=i, column=2)
Notes:
Your first while loop can be expressed as this pretty and concise list comprehesion:
buttons = ['button' + str(x) for x in range(10)]
Try using this notation when possible, since they will save you typing time and is far more readable.
Your second while loop overwrites the list created in the first one, so there is no need to do the first while loop.
I took the liberty of placing all your formating related parameters for the Button constructor in a dictionary, you can pass them all at once passing **formating to the constructor. You can place these parameters in your global scope (outside functions) and define models for every type of button, saving you some time and making the code more readable.
If you have a fixed number of iteration is a loop, use for i in range(n) instead of a while, it will avoid some infinite loops when you forget the i+=1

Why I can't loop a variable inside a list as a *args? [duplicate]

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 6 months ago.
(As the 'homework' tag indicates, this is part of a big project in Computer Science.)
I am writing a Jeopardy! simulation in Python with tkinter, and I'm having a big problem regarding the use of the lambda function in buttons. Assume root = Tk() and categories is a list.
# Variable to keep the buttons
root._buttons = {}
# Display headers on top of page
for i in range(5):
# Get category name for display in main window
name = categories[i]
b = Label(root, text=fill(name.upper(), 10), width=18, height=3,\
bg="darkblue", fg="white", font=("Helvetica bold", "", 11))
b.grid(row=0, column=i)
# Create list of buttons in that variable (root._buttons)
btnlist = [None]*5
# Display individual questions
for j in range(5):
# Make a button for the question
b = Button(root, text="$" + str(200 * (j+1)), width=8, height=1,
bg="darkblue", fg="orange", font=("Impact", "", 30))
b.cat = name
b.value = 200 * (j + 1)
b.sel = lambda: select(b.cat, b.value)
# Add callback event to button
print(b.cat, b.value, b.sel)
b.config(command=b.sel)
# Add button to window
b.grid(row=j+1, column=i)
# Append to list
btnlist[j] = b
root._buttons[categories[i]] = btnlist
For all of the code, see my little Code Viewer (under construction!)
It's at lambda: select(b.cat, b.value) where the problem seems to occur, because when I click any button on the board, it always goes to the one last button on the board. I've tried other approaches, unfortunately all using lambda, and I have not seen any approach that does not involve lambda.
Change
lambda: select(b.cat, b.value)
to
lambda b = b: select(b.cat, b.value)
In your original code, b is not a local variable of the lambda; it is found in enclosing scope. Once the for-loop is completed, b retains it last value. That is why the lambda functions all use the last button.
If you define the lambda to take one argument with a default value, the default value is determined (and fixed) at the time the lambda is defined. Now b is a local variable of the lambda, and when the lambda is called with no arguments, Python sets b to the default value which happily is set to various different buttons as desired.
It would let you be more expressive if you replaced the lambda expression with a function factory. (presuming that you're going to call this multiple times). That way you can do assignments, add more complicated logic, etc later on without having to deal with the limitations of lambda.
For example:
def button_factory(b):
def bsel():
""" button associated with question"""
return select(b.cat, b.value)
return bsel
Given an input b, button_factory returns a function callable with () that returns exactly what you want. The only difference is that you can do assignments, etc.
Even though it may take up more lines of code initially, it gives you greater flexibility later on. (for example, you could attach a counter to bsel and be able to count how many times a particular question was selected, etc).
It also aids introspection, as you could make each docstring clearly identify which question it is associated with, etc.

How to apply function with arguments to buttons in TkInter [duplicate]

This question already has an answer here:
tkinter: lambda multiple Buttons to a Label?
(1 answer)
Closed 3 years ago.
I am trying to make an interface where a user clicks a TkInter button to select a language, and then the button calls a function (with an argument for the specific language) to set the language for the program.
I tried using Lambdas for passing the functions, but that didn't work.
def showLangButtons():
tk = Tk()
root = Canvas(tk, width=100, height=100)
root.pack()
langButtons = []
langs = []
for a in langf:
langs.append(a)
for a in sorted(langs):
langButtons.append(Button(root, text=lang_names[a][a], width=19,
height=2, command = lambda:setLang(a)))
# This part of the function displays the buttons on a grid
const = 0
while const < (len(langButtons))**(1/2)/1.75:
const += 1
n = 0
while n < len(langButtons):
langButtons[n].grid(row = int(n/const), column = n%const, sticky = W)
n+=1
tk.update()
langf is a dictionary which contains the list of supported languages. lang_names is a dictionary which contains the names of each language (indexed by the ISO 639-3 code). setLang() takes a string as its argument, specifically the ISO 639-3 code of the language.
I expect the language to be set corresponding to whichever button the user clicks, but it always sets the language to the last language in the language list. For example, there are currently 2 supported languages: English and Icelandic. Regardless of which button I click, it always sets the language to Icelandic, because it is last in alphabetical order.
You need to force a closure when you use lambda:
command=lambda lang=a: setLang(lang)

TKinter CheckButtons won't properly update values

I know this question has been asked a few times, but not one of the other solutions has applied to my problem.
I have a variable list of "anomalies" stored as a tuple holding the anomaly name and either a 0 or 1, determining whether or not to notify the user of something. Because the list is of variable length, the checkbuttons need to be created in a for loop.
I want to create a popup that shows a list of checkbuttons, to allow the user to edit the notification values to their preference. However, the implementation of this idea that I've used causes the checkbuttons to not change the value of their variables or display the proper on/off state.
Here's my code:
notif_anoms = [("Anomaly 1", 1), ("Anomaly 2", 0)]
checkbox_vars = []
def select_desired_anomaly_checks(self):
popup = Tk()
popup.wm_title("Desired Anomalies")
len_a = len(self.notif_anoms)
for i in range(0, len_a):
msg, on = self.notif_anoms[i]
self.checkbox_vars.append(IntVar(value=on))
self.checkbox_vars[-1].set(on)
tk.Checkbutton(popup, text=msg, variable=self.checkbox_vars[-1], onvalue=1, offvalue=0, command=self.update_vars).grid(row=i, sticky=W)
popup.resizable(0, 0)
popup.mainloop()
def update_vars(self):
for i in range(0, len(self.checkbox_vars)):
var = self.checkbox_vars[i]
print(var.get())
self.notif_anoms[i] = (self.notif_anoms[i][0], var.get())
print('------------------')
The only problem I can think of with this is that I'm setting the IntVar inside of the for loop, but as far as I can think of, there's no other way to create a list of checkboxes at runtime with unknown length.
Any and all help is appreciated, thank you.

How to get value from entry(Tkinter), use it in formula and print the result it in label

from Tkinter import *
top=Tk()
First Value A that user will input
A = Entry(top)
A.grid(row=1, column=1)
Second value B that user also inputs
B = Entry(top)
B.grid(row=1, column=2)
Calculation - Now I want to add those values (Preferably values with decimal points)
A1=float(A.get())
B1=float(B.get())
C1=A1+B1
Result - I want python to calculate result and show it to user when I input the first two values
C = Label(textvariable=C1)
C.grid(row=1, column=3)
top.mainloop()
First off, welcome to StackOverflow and nice job- your code does (mostly) everything you want it to do! The timing is just off- you create your objects and get the value, but the value hasn't been input by the user yet.
To solve that, you need to put your .get()'s in a function, and you should be using an actual text-variable that you set() after each one (if you just use C1=(float), you'll end up making new floats so the Label isn't pointing to the right one).
(setup... )
B.grid(...)
C1 = Tkinter.StringVar()
C = Label(textvariable=C1) # Using a StringVar will allow this to automagically update
def setC():
A1=float(A.get())
B1=float(B.get())
C1.set(str(A1+B1))
Additionally, you need to set this function so it goes off more than just "immediately on running the program". The simple way to do this is to just have the function call itself .after() some time (in milliseconds).
def setC():
# Body above
top.after(1000, setC) # Call setC after 1 second, so it keeps getting called.
setC() # You have to call it manually once, and then it repeats.
The slightly more advanced and efficient way to update involves events and bindings (binding setC() to fire every time A1 or B1 is changed), but the writeup on that is long so I'll give you that tip and send you to some documentation on that. (Effbot is good tkinter documentation regardless)

Categories