Tkinter Checkbutton: getting the variable for multiple objects - python

I want this to create a bunch of Checkboxes on the fly, and when the 'Submit' button is pressed, to look for all checked boxes. Currently, when pressing 'Submit' to call select_adgroup(), it prints 0 for every item whether they're checked or not, unless every box is checked, in which case it prints 1 for every item. But I want it to only print 1 for the boxes that are checked.
def search_adgroups(self):
self.adgroups = adgroup(checkBoxVal.get())
self.inc1 = 1
self.cbuts1 = []
for index, item in enumerate(self.adgroups):
self.adBoxVal = IntVar()
self.adgroup_check = (Checkbutton(self, variable=self.adBoxVal, text = item))
self.cbuts1.append(self.adgroup_check)
self.cbuts1[index].grid(row=self.inc1, sticky=W)
self.inc1 += 1
self.button2 = Button(self, text="Submit", command=self.select_adgroup)
self.button2.grid(row=self.inc1, sticky=W)
def select_adgroup(self):
for item in self.cbuts1:
print(self.adBoxVal.get())

First of all it's not if all checked, but if last one checked it will print 1s. So overall it is only printing last item's value. Which says a lot about the problem.
After for loop, self.adBoxVal's value will be the last one so when you try to get its value in your method with self.adBoxVal.get(), you only get the last ones value.
To get over this problem, you need to store all self.adBoxVals in a list, then iterate over it.
def search_adgroups(self):
...
...
self.chks = [] #your list for IntVars
for index, item in enumerate(self.adgroups):
adBoxVal = IntVar() #no need self here as BryanOakley points out
self.chks.append(self.adBoxVal)
#no need self at below also
adgroup_check = Checkbutton(self.root, variable=adBoxVal, text = item)
self.cbuts1.append(adgroup_check)
self.cbuts1[index].grid(row=self.inc1, sticky=W)
self.inc1 += 1
self.button2 = Button(self.root, text="Submit", command=self.select_adgroup)
self.button2.grid(row=self.inc1, sticky=W)
def select_adgroup(self):
for item in self.chks: #here you need to iterate over IntVars
#to get thier value
print (item.get())
Also for your future questions, it will be nice if you post a working code that reproduces error without some unknown functions or variables.
EDIT: Let's use print's to make it clear. When you add print(self.addBoxVal) under self.adBoxVal = IntVar() you will see every item is different than eachother.
for index, item in enumerate(self.adgroups):
adBoxVal = IntVar()
print (adBoxVal)
>>>
PY_VAR0 #I assigned range(5) to adgroups that's why
PY_VAR1 #there are 5 elements here
PY_VAR2
PY_VAR3
PY_VAR4
If you add print (adBoxVal) in select_adgroup method in your code, you will see it is PY_VARX(last one) and code only works with/on that. Since you get Variable Classes' value(IntVar in your case) by using .get() method, you need different ones. Hence you need to save each element in a list, then iterate over it.

Related

Tkinter, update widgets real time if list is modified

