Ttk Indeterminate progress bar on button press - python

I am trying to create a progress bar that runs as long as my function is running to show the user that things are happening and not just frozen. My function (generate_reports) makes queries to the database and writes to CSV files. Here is an abstract version of my code:
from tkinter import *
from tkinter import ttk
from billing import generate_reports
class app:
def __init__(self, root):
self.mainframe = ttk.Frame(root, padding = '4 4 12 12')
self.mainframe.grid(column = 0, row = 0, sticky = (N, W, E, S))
ttk.Button(self.mainframe, text = "Generate Billing Reports", command = self.do_reports).grid(column = 2, row = 3, sticky = (W, E))
def do_reports(self, *args):
pbar = ttk.Progressbar(self.mainframe, orient = HORIZONTAL, mode = 'indeterminate')
pbar.grid(row = 4, column = 3, sticky = (W, E))
t1 = threading.Thread(target = generate_reports, args = [start, end])
t1.start()
pbar.start()
t1.join()
pbar.stop()
return
root = Tk()
BillingApp(root)
root.mainloop()
With this code, the progress bar doesn't pop up until after the generate_reports thread is completed and it is unmoving. If I remove the join, everything works fine but it never stops loading. How can I make the loading bar run for only the duration of the generate_reports thread?

Heh welcome to the fun world of event driven programming :). You can't use join here, the point of that function is to block until the thread is done and the whole point of using a thread is to avoid blocking the mainloop. You have 2 choices: either set up the GUI to constantly poll the thread to see if it's still running, or set up the thread to send a message back to the GUI when it's done. This latter option is probably the cleanest, and it's often done using tkinter's event mechanism.
from tkinter import *
from tkinter import ttk
import threading
import time
def generate_reports(start, end):
print("provide a mcve next time!")
time.sleep(5)
def run_report(root, *args):
generate_reports(*args)
root.event_generate("<<PhishDoneEvent>>") # yes, this is using tkinter in a thread, but some tkinter methods are ok to use in threads
class BillingApp:
def __init__(self, root):
self.mainframe = ttk.Frame(root, padding = '4 4 12 12')
self.mainframe.grid(column = 0, row = 0, sticky = (N, W, E, S))
ttk.Button(self.mainframe, text = "Generate Billing Reports", command = self.do_reports).grid(column = 2, row = 3, sticky = (W, E))
root.bind("<<PhishDoneEvent>>", self.report_done)
def do_reports(self, *args):
# note this makes a new widget with every click ... this is bad. Refactor to reuse the widget.
self.pbar = ttk.Progressbar(self.mainframe, orient = HORIZONTAL, mode = 'indeterminate')
self.pbar.grid(row = 4, column = 3, sticky = (W, E))
start, end = 4,5
t1 = threading.Thread(target = run_report, args = [root, start, end])
t1.start()
self.pbar.start()
def report_done(self, event=None):
self.pbar.stop()
Label(self.mainframe, text="report done").grid(row = 4, column = 3)
root = Tk()
BillingApp(root)
root.mainloop()

I see that you have accepted the answer above. However, I would post my answer since it is not based on threading, which I think is simpler and may be more suitable:
from tkinter import *
from tkinter.ttk import *
import os
root = Tk()
# I set the length and maximum as shown to demonstrate the process in the
# proceeding function
progress = Progressbar(root, orient = HORIZONTAL,
length = 200/5, maximum=200/5, mode = 'determinate')
# Function
def my_func():
t=0
r= 1/5
for i in range(200):
print(i)
t=t+r
progress['value'] = t
root.update_idletasks()
progress.pack()
# Button
Button(root, text = 'Start', command = bar).pack(pady = 10)
mainloop()

Related

Issue with progress bar thread

