Tkinter : Recording entries dynamically created by buttons - python-3.x

I'm trying to allow the user to enter as many strings as he wants by clicking some sort of '+' button, and keep the strings in a list. (He enters a first string, clicks '+', another entry box appears, etc.)
For the moment, here's what I've got:
def addEntry(window, r, e):
if r < 9:
global entries
entries.append(e.get())
r += 1
e = tk.Entry(window)
e.grid(column=1, row=r)
add = tk.Button(window, text=' + ', command=lambda:addEntry(window, r, e))
add.grid(column=2, row=r, sticky=tk.W)
else:
errmsg = 'Max. 10 items'
tk.Label(window, text=errmsg).grid(column=1, row=r+1)
import tkinter as tk
global entries # the main list of strings
entries = []
r = 0 # index for rows ; will not be 0 in the final code as there will be other widgets
win = tk.Tk()
e = tk.Entry(win)
e.grid(column=1, row=r)
add = tk.Button(win, text=' + ', command=lambda:addEntry(win, r, e))
add.grid(column=2, row=r, sticky=tk.W)
win.mainloop()
This isn't elegant, and the last entry is not recorded.
I've tried making entries a list of Entry() items (not e.get() items), but then I can't access the strings (TclError: invalid command name ".!entry4"). I've tried emulating this, which led me to make entries a global variable ; I've tried using this, but I don't fully understand the first answer, and as far as I can tell the strings aren't recorded ; I've tried adapting the class defined in the second answer, but I wasn't able to add buttons dynamically. (I like the idea of making a class, though.) I feel like I should be able to do this, but after a wasting a day on it, might as well ask for help.

Don't put the value in a list, put the actual widget. You should only call the get method when you actually need the values.
Also, I strongly recommend you put the entries in a dedicated frame. That way you don't have to worry about what other widgets might be in the window, and you don't have to juggle row and column numbers. Put them in a frame and use pack since they are stacked top-to-bottom and all share the same size.
Example:
import tkinter as tk
def addEntry(window):
global entries
if len(entries) > 10:
error_message.configure(text="Max. 10 items")
else:
error_message.configure(text="")
entry = tk.Entry(window)
entry.pack(side="top", fill="x")
entries.append(entry)
def show():
for entry in entries:
print("=>", entry.get())
entries = []
win = tk.Tk()
entry_frame = tk.Frame(win)
error_message = tk.Label(win)
error_message.grid(row=1, column=0, sticky="nsew")
add = tk.Button(win, text=' + ', command=lambda: addEntry(entry_frame))
show = tk.Button(win, text="Show values", command=show)
add.grid(row=0, column=1, sticky=tk.NW)
show.grid(row=0, column=2, sticky=tk.NW)
entry_frame.grid(row=0, column=0, sticky="nsew")
# create the first entry automatically
addEntry(entry_frame)
win.mainloop()

Related

How to retrieve entry value in tkinter from a custom form

