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]
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'm currently trying to (re-)create a card game with Python 3.10 and Tkinter. I now want to code a hand of cards which is drawn dynamically each round. The number of cards drawn may vary (value is stored in drawHandCalc()). My problem is this: I want every card (a button on screen) to be named the correct item from the list of cards (the deck), this is what I got so far:
drawing_deck = starting_deck
random.shuffle(drawing_deck)
i = 0
while i < drawHandCalc():
drawnCard = tk.StringVar()
drawnCard.set(drawing_deck[i])
print(drawing_deck[i])
handcardVar = Button(root, text=str(drawnCard), padx=30, pady=80)
handcardVar.grid(row=3, column=3+i)
i += 1
I want to access item [i] in the list drawing_deck and use it as the label for the button. The print in console works fine, but on the UI, the buttons are labeled PY_VAR1 , PY_VAR2, PY_VAR3 and so on. Why does this happen and how can I get Tkinter to use the string item from the list drawing_deck?
Solved!
As jasonharper pointed out, I used the name of the StringVar, not the contents. The code below works as intended!
while i < drawHandCalc():
drawnCard = tk.StringVar()
drawnCard.set(drawing_deck[i])
print(drawing_deck[i])
handcardVar = Button(root, text=drawnCard.get(), padx=30, pady=80)
handcardVar.grid(row=3, column=3+i)
i += 1
Is there anyway I can plug my list of integers into my list of entry boxes? The list of integers is constantly changing..
This would not be a problem if the list of integers and list of entry boxes had the same number of data points, however I can't determine that initially because I want user input to determine this entry list length in future code. I've tried using Insert to solve this problem, to no avail, given that I couldn't use the index of entry to configure its text option.
from tkinter import *
def entry_list_extender():
entrylist.extend(number)
gx=10
number=0
root=Tk()
frame=Frame(root)
frame.pack()
entry=[]
entrylist=[1,2,3,4]
var = []
entrybox=Entry(frame,bg='blue',textvariable=number)
entrybox.pack()
button=Button(frame,bg='red',command=entry_list_extender)
button.pack()
for i in range(gx):
entry.append(Entry(frame, textvariable=entrylist[i]))
entry[-1].pack()
root.mainloop()
A solution or path I could take to get the results I want would be appreciated.
Edit: my original question was quite ambiguous. This should make it more clear
UPDATE:
I am going to have to make an assumption here to make this work.
I am assuming that gx is the user defined variable you want to use down the road.
If that is the case then you need to change you your code a bit to re-create the entry fields when you press the button and also use the value of gx to decide on how many entry fields you should use.
Let me know if this is closer to what you are trying to do as it is still not very clear what your goal is.
from tkinter import *
root=Tk()
gx=10
number=0
entry=[]
entrylist=[1, 2, 3, 4]
var = []
def entry_list_extender():
global frame, entrylist, entry
entry = []
entrylist = []
for i in range(gx):
entrylist.append(i)
frame.destroy()
create_entry_fields()
entrybox=Entry(root, bg='blue', textvariable = number)
entrybox.pack()
button=Button(root, bg='red', command = entry_list_extender)
button.pack()
def create_entry_fields():
global frame, entrylist, entry
frame = Frame(root)
frame.pack()
print (len(entrylist))
for i in range(len(entrylist)):
entry.append(Entry(frame, textvariable = i))
entry[-1].pack()
create_entry_fields()
root.mainloop()
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.
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.