I created a small GUI using Tkinter. It has a button on click of which some data is written to Excel.
To show the progress, I added a progress bar but since the process is resource intensive, the progress bar appears only at the end. Therefore, I used threading as shown.
In the Main function below, I initialized the progress bar in a different thread but I want to update the current value of the task in Start function.
Because of this line:
progressbar.Start()
it is just continuously running without anything to do with the current progress.
def Start():
x = 0
progressbar["value"] = x
for idx, val in enumerate(rows):
region_url = val[1]
if (model_url != '-'):
url = 'http://testurl/' + region_url
x = x + 1
if (x > 4):
break
# Main
if __name__ == '__main__':
window = Tk()
new = progress(window)
# Add a grid
mainframe = Frame(window)
mainframe.grid(column=0,row=0, sticky=(N,W,E,S) )
mainframe.columnconfigure(0, weight = 1)
mainframe.rowconfigure(0, weight = 1)
mainframe.pack(pady = 100 , padx = 150)
# DropDown
popupMenu = OptionMenu(mainframe, tkvar, *regionList)
Label(mainframe, text="Region").grid(row = 1, column = 0)
# Button
btnSubmit = Button(mainframe, text= "Execute",command=StartScrap).grid(row = 2, column = 18)
popupMenu.grid(row = 2, column =0)
# Progress Bar
progressbar = ttk.Progressbar(window, orient = HORIZONTAL,length=300, mode = 'indeterminate')
progressbar.pack()
t = threading.Thread()
progressbar["maximum"] = 4
progressbar.start()
window.mainloop()
There are a couple things in your code that needs fixing.
Firstly you defined the function Start which controls how your progressbar is updated, but you never call it.
Secondly, you define the progressbar mode as indeterminate and that just start the progress bar - it works but it will just periodically move the progressbar.
Thirdly, you defined the Thread t but never put a target function.
Here's how to move your progressbar in a thread:
from tkinter import *
from tkinter import ttk
import threading
import time
def Start():
def update_pbar():
for i in range(5): #replace this with your method to update your progressbar
time.sleep(1)
progressbar["value"] = i
t = threading.Thread(target=update_pbar)
t.start()
# Main
if __name__ == '__main__':
window = Tk()
progressbar = ttk.Progressbar(window, orient = HORIZONTAL,length=300, mode = 'determinate')
progressbar.pack()
progressbar["maximum"] = 4
btnSubmit = Button(window, text="Execute", command=Start).pack()
window.mainloop()

How to show data in tkinter module dynamically-Python

I am new to GUI creation in python using Tkinter. I found a script in stack overflow which gives a GUI in which i can i add data manually. I tried to add data dynamically thus automating the whole process using following code:
def insert_data(self):
"""
Insertion method.
"""
for l in range(10):
time.sleep(3)
print(i)
self.treeview.insert('', 'end', text="Item_"+str(self.i)+str(l), values=(self.dose_entry.get()+" mg", self.modified_entry.get()))
# Increment counter
self.i = self.i + 1
This insert data snippet gives proper output and even shows data in the GUI. But data inserted in gui shows up after execution of for loop. TO be more elaborate .:
print(i) in the above for loop print 0 to 9 in IDLE but during that execution the data shown in the IDLE does not appear simultaneously in GUI. It prints data in IDLE until the loop is not finished and the GUI would not show any data until the loop is finished and then it shows all the numbers all at once.
I want to show data in GUI as it prints in IDLE i.e. for every iteration data should be visible in GUI the as it prints in IDLE. Example:
In first iteration for i = 0
print(i) will print '0'.I want to show it in GUI as it prints in IDLE. Following is my code:
import tkinter
from tkinter import ttk
import urllib
import requests
import time
class Begueradj(tkinter.Frame):
'''
classdocs
'''
def __init__(self, parent):
'''
Constructor
'''
tkinter.Frame.__init__(self, parent)
self.parent=parent
self.initialize_user_interface()
def initialize_user_interface(self):
"""Draw a user interface allowing the user to type
items and insert them into the treeview
"""
self.parent.title("Canvas Test")
self.parent.grid_rowconfigure(0,weight=1)
self.parent.grid_columnconfigure(0,weight=1)
self.parent.config(background="lavender")
# Define the different GUI widgets
self.dose_label = tkinter.Label(self.parent, text = "Dose:")
self.dose_entry = tkinter.Entry(self.parent)
self.dose_label.grid(row = 0, column = 0, sticky = tkinter.W)
self.dose_entry.grid(row = 0, column = 1)
self.modified_label = tkinter.Label(self.parent, text = "Date Modified:")
self.modified_entry = tkinter.Entry(self.parent)
self.modified_label.grid(row = 1, column = 0, sticky = tkinter.W)
self.modified_entry.grid(row = 1, column = 1)
self.submit_button = tkinter.Button(self.parent, text = "Insert", command = self.insert_data)
self.submit_button.grid(row = 2, column = 1, sticky = tkinter.W)
self.exit_button = tkinter.Button(self.parent, text = "Exit", command = self.parent.quit)
self.exit_button.grid(row = 0, column = 3)
# Set the treeview
self.tree = ttk.Treeview( self.parent, columns=('Dose', 'Modification date'))
self.tree.heading('#0', text='Item')
self.tree.heading('#1', text='Dose')
self.tree.heading('#2', text='Modification Date')
self.tree.column('#1', stretch=tkinter.YES)
self.tree.column('#2', stretch=tkinter.YES)
self.tree.column('#0', stretch=tkinter.YES)
self.tree.grid(row=4, columnspan=4, sticky='nsew')
self.treeview = self.tree
# Initialize the counter
self.i = 0
def insert_data(self):
"""
Insertion method.
"""
for l in range(10):
time.sleep(3)
print(l)
self.treeview.insert('', 'end', text="Item_"+str(l), values=(self.dose_entry.get()+" mg", self.modified_entry.get()))
# Increment counter
self.i = self.i + 1
def main():
root=tkinter.Tk()
d=Begueradj(root)
root.mainloop()
if __name__=="__main__":
main()
When you use time.sleep(3) the execution suspends, and also any updates to the GUI. The GUI update tasks are in a queue and they will be handled as soon as the application has time on its hands. And there is no time to spare until the loop ends.
To force the treeview to update you can insert the line:
self.treeview.update_idletasks() # Force widget update
after you have added the treeview item.

