progressbar in Tkinter with a label inside - python

Is It possible to improve my progressbar in Tkinter-Python adding a label in the middle (ex: reading file)?
I tried to find a elegant coding solution but without a real result
from Tkinter import *
import ttk
import tkFileDialog
import time
class MainWindow(Frame):
def __init__(self):
Frame.__init__(self)
self.master.title("ProgressBar example")
self.master.minsize(200, 100)
self.grid(sticky=E+W+N+S)
top = self.winfo_toplevel()
top.rowconfigure(0, weight=1)
top.columnconfigure(0, weight=1)
self.start_ind = Button(self, text='Start indeterminate', command=self.start_ind, activeforeground="red")
self.start_ind.grid(row=0, column=0, pady=2, padx=2, sticky=E+W+N+S)
self.pbar_ind = ttk.Progressbar(self, orient="horizontal", length=300, mode="indeterminate")
self.pbar_ind.grid(row=1, column=0, pady=2, padx=2, sticky=E+W+N+S)
def start_ind(self):
for i in xrange(50):
self.pbar_ind.step(1)
self.update()
# Busy-wait
time.sleep(0.1)
if __name__=="__main__":
d = MainWindow()
d.mainloop()

I added the label inside the progressbar by creating a custom ttk style layout. The text of the label is changed by configuring the style:
from tkinter import Tk
from tkinter.ttk import Progressbar, Style, Button
from time import sleep
root = Tk()
s = Style(root)
# add the label to the progressbar style
s.layout("LabeledProgressbar",
[('LabeledProgressbar.trough',
{'children': [('LabeledProgressbar.pbar',
{'side': 'left', 'sticky': 'ns'}),
("LabeledProgressbar.label", # label inside the bar
{"sticky": ""})],
'sticky': 'nswe'})])
p = Progressbar(root, orient="horizontal", length=300,
style="LabeledProgressbar")
p.pack()
# change the text of the progressbar,
# the trailing spaces are here to properly center the text
s.configure("LabeledProgressbar", text="0 % ")
def fct():
for i in range(1, 101):
sleep(0.1)
p.step()
s.configure("LabeledProgressbar", text="{0} % ".format(i))
root.update()
Button(root, command=fct, text="launch").pack()
root.mainloop()

Have you tried creating a text Label and putting it in the same row/column and setting it the same size like so:
self.Lab = Label(self,length=200)
self.Lab.grid(row=1,column=0,pady=2,padx=2,sticky=E+W+N+S))
But you would want to put it after the progress bar widget.

I created a solution for it, which works.
I use a label that is placed on top of the progressbar and the background of the label updates in sync with the progressbar using relwidth and the same color as the progressbar.
from threading import Thread
from tkinter import *
from tkinter import ttk
import time
#Tkinter window
class GUI(Frame, object):
def __init__(self, progress_frame):
super().__init__(progress_frame)
self.progress_frame = progress_frame
self.progress_frame.geometry('300x100')
self.progress_frame.title('Progressbar')
self.progressbar = ttk.Progressbar(self.progress_frame, orient='horizontal', mode='determinate', length=280)
# place the progressbar
self.progressbar.grid(column=0, row=1, columnspan=2, padx=10, ipady=3)
# initialize label
self.value = StringVar()
self.value.set(self.update_progress_label("0 MB/s"))
self.value_label = Label(self.progress_frame, textvariable=self.value, font='default 10', borderwidth=0)
#set background to grey
self.value_label['bg'] = '#e6e6e6'
self.value_label.grid(column=0, row=1, columnspan=2, padx=10, pady=20)
self.current_value = 0
self.start_download()
def update_progress_label(self, mb_s): #the value you want to show in the label
return f"{self.progressbar['value']}% / {mb_s}"
def start_download(self): #start thread that does calculation
download_thread = Download()
download_thread.start()
self.monitor(download_thread)
def monitor(self, download_thread): # monitor download progress
""" Monitor the download thread """
if download_thread.is_alive():
progress = download_thread.value
# update the label
self.value.set(self.update_progress_label(download_thread.mb_s))
widget_x, widget_width = self.value_label.winfo_x(), self.value_label.winfo_width() # get position and width of text label
progressbar_pixellen = self.progressbar.winfo_width() # get total width of progressbar
# get the current position in pxl of progressbar
calculation_ppixel = progress*progressbar_pixellen/100
# get the overlap with the text label
calculation_ptext = calculation_ppixel-widget_x+self.progressbar.winfo_x()
# ensure that no negative value is set
calculation_text = max(calculation_ptext/widget_width, 0)
if calculation_text>0: # if calculation_text (relwidth) is 0 it will still show a small bar, so don't update the label
# update the label with the new text value and the color of the progressbar
self.pblabel = Label(self.value_label, textvariable=self.value, font='default 10', background='#06b025', borderwidth=0, anchor="w")
# relwidth will only show the given percentage of the text in the color
self.pblabel.place(x=0, y=0, anchor="nw", relwidth=calculation_text)
# update the progressbar progress
self.progressbar['value'] = progress
# update the label
self.value_label.update_idletasks()
# rerun this method in 100ms
self.after(100, lambda: self.monitor(download_thread))
class Download(Thread):
def __init__(self):
super().__init__()
self.picture_file = None
self.value = 0
self.mb_s = 0
def run(self):
""" do some calculation like downloading a file """
for i in range(100):
time.sleep(1)
self.value = i
self.mb_s = "100 MB/s"
if __name__ == '__main__':
progress_frame = Tk()
app = GUI(progress_frame)
app.mainloop()
Example

