Ever expanding amount of Entries with TKinter - python-3.x

What I'm trying to do is to make a GUI where when you start typing in an entry-box another shows up just beneath the one you are typing in. Then when you start typing in the one that popped up, another pops up. Is this possible with TKinter and Python?
Edit:
So what I currently have is this:
entry1 = StringVar()
numberLabel3 = Label(window, text = "3. External meeting attendees")
r+=1
numberLabel3.grid(column = 0, row = r, sticky = W)
externalAtendeesLabel = Label(window, text = "input name of external meeting atendee: ")
r+=1
externalAtendeesLabel.grid(column = 1, row = r, sticky = E)
externalAtendeesEntry = Entry(window, textvariable = entry1)
externalAtendeesEntry.grid(column = 2, row = r)
#Note to self: Find a smart way of dynamically expanding this "list" of entries
(There is more code above and below this, but this is the relevant code for my question)
where r is a variable I made to make it easier to insert stuff into the middle of my rather long code.
The imports I'm using are:
from tkinter import *
from PIL import ImageTk
from PIL import Image
import os
I use the image modules and OS to insert an image further up in my GUI.
What I was thinking was to make a function that I could somehow setup to check the newest Entry-box, but I've run into the problem that for this to be potentially infinite I would have to dynamically create new variables, so that I can access the information that the user inputs. These variables would save the info just like my entry1 variable does it for the externalAtendeesEntry.
I would also have to dynamically make variables for more entries.
How do I dynamically create a potentially infinite amount of variables?
I know that this is kind of a re-post, but the other ones I've found all say that you should use dictionaries, but in that case it can't be infinite. It can only be finite to the point where my dictionary is no longer.

For one, you don't need to use StringVar. It only complicates your code without providing any real value. The other part of the answer is to store the entries in a list.
For example, create a function called addEntry that creates an entry and adds it to a list:
entries = []
...
def addEntry():
entry = tk.Entry(...)
entry.pack(...)
entries.append(entry)
To get the values at a later date, just iterate over the list:
for entry in entries:
print(entry.get())
With that, you can add entries whenever you want. You could, for example, bind to <Any-KeyRelease> to create a new entry as the user types (being sure to only do it if there isn't already a blank entry). Or, bind to <Return> or <FocusOut>, or on the click of a "new person" button, or however else you decide.

Related

How do I dynamically set the name of a tkinter label

I have this code in my project:
def savespack():
savelabelcount = 0
for i in range(saveamount):
savelabelcount = savelabelcount + 1
savebuttontext = "Save " + str(savelabelcount)
Button(scrollable_frame, text=savebuttontext, width="53", height="5").pack()
The user can create multiple saves in the program, and when the project is reopened, the correct amount of save files will be shown as Tkinter buttons. I've created the buttons by reading a number from an external file. I need to make the buttons have different variable names to call the correct save file and load the correct properties when a button is pressed (for example "Save 2" needs to load save2.txt's properties)
Simply put I need to give Button(scrollable_frame, text=savebuttontext, width="53", height="5").pack() a new variable name for each iteration of the loop, I dont know how to do this. I read something about dictionaries and lists but it was very confusing and I dont know if it can be applied in this situation.
The variable names should be something like: savebutton1, savebutton2, savebutton3 etc.
Any way I could do this would be appreciated, thanks!
The example below shows how to save each button widget inside a list rather than as separate variables.
It also will call the saveFile function when the button is pressed which currently just prints to the screen the save file number but this function could be replaced with the code that actually saves each file. With this method, it might not even be necessary to keep the buttons in a list.
I'd normally do this using classes rather than globals but this will work for your purposes
import tkinter as tk
saveamount = 5
save_buttons = []
def saveFile(fileNumber):
print("Saving File ", fileNumber)
def savespack():
global save_buttons
savelabelcount = 0
for i in range(saveamount):
savelabelcount = savelabelcount + 1
savebuttontext = "Save " + str(savelabelcount)
currentButton = tk.Button(scrollable_frame, text=savebuttontext, width="53", height="5")
currentButton['command'] = lambda x=savelabelcount: saveFile(x)
currentButton.pack()
save_buttons.append(currentButton)
root = tk.Tk()
scrollable_frame = tk.Frame(root)
scrollable_frame.pack()
savespack()
root.mainloop()
You could use a dictionary rather than a list but given your use case, I'm not sure there is any benefit. Just use save_buttons[2] to access the third button.

populating one combobox based on another combo box using tkinter python