Let say I've a list of widgets that are generated by tkinter uisng a loop (it's customtkinter in this case but since tkinter is more well known so I think it'd be better to make an example with it), each widgets lie in the same frame with different label text. Here is an example for the code:
x=0
self.scrollable_frame = customtkinter.CTkScrollableFrame(self, label_text="CTkScrollableFrame")
self.scrollable_frame.grid(row=1, column=2, padx=(20, 0), pady=(20, 0), sticky="nsew")
self.scrollable_frame.grid_columnconfigure(0, weight=1)
self.scrollable_frame_switches = []
for i in range(x,100):
switch = customtkinter.CTkSwitch(master=self.scrollable_frame, text=f"CTkSwitch {i}")
switch.grid(row=i, column=0, padx=10, pady=(0, 20))
self.scrollable_frame_switches.append(switch)
My question is, if the list that help generated those widgets change (in this case it's just a loop ranging from 0-100, might change the widgets text, list size..), what would be the best way for real time update the tkinter window contents?
Ps: I've tried to look for my answer from many places but as of right now, the best answer I can come up with is to update the whole frame with same grid but changed list content, I'll put it bellow. Is there any way better than this? Thank you
Like I said before, while the existing answer might work, it might be inefficient since you are destroying and creating new widgets each time there is a change. Instead of this, you could create a function that will check if there is a change and then if there is extra or less items, the changes will take place:
from tkinter import *
import random
root = Tk()
def fetch_changed_list():
"""Function that will change the list and return the new list"""
MAX = random.randint(5, 15)
# Create a list with random text and return it
items = [f'Button {x+1}' for x in range(MAX)]
return items
def calculate():
global items
# Fetch the new list
new_items = fetch_changed_list()
# Store the length of the current list and the new list
cur_len, new_len = len(items), len(new_items)
# If the length of new list is more than current list then
if new_len > cur_len:
diff = new_len - cur_len
# Change text of existing widgets
for idx, wid in enumerate(items_frame.winfo_children()):
wid.config(text=new_items[idx])
# Make the rest of the widgets required
for i in range(diff):
Button(items_frame, text=new_items[cur_len+i]).pack()
# If the length of current list is more than new list then
elif new_len < cur_len:
extra = cur_len - new_len
# Change the text for the existing widgets
for idx in range(new_len):
wid = items_frame.winfo_children()[idx]
wid.config(text=new_items[idx])
# Get the extra widgets that need to be removed
extra_wids = [wid for wid in items_frame.winfo_children()
[-1:-extra-1:-1]] # The indexing is a way to pick the last 'n' items from a list
# Remove the extra widgets
for wid in extra_wids:
wid.destroy()
# Also can shorten the last 2 steps into a single line using
# [wid.destroy() for wid in items_frame.winfo_children()[-1:-extra-1:-1]]
items = new_items # Update the value of the main list to be the new list
root.after(1000, calculate) # Repeat the function every 1000ms
items = [f'Button {x+1}' for x in range(8)] # List that will keep mutating
items_frame = Frame(root) # A parent with only the dynamic widgets
items_frame.pack()
for item in items:
Button(items_frame, text=item).pack()
root.after(1000, calculate)
root.mainloop()
The code is commented to make it understandable line by line. An important thing to note here is the items_frame, which makes it possible to get all the dynamically created widgets directly without having the need to store them to a list manually.
The function fetch_changed_list is the one that changes the list and returns it. If you don't want to repeat calculate every 1000ms (which is a good idea not to repeat infinitely), you could call the calculate function each time you change the list.
def change_list():
# Logic to change the list
...
calculate() # To make the changes
After calculating the time for function executions, I found this:
Widgets redrawn
Time before (in seconds)
Time after (in seconds)
400
0.04200148582458496
0.024012088775634766
350
0.70701003074646
0.21500921249389648
210
0.4723021984100342
0.3189823627471924
700
0.32096409797668457
0.04197263717651367
Where "before" is when destroying and recreating and "after" is only performing when change is needed.
So I've decided that if I want to click a button, that button should be able to update the list. Hence, I bind a non-related buttons in the widget to this function:
def sidebar_button_event(self):
global x
x=10
self.scrollable_frame.destroy()
self.after(0,self.update())
Which will then call for an update function that store the change value, and the update function will just simply overwrite the grid:
def update(self):
self.scrollable_frame = customtkinter.CTkScrollableFrame(self, label_text="CTkScrollableFrame")
self.scrollable_frame.grid(row=1, column=2, padx=(20, 0), pady=(20, 0), sticky="nsew")
self.scrollable_frame.grid_columnconfigure(0, weight=1)
self.scrollable_frame_switches = []
for i in range(x,100):
switch = customtkinter.CTkSwitch(master=self.scrollable_frame, text=f"CTkSwitch {i}")
switch.grid(row=i, column=0, padx=10, pady=(0, 20))
self.scrollable_frame_switches.append(switch)

How do I collect multiple entries into a single label using tkinter?

I'm trying to create a tkinter program that allows for dynamically adding Entry fields using a button. I have that in place but now I am trying to store the user inputs as variables for however many entry boxes are added. Ideas?
For example if I hit the button 4 times and 4 Entry boxes are added I want to store those 4 user entries as 4 variables (or at least strings) for further use
Current code:
# add() adds multiple text boxes
all_unq = []
count = 0
def add():
global count
MAX_NUM = 15
if count <= MAX_NUM:
all_unq.append(tk.Entry(main)) # Create and append to list
all_unq[-1].grid(row=14+count,column=1,`enter code here`pady=5)
# Place the just created widget
count += 1 # Increase the count by 1
First, you'll need some initial variables...
import tkinter as tk
root = tk.Tk()
entries = []
createEntry = tk.Button(root, text="click me!")
createEntry.grid(column=0, row=0, pady=15)
Then, you will need a function to create the entry when the button is clicked...
def makeEntry():
entry = tk.Entry(root)
entry.grid(column=len(entries)+1, row=0, pady=3)
entries.append(entry)
Finally, you wrap everything up by calling root.mainloop and binding the button to the mouse to make it responsive...
createEntry.bind("<Button-1>", command=makeEntry)
root.mainloop()
You have a list of entry widgets, just iterate over it. For example, here's how to print the value from each entry widget. Note that you'll need to declare all_unq as global if you want to do this in a separate function.
for entry in all_unq:
print(entry.get())
If you want a list with all of the values, you can use a list comprehension:
all_values = [entry.get() for entry in all_unq]

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

Python - arrays of tkinter widgets changing with radiobutton clicks

I'm working on a GUI in Python using tkinter. I'm reading a text file in and creating GUI elements dynamically based on the lines in the text file. I have an array for each of my element types, which include labels, radiobutton variables (StringVars), and colored circles (drawn with create_oval). My goal is that when the user changes a radiobutton from "not assigned" to "in" or "out", the colored circle on that line will change from yellow to green. Here's how the GUI looks after the text file has been read in:
Item 1: (o) in () out () not assigned (G)
Item 2: () in () out (o) not assigned (Y)
Currently, I have a trace on the radiobutton StringVars so that I can call a method whenever one of the buttons is changed. My problem is figuring out which radiobutton was changed so that I can change the color of the circle on that line...
I'm currently going the route of duplicating the whole radiobutton StringVar array into a temp global array. When the trace function is called, I compare the temp array with what's currently in the array to figure out where the change is. I duplicate the array with: temp_radiobutton_vars = list(radiobutton_vars), but I'm not sure if this is the right route. My temp list and the current list always show the same results when I get() the StringVar, even after I changed the button. Any ideas on how to fix this, or maybe there's a better method to do what I'm looking to do...
Sorry for the long and not great explanation. If anyone needs more info or snippets of code, let me know. Thanks!
There are many ways to solve this problem. Since you are already using variable traces, perhaps the simplest solution is to pass the index of the canvas item to the callback. You can use lambda or functools.partial for this task. You could also not use variable traces, but instead, associate a command with each radiobutton. In both cases you simply need to tell the callback which index to operate on.
In the following example, the callback takes a reference to a variable and the index to the canvas item. It fetches the value, looks up the color in a table, and then configures the canvas item:
def on_radiobutton(var, index):
value = var.get()
color = {"in": "green", "out": "red", "unassigned": "yellow"}
self.canvas.itemconfigure(index, fill=color[value])
This is how the trace is set up using lambda (note that name1, name2 and op are automatically sent by tkinter for every trace):
var = tk.StringVar()
rb0 = tk.Radiobutton(..., variable=var, value="in", text="in")
rb1 = tk.Radiobutton(..., variable=var, value="out", text="out")
rb2 = tk.Radiobutton(..., variable=var, value="unassigned", text="not assigned")
var.trace("w", lambda name1, name2, op, index=i, var=var:
on_radiobutton(var, index))
It sounds like you have the wrong idea with Radiobuttons. All "connected" Radiobuttons should have the same variable value; in this way, you can call theVariable.get() and compare that with the value of each Radiobutton; you shouldn't need a reference to every Radiobutton; nor should you have a StringVar for each Radiobutton, only each line.
Edit: I've expanded my example to show how this would work for more than one line. All that changed is now I check which line I have passed in my callback, and using that I know which line to update (in your case, which canvas to color). It's just some 2D list processing to check which Radiobutton is selected based upon which line is issuing the callback.
from Tkinter import *
root = Tk()
root.geometry("300x200+500+400")
lines = [StringVar(), StringVar()]
strings = [["Hello", "Stack", "Overflow"], ["Whats", "Going", "On"]]
buttons = [[],[]]
l1 = Label(root, text = "Selection: ", justify = LEFT)
l1.grid(column = 0, row = 0, sticky = NW, padx = (0, 250))
l1.grid_propagate(False)
l2 = Label(root, text = "Selection: ", justify = LEFT)
l2.grid(column = 0, row = 4, sticky = NW, padx = (0, 250))
l2.grid_propagate(False)
def process(line):
global l1, l2, strings, lines
if line == lines[0]:
# Since lines[0] was passed in to the callback, we know to update line 0;
# take that line's label (or canvas in your case)
updateLine = 0
updateLabel = l1
else:
# Otherwise take the other line
updateLine = 1
updateLabel = l2
# These operations are performed within if/elif/else to show how you coul
# choose a different process for each Radiobutton: example, coloring a canvas differently
if lines[updateLine].get() == strings[updateLine][0]:
# This means the first button of whatever line was selected
updateLabel.config(text = "Selection: %s" %strings[updateLine][0])
elif lines[updateLine].get() == strings[updateLine][1]:
# This means the second button of whatever line was selected
updateLabel.config(text = "Selection: %s" %strings[updateLine][1])
else:
# You get the idea
updateLabel.config(text = "Selection: Bet you thought I'd say %s" %strings[updateLine][2])
# Must have a seperate row number because with multiple lines, we can't simply use 'i' or 'j'
rowNum = 1
for i in range(len(lines)):
for j in range(len(strings[i])):
buttons[i].append(Radiobutton(root, text = strings[i][j], variable = lines[i], value = strings[i][j], command = lambda line = lines[i]: process(line)))
buttons[i][j].grid(column = 0, row = rowNum, sticky = NW)
rowNum +=1
rowNum += 2
root.mainloop()

Categories