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

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)

Related

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 do I retrieve the values from a particular grid location in tkinter?

I am working on my first GUI project, and I have placed my code at the bottom of the post (this is a work in progress, so please bear with any ugliness or inefficiency in the code).
I'm making a GURPS character sheet which will automate character creation for my players, and then (though it isn't implemented yet) spit out a nicely formatted PDF.
The way the program works currently, I have functions which perform cost calculations based on the desired rank in an attribute, derived attribute, or skill. Pressing the "calculate" button then spits out the point cost of taking the attribute or skill at the desired level.
I generate my rows using the while-loops near the end of the class definition. The loops call functions which tell the program to create rows that carry out a certain type of calculation.
By choice, all output values appear in column 4 of each row. I would like to know if there is a way for me to easily find the value of those columns and rows without tracking the values as I go. Perhaps a method, like .grid(column,row).get() or something that would return whatever is in some specific grid location.
class Character_sheet:
#Our default class which will house our character sheet.
def __init__(self):
#Total Point Calculator?
def sum_of_values():
list = self.grid_slaves(column=3)
sum = 0
for each in list:
sum += int(each["text"])
total_cost.set(sum)
#Generators for Rows and Columns.
def attr_widget_10(index):
#The below syntax/structure works.
def attr_10():
cost.set((rank.get()-10)*10)
return None
rank = IntVar()
rank.set(10)
cost = IntVar()
input = ttk.Entry(self.window, textvariable = rank).grid(column=2, row=index)
ttk.Button(self.window, text='Calculate', command=attr_10).grid(column=3,row=index)
ttk.Label(self.window, width=7, textvariable=cost).grid(column=4,row=index)
return None
def attr_widget_20(index):
def attr_20():
cost.set((rank.get()-10)*20)
return None
rank = IntVar()
rank.set(10)
cost = IntVar()
input = ttk.Entry(self.window, textvariable = rank).grid(column=2, row=index)
ttk.Button(self.window, text='Calculate', command=attr_20).grid(column=3,row=index)
ttk.Label(self.window, width=7, textvariable=cost).grid(column=4,row=index)
def derived_attr_widget(dictionary, index):
return None
def skill_widget(dictionary, index):
return None
def total_cost():
return None
#Basic window functions.
self.root = tk.Tk()
self.root.title('GURPS Character Sheet')
self.window = ttk.Frame(self.root)
self.window.grid()
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
"""Core Functionality:
Below are labels for set attributes. Each references an appropriate calculator.
This does not address skills.
For now, inputs start on row 1.
"""
#Labels for attributes and derived attributes.
#ATTRIBUTES
ttk.Label(self.window, width=10, text='Strength').grid(column=1, row=1)
ttk.Label(self.window, width=10, text='Health').grid(column=1, row=2)
ttk.Label(self.window, width=10, text='Intelligence').grid(column=1, row=3)
ttk.Label(self.window, width=10, text='Dexterity').grid(column=1, row=4)
#DERIVED ATTRIBUTES
ttk.Label(self.window, width=10, text='HP').grid(column=1,row=5)
ttk.Label(self.window, width=10, text='FP').grid(column=1,row=6)
ttk.Label(self.window, width=10, text='Will').grid(column=1,row=7)
ttk.Label(self.window, width=10, text='Perception').grid(column=1,row=8)
ttk.Label(self.window, width=10, text='Basic Speed').grid(column=1,row=9)
ttk.Label(self.window, width=10, text='Basic Move').grid(column=1,row=10)
index = 1
while index <= 2:
attr_widget_10(index)
index += 1
while index <= 4:
attr_widget_20(index)
index += 1
total_cost = IntVar()
#ttk.Button(self.window, text='Total Cost', command=sum_of_values).grid(column=2,row=index+1)
#ttk.Label(self.window, width=7, textvariable=total_cost).grid(column=4,row=index+1)
###CREATES WINDOW###
self.window.mainloop()
A couple of things to note right off:
stovfl's comment answers the question as it is written
I agree fully with furas' comment about separating the gui fully from the logic. Your code should be refactored- imo- so that the Sheet GUI should be separate from the Character as an abstract collection of statistics, and also should be separate from the code which executes/manages the GUI (which is currently all handled under the umbrella Character_sheet class).
While I'll leave fully disentangling the Character_sheet to you, we can at least get you started while developing a pattern for gaining access to the values in the GUI.
Each of the first 4 rows represent statistics that the user can change and relate to a label, which you created already. Two of the statistics have a cost modifier of 10, and the other two have a modifier of 20.
## Place in the global space for the time being
BASE_STATISTICS = ["Strength","Health","Intelligence","Will"]
## Note that prior to Python 3.7 dictionary order was not guaranteed, so
## collections.OrderedDict would be preferable for versions before that
STATISTIC_COSTS = {"Strength":10,"Health":10,"Intelligence":20,"Will":20}
(collections.OrderedDict)
Presumably, each given Character Sheet would have its own, independent widgets and values for these statistics. Again, you should rewrite the code to be more detached, but for now we'll preserve as much of your code as possible.
## Place at the top of Character_sheet.__init__
## The value for each stat is a dictionary in order to store arbitrary data until the code is reworked further
self.base_stats = {stat:{} for stat in BASE_STATISTICS}
With these additions we now have a framework for both referring to the widget rows that you are creating and for determining what the cost modifier is for those Statistics.
## This will replace the Label and attr_widget_X loops and functions
## You can place it where the Attributes labels currently are, and delete both attr_widget_x functions
## enumerate pairs each element of an iterable with a sequential integer
for i,stat in enumerate(BASE_STATISTICS):
## These IntVars are useful, so we'll keep them around
rank = IntVar()
rank.set(10)
cost = IntVar()
## We'll set up the gui just like you did, just with a minor tweak
ttk.Label(self.window, width=10, text=stat).grid(column=1, row=i)
ttk.Entry(self.window, textvariable = rank).grid(column=2, row=i)
## I've removed the Generate button for reasons I'll get into below
ttk.Label(self.window, width=7, textvariable=cost).grid(column=3,row=i)
## Here we save all our references so that we can come back to them later
## self.base_stats[stat]['row'] will tell us which row of the grid the widgets are located
## self.base_stats[stat]['rank'] will now give us direct access to the rank IntVar at all times
## self.base_stats[stat]['cost'] likewise gives us easy access to the cost IntVar whenever we need it
self.base_stats[stat].update({'row':i,'rank': rank,'cost':cost})
(enumerate)
Tkinter gives you access to different signal types; specifically for our uses, tkinter Variables can be bound using their trace method. By using the 'w' mode, whenever the Variable changes, the given callback (function) will be called. Using this we can make the GUI more responsive by getting rid of the need to constantly hit the Generate Button.
## This should go right after "cost = IntVar()"
## The lambda statement here is technically the function that is being passed to trace
## The lambda itself is capturing all information it gets passed as e
## stat = stat creates a reference within the lambda definition to the current value of stat
## (as you iterate, the stat value in the local scope will change, so we need to preserve it)
## and then calling self.updatestat and passing that the stat we're updating.
rank.trace('w',lambda *e,stat = stat: self.updatestat(stat))
(lambda)
And now we can add Character_sheet.updatestat so it actually functions:
def updatestat(self,stat):
""" Queries the current value of the stat's rank and then sets the cost appropriately """
## Get the IntVar for the given stat from your stats dict
rankvar = self.base_stats[stat]['rank']
## Since we're using an Entry (instead of e.g.- a spinbox), there's
## no garauntee that it contains a valid integer, so we use try/except
## to catch the mistake
try:
rank = rankvar.get()
rank = int(rank)
except:
## We'll reset the value if it's invalid
rank = 10
rankvar.set(rank)
## Use STATISTIC_COSTS to determine the cost modifier
## Calculate cost
cost = (rank - 10)*STATISTIC_COSTS[stat]
## find our IntVar for the given stat
costvar = self.base_stats[stat]['cost']
## Set it to cost
costvar.set(cost)
## Note that "return None" is the implicit default
And that gets you just a little closer to getting your GUI separated from your programming logic while allowing you to reference those values in the rows and columns like you were trying to do (i.e.- self.stats['Strength']['rank'].get())

