Python Tkinter :removing widgets that were created using a for loop - python

I'm currently learning how to use the Tkinter library on python to create a GUI that takes in longitude and latitude points and outputing that into a file. Basically I'm trying to automate the process of having to copy the correct format of line of points to another file.
So I created a Entry and button field to see how many long/lat points are needed to generate a 'shape'. Using this integer input from user, I have a for loop to populate the GUI with multiple widgets asking for the long/lat points. I have that working properly, but now I am trying to have a clear button, which would allow the user to clear all these long/lat points and give them the ability to repopulate the field with the amount of points the other shape requires.
So far I have:
def clearGrid():
coordAmount = int(pointText.get())
latLabel.grid_forget()
longLabel.grid_forget()
.....(contains code that populates the GUI)
#creating clear site Button
clearButton = Button(main_gui, text="Clear Sites!",command=clearGrid)
clearButton.grid(row=lastRow+1, column=5, pady=10)
However, the problem that I am running into is that when the clear button is clicked, it only clears the latest instance of the widgets not all of them. So in a for loop that creates 5 instances/iteration of widgets, it will remove only the 5th instance/iteration of widgets.
I'm trying to have the clear button functionality be able to delete all 5 instances of these widgets.
So here is a shortened code of how I am populating the GUI with widgets
def generatePoints():
for x in range(0,3):
degLong_label = Label(main_gui, text="Degree:", height=2)
degLong_label.grid(row=y,column=6,sticky=E)
degLong = Entry(main_gui, width=4)
degLong.grid(row=y,column=7,sticky=W)
#minute
minLong_Label = Label(main_gui,text="Minutes:", height=2)
minLong_Label.grid(row=y,column=8,sticky=W)
minLong = Entry(main_gui,width=3)
minLong.grid(row=y,column=8,sticky=E)
#seconds
secLong_Label= Label(main_gui,text="Sec:",height=2)
secLong_Label.grid(row=y,column=9,sticky=W,padx=20)
secLong = Entry(main_gui,width=3)
secLong.grid(row=y,column=9,sticky=E,padx=20)
#direction
dirLong_Label = Label(main_gui,text="Direction:",padx=5,height=2)
dirLong_Label.grid(row=y,column=12,sticky=W)
dirLong = Entry(main_gui,width=3)
dirLong.grid(row=y,column=13)

You need to hold on to references to all those widgets, usually via a list. Try initializing a list (list_of_widgets) before your loop, then every time you create a widget, append it to that list. When you clear, you can iterate through that list of widgets and destroy each one. Once you're done clearing them, you can clear the list so you don't try to destroy a widget twice (Tkinter will error at that point).
def generatePoints():
list_of_widgets = [] # or take the list as a parameter
for x in range(3):
degLong_label = Label(...)
degLong_label.grid(...)
list_of_widgets.append(degLong_label)
degLong = Entry(...)
degLong.grid(...)
list_of_widgets.append(degLong)
# et al.
def clearGrid(list_of_widgets):
for widget in list_of_widgets:
widget.destroy()
Note that you probably want to actually destroy the widgets if you aren't planning on showing that specific widget again (initializing a new one doesn't count).

Related

How to update layers of tkinter widgets dynamically?

