Using Python 3.7 - Working in Pycharm
I am currently working on a project where I am constantly generating new widgets and removing them to show different things, and I came across a fairly annoying problem that I can't seem to figure out, let alone find information on.
When the top widget(frame) in a row of frames has been made 255 times it crashes. I guess it has something to do with how it stores the information in bytes.
Edit*
I realize that my initial explaination was more complicated than it need to so here is a simplified version:
from tkinter import *
list = []
for x in range(1):
e = Frame()
list.append(e)
print(list) #Prints .!frame
list.clear()
print(list) #Prints [], the list is now empty
for x in range(1):
e = Frame()
list.append(e)
print(list) #Prints .!frame2, which is one step up from the previous one even the list started empty
list[0].destroy()
list[0].forget()
print(list) #.!frame2 is still there
list.clear()
print(list) #list is now empty again
for x in range(1):
e = Frame()
list.append(e)
print(list) #Prints .!frame3, and it still remembers.
As you can see, it keeps adding to the .!frame number.
What I am looking for is a way to keep it from reaching .!frame255
When the top widget(frame) in a row of frames has been made 255 times it crashes. I guess it has something to do with how it stores the information in bytes.
No, it has nothing to do with that. I think your assertion that the program crashes when the number reaches 255 is likely wrong. It's very easy to create a tkinter program that continues to work even when that number reaches into the thousands. If you look in the tkinter source code you'll see that it's just a plain integer that is appended to a string.
You seem to misunderstand the relationship between your list variable and the widgets that are stored in it. Clearing the list will not destroy the widgets in the list. You must delete each one individually, or destroy their parent. In this case their parent is the root window so that's not a viable solution.
If you want to destroy all of the frames, use a small loop before you clear the list:
for frame in list:
frame.destroy()
The number that tkinter assigns to the widget is inconsequential. It doesn't matter what the number is, and it may or may not reflect how many widgets actually exist. It's an internal detail that is unimportant.
If you want to see how many widgets actually exist, you can call winfo_children on the root window. For that you need a reference to the root window. The easiest and best way to do that is to explicitly create the root window at the start of your code:
root = Tk()
At the end of your script you can print out all of the windows that actually exist:
print("all windows:", root.winfo_children())
In your example code it shows exactly two widgets, which is what is expected. You create a widget, then you create a second widget, then you delete the first widget, and then you create a third widget.
Related
This script works fine for doing what I want it to, but I think there's likely a far more direct alternative that I'm missing.
All I have is a tkinter checkbox that when pressed, runs this function that maps all the info from column 0's rows of entry boxes into column 1's entry boxes. Currently, this is the script:
def update_company_new():
company = str()
for child in frame.winfo_children():
if child.grid_info()["column"] == 0:
company = child.get()
if child.grid_info()["column"] == 1:
child.insert(0, company)
Is there a more direct way to do this? This seems something that could normally be done with a simple list comprehension but I can't find enough details on additional options for winfo_children() and grid_info() that make it adaptable to tkinter grid references.
It can be done using list comprehension:
[frame.grid_slaves(row=w.grid_info()['row'], column=1)[0].insert(0, w.get()) for w in frame.grid_slaves(column=0)]
But it is hard to understand for beginner and it created an useless list of None.
It is better to use a simple for loop instead:
for w in frame.grid_slaves(column=0):
row = w.grid_info['row']
peer = frame.grid_slaves(row=row, column=1)
# check if there is Entry in column 1
if peer:
peer[0].insert(0, w.get())
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()
Is it possible to control the update frequency of the Listbox widget? Right now I do a lot of insert and delete operations at a high frequency and the Listbox doesn’t refresh very well. Maybe there is a way to override some draw function of the Listbox to fix this issue?
I am not able to find a way to disable visual updates of your listbox so I had to build a work around. If someone knows if you can disable the visual update of listbox please let me know.
My workaround will involve a list and 2 functions.
My first function will take the data that is going to be added to the listbox and instead add it to a list. This function simply simulates new values being added faster than what we want to update for a good visual on the method. You can adapt this code to yours to see how it will work with your inserts.
My second function will run once a second and take all the new values of this list and add them to the listbox by index.
This is a simple example but it should be a good starting point for you.
import tkinter as tk
root = tk.Tk()
add_tracker = 1
new_lb_items = []
lb = tk.Listbox(root)
lb.pack()
def add_to_listbox():
global add_tracker, new_lb_items, root
new_lb_items.append([add_tracker, "Number {}".format(add_tracker)])
add_tracker += 1
root.after(250, add_to_listbox)
def update_listbox_display():
global lb, new_lb_items, root
for item in new_lb_items:
lb.insert(item[0], item[1])
new_lb_items = [] # resets the list so only new values are added next time.
root.after(1000, update_listbox_display)
add_to_listbox()
update_listbox_display()
root.mainloop()
I am working on a large program that opens new windows from a desktop widget. The desktop widget has a 'ticker' style label that displays a piece of text representing an iteration through a list. My problem is when I first wrote the program I called mainloop() with each new window I opened. The result was the new window and program would run as designed, but the ticker would freeze. Even upon closing the newly created window, the ticker would not restart. So I removed the mainloop() line. The result of this is the ticker continues to run and I can work within the new window, but everything is soooo laggy. I suspect this has something to do with the after() method?
Attached is a test code that I am using to try to sort this out before applying the correct code to my program. And I'm sure you can tell by reading the code, but I am self taught and an absolute newb, so please dumb down the explanations if possible. Thank so much!
from tkinter import *
def new_window():
nw = Tk()
item = Text(nw)
item.grid()
L = [1, 2, 3, 4, 5]
root = Tk()
Button(root, text = 'Open', command = new_window).grid(row = 1)
while True:
for i in L:
num = Label(root, text = i)
num.grid(row = 0)
root.after(2500)
num.update()
root.mainloop()
A tkinter application should always have exactly one instance ofTk, and you should call mainloop exactly once. If you have more than one instance the program will not likely work the way you expect. It's possible to make it work, but unless you understand exactly what is happening under the hood you should stick to this rule of thumb.
If you need more windows, create instances of Toplevel. You should not call mainloop for each extra window.
Also, you shouldn't have an infinite loop where you call after the way that you do. mainloop is already an infinite loop, you don't need another. There are several examples on this website of using after to call a function at regular intervals without creating a separate loop.
I'm writing an app that doesn't have a main window (it runs inside a Python interpreter in another app), and I thought I had a good solution for getting Tkinter to cooperate--I made the Tkinter.Tk class into a Borg.
class RootWindow(Tk):
""" Invisible window that serves as the default parent
of all others.
"""
groupDict = {}
def __init__(self):
self.__dict__ = self.groupDict
if self.__dict__ == {}: # first instance
Tk.__init__(self)
self.withdraw()
Then I have my Windows, which are subclassed from Tkinter.Toplevel, default to parent=RootWindow(). The result should be that I can create any number of Windows and they'll use the same root.
It works once fine for the first Window, but after that things get all messed up. :(
see pic
What am I doing wrong? Is this even a feasible solution?
Thanks
EDIT: I should add that even though there's other stuff running in the picture, the problem can be duplicated just by using RootWindow as the parent of a Tkinter.Toplevel.
EDIT: I overrode Window.mainloop so everything uses the RootWindow event loop.
def mainloop(self):
self.master.wait_window(self)
Then I create each visible window like this:
test = Window()
test.mainloop()
It seems to work because the windows do show up, but their contents are packed in an odd way that's hard to describe. It alternates between no contents at all and having everything squished horizontally and expanded vertically.
One problem appears to be that you are forgetting to start the event loop. Or, based on further edits of your question, you may be starting more than one main loop. You need exactly one main loop that is run from the root window.
Tkinter certainly allows an arbitrary number of top level windows, but your question doesn't appear to have enough details to show what is wrong unless my first guess is correct.