Tkinter - dragging mouse across disabled Textbox stops update()

I'm making a program that uses a some kind of widget that from the user's side, lists 'uneditable' and 'unselectable' lines of data.
The widget should also have a scrollbar so my widget options are a bit more limited from what I understand.
Furthermore, the application displays continuously updating numbers.
I went for a textbox - however, when I hold down left click and move my mouse across the textbox root.update() stops/waits.
I wrote some example code below to demonstrate this phenomenon.
import time
from tkinter import *
class App:
def __init__(self):
self.root = Tk()
self.root.geometry("500x500")
self.root.resizable(False, False)
self.main_frame = Frame(self.root)
self.main_frame.grid(row = 0, column = 0, sticky = "news")
self.main_text_box = Text(self.main_frame)
self.main_text_box.grid(row = 0, column = 0, sticky = "news")
self.main_text_box.tag_configure("bold", font = "Helvetica 50")
self.main_text_box.insert(END, "Example text", "bold")
self.main_text_box.configure(state = DISABLED)
def update(self):
self.root.update()
def main():
application = App()
time_start = time.time()
while True:
application.update()
print("Program running, {} seconds since start".format(
round(time.time() - time_start, 3)))
if __name__ == "__main__":
main()
When the user drags the mouse across the textbox, the print statement in
while True:
waits for root.update().
Basically, my question is: Is there any way to not have root.update() wait if the mouse is dragged across a disabled textbox?
(note - I'm new to this site so if I'm being unclear or something, please do point out what I could've done better in my question :))
Thanks!
edit: Sorry, I forgot to mention that I'm using update() because in my actual program (which I didn't post b/c it's 800+ lines), I have other non-tkinter update() methods in the while loop so that I can update other data each frame.
You do not need to manage the loop of the tkinter instance with update() the mainloop() will do this for you.
Instead lets write this where the class inherits from Tk() and then make the time print function part of the class. We can also use after() to update the print. Lastly we should set time_start to a class attribute to be used in our time function.
import time
import tkinter as tk
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.geometry("500x500")
self.resizable(False, False)
self.time_start = time.time()
self.main_frame = tk.Frame(self)
self.main_frame.grid(row = 0, column = 0, sticky = "news")
self.main_text_box = tk.Text(self.main_frame)
self.main_text_box.grid(row = 0, column = 0, sticky = "news")
self.main_text_box.tag_configure("bold", font = "Helvetica 50")
self.main_text_box.insert("end", "Example text", "bold")
self.main_text_box.configure(state = "disabled")
self.time_check()
def time_check(self):
print("Program running, {} seconds since start".format(round(time.time() - self.time_start, 3)))
self.after(100, self.time_check)
if __name__ == "__main__":
my_app = App()
my_app.mainloop()
I could not quite reproduce the problem described; however, some anti-patterns need correction in the code posted:
Use mainloop instead of a while loop.
avoid calling update, the mainloop handles that for you.
Use root.after to repeatedly call a method.
I change your App class to have it inherit from tk.Tk; you could inherit from Frame instead; in this case, you have to provide a master to it.
I also placed the console printing in a function outside the class, as it felt more suitable to keep a console output separated.
import time
import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
self.geometry("500x500")
self.resizable(False, False)
self.main_frame = Frame(self)
self.main_frame.grid(row = 0, column = 0, sticky = "news")
self.main_text_box = Text(self.main_frame)
self.main_text_box.grid(row = 0, column = 0, sticky = "news")
self.main_text_box.tag_configure("bold", font = "Helvetica 50")
self.main_text_box.insert(END, "Example text", "bold")
self.main_text_box.configure(state = DISABLED)
self._update_me()
def _update_me(self):
print_time()
self.after(500, self._update_me)
def print_time():
print("Program running, {} seconds since start".format(
round(time.time() - time_start, 3)))
def main():
application = App()
application.mainloop()
if __name__ == "__main__":
time_start = time.time()
main()

Python - tkinter Treeview not updating integer variable

The first set of code works. It displays the updating variable. However, in the second code, when I put the variable in a Treeview widget instead, the variable just stays on 0 and does not update. How come this works with the label but not with treeview? What is the easiest way to fix this?
First (works):
from tkinter import *
from tkinter import ttk
import threading
import time
def fun():
for i in range(10):
var.set(var.get() + 1)
time.sleep(.5)
t = threading.Thread(target=fun)
root = Tk()
var = IntVar()
var.set(0)
mainframe = ttk.Frame(root)
mainframe.grid(column = 0, row = 0)
label = ttk.Label(mainframe, textvariable=var)
label.grid(column = 0, row = 0)
t.start()
root.mainloop()
Second (does not work):
from tkinter import *
from tkinter import ttk
import threading
import time
def fun():
for i in range(10):
var.set(var.get() + 1)
time.sleep(.5)
t = threading.Thread(target=fun)
root = Tk()
var = IntVar()
var.set(0)
mainframe = ttk.Frame(root)
mainframe.grid(column = 0, row = 0)
tree = ttk.Treeview(mainframe, columns = ('number'), height = 1)
tree.insert('', 'end', text = 'Number', values = var.get())
tree.grid(column=0, row=0)
t.start()
root.mainloop()
modify fun to :
def fun():
for i in range(10):
var.set(var.get() + 1)
x = tree.get_children()
tree.item(x, text = 'Number', values = var.get())
time.sleep(.5)
The get_children method returns a list of tuples item IDs, one for each child of the tree. With tree.item then update the child with the required id.
In the second program along with the variable var, you also have to update the child of the tree

Getting tkinter dialog to notify the user that their input is invalid

I was wondering how you get the tkinter to notify the user if their input is invalid. When they input a negative integer or something that's not an integer, a dialog would pop up and say that their input is invalid. It would then let the user know and then it would let the user go back to the program. I've got the 2nd part working, but I'm getting some errors when I try to do the invalid input popup. It also pops up 2 windows as well which I have no idea why.
Code in question:
import tkinter
from tkinter import *
import tkinter as tk
class TimeConverterUI():
def __init__(self):
#main functions
self.root_window = Tk()
self.root_window.geometry('400x150')
self.root_window.title('Seconds Converter')
self.text()
self.quitValue=tk.Toplevel()
self.invalidinputDialog=tk.Toplevel()
self.calculate_button()
self.quit_button()
self.root_window.wait_window()
def text(self):
#label for seconds text along with the grid for it
row_label = tkinter.Label(
master = self.root_window, text = 'Seconds: ')
row_label.grid( row = 0,
sticky = tkinter.W)
self.secondsEntry = Entry(master = self.root_window)
self.secondsEntry.grid(row = 0, column = 1)
#label for converted time along with the grid
convert_label = tkinter.Label(
master = self.root_window, text = 'Converted Time(H:M:S): ')
convert_label.grid(row=1)
self.result = Entry(master= self.root_window)
self.result.grid(row = 1, column = 1)
def calculate_button(self):
#calculate button along with the placement
quit = Button(self.root_window, text = "Calculate", command = self.calculate)
quit.grid(row = 3, column = 0, columnspan = 3, pady=20,
sticky = tkinter.W)
def calculate(self):
try:
#divides the seconds into minutes
m,s = divmod(int(self.secondsEntry.get()),60)
#divides the minutes into hours and returns h:m:s format
h,m = divmod(m,60)
c= ("%d:%02d:%02d" % (h, m, s))
#after displaying the result, if the user wants to calculate again, deletes
#previous result and recalculates
self.result.delete(0,END)
self.result.insert(0,c)
except ValueError:
#if user enters an input that's not an integer, exception is placed
#d= 'Invalid Input'
self.invalidinputDialog()
def invalidinputDialog(self):
self.invalidValue = tk.Toplevel()
messageLabel = tk.Label(master=self.invalidValue,
text="Invalid Input.").grid(row=0,column=0)
invalidContinue = tk.Button(master=self.invalidValue, text='Close',
command = self.invalidValue.destroy).grid(row=1,column=1)
self.result.delete(0,END)
self.result.insert(0,d)
def quit_button(self):
#button for grid
quit = Button(self.root_window, text = "Quit", command = self.quitDialog)
quit.grid(row = 3, column = 3, columnspan = 3, pady=20,
sticky = tkinter.E)
def quitDialog(self):
self.quitValue = tk.Toplevel()
messageLabel = tk.Label(master=self.quitValue,
text="Are you sure you want to quit?").grid(row=0,column=0)
#closes both the main window and the message window
continueButton = tk.Button(master=self.quitValue,text='Continue',
command=self.root_window.destroy).grid(row=1,column=2)
#lets the user go back to previous screen if they cancel
cancelButton = tk.Button(master=self.quitValue,text='Cancel',
command=self.quitValue.destroy).grid(row=1,column=1)
def quit(self) -> bool:
#quits the program and shell is refreshed
self.root_window.destroy()
return True
if __name__ == '__main__':
convert=TimeConverterUI()
Here's where the problem lies.
def calculate(self):
try:
#divides the seconds into minutes
m,s = divmod(int(self.secondsEntry.get()),60)
#divides the minutes into hours and returns h:m:s format
h,m = divmod(m,60)
c= ("%d:%02d:%02d" % (h, m, s))
#after displaying the result, if the user wants to calculate again, deletes
#previous result and recalculates
self.result.delete(0,END)
self.result.insert(0,c)
except ValueError:
#if user enters an input that's not an integer, exception is placed
#d= 'Invalid Input'
self.invalidinputDialog()
def invalidinputDialog(self):
self.invalidValue = tk.Toplevel()
messageLabel = tk.Label(master=self.invalidValue,
text="Invalid Input.").grid(row=0,column=0)
invalidContinue = tk.Button(master=self.invalidValue, text='Close',
command = self.invalidValue.destroy).grid(row=1,column=1)
self.result.delete(0,END)
self.result.insert(0,d)
If you are looking to simply tell the user the input was invalid you can do
from tkinter import messagebox
messagebox.showinfo("Title", "Input invalid.")
Messagebox does need to be imported separately from tkinters main library.
That being said you need to import tkinter only once. You are currently importing tkinter 3 times with:
import tkinter
from tkinter import *
import tkinter as tk
instead you should use:
import tkinter as tk
you can use this for most of tkinter methods. You just need to use the prefix tk. on your widgets.
examples tk.Entry(), tk.Label(), tk.Text() and so on.
As for your extra blank windows that are opening they are coming from your __init__ portion of your class.
class TimeConverterUI():
def __init__(self):
self.quitValue=tk.Toplevel() # causing an empty toplevel window to open in init
self.invalidinputDialog=tk.Toplevel() # causing an empty toplevel window to open in init
You do not need to set up the toplevel ahead of time. You can simply create it in a method when you need one.

Categories