Wrote a custom frame class so I can easily generate labels, entries etc.
I can input the number of labels, entries I want each form to have. By creating different instances of the class I don't need to write lots of labels and entries each time.
But when I run the code nothing happens. I get no error to guide me.
If I place a print statement in get_first_entry(self) function then it prints the value from the entry, but I would like to access the value from outside the class.
import tkinter as tk
class MyFrame(tk.Frame):
def __init__(self, parent, num_labels, num_entries, num_buttons, label_names=None, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
# If label names are provided, use them instead of default labels
if label_names is None:
label_names = [f"Label {i+1}" for i in range(num_labels)]
# Create labels and entries
self.entries = []
for i in range(num_labels):
label = tk.Label(self, text=label_names[i])
label.grid(row=i, column=0)
for i in range(num_entries):
entry = tk.Entry(self)
entry.grid(row=i, column=1)
self.entries.append(entry)
# Create buttons
for i in range(num_buttons):
if i == 0:
button = tk.Button(self, text=f"Get RA", command=self.get_first_entry)
elif i == 1:
button = tk.Button(self, text=f"Get DEC", command=self.get_second_entry)
#else:
#button = tk.Button(self, text=f"Button {i+1}")
button.grid(row=num_labels+i, column=0, columnspan=2, pady=2)
def get_first_entry(self):
value = self.entries[0].get()
return value
def get_second_entry(self):
value = self.entries[1].get()
return value
root = tk.Tk()
label_RADEC = ['RA','DEC']
label_Time = ['LST','local time']
# Create first frame
frame1 = MyFrame(root, num_labels=2, num_entries=2, num_buttons=2, label_names=label_RADEC)
frame1.grid(row=0, column=0, padx=10, pady=10)
# Access the first entry
first_entry_value = frame1.get_first_entry()
print(first_entry_value)
root.mainloop()
Updated with method to gel all entries as array.
Now it prints empty array at start, but if I input other values in the entries, still doesn't print anything
'',''
The problem isn't that get_entries isn't working, it's that you're using it in the wrong way.
When you first call get_entries and print the results, it's before the user has had a chance to enter anything so it prints a list of empty strings.
When you call get_entries by pressing the button, it computes the values just fine. However, because it only returns the values, the values are thrown away since the caller (mainloop) ignores the return values of all functions it calls since it doesn't know what to do with them.
You can see this by adding a print statement inside get_entries. When you do that, you can see it's getting the values just fine:
def get_entries(self):
result = [entry.get() for entry in self.entries]
print(f"get_entries result: {result}")
return result
When I insert "this is RA" in the first entry, and "this is DEC" in the second entry, when I press the button I get this output:
get_entries result: ['this is RA', 'this is DEC']
If you leave the return statement in, you can use it to get the values outside of the class. It's just that you must do so after the user has entered values and not before.

making a menu(restaurant menu) in python with the tkinter module

using tkinter with python, i am making a menu that shows what you put in. then can be removed by pressing a button, my question is how do i delete the Labels in the right order. the order should be from top to bottom. the "orders" come in by date, the oldest being at the top and the newest being put at the bottom of the list. By using destroy(), i can only manage to delete the very last label(the newest). i need help to figure out how to delete a specific label.
#imports-----------------------
from tkinter import *
#from PIL import ImageTk,image
from tkinter import messagebox
import time
#imports----------------------
#start-----------
x=1
height=0
l="0"
root = Tk()
root.geometry("300x200")
#start-----------
def myClick():
global x #number of the label
global height
global l #name of the label
global vartoprint
global y #to retain the old value of x
if x == 1:
global statehold
statehold=1
l=str(statehold)
vartoprint="order one"
y=1
if x > y: #when x is different than the old value of y, we assume the state has gone up
#and therefor a new entry has come, time to print this new value
height=height+1
statehold= statehold + 1
l=str(statehold)
vartoprint="order 'xxx'" #in this case, the new value is always "order xxx" for testing
l = Label(root, text=vartoprint)
l.grid(row=height, column=L)
x=x+1
mybutton = Button(root, text="next", padx=10, pady=8, command=myClick, fg="black", bg="white")
mybutton.grid(row=1, column=5)
def mydelete():
global statehold
global x
global l
statehold= statehold - 1 #tells the system that 1 label has been removed
l.destroy() #destroys the label
x=x-1 #tells the system that 1 label has been removed
DeleteButton = Button(root, text="next and delete", command=mydelete)
DeleteButton.grid(row=1, column=6)
#end-------------
time.sleep(3)
root.mainloop()
#end-------------
thanks using a list and deleting the first list item, i am able to do my task.
I use
my_list.append(l)
to insert the variable at the end of the list.
and like you said, i use
(my_list.pop(0)).destroy()
do remove the first item in the list.
thanks, you were great!

Python 3 Radio button controlling label text

I am in the process of learning Python3 and more of a necessity, the TkInter GUI side. I was working my way through a book by James Kelly, when I encountered this problem. All his examples made a new window with just label/canvas/check box etc which seemed to work OK.
But as I wanted to experiment in a more real world scenario I put most things on one window. This where I encountered my problem. I can not get the radio button in the frame to alter the wording of a label in the parent window.
Complete code is:-
#! /usr/bin/python3
from tkinter import *
def win_pos(WL,WH,xo=0,yo=0) :
# Screen size & position procedure
# Screen size
SW = home.winfo_screenwidth()
SH = home.winfo_screenheight()
# 1/2 screen size
sw=SW/2
sh=SH/2
# 1/2 window size
wl=WL/2
wh=WH/2
# Window position
WPx=sw-wl+xo
WPy=sh-wh+yo
# Resulting string
screen_geometry=str(WL) + "x" + str(WH) + "+" + str(int(WPx)) + "+" \ + str(int(WPy))
return screen_geometry
# Create a window
home=Tk()
home.title("Radio buttons test")
# Set the main window
home.geometry(win_pos(600,150))
lab1=Label(home)
lab1.grid(row=1,column=1)
fraym1=LabelFrame(home, bd=5, bg="red",relief=SUNKEN, text="Label frame text")
fraym1.grid(row=2,column=2)
laybl1=Label(fraym1, text="This is laybl1")
laybl1.grid(row=0, column=3)
var1=IntVar()
R1=Radiobutton(fraym1, text="Apple", variable=var1, value=1)
R1.grid(row=1, column=1)
R2=Radiobutton(fraym1, text="Asus", variable=var1, value=2)
R2.grid(row=1, column=2)
R3=Radiobutton(fraym1, text="HP", variable=var1, value=3)
R3.grid(row=1, column=3)
R4=Radiobutton(fraym1, text="Lenovo", variable=var1, value=4)
R4.grid(row=1, column=4)
R5=Radiobutton(fraym1, text="Toshiba", variable=var1, value=5)
R5.grid(row=1, column=5)
# Create function used later
def sel(var) :
selection="Manufacturer: "
if var.get() > 0 :
selection=selection + str(var.get())
lab1.config(text=selection)
R1.config(command=sel(var1))
R2.config(command=sel(var1))
R3.config(command=sel(var1))
R4.config(command=sel(var1))
R5.config(command=sel(var1))
R1.select()
mainloop()
I realise that there is room for improvement using classes/functions but I need to get this resolved in my head before I move on. As it can be hopefully seen, I'm not a complete novice to programming, but this is doing my head in.
Can a solution, and reasoning behind the solution, be given?
You can modify your label's text by assigning the same variable class object, var1 as its textvariable option as well but since lab1's text is slightly different, try removing:
R1.config(command=sel(var1))
R2.config(command=sel(var1))
R3.config(command=sel(var1))
R4.config(command=sel(var1))
R5.config(command=sel(var1))
R1.select()
and modify sel to:
def sel(*args) :
selection="Manufacturer: "
selection=selection + str(var1.get())
lab1.config(text=selection)
and then call var1.trace("w", sel) somewhere before mainloop as in:
...
var1.trace("w", sel)
mainloop()
Also for a simple example:
import tkinter as tk
root = tk.Tk()
manufacturers = ["man1", "man2", "man3", "man4", "man5"]
lbl = tk.Label(root, text="Please select a manufacturer.")
lbl.pack()
# create an empty dictionary to fill with Radiobutton widgets
man_select = dict()
# create a variable class to be manipulated by radiobuttons
man_var = tk.StringVar(value="type_default_value_here_if_wanted")
# fill radiobutton dictionary with keys from manufacturers list with Radiobutton
# values assigned to corresponding manufacturer name
for man in manufacturers:
man_select[man] = tk.Radiobutton(root, text=man, variable=man_var, value=man)
#display
man_select[man].pack()
def lbl_update(*args):
selection="Manufacturer: "
selection=selection + man_var.get()
lbl['text'] = selection
#run lbl_update function every time man_var's value changes
man_var.trace('w', lbl_update)
root.mainloop()
Example with label's identical to that of radiobutton's value:
import tkinter as tk
root = tk.Tk()
# radiobutton group will the button selected with the value=1
num = tk.IntVar(value=1)
lbl = tk.Label(root, textvariable=num)
zero = tk.Radiobutton(root, text="Zero", variable=num, value=0)
one = tk.Radiobutton(root, text="One", variable=num, value=1)
#display
lbl.pack()
zero.pack()
one.pack()
root.mainloop()

Passing OptionMenu into a callback (or retrieving a reference to the used widget)

I'm working on a (toplevel in a) GUI that consists of an array of 8 OptionMenus, each of them containing the same option list. Currently, Im building these widgets using a for-loop, and I save references in a dictionary. All OptionMenus link to the same (lambda) callback function.
To stay practical: the items in the option list represent a sequence of processing steps, and the user can alter the order of processes.
A change in one of the lists will result in one process being executed twice, and one process not at all. However, I want each item to occur only once. Hence, each user input should be accompanied by a second OptionMenu alteration.
For example: initial order 1-2-3 --> user changes the second process: 1-3-3, which autocorrects to: 1-3-2, where each process is again executed only once.
To my understanding, I can only get this to work if I have a reference to the OptionMenu that was just altered (from within the callback function). I was looking into passing the widget into the callback. The sample code is an attempt to implement the second suggested method, but the result is not what I would have expected.
The thing is that the OptionMenu widget seems to behave somewhat differently from other widgets. The OptionMenu does not allow for a re-defintion of the command function. No matter what input I pass along with the command function, the callback only seems to retrieve the OptionMenu selection, which is insufficient information for me to determine my process order.
Suggestions would be much apreciated!
import tkinter as tk
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W)
self.create_widgets()
def create_widgets(self):
self.active_procs = ['proc 1','proc 2','proc 3','proc 4',
'proc 5','proc 6','proc 7','proc 8']
itemnr, widgets = dict(), dict()
for index in range(8):
name_construct = 'nr' + str(index)
itemnr[name_construct] = tk.StringVar(root)
itemnr[name_construct].set(self.active_procs[index])
widgets[name_construct] = tk.OptionMenu(self, itemnr[name_construct], *self.active_procs,
command=lambda widget=name_construct:
self.order_change(widget))
widgets[name_construct].grid(row=index+2, column=2, columnspan=2,
sticky="nwse", padx=10, pady=10)
def order_change(self,widget):
print(widget)
root = tk.Tk()
root.title("OptionMenu test")
app = Application(master=root)
root.mainloop()
The OptionMenu will pass the new value to the callback, so you don't have to do anything to get the new value. That's why your widget value isn't the value of name_construct -- the value that is passed in is overwriting the default value that you're supplying in the lambda.
To remedy this you simply need to add another argument so that you can pass the value of name_construct to the callback to go along with the value which is automatically sent.
It would look something like this:
widgets[name_construct] = tk.OptionMenu(..., command=lambda value, widget=name_construct: self.order_change(value, widget))
...
def order_change(self, value, widget):
print(value, widget)
Note: the OptionMenu isn't actually a tkinter widget. It's just a convenience function that creates a standard Menubutton with an associated Menu. It then creates one item on the menu for each option, and ties it all together with a StringVar.
You can get the exact same behavior yourself fairly easily. Doing so would make it possible to change what each item in the menu does when selected.
For those interested, below you can find an example code of how I got the widget behaviour I wanted. I took Bryan's advice to replace the OptionMenu for a Menubutton/Menu combination. I also made use of this post to find duplicate entries in my process order list.
Any thoughts or suggestions on how to implement this in a cleaner or shorter way, or how to get the same functionality with a different interface (e.g. drag and drop), are ofcourse welcome!
import tkinter as tk
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W)
self.create_widgets()
def create_widgets(self):
# Assisting text
l1 = tk.Label(self, text = "Data in", font=(None, 15))
l1.grid(row=0, column=2)
l2 = tk.Label(self, text = u'\N{BLACK DOWN-POINTING TRIANGLE}', font=(None, 15))
l2.grid(row=1, column=2)
l3 = tk.Label(self, text = "Data out", font=(None, 15))
l3.grid(row=11, column=2)
l4 = tk.Label(self, text = u'\N{BLACK DOWN-POINTING TRIANGLE}', font=(None, 15))
l4.grid(row=10, column=2)
# Process list
self.active_procs = ['proc a','proc b','proc c','proc d',
'proc e','proc f','proc g','proc h']
self.the_value, self.widgets, self.topmenu = dict(), dict(), dict()
for index in range(8):
name_construct = 'nr' + str(index)
self.the_value[name_construct] = tk.StringVar(root)
self.the_value[name_construct].set(self.active_procs[index])
self.widgets[name_construct] = tk.Menubutton(self, textvariable=
self.the_value[name_construct],
indicatoron=True)
self.topmenu[name_construct] = tk.Menu(self.widgets[name_construct],
tearoff=False)
self.widgets[name_construct].configure(menu=self.topmenu[name_construct])
for proc in self.active_procs:
self.topmenu[name_construct].add_radiobutton(label=proc, variable=
self.the_value[name_construct],
command=lambda proc=proc,
widget=name_construct:
self.order_change(proc,widget))
self.widgets[name_construct].grid(row=index+2, column=2, columnspan=2,
sticky="nwse", padx=10, pady=10)
def order_change(self,proc,widget):
# Get the index of the last changed Menubutton
index_user_change = list(self.widgets.keys()).index(widget)
procs_order = [] # Current order from widgets
for index in range(8):
name_construct = 'nr' + str(index)
procs_order.append(self.widgets[name_construct].cget("text"))
# 1 change may lead to 1 double and 1 missing process
doubles = self.list_duplicates_of(procs_order,proc)
if len(doubles) == 2: # If double processes are present...
doubles.remove(index_user_change) # ...remove user input, change the other
missing_proc = str(set(self.active_procs)^set(procs_order)).strip('{"\'}')
index_change_along = int(doubles[0])
# Update references
self.active_procs[index_user_change] = proc
self.active_procs[index_change_along] = missing_proc
# Update widgets
name_c2 = 'nr'+str(index_change_along)
self.the_value[name_c2].set(self.active_procs[index_change_along])
self.widgets[name_c2].configure(text=missing_proc)
def list_duplicates_of(self,seq,item):
start_at = -1
locs = []
while True:
try:
loc = seq.index(item,start_at+1)
except ValueError:
break
else:
locs.append(loc)
start_at = loc
return locs
root = tk.Tk()
root.title("OptionMenu test")
app = Application(master=root)
root.mainloop()