Related

Modify Tkinter Label from another class in an other file

In main.py there is class MainFrame which display Player._money
In lands.py there is class Lands which allows the player to buy more lands.
Inside class Lands, there is a function called def buy_lands(self) that respond to a button.
here's my problem:
When buy_lands(self) is clicked, i've managed to update the label that shows the Player._lands as it's in the same class, same frame. However, the label that display the money is in a different frame, different class and a different file as it's in main.py class MainFrame.
How can i update Player._money in MainFrame from buy_lands(self) in class Lands without having a circular import error?
Here's the code if that's help:
main.py
import tkinter as tk
from tkinter import ttk
from lands import Lands
from player import Player
class MainFrame(ttk.Frame):
def __init__(self, container):
super().__init__(container)
options = {'padx': 5, 'pady': 5}
self.player = Player._name
self.__create_widgets()
# show the frame on the container
self.pack(**options)
def __create_widgets(self):
# Initialize style
s = ttk.Style()
# Frame Top Menu
s.configure('Menu.TFrame', background='blue') # define the style
self.menu = ttk.Frame(self, height=100, width=450, style='Menu.TFrame') # create the frame
self.menu.pack() # place the frame
# Main Frame
s.configure('Main.TFrame', background='red')
self.main = ttk.Frame(self, height=300, width=300, style='Main.TFrame')
self.main.pack()
# Create Widgets
self.name_label = ttk.Label(self.menu, text=f'Welcome {Player._name}')
self.money_label = ttk.Label(self.menu, text=f'money: {Player._money} £')
self.button_1 = ttk.Button(self.menu, text="Button 1")
self.lands_button = ttk.Button(self.menu, text="Lands", command=self.show_lands)
self.button_3 = ttk.Button(self.menu, text="Button 3")
self.button_4 = ttk.Button(self.menu, text="Button 4")
# Display Widgets
self.name_label.grid(column=0, columnspan=1, row=0, sticky='w')
self.money_label.grid(column=2, row=0, columnspan=3, sticky='e')
self.button_1.grid(column=0, row=1)
self.lands_button.grid(column=1, row=1)
self.button_3.grid(column=2, row=1)
self.button_4.grid(column=3, row=1)
def show_lands(self):
for widget in self.main.winfo_children():
widget.destroy()
Lands(self.main)
lands.py
import tkinter as tk
from tkinter import ttk
from player import Player
class Lands(ttk.Frame):
def __init__(self, container):
super().__init__(container)
self.lands = Player._lands
options = {'padx': 5, 'pady': 5}
# show the frame on the container
self.pack(**options)
self.__create_widgets()
def __create_widgets(self):
self.lands_label = ttk.Label(self, text=f'You have {Player._lands} acres of lands')
self.buy_lands_button = ttk.Button(self, text="Buy Lands", command=self.buy_lands)
self.lands_label.pack()
self.buy_lands_button.pack()
def buy_lands(self):
Player._money -= 5000
Player._lands += 10
self.lands_label.config(text=f'You have {Player._lands} acres of lands')
self.lands_label.pack()
print(Player._money)
print(Player._lands)
i've tryed lambda methods but as i'm still learning i'm not too sure how to use it. I've tried global method but again because it's not in the same file, it doesn't work. And i tried to import MainFrame which shows a circular import error.

