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()
Related
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)
I will try to exemplify as much as possible.
I am developing a program in which I have data recorded in a database.db and access it through the SQLite3 library. I created a function to return the data for a specific column that I want and it will be several Labels with the data returned.
In this program I use the .pack() method and I know that if I use the pack_forget() command, the Label will be removed from the window with every new query I make.
The problem is that as there are several Labels for each data returned, I do not create names for each Label and now I have this problem. For each search, I want to remove all Labels for the newest search. How can I remove Labels from the window if they do not have a variable name. Below is an example:
def clear():
# ???
Label.pack_forget()
def search(v):
# 'v' cames from Entry's textvariable
tmp4Label = []
var4Len = list(c.execute(
f'SELECT * FROM table WHERE element = "{v.get()}"').fetchall())
for i in range(len(var4Len)):
# Getting just the first column data
#'c' is the cursor
a.append(list(c.execute(
f'SELECT * FROM table WHERE element = "{v.get()}"').fetchall())[i][0])
for i in tmp4Label:
# Labels for each data, removing '[' and ']' characters
Label(root, text=f"{str([i]).replace('[', '').replace(']', '')}",font=('Arial 18 bold')).pack()
One solution is to store the Labels in a list:
list_of_labels = []
def clear():
while list_of_labels:
list_of_labels.pop().pack_forget() # remove from the list
def search(v):
for i in tmp4Label:
lbl = Label(root, text=i)
lbl.pack()
list_of_labels.append(lbl) # add to the list
Another solution is to use a single label instead of a stack of them.
def clear():
data_label.pack_forget()
def search(v):
global data_label
data_label = Label(root, text='\n'.join(map(str, tmp4Label)))
data_label.pack()
Whatever you do it's extremely important to use 2 lines to define the Label. The first to define it, the second to lay it out (pack it). You cannot combine those 2 lines.
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.
I'm not great at tkinter or even python so I've run into what should be a simple problem.
I have something like this in the middle of my project:
visible = numLevels * [IntVar(value=1)]
top = Toplevel()
settingslabel = Label(top, text='Settings', height=0, width=100)
for i in range(0, numLevels ):
check = ttk.Checkbutton(settingslabel, text='Level ' + str(i), variable=visible[i])
check.grid(column = 0, row = i)
check.var = visible[i]
settingslabel.grid(column = 0, row=0)
I want to have settings screen with a checkbox for every level, while maintaining an array of integers that represent the status of each button.
However, all checkboxes are synchronized. Meaning, when I check a box, all other boxes also become checked. I believe that this is because of the 'variable' field of the checkbutton. As the loop continues, i is updated, and as a result, visible[i] changes as well. I want to preserve the variable when I created the checkbutton. I don't understand how tkinter/python work well enough to know.
The number is levels can be any integer > 0 and is determined at runtime so I can't just unroll the loop.
Is there a better way to do this? Thanks in advance.
This is a duplicate of this SO post, but explanation below.
Its because all your boxes are sharing the same tkinter.Intvar() object:
numlevels = 5
visible = numlevels * [IntVar(value = 1)]
for i in range(len(visible)):
print (hex(id(visible[i]))
# Outputs:
'0x67f8190'
'0x67f8190'
'0x67f8190'
'0x67f8190'
'0x67f8190'
To solve: visible = [IntVar(value = 1) for i in range(numlevels)]
Sorry for the vague title but I didn't know how to explain myself better. Basically what I try to do in tkinter here is adding and removing labels. The label value gets updated so that I always have an increment of 1 even though I deleted a label in the beginning. If I generate labels and delete them from the bottom up I have no problems but it I delete one from the middle and then try to clean my list I get an error:
Exception in Tkinter callback
Traceback (most recent call last):
File "/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-tk/Tkinter.py", line 1536, in __call__
return self.func(*args)
File "/Users/XXXX/Helper/development/dynamicListLabels.py", line 21, in <lambda>
labelList[index].append(ttk.Button(root, text="Remove", command=lambda: removeLabel(labelList[index][0], index)))
IndexError: list index out of range
My python code looks like this:
#!/usr/bin/python
from Tkinter import *
import ttk
def removeLabel(labelToRemove, bla):
labelList[labelToRemove.get()][1].destroy()
labelList[labelToRemove.get()][2].destroy()
del labelList[labelToRemove.get()]
for label in labelList:
index = labelList.index(label)
label[0].set(index)
def addNewLabel():
labelList.append([IntVar()])
index = len(labelList) - 1
labelList[index][0].set(index)
labelList[index].append(ttk.Label(root, textvariable=labelList[index][0]))
labelList[index].append(ttk.Button(root, text="Remove", command=lambda: removeLabel(labelList[index][0], index)))
labelList[index][1].grid(column=0)
labelList[index][2].grid(column=1, row=labelList[index][1].grid_info()['row'])
root = Tk()
labelList = []
ttk.Button(root, text="add label", command=addNewLabel).grid(column=1, row=0)
root.mainloop()
And my GUI looks like this:
Thanks for your help!
Design
The main problem comes when dealing with different indexes. Trying to manipulate them carefully leads to complicated operations resulting in a long and inefficient code. To remedy to this problem, we simply get rid of them and take advantage of the label class variable Tkinter.IntVar() you already are using. This gives us full control of the labels and associated widgets.
An other efficient decision to take that prevents from getting lot of headache is to attach each (label, button) couple widgets to a unique Tkinter.Frame() instance. This offers the advantage of deleting the frame using destroy() method leading automatically to the destruction of the widgets it contains. In the same time, this keeps the look of your GUI and makes your it scalable as it offers you the possibility to add more widgets.
Designing addNewLabel()
There is nothing new here compared to your original code except, as I said in 2. each (label, button) couple will be drawn into a single and unique Tkinter.Frame() instance. Of course, the list frames must be declared global in this method.
Designing removeLabel()
From 1. the only argument we need to pass to removeLabel() is the Tkinter variable (var in the code below) inherent to the label we want to get rid of.
We need then to loop over list of frames (frames in the code below) using winfo_children() to seek for the label which has the text variable we are looking for.
Note that because I draw the label before the button inside individual frames, winfo_children() returns as first widget list element the label
winfo_children():
Returns a list containing the path names of all the children of window. Top-level windows are returned as children of their logical
parents. The list is in stacking order, with the lowest window first,
except for Top-level windows which are not returned in stacking order.
Use the wm stackorder command to query the stacking order of Top-level
windows.
This is why it is correct to write : if frame.winfo_children()[0].var == var and destroy the frame that contains the label which satisfies this condition.
Solution
Here is the program. I commented the lines which I think deserve to be commented:
'''
Created on Jun 25, 2016
#author: billal begueradj
'''
from Tkinter import *
import ttk
def removeLabel(var):
global frames
z = -1
# Loop over the list of rames
for frame in frames:
z = z + 1
# Check the text variable of the label of this frame
if frame.winfo_children()[0].var == var:
# Destroy the related frame
frame.destroy()
# Update the size of the list of frames
frames = frames[:z] + frames[z+1:]
# Do not forget to always rest this flag back to -1
z = -1
# Update the labels' numbers
r = 0
for frame in frames:
frame.winfo_children()[0].var.set(r)
r = r + 1
def addNewLabel():
global frames, i
var = IntVar()
frame = Frame(root)
i = i + 1
frame.grid(row=i, column=0)
var.set(len(frames))
l = ttk.Label(frame, textvariable=var)
l.grid(row=0, column=0)
l.var = var
b = ttk.Button(frame, text="Remove", command=lambda: removeLabel(var))
b.grid(row=0, column=1)
frames.append(frame)
if __name__ == '__main__':
root = Tk()
frames = []
i = 1
ttk.Button(root, text="add label", command=addNewLabel).grid(column=0, row=0)
root.mainloop()
Demo
Let us create 6 labels:
Now let us delete the label number 3. You can see that the numbering of the labels is automatically updated:
Now let us add a new label. You can see the newly added label has a number which is consecutive to the last existing label number in the list:
Note that the length of the list is updated all the time as you wanted.