Imposible change text in a tkinter label

I'm making an editor with tkinter, I've made a status bar and i have 2 labels to show the actual line and column, and the total number of lines but the text of the labels don't change anything.
My code is very very long to show here.
I figure out the line, column and lines with these 2 functions:
def get_position(self, event=None):
"""get the line and column number of the text insertion point"""
self.line = tk.StringVar()
self.column = tk.StringVar()
self.line, self.column = self.textView.index('insert').split('.')
self.s = tk.StringVar()
self.s.set(('Line : {0} - Column : {1}'.format(self.line, self.column)))
print(self.s)
return self.s
def getwindowlines(self, event=None):
self.numberoflines = int(self.textView.index('end-1c').split('.')[0])
return self.numberoflines
And the function of my status bar is the next one:
def statusBar(self):
self.frameStatus = tk.Frame(self.master, border=2, bg='#272822',
relief='sunken')
self.frameStatus.pack(side='bottom', after=self.toolbar,
fill='x', padx=5, pady=1)
numberoflinestxt = str(self.getwindowlines())
self.labelNumberOfLines = tk.Label(self.frameStatus,
text='Lines: {0} '.format(numberoflinestxt))
self.labelNumberOfLines.configure(bg='#272822', fg='white')
self.labelNumberOfLines.pack(side='right', fill='x', padx=10, pady=2)
self.labelLinePosition = tk.Label(self.frameStatus,
textvariable=self.get_position())
self.labelLinePosition.configure(bg='#272822', fg='white')
self.labelLinePosition.pack(side='left', fill='x', padx=10, pady=2)
All the code is in Github Code Link in the file IdlePlus.py
With print console all works fine, but with a Label the numbers of lines and columns don't change.
Thanks
Your code doesn't seem to be trying to change the labels in the statusbar. I don't understand why you think they should change. In your get_position function you're creating new StringVars each time it is called.
I wouldn't use StringVars at all here, though you can. If you want to use them, you create them exactly once and associate them with Label widgets, and then whenever you want the labels to change, you change the variables. If you want to use .format(...), you have to call that when you change the values, not when you create the label.
For example:
def statusBar(self):
...
self.line = tk.StringVar()
self.column = tk.StringVar()
self.labelLinePosition = tk.Label(self.frameStatus,
textvariable=self.self.column)
self.labelLinePosition = tk.Label(self.frameStatus,
textvariable=self.column)
...
def get_position(self):
line, column = self.textView.index('insert').split('.')
self.line.set("Line: {0}".format(line))
self.column.set("Position: {0}".format(column)
That will cause the labels to update every time get_position is called.
However, there's really no need for the special StringVars. You can directly set the text of the label, eliminating a couple of objects and thus reducing the complexity of your code slightly:
def statusbar(self):
...
self.labelNumberOfLines = tk.Label(self.frameStatus)
self.labelLinePosition = tk.Label(self.frameStatus)
...
def get_position(self):
line, column = self.textView.index('insert').split('.')
self.labelNumberOfLines.configure(text="Lines: {0}".format(lines))
self.labelLinePosition.configure(text="Character: {0}".format(column))

Resources