Making a window UI scroll down and deleting labels from frame

I am making a random generator for my friends and I'm stuck trying to make a scroll down option. So if you generate more the window can show, a scroll down window should be possible. But I can't seem to get any to work. I've tried many online tutorials.
And my second issue with my code is that I can't clear the generated labels from the window. I got it working that it expands the window.
from cProfile import label
from pickle import FRAME
import random
import tkinter as tk
from tkinter import BOTH, DISABLED, LEFT, RIGHT, VERTICAL, Y, Frame, Label, filedialog, Text
import os
from tkinter import ttk
from tkinter.font import NORMAL
from tkinter.messagebox import YES
root = tk.Tk()
root.title('guesser')
#Pelin arvonta ohjelma !
def delete():
for child in root.children.values():
info = child.grid_info()
if info['column'] == 0:
child.grid_forget()
def arvonta():
global label
list1 = []
lista = ["Valorant","Rainbow","Vampire: The masquerade","Playerunknown's battlegrounds","Fortnite","Left 4 Dead 2","Counter strike Global offensive","Realm roayale","Black ops 1 zombies/multiplayer","Black ops 2 zombies/multiplayer","Black ops 3 zombies/multiplayer"]
numero = random.randint(0, 10)
hahmo = (lista[numero])
list1.append(hahmo)
for app in list1:
label = tk.Label(frame, text=app, bg="red",font=('Helvetica',20))
label.pack()
def valorant():
list2 = []
lista2 = ["Brimstone","Viper","Omen","Killjoy","Cypher","Sova","Sage","phoenix","Jett","Reyna","Raze","Raze","Breach","Skye","Yoru","Astra","Kay/o","Chamber","Neon","Fade"]
numero = random.randint(0, 19)
randomValorantagent=(lista2[numero])
list2.append(randomValorantagent)
for app in list2:
label = tk.Label(frame, text=app, bg="red",font=('Helvetica',20))
label.pack()
def quitter():
quit()
canvas = tk.Canvas(root,height=700,width=700,bg="#263D42")
canvas.pack(side=LEFT,fill=BOTH,expand=1)
frame = tk.Frame(root,bg="green")
frame.place(relwidth=0.8,relheight=0.8,relx=0.1,rely=0.1)
frame.pack(fill=BOTH,expand=1)
my_scrollbar = ttk.Scrollbar(frame, orient=VERTICAL, command=canvas.yview)
my_scrollbar.pack(side=RIGHT, fill=Y)
# Configure The Canvas
canvas.configure(yscrollcommand=my_scrollbar.set)
canvas.bind('<Configure>', lambda e: canvas.configure(scrollregion = canvas.bbox("all")))
# Create ANOTHER Frame INSIDE the Canvas
second_frame = Frame(canvas)
# Add that New frame To a Window In The Canvas
canvas.create_window((0,0), window=second_frame, anchor="nw")
#rlls the game
openfile = tk.Button(second_frame,text="Roll a game",padx=10,pady=5,fg="white",bg="#263D42", command=arvonta)
openfile.pack()
#rolls a valorant agent
valorantA = tk.Button(second_frame,text='Roll valorant agent',padx=10,pady=5,fg="white",bg="#263D42",command=valorant)
valorantA.pack()
# stops program
stop = tk.Button(second_frame,text="Quit",padx=10,pady=5,fg="white",bg="#263D42",command=quitter)
stop.pack()
# deletes all info from screen.
deletor = tk.Button(second_frame,text="delete info",padx=10,pady=5,fg="white",bg="#263D42",command=delete)
deletor.pack()
root.mainloop()```
The following does most of what you want. I wrote it some time ago to test Scrollbars because they are wonky IMHO
from tkinter import *
from functools import partial
class ButtonsTest:
def __init__(self):
self.top = Tk()
self.top.title("Click a button to remove")
self.top.geometry("425x200+50+50")
Label(self.top, text=" Click a button to remove it ",
bg="lightyellow", font=('DejaVuSansMono', 12)
).grid(row=0, sticky="nsew")
Button(self.top, text='Exit', bg="orange", width=9,
command=self.top.quit).grid(row=1,column=0,
sticky="nsew")
self.add_scrollbar()
self.button_dic = {}
self.buttons()
self.top.mainloop()
##-------------------------------------------------------------------
def add_scrollbar(self):
self.canv = Canvas(self.top, relief=SUNKEN)
self.canv.config(width=400, height=200)
self.top_frame = Frame(self.canv, height=100)
##---------- scrollregion has to be larger than canvas size
## otherwise it just stays in the visible canvas
self.canv.config(scrollregion=(0,0, 400, 500))
self.canv.config(highlightthickness=0)
ybar = Scrollbar(self.top, width=15, troughcolor="lightblue")
ybar.config(command=self.canv.yview)
## connect the two widgets together
self.canv.config(yscrollcommand=ybar.set)
ybar.grid(row=3, column=2, sticky="ns")
self.canv.grid(row=3, column=0)
self.canv.create_window(1,0, anchor=NW,
window=self.top_frame)
##-------------------------------------------------------------------
def buttons(self):
b_row=1
b_col=0
for but_num in range(1, 51):
## create a button and send the button's number to
## self.cb_handler when the button is pressed
b = Button(self.top_frame, text = str(but_num), width=5,
command=partial(self.cb_handler, but_num))
b.grid(row=b_row, column=b_col)
## dictionary key=button number --> button instance
self.button_dic[but_num] = b
b_col += 1
if b_col > 4:
b_col = 0
b_row += 1
##----------------------------------------------------------------
def cb_handler( self, cb_number ):
print("\ncb_handler", cb_number)
self.button_dic[cb_number].grid_forget()
##===================================================================
BT=ButtonsTest()

Python: 'NoneType' object has no attribute 'get'

I'm creating a GUI in python using tkinter and am having trouble when running it. I have an entry box widget, a radiobutton widget, and a button widget. When I press the button, what I want is the user to type a number into the entry box and select an option from the list of radiobuttons. When the user presses the button, I'd like the values to be retrieved and displayed in the other frame for testing. What I get instead is when the button gets pressed, I get the error 'NoneType' object has no attribute 'get'. The error is referring to the value inside of the entry box: self.tune_entry
The code I have is as follows:
SA_main.py
import os
import tkinter as tk
from tkinter import ttk
from tkinter import font
import SA_gui
def main():
x_vals = [0,1,2,3,4]
y_vals = [0,1,2,3,4]
root = SA_gui.tk.Tk()
UI = SA_gui.Window(root, x_vals, y_vals)
root.mainloop()
if __name__ == "__main__":
main()
SA_gui.py
import os
import tkinter as tk
from tkinter import ttk
from tkinter import font
# Class to define, setup, and build the GUI
class Window:
# Dimensions of the GUI
HEIGHT = 600
WIDTH = 1200
# Colors for main layout
bg_color = "#B0E0E6"
frame_color1 = "#73B1B7"
white_color = "#FFFFFF"
def __init__(self, master, x_vals, y_vals):
# Take in the lists of files for later use
self.x_vals = x_vals
self.y_vals = y_vals
#--------------------------------------------------------------
# Define and create the window
self.master = master
master.title("Signal Analysis")
master.geometry("{}x{}".format(Window.WIDTH, Window.HEIGHT))
# Create and place the background frame
self.bg_frame = tk.Frame(self.master, bg=Window.bg_color, bd=5)
self.bg_frame.place(relwidth=1, relheight=1)
# Create the main title
self.main_title = tk.Label(self.bg_frame, text="Software Defined Radio Signal Analysis",
bg=Window.bg_color, font=("Courier", 14))
self.main_title.pack(side="top")
#--------------------------------------------------------------
# Create and place the frame for tuning
self.tune_frame = tk.Frame(self.bg_frame, bg=Window.frame_color1, bd=4)
self.tune_frame.place(relx=0.05, rely=0.1, relwidth=0.2428, relheight=0.8)
# Create and place the title for the tuning frame
self.tune_title = tk.Label(self.tune_frame, text="Tune", bg=Window.frame_color1, font=
("Courier", 11))
self.tune_title.place(relwidth=1, anchor="nw")
# Create and place the contents of the tuning frame
self.tune_cont = tk.Frame(self.tune_frame, bg=Window.white_color, bd=4)
self.tune_cont.place(relx=0.05, rely=0.05, relwidth=0.9, relheight=0.925)
#Label for frequency entry
self.tune_label = tk.Label(self.tune_cont, text='Enter carrier frequency: (kHz)',
bg=Window.white_color)
self.tune_label.place(relx=0.025, rely=0)
#Entry Box for frequency entry
self.tune_entry = tk.Entry(self.tune_cont)
self.tune_entry.place(relx=0.025, rely=0.075, relwidth=0.95, relheight=0.05)
#Label for divider
self.tune_div = ttk.Separator(self.tune_cont, orient="horizontal")
self.tune_div.place(rely=0.175, relwidth=1)
#Label for display mode
self.disp_label = tk.Label(self.tune_cont, text='Select Display:', bg=Window.white_color)
self.disp_label.place(relx=0.025, rely=0.2)
#Variable for radiobuttons
self.var = tk.IntVar(self.tune_cont).set("1")
#Radio Button for Spectral Analysis
self.SA_select = tk.Radiobutton(self.tune_cont, text="Spectral
Analysis",bg=Window.white_color, padx=20, variable=self.var, value=1)
self.SA_select.place(relx=0.025, rely=0.275)
#Radio Button for Option 2
self.opt2_select = tk.Radiobutton(self.tune_cont, text="Option 2",bg=Window.white_color,
padx=20, variable=self.var, value=2)
self.opt2_select.place(relx=0.025, rely=0.35)
#Radio Button for Option 3
self.opt3_select = tk.Radiobutton(self.tune_cont, text="Option 3",bg=Window.white_color,
padx=20, variable=self.var, value=3)
self.opt3_select.place(relx=0.025, rely=0.425)
#Button for selection
self.tune_button = ttk.Button(self.tune_cont, text="Enter", command=lambda:
self.print_selected(self.var.get(), self.tune_entry.get()))
self.tune_button.place(relx= 0.775, rely=0.9, relwidth=0.2, relheight=0.075)
#-----------------------------------------------------------------
# Create and place the frame for the plot
self.plot_frame = tk.Frame(self.bg_frame, bg=Window.frame_color1, bd=4)
self.plot_frame.place(relx=0.3428, rely=0.1, relwidth=0.6071, relheight=0.8)
# Create and place the title for the plot frame
self.plot_title = tk.Label(self.plot_frame, text="Plot", bg=Window.frame_color1, font=
("Courier", 11))
self.plot_title.place(relwidth=1, anchor="nw")
# Create and place the contents of the plot frame
self.plot_cont = tk.Frame(self.plot_frame, bg=Window.white_color, bd=4)
self.plot_cont.place(relx=0.025, rely=0.05, relwidth=0.95, relheight=0.925)
def print_selected(self, disp, freq):
if disp == 1:
disp_mode = "Spectral Analysis"
elif disp == 2:
disp_mode = "Option 2"
else:
disp_mode = "Option 3"
#Label for this test
self.prnt_label = tk.Label(self.plot_cont, text="Display: " + disp_mode + ", Center Freq: " +
freq, bg=Window.white_color)
self.prnt_label.place(relx=0.025, rely=0.2)
Any help to resolve this issue is greatly appreciated!
Consider this code:
self.var = tk.IntVar(self.tune_cont).set("1")
Anytime you do x=y().z() python assigns the return value of z() to x. Thus, in your code you're assiging the result of .set("1") to self.var. The set method is returning None so self.var is None. Thus, when you later try to call self.var.get() it's the same as doing None.get().
If you want to initialize a variable at the time of creation, there is no need to call set. Also, while it works to pass a string, if you're setting an IntVar you really ought to be setting it to an integer.
self.var = tk.IntVar(value=1)

Adding buttons in TkInter on pressing button

I am having button and on pressing it I want to create new Button and new Label.
Label must have random color and must change it on pressing this button to another random color.
My code even can not add buttons correctly, there is problems with placing new(sizes are strange).
How can I improve this? And how can I later create func for new buttons which will change their label's colours, cause I dont have label's names.
import random
from tkinter import *
def color(*args):
pass
def dump( *args):
global count
Butt = Button(root, text="color ", command=color)
Butt.config(width=int(root.winfo_width() / 10), height=int(root.winfo_height() / 10))
Butt.grid(row=0, column=count)
Txt = Label(root, text="Color", bg="#" + ("%06x" % random.randint(0, 16777215)))
Txt.config(width=int(root.winfo_width() / 10), height=int(root.winfo_height() / 10))
Txt.grid(row=1, column=count)
count+=1
root.mainloop()
count=2
TKroot = Tk()
TKroot.title("Hello")
root = Frame(TKroot)
root.place(relx=0, rely=0, relheight=1, relwidth=1)
root.columnconfigure(0, weight=10)
root.columnconfigure(1, weight=10)
root.rowconfigure(0, weight=10)
root.rowconfigure(1, weight=10)
Butt = Button(root, text="Butt ON")
Butt.bind('<Button-1>', dump)
Butt.config(width=int(root.winfo_width() / 10), height=int(root.winfo_height() / 10))
Butt.grid(row=0, column=0)
Exit = Button(root, text="Quit!", command=root.quit)
Exit.config(width=int(root.winfo_width() / 10), height=int(root.winfo_height() / 10))
Exit.grid(row=0, column=1)
Txt = Label(root, text="This is a label", bg="PeachPuff")
Txt.grid(row=1, column=1, columnspan=1)
TKroot.mainloop()
print("Done")
I see a few issues with your code.
1st is you are using place for your frame.
This is going to cause issues when adding new buttons as it will not allow the window to resize correctly with the new layout.
2nd is how you are writing your code. You name your frame root and use the quit method on the frame and not on your actually root window. The way you are writing things makes it harder to follow so consider following PEP8 guidelines when writing your code.
3rd you are trying to apply mainloop to your frame in the dump function. You only ever need 1 instance of mainloop and this applies to the actual root window (Tk()).
To address your question on how to change the label color later on I would use a list to store your buttons and labels. This way we can reference their index values and apply your random color code to the labels on button click.
I have re-written most of your code to follow PEP8 and done some general clean up.
Let me know if you have any questions.
import tkinter as tk
import random
def color(ndex):
button_label_list[ndex][1].config(bg="#%06x" % random.randint(0, 16777215))
def dump():
global count, button_label_list
button_label_list.append([tk.Button(frame, text="color", command=lambda x=count: color(x)),
tk.Label(frame, text="Color", bg="#" + ("%06x" % random.randint(0, 16777215)))])
button_label_list[-1][0].grid(row=0, column=count, sticky='nsew')
button_label_list[-1][1].grid(row=1, column=count, sticky='nsew')
frame.columnconfigure(count, weight=1)
count += 1
root = tk.Tk()
count = 0
button_label_list = []
root.title("Hello")
root.rowconfigure(1, weight=1)
root.columnconfigure(2, weight=1)
frame = tk.Frame(root)
frame.rowconfigure(1, weight=1)
frame.grid(row=0, column=2, sticky='nsew', rowspan=2)
tk.Button(root, text="butt ON", command=dump).grid(row=0, column=0, sticky='nsew')
tk.Button(root, text="Quit!", command=root.quit).grid(row=0, column=1, sticky='nsew')
tk.Label(root, text="This is a label", bg="PeachPuff").grid(row=1, column=1, columnspan=1, sticky='nsew')
root.mainloop()
Results:
A window that can add new buttons and be able to change colors on each label. The main 2 buttons the window starts with are static in that they cannot be pushed out of the window like in you code example and will remain on the left anchored in place.
below an object oriented version.
Every time you press on Color button, you create a new label and a new button
and put label reference in a dictionary.
The color of the label is randomly generate.
After creation if we click on a new button we change the relative label color.
The coolest part of the script is:
command=lambda which=self.count: self.change_color(which)
lambda funcion it's used to keep a reference to the button and label just
create when we call the change_color function.
import tkinter as tk
import random
class App(tk.Frame):
def __init__(self,):
super().__init__()
self.master.title("Hello World")
self.count = 0
self.labels = {}
self.init_ui()
def init_ui(self):
self.f = tk.Frame()
w = tk.Frame()
tk.Button(w, text="Color", command=self.callback).pack()
tk.Button(w, text="Close", command=self.on_close).pack()
w.pack(side=tk.RIGHT, fill=tk.BOTH, expand=0)
self.f.pack(side=tk.LEFT, fill=tk.BOTH, expand=0)
def callback(self):
text_label = "I'm the {} label".format(self.count)
text_button = "I'm the {} button".format(self.count)
color = "#" + ("%06x" % random.randint(0, 16777215))
obj = tk.Label(self.f, text=text_label, bg=color)
obj.pack()
self.labels[self.count]=obj
tk.Button(self.f,
text=text_button,
command=lambda which=self.count: self.change_color(which)).pack()
self.count +=1
def change_color(self,which):
color = "#" + ("%06x" % random.randint(0, 16777215))
self.labels[which].config(bg=color)
def on_close(self):
self.master.destroy()
if __name__ == '__main__':
app = App()
app.mainloop()

Why is my tkinter layout changing?

I made this tkinter application, and I have a bug where the layout changes and the button at the bottom disappears.
I am unable to reproduce the bug 100%. It happens at random times.
For this reason, this SSCCE may contain more than it needs. It's still a lot less than my original app.
from random import random
from threading import Thread, Lock
from time import sleep
from tkinter import Tk, LEFT, RIGHT, BOTH, X, Y, Listbox, Scrollbar, VERTICAL, END
from tkinter.ttk import Frame, Button, Style, Entry
class ListManager:
def __init__(self, ui):
self.entry_list = []
self.ui = ui # to notify of list update
self.timer = ListManager.interval + 1
self.stop = False
interval = 1 # between updates
#staticmethod
def api_request() -> dict:
new_list = ["line"]
while random() > .4:
new_list.append("line")
return {"data": new_list}
def get_entries(self):
r = self.api_request()
self.entry_list = []
for line in r["data"]:
self.entry_list.append(line)
def run(self):
self.timer = ListManager.interval + 1
while not self.stop:
if self.timer > ListManager.interval:
self.get_entries()
if self.ui is None:
print("entries:", len(self.entry_list))
for entry in self.entry_list:
print(entry)
else:
self.ui.receive(self.entry_list)
self.timer = 0
else:
self.timer += 1
sleep(1)
class UI(Frame):
def __init__(self):
super().__init__()
self.style = Style()
self.listbox = None
self.name_entry = None
self.entries = []
self.mutex = Lock()
self.init_pack()
def init_pack(self):
# TODO: investigate open button disappearing (probably need to repack in receive function)
self.master.title("list")
self.style.theme_use("default")
add_streamer_frame = Frame(self)
add_button = Button(add_streamer_frame, text="Add Line")
add_button.pack(side=RIGHT, padx=5, pady=5)
self.name_entry = Entry(add_streamer_frame)
self.name_entry.pack(fill=X, padx=5, expand=True)
add_streamer_frame.pack(fill=X)
list_frame = Frame(self)
self.listbox = Listbox(list_frame, height=20)
self.listbox.configure(font=("Courier New", 12, "bold"))
scrollbar = Scrollbar(self.listbox, orient=VERTICAL)
self.listbox.config(yscrollcommand=scrollbar.set)
scrollbar.config(command=self.listbox.yview)
scrollbar.pack(side=RIGHT, fill=Y)
self.listbox.pack(fill=BOTH, padx=5, expand=True)
list_frame.pack(fill=BOTH, expand=True)
self.pack(fill=BOTH, expand=True)
open_button = Button(self, text="Open")
open_button.pack(side=LEFT, padx=5, pady=5)
def receive(self, entries):
self.mutex.acquire()
self.listbox.delete(0, END)
self.entries = []
for entry in entries:
self.listbox.insert(END, entry)
self.entries.append(entry.split(" ")[0])
self.mutex.release()
def main():
root = Tk()
root.geometry("800x400+300+300")
app = UI()
tl = ListManager(app)
thread = Thread(target=tl.run)
thread.start()
root.mainloop()
tl.stop = True
thread.join()
if __name__ == "__main__":
main()
Here's what it normally looks like most of the time, and should look like:
Here's what it looks like after the bug:
Add:
self.ui.update_idletasks()
after:
sleep(1)
or:
Replace:
self.listbox = Listbox(list_frame, height=20)
with:
self.listbox = Listbox(list_frame)
# or self.listbox = Listbox(list_frame, height=17)
It must be that when too much is going on there's no time spent on updating the GUI. I couldn't reproduce the 2nd screenshot directly without lowering sleep argument as low as 0.001.
The root of your problem seems to be that each time something's added into the listbox its height(winfo_height which is in pixels) is recalculated and resized down from 20 character height, to 17, for which there isn't always time for before a new insertion.
Basically, your widget does not necessarily has a fixed 20 character height at all times, it resizes based on its content and other widgets, unless propagation is disabled by calling self.listbox.pack_propagate(False).
Add:
print(self.ui.listbox.winfo_height())
after:
sleep(1)
to see its height changing for yourself.

Categories