Tkinter Requirements
So I am relatively new to using tkinter and I am struggling with a very specific doubt here. I tried finding solutions to this but as much as I find it obvious, the solution to this doesn't seem to be easy to understand. So if you can see the image above, I am trying to create a GUI for a particular project which requires multi-layer (I am calling it 3D array based) widgets.
Let's say the variables used for this pointer system are i, j, and k.
I am creating individual layer widgets using for loop:
for n in range(i):
frame_x[i] = Frame(root).grid(row = 1, column = i)
entry_x[i] = Entry(frame_x[i]).grid(row = 2, column = i)
button_x[i] = Button(frame_x[i]).grid(row=3, column = i)
Please note this is not a functional code, I have tried to keep it to the point just to give an idea of the method I am using. (Let me know if you want a more detailed code block.)
Now coming to the problem. I am able to do the basic part of this. But the problem is that I want it to work dynamically.
Let's say if the user enters j = 4 first. 4 blocks will be created.
Later if he changes the value to j = 2 and the presses the button, ideally it should make the widgets at block j= 3 and 4 disappear. But I guess tkinter works on overlapping basis and doesn't change a grid element until something is specifically overlapped over it. How do I do that. I tried destroying the entire frame just after entering the for loop, but that doesn't work as for the first time no widget is created before destroying and python throws NameError saying I can't use a variable before assignment.
Anyways, please let me know how do I do this efficiently.
And also in general, if there is a better way to go about the whole thing. Please refer the image above and let me know if it doesn't make sense.
I am not very comfortable with classes in general. I prefer the inefficient way by only using functions to do everything I have to. So it would be great if you can share me a framework without using classes. But its okay if you use them. I know I should start working with classes at some point.
First off, I want to address this part of the question:
I guess tkinter works on overlapping basis and doesn't change a grid element until something is specifically overlapped over it.
I'm not entirely sure what you mean by that, but if it means what I think it means, it is a false statement. tkinter doesn't "work on an overlapping basis". If you destroy a widget, it is destroyed. It doesn't matter if it's overlapped or not.
Based on the tiny bit of code you posted, the main problem is that you aren't putting the entry and button in the frame. Because of that, they are not destroyed when you destroy the frame.
The reason you aren't putting the widgets into the frame is because of this line:
frame_x[i] = Frame(root).grid(row = 1, column = i)
In python, when you do x=y().z(), x has the value of z(). Thus, when you do frame_x[i] = Frame(...).grid(...), frame_x[i] has the value of .grid(...), and .grid(...) always returns None. Thus, frame_x[i] will be None.
When you next do entry_x[i] = Entry(frame_x[i]).grid(...), it's the same as doing entry_x[i] = Entry(None).grid(...). Because the master of the Entry is None, it becomes a child of the root window.
So, the first step is to separate the creation of the widget from the layout of the widget.
frame_x[i] = Frame(root)
frame_x[i].grid(row = 1, column = i)
Once you do that, the Entry and Button widgets will become a child of the frame, and you can remove widgets you don't want by destroying the frame (eg: frame_x[i].destroy()), since destroying a widget will also cause all children of the widget to be destroyed.
Once you have that in place, you can destroy unwanted widgets by simply calling .destroy() on the frame. For example, if you have previously created 10 groups and now need only 5, you can destroy the others and then remove them from the list like this:
# assume 'num' contains the number of frames that we want,
# and that it is smaller than the number of items in frames_x
for frame in frames_x[num:]:
frame.destroy()
frames_x = frames_x[:num]
Here is a complete working program to illustrate. Enter a number and click the button. It will create that many frame+entry+button combinations. Enter a new number that is larger or smaller and it will either add or remove widgets.
This would be easier if you used classes, but you specifically asked for a solution that doesn't use classes. In your real code you probably need to also save the entry widgets in an array so that you can reference them later, but this example is focuses on the creation of the widgets rather than writing your whole program for you.
import tkinter as tk
frames_x = [] def create_widgets():
global frames_x
num = int(num_widgets.get())
# if the number is less than the previous number of
# widgets, delete the widgets we no longer want
for frame in frames_x[num:]:
frame.destroy()
frames_x = frames_x[:num]
# if the number is greater than the previous number of
# widgets, create additional widgets
for i in range(len(frames_x), num):
# create new widget
frame = tk.Frame(root, bd=1, relief="raised")
entry = tk.Entry(frame)
button = tk.Button(frame, text="click me")
# pack entry and button in frame
button.pack(side="right")
entry.pack(side="left", fill="x", expand=True)
# grid the frame in the parent
frame.grid(row=i+1, column=0, columnspan=2)
# save the new frame in the array
frames_x.append(frame)
root = tk.Tk() num_widgets = tk.Entry(root) button = tk.Button(root, text="Create widgets", command=create_widgets)
button.grid(row=0, column=1) num_widgets.grid(row=0, column=0, sticky="ew")
root.mainloop()

Python3/Tkinter text "displaylines" behavior issue

