Tkinter: Update frequency of Listbox - python

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()

Related

Python/TKinter - How to generate a list of selected widgets by name

I've re-written the question for clarity. I'm open to anything- complete redesign if needed.
I am having a problem and trying to start from the ground up since I can't find any solutions that work.
I have a file with a 75 columns, and each column is a feature. I would like the user to be able to select which features to include in the analysis.
Question 1. What is the best widget to use for this?
First, I tried using a listbox, but it wouldn't let me select non-consecutive items except on the default state which required selecting each individual item (there are 75 items so this would be a lot of click).
Next, I tried using checkboxes. I think this is the best solution, but I'm having problems implementing. Here's what the UI looks like:
I am trying to associate this with a list of 'clicked' boxes that I can then pass to my back-end to remove unwanted variables since the rest of the application is pretty data intensive.
The select all and deselect all buttons work fine; my problem is with the individual selections.
Is this the right way to accomplish this goal at all? If so, how might this be accomplished? TIA- I started using tkinter yesterday so I know very little.
Here is how I'm generating the below (simplified)
Code that creates the button:
import tkinter as tk
settings.data_included_cols = ['button1'] #This is the list of clicked buttons
checkbox_buttons=dict()
checkbox_variables=dict()
button_names=['button1', 'button2', 'button3']
i=0
for i in range(len(button_names)):
checkbox_variables[i]=1
checkbox_button[i] = tk.Checkbutton(frame, text=button_names[i],
variable=checkbox_variables[i],
command=checkbox_click)
Command Code (checkbox_click)- I don't know what goes here but nothing I've tried so far has worked. Originally I was calling it as: command=lambda: checkbox_click(i), and it tried to work like the below:
def checkbox_click(i):
if i in settings.data_included_cols:
settings.data_included_cols.remove(button_name[i])
This doesn't work because 'i' is not associated with the button, it's associated with the loop so it will always be whatever the final value is (in this case 3 which maps to button3).
Any thoughts?
Checkboxes do not need to have a command specified.
They do however need a StringVar or IntVar associated with them, so you can query the values.
So I would build your code like this:
names = {
"id", "member_id", "loan_amnt"
}
values = {}
boxes = {}
for name in names:
values[name] = tk.Intvar(value=1)
boxes[name] = ttk.Checkbox(root, text=name, variable=values[name])
Once the user has made submitted their choice, you can query the dict of values to see which options were selected.
The problem is that although you assign each check-button different "feature" as you create them, when one of the buttons is clicked, the program will execute the function
lambda: feature_list(feature)
where it will try to get the value of "feature", and will find that it equal to the last one in the list.
The seemingly only solution is to assign each check-button a variable, such as something like this:
import tkinter as tk
from tkinter import ttk
def click():
temp = []
for i in range(3):
if value[i].get() == 1:
temp.append(feature[i])
print(temp)
if __name__ == "__main__":
root = tk.Tk()
feature = ["one", "two", "three"]
check = {}
value = {}
for i in range(3):
value[i] = tk.IntVar(value=0)
check[i] = ttk.Checkbutton(root, text=feature[i], variable=value[i], onvalue=1, offvalue=0, command=click)
check[i].grid(row=0, column=i)
root.mainloop()
then when you select some buttons, for example "one" and "three", then it will print
['one', 'three']
You should not be stingy for creating Tk variables, since the space consumed by a check-button should be much bigger than that consumed by a variable. Also, creating variables to get the state of widgets is the most "standard" and "Pythonic" way which you should always use.

Simplifying this Tkinter For Loop to Update Entry Values between two columns

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())

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()

Resetting generated widget count in Python with Tkinter

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.

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

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).

Categories