from tkinter import *
from tkinter.ttk import Combobox
v1=[]
root = Tk()
root.geometry('500x500')
frame1=Frame(root,bg='#80c1ff',bd=5)
frame1.place(relx=0.5,rely=0.1,relwidth=0.75,relheight=0.1,anchor='n')
lower_frame=Frame(root,bg='#80c1ff',bd=10)
lower_frame.place(relx=0.5,rely=0.25,relwidth=0.75,relheight=0.6,anchor='n')
v=[]
def maincombo():
Types=["MA","MM","MI","SYS","IN"]
combo1=Combobox(frame1,values=Types)
combo1.place(relx=0.05,rely=0.25)
combo2=Combobox(frame1,values=v)
combo2.bind('<<ComboboxSelected>>', combofill)
combo2.place(relx=0.45,rely=0.25)
def combofill():
if combo1.get()=="MA":
v=[1,2,3,45]
combo2=Combobox(frame1,values=v)
combo2.place(relx=0.45,rely=0.25)
if combo1.get()=="MM":
v=[5,6,7,8,9]
combo2=Combobox(frame1,values=v)
combo2.place(relx=0.45,rely=0.25)
maincombo()
root.mainloop()
I want to populate the one combobox based on selection of other combobox I,e types.But failed to do so with simple functions.
Looking at you code, most of what you need is already there. The changes I have made are as follows:
Bound to combo1 rather than combo2 (as combo1 is the one you want to monitor)
Set combo1 and combo2 as global variables (so they can be used in the combofill method)
Set the combofill method to accept the event arg (it would raise a TypeError otherwise)
Use the .config method on combo2 rather than creating a new one each time
Set combo2 to be empty when neither "MA" or "MM" are selected
Here is my implementation of that:
from tkinter import *
from tkinter.ttk import Combobox
v1=[]
root = Tk()
root.geometry('500x500')
frame1=Frame(root,bg='#80c1ff',bd=5)
frame1.place(relx=0.5,rely=0.1,relwidth=0.75,relheight=0.1,anchor='n')
lower_frame=Frame(root,bg='#80c1ff',bd=10)
lower_frame.place(relx=0.5,rely=0.25,relwidth=0.75,relheight=0.6,anchor='n')
v=[]
def maincombo():
global combo1, combo2
Types=["MA","MM","MI","SYS","IN"]
combo1=Combobox(frame1,values=Types)
combo1.place(relx=0.05,rely=0.25)
combo1.bind('<<ComboboxSelected>>', combofill)
combo2=Combobox(frame1,values=v)
combo2.place(relx=0.45,rely=0.25)
def combofill(event):
if combo1.get()=="MA":
v=[1,2,3,45]
elif combo1.get()=="MM":
v=[5,6,7,8,9]
else:
v=[]
combo2.config(values=v)
maincombo()
root.mainloop()
A couple other ideas for potential future consideration:
I would recommend using the grid manager rather than the place manager as it will stop widgets overlapping, etc. (on my system, combo2 slightly covers combo1)
Use a dictionary rather than if ... v=... elif ... v= ... and then use the get method so you can give the default argument. For example:
v={"MA": [1,2,3,45],
"MM": [5,6,7,8,9]}. \
get(combo1.get(), [])
EDIT:
Responding to the question in the comments, the following is my implementation of how to make a "toggle combobox" using comma-separated values as requested.
As the combobox has already overwritten the value of the text area when our <<ComboboxSelected>> binding is called, I had to add a text variable trace so we could keep track of the previous value of the text area (and therefore append the new value, etc.). I am pretty sure that explanation is completely inadequate so: if in doubt, look at the code!
from tkinter import *
from tkinter.ttk import Combobox
root = Tk()
def log_last():
global last, cur
last = cur
cur = tx.get()
def append_tx(event):
if last:
v = last.split(",")
else:
v = []
v = list(filter(None, v))
if cur in v:
v.remove(cur)
else:
v.append(cur)
tx.set(",".join(v))
combo.selection_clear()
combo.icursor("end")
last, cur = "", ""
tx = StringVar()
combo = Combobox(root, textvariable=tx, values=list(range(10)))
combo.pack()
combo.bind("<<ComboboxSelected>>", append_tx)
tx.trace("w", lambda a, b, c: log_last())
root.mainloop()

Efficiently create tkinter images using a loop