I am trying to count the actual lines used in a text box when it is pre-filled with data, and again when the form is submitted.
With the code below, it incorrectly shows the number of lines as chars (93) when it starts, but when you push the count button it shows the correct number of lines (4).. from the same code execution. What am I missing? (I'm new to posting on here, go easy.....)
import tkinter
text="adfa asdfkljds ;das asdjfkds fkldjasf dsf;dlsjfdkls jdkls fjd;lsfjd;ls fjd;lsafj ;dlsfj;asdlf"
def countit():
print(DE.count('1.0', 'end', 'displaylines')[0])
top = tkinter.Tk()
DE = tkinter.Text(top, height=5, width=30, wrap="word")
DE.pack()
DEButton = tkinter.Button(top, text="count", command=countit)
DEButton.pack() # WHEN BUTTON IS PRESSED, CORRECT NUMBER IS DISPLAYED
DE.insert("1.0", text)
countit() # FIRST RUN, INCORRECT NUMBER DISPLAYED
top.mainloop()
displaylines can't be calculated if the data isn't displayed, since the value depends on various factors such as the screen resolution, the actual font (which might be different than the requested font), the size of the widget once pack, place, or grid and done it's calculations, and so on. These can't be determined until the window is actually mapped to the screen.
To illustrate the point, add top.update() immediately before calling countit() and you'll see that the value is correctly printed the first time.
A better fix would be to not call countit until the window has been mapped to the screen. For example, you can add this line of code after creating the text widget:
DE.bind("<Map>", lambda event: countit())
The above will call countit() immediately after the widget has been mapped to the screen.

Tkinter box packing in loop is slow

I'm running a slowish process of building a set of PDFs using LaTeX that's put together by my script.
The PDFs are built in a for loop. I wanted to show a status window that would add a line for each student that the loop goes through, so that you could see the progress. I have been doing this with print, but I wanted something that integrated well with the Tkinter interface that I have moved to.
I have this:
ReStatuswin = Toplevel(takefocus=True)
ReStatuswin.geometry('800x300')
ReStatuswin.title("Creating Reassessments...")
Rebox2 = MultiListbox(ReStatuswin, (("Student", 15), ("Standard", 25), ("Problems", 25) ))
Rebox2.pack(side = TOP)
OKR = Button(ReStatuswin, text='OK', command=lambda:ReStatuswin.destroy())
OKR.pack(side = BOTTOM)
and then the loop:
for row in todaylist:
and then, inside the loop, after the PDF has been made,
Rebox2.insert(END, listy)
It inserts the row fine, but they all show up (along with the ReBox2 window itself) only after the entire loop is finished.
Any idea about what's causing the delay in display?
Thanks!
Yes, from what I can tell, there are two problems. First, you are not updating the display with each new entry. Second, you are not triggering the for loop with a button but instead having it run on startup (which means that the display won't be created until after the loop exits). Unfortunately however, I can't really work with the code you gave because it is a snippet of a much larger thing. However, I made a little script that should demonstrate how to do what you want:
from Tkinter import Button, END, Listbox, Tk
from time import sleep
root = Tk()
# My version of Tkinter doesn't have a MultiListbox
# So, I use its closest alternative, a regular Listbox
listbox = Listbox(root)
listbox.pack()
def start():
"""This is where your loop would go"""
for i in xrange(100):
# The sleeping here represents a time consuming process
# such as making a PDF
sleep(2)
listbox.insert(END, i)
# You must update the listbox after each entry
listbox.update()
# You must create a button to call a function that will start the loop
# Otherwise, the display won't appear until after the loop exits
Button(root, text="Start", command=start).pack()
root.mainloop()

How do you create a LabelFrame with a scrollbar in Tkinter?

I'm using Python and Tkinter to create a GUI for a program I'm writing, and I'm having a couple of problems.
I have three objects descended from LabelFrame in an object descended from Frame. One of the LabelFrame descendants is two columns of corresponding Label and Entry objects.
The problem is that there are a varying number of Label and Entry pairs, and there can be more than fit on the screen. I need a way to make a scrollbar for this LabelFrame so that everything fits on the screen. I've tried various ways of making a Scrollbar object, but nothing seems to work. How can I bind a scrollbar to this frame?
Also, I need to be able to refresh or reload this LabelFrame when the load_message() method is called, but it just redisplays the new pairs on top of the old ones (so when there are less pairs in the new set, the old set is still visible at the bottom). I've tried using grid_forget() but either nothing changes or the whole frame doesn't display. How can I forget this display and then redisplay it?
Here is the code for this class:
class freq_frame(LabelFrame):
def __init__(self, master = None, text = 'Substitutions'):
LabelFrame.__init__(self, master, text = text)
self.grid()
def load_message(self):
self.frequency = get_freq(message)
self.create_widgets()
def create_widgets(self):
self.label_list = [Label(self, text = get_label(char, self.frequency[char]), justify = LEFT) for char in self.frequency.keys()]
self.entry_list = [Entry(self, width = 1) for char in self.frequency.keys()]
for n in range(len(self.label_list)):
self.label_list[n].grid(column = 0, row = n)
for n in range(len(self.entry_list)):
self.entry_list[n].grid(column = 1, row = n)
If anyone can help with either of these problems, I'd appreciate it.
Also, this question seems like it might be a little thin, but I don't know what to add. Don't hesitate to ask for more information (but be specific).
Thanks!
Labelframes don't support scrolling. So the short answer to your question is "you can't". It sounds obvious, but if the documentation for a widget doesn't say it supports scrolling, it doesn't support scrolling.
However, there is a simple solution. First, add a canvas as a child to the labelframe and pack it so that it fills the labelframe. Attach scrollbars to the canvas and add them to the labelframe too. Then embed a frame within the canvas, add your widgets to that inner frame, and then adjust the scrollregion of the canvas to match the size of the frame after you've added all the inner labels and entries.
It sounds complicated, but it's really very straight-forward.
As for re-creating the widgets when you call load_message, calling grid_forget only removes them from view, it doesn't actually destroy the widgets. Over time you could potentially end up with hundreds of non-visible widgets which is almost certainly not what you want.
Instead, you want to first destroy all the existing widgets. That's pretty easy if they all are in the same parent, since you can ask the parent for a list of all its children. Just iterate over that list to delete each child, then add any new children. An even easier solution is to destroy and recreate that inner frame that contains the labels and entries. When you delete a widget, all child widgets get automatically destroyed. So, delete that inner frame, create a new one, and add your labels and entries again.

accessing the return value of function that was bound to an event (tkinter)

Basically, what I've done is bound a click event to a function. For example:
self.button1.bind("<Button-1>",self.chooseDice)
What I want now is to access the result of chooseDice() in another function. What is the best way to go about doing that?
class GraphicsInterface:
#we want to initialize the game board here, set up the dice and buttons
def __init__(self):
self.window = Tk()
self.window.geometry("720x500")
#buttons under each die
self.clicked=[] #empty list to collect all the buttons that were clicked (see chooseDice function)
self.button1 = Button(self.window, text="Dice 1", width=13) #create the button object
self.button1.place(x=60, y=160)
#bind button click event to a function (chooseDice())
self.button1.bind("<Button-1>",self.chooseDice)
self.button2 = Button(self.window, text="Dice 2", width=13)
self.button2.place(x=185, y=160)
self.button2.bind("<Button-1>",self.chooseDice)
#using the event as an argument, append the text to the clicked list
def chooseDice(self, event):
self.clicked.append(event.widget.cget('text'))
self.diceList=[] #create a new empty list
for i in range(len(self.clicked)):
self.diceList.append(int(self.clicked[i][5])) #get just the int value of the last character (i.e. the dice number)
self.deactivate(event.widget) #deactivate the button
return self.diceList
You are already doing what you need to do. Your example code sets self.diceList to some value. Anywhere else in your code you can directly use self.diceList.
By the way -- you're writing code that is going to be hard to maintain over time. For example, what if you change the dice label to "Dice One" or simply "One" rather than "Dice 1"? Or, as your app progresses you might want graphical images instead of text on the buttons. You'll have to modify the code that parses the button name. You are essentially encoding information in a button label which is not a good idea.
A simple solution, that also makes your chooseDice method simpler and easier to understand, is to pass in the dice number in the callback. For example:
self.button1.configure(command=lambda btn=self.button1: self.chooseDice(btn, 1))
The above passes two parameters to the chooseDice method: the button instance (so you can disable it) and the button number (so you don't have to parse the button name to get it)
This also allows you to create your dice in a loop rather than hard-coding multiple copies of the same block of code. Here's a complete working example:
from Tkinter import *
class GraphicsInterface:
def __init__(self):
self.window = Tk()
self.window.geometry("720x500")
self.clicked=[]
self.buttons = []
for n in range(1, 3):
btn = Button(text="Button " + str(n))
btn.configure(command=lambda btn=btn, n=n: self.chooseDice(btn, n))
btn.pack()
self.buttons.append(btn)
btn = Button(text="Go!", command=self.go)
btn.pack()
self.window.mainloop()
def go(self):
print "buttons:", self.clicked
self.reset()
def reset(self):
'''Reset all the buttons'''
self.clicked = []
for button in self.buttons:
button.configure(state="normal")
def chooseDice(self, widget, number):
self.clicked.append(number)
widget.configure(state="disabled")
app = GraphicsInterface()
Finally, some last bits of advice:
Don't use place, it makes your GUIs harder to create, and they won't react well to changes in window size, changes in font, changes in platform, etc. Use pack and grid instead. Also, don't create fixed-width buttons. Again, this is to better handle changes in fonts. There are times when you want fixed width buttons, but it doesn't look like your code has any reason to use them.
Finally, I don't know what you're actually trying to accomplish, but usually if you're using buttons to track state (is this thing pressed or not?) you want to use checkboxes (pick N of N) or radiobuttons (pick 1 of N). You might want to consider switching to those instead of to buttons.
Refactor. Split this into two functions.
One returns the proper result, usable by other objects.
The other is bound to a GUI control, and uses the proper result to activate and deactivate GUI objects.
Indeed, you should always do this. You should always have functions that do normal Python stuff, work correctly without the GUI and can be unit tested without the GUI. Then you connect this working "model" to the GUI.
just add a self.result attribute to your class and set it at chooseDice()

Categories