How do I make a tkinter button in an list of buttons return its index? [duplicate]

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 4 years ago.
I am new to coding in general (Python is my first language) and I ran into this problem while learning tkinter. My code is as follows:
from tkinter import *
window = Tk()
buttons = []
def position(pos):
print(pos)
for i in range(7):
buttons.append(Button(window, width = 10, height = 5, bg = "red", command = position(i)).grid(row = 0, column = i))
window.mainloop()
This does not work. I want to print the index of the button when that button is clicked. I have tried a few methods to accomplish this, but with no success. The buttons do not necessarily have to be in a list, however the first button must return 0, the second return 1, the third 2 etc. What is the simplest way to do this?
See this:
from tkinter import *
root = Tk()
files = [] #creates list to replace your actual inputs for troubleshooting purposes
btn = [] #creates list to store the buttons ins
for i in range(50): #this just popultes a list as a replacement for your actual inputs for troubleshooting purposes
files.append("Button"+str(i))
for i in range(len(files)): #this says for *counter* in *however many elements there are in the list files*
#the below line creates a button and stores it in an array we can call later, it will print the value of it's own text by referencing itself from the list that the buttons are stored in
btn.append(Button(root, text=files[i], command=lambda c=i: print(btn[c].cget("text"))))
btn[i].pack() #this packs the buttons
root.mainloop()
Taken from: How can I get the button id when it is clicked?

Assigning functions to buttons created in a for loop in Tkinter [duplicate]

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 5 years ago.
I have a for loop which creates an amount of buttons (in tkinter) based on the length of my list, compkeys. When I make each button, it is given a previously made function which takes one input. I am trying to make the input of the function specific to the iteration of the for loop. For example, the first button that is created in the loop should have the first item in the list comp keys as input in its function.
However, each button is only receiving an input of the final value of x, instead of the value of x depending on how many times the loop has repeated. Thank you for any and all help:)
import tkinter
compkeys = [2017onsc, 2017onwat]
for x in range(len(compKeys)):
compButton = Button(root, text = compKeys[x], command=lambda: compBuDef(compKeys[x]))
compButton.place(x=x * 100 + 200, y=300)
You must pass the parameter through the lambda function:
for x in range(len(compKeys)):
compButton = Button(root, text=compKeys[x], command=lambda z=compKeys[x]: compBuDef(z))
compButton.place(x=x*100+200, y=300)
or better, iterating over elements:
for idx, ckey in enumerate(compKeys):
compButton = Button(root, text=ckey, command=lambda z=ckey: compBuDef(z))
compButton.place(x=idx*100+200, y=300)

Tkinter lambda function [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.

Categories