Currently I loop through a list of image names and make them into a tkinter PhotoImage. These are saved as variables under their respective names from the list using vars()[var].
In this case vars()[var] represents the value of the item in a list making it into a variable name for the images.
example shown below:
list = ["a","b","c","d"] #and so on...
image_id = []
for x in range((len)list):
var = list[x]
vars()[var] = tk.PhotoImage(file = var + ".gif")
image_id.append(vars()[var]) #this adds the identity of all photoimages to a list for later use
According to my tests I can use the images via the variable names or identities as long as it is within the same function. This is due to the variables being local.
The problem is that even though the image identities are in a list they do not work when used outside of the original function. I believe this happens because the images are tied to their variable names and since those are all local tkinter garbage collects them resulting in my error which is "pyimagenumber does not exist." This is because the identity for a tkinter image is just "pyimage" and its corresponding number. My line of thinking is that I want to make all variable names that store images to be global without needing an extra line per image since that defeats the purpose of using a loop and list. Is there any way i can set the "vars()[var]" to be global while also giving it a value?
For any solutions I would like to avoid fundamental changes or using pillow.
Any help is appreciated and if you have further questions about context please ask!
Simply move your empty list outside of your function. Also instead of for x in range(len(list)), use for x in something instead. It could be something like this:
import tkinter as tk
root = tk.Tk()
image_id = []
def image_creation():
imlist = ["a","b","c","d"] #don't name it as list - it shadows the built in list
for var in imlist:
image_id.append(tk.PhotoImage(file = var + ".gif"))
... #the rest of your code
root.mainloop()
There's no need to use vars(). Just create a dictionary in the global namespace and add your images to it. You can even use the original item from the list as the key.
Example:
global images
...
list = ["a","b","c","d"]
images = {}
for x in list:
images[x] = tk.PhotoImage(file = var + ".gif")
Then, the image for "a" can be accessed at any time as images['a'].

Problem: Can only check multiple checkbuttons together?

I would like to generate checkbuttons for multiple items. Due to the repetition, I used a loop to initialize the widget and the default state, saving the BooleanVar() and widgets in separate lists. But by doing so, I can only check either check or uncheck all of them together.
I already tried to set different value to the BooleanVar in the list from within the loop, but to no avail.
ckbtnVarList = [tk.BooleanVar()]*len(ckbtnDict["Tag"])
ckbtnWdtList = [None]*len(ckbtnDict["Tag"])
for idx in range(len(ckbtnDict["Tag"])):
ckbtnVarList[idx].set(1)
ckbtnWdtList[idx]=ttk.Checkbutton(mainfrm, text=ckbtnDict["Text"][idx], variable=ckbtnVarList[idx]).pack()
As specified in the comments above, you need to create your list of BooleanVar's with a list comprehension or a list. The below code shows how to do this.
Since you didn't provide a complete code example, I've had to make some assumptions about your input data.
import tkinter as tk
ckbtnDict = {}
ckbtnDict['Tag'] = ["Tag1","Tag2","Tag3"]
ckbtnDict["Text"] = ["Txt1","Txt2","Txt3"]
mainfrm = tk.Tk()
ckbtnVarList = [tk.BooleanVar() for i in range(len(ckbtnDict["Tag"]))]
ckbtnWdtList = [None for i in range(len(ckbtnDict["Tag"]))]
for idx in range(len(ckbtnDict["Tag"])):
ckbtnVarList[idx].set(1)
ckbtnWdtList[idx]=tk.Checkbutton(mainfrm, text=ckbtnDict["Text"][idx], variable=ckbtnVarList[idx])
ckbrnWdtList[idx].pack()
mainfrm.mainloop()

Tkinter Entry validatation

I am writing a program in Python 3.6 using Tkinter where a customer has multiple(11) entry fields. I want these entry fields to only accept integers and also be able to define the maximum amount of characters.
I already have a function that does this. But this function only works for one entry field. I have tried entering variables with calling the function so it changes another entry field for example. I was not able to do this.
This is the function I have that works with 1 entry field.
def limitMuntgeld(self, *args):
value = self.invoerM.get()
if len(value) > 5:
self.invoerM.set(value[:5])
if value.lower() in "abcdefghijklmnopqrstuvwxyz-=[];/":
self.invoerM.set(value[:0])
This is the example entry field code that works with the function
self.invoerMuntgeld = Entry(self, font=('Arial', 14), textvariable=self.invoerM)
This is combined with a trace on the entry field posted below.
self.invoerM = StringVar()
self.invoerM.trace('w', self.limitMuntgeld)
I have also tried it with vcmd and validatecommand. However, no good results.
My endresult would be one function working with all entry fields. If anyone has any suggestions, I am all ears!
The proper way to do entry validation is with the validatecommand option rather than using trace. With the validation feature built into the widget you don't need a reference to the widget itself (though you can use it if you want).
When the validatecommand is run, you can have it pass in what the new value will be if the input is valid. You only need to check this value and then return True or False, without having to know which widget it applies to.
For example:
import tkinter as tk
def validate_input(new_value):
valid = new_value .isdigit() and len(new_value) <= 5
return valid
root = tk.Tk()
validate = root.register(validate_input)
for i in range(10):
entry = tk.Entry(root, validate="key", validatecommand=(validate, "%P"))
entry.pack(side="top", fill="x")
root.mainloop()
For information about what %P represents, and what else can be used as arguments to the command, see this question: Interactively validating Entry widget content in tkinter

Resources