I'm writing an app using tkinter for the GUI, and I want an indeterminate progress bar to be going back and forth while the main function is running, which sometimes takes a few seconds, depending on user input. Normally, the whole program freeze while the main function is running, so I am trying to establish a threaded process for the progress bar so it moves while main() is doing its thing (both functions are called withinscan_and_display()).
from tkinter import filedialog
from tkinter import *
from tkinter.ttk import Progressbar
from main import main
import graphs
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from PIL import ImageTk, Image
import threading
root = Tk()
launch_frame = Frame(root)
button_frame = Frame(root)
graph_frame = Frame(root, height=1000, width=1200)
# log directory button/text
logDirButton = Button(master=launch_frame, text='Select log storage location...',
command=lambda: get_directory(log_text), width=22)
log_text = Text(master=launch_frame, height=1, width=25)
logDirButton.grid(row=1, column=0)
log_text.grid(row=1, column=1)
# scan directory button/text
dirButton = Button(master=launch_frame, text="Select scan directory...", command=lambda: get_directory(t), width=22)
t = Text(master=launch_frame, height=1, width=25)
dirButton.grid(row=2, column=0)
t.grid(row=2, column=1)
# main scan button
mainButton = Button(master=launch_frame, text="SCAN!", state=DISABLED,
width=50, height=10, bg='#27b355')
mainButton.grid(row=3, column=0, columnspan=2)
# progress bar
progress = Progressbar(launch_frame, orient=HORIZONTAL, length=100, mode='indeterminate')
progress.grid(row=4, column=0, columnspan=2)
launch_frame.grid(row=0, column=0, sticky=NW)
def get_directory(text):
# first clear form if it already has text
try:
text.delete("1.0", END)
except AttributeError:
pass
directory = filedialog.askdirectory()
# store the first directory for later specific reference
text.insert(END, directory)
# disable scan button until user has given necessary info to run (log storage location, scan directory)
enable_scan_button(log_text, t)
return directory
def enable_scan_button(logText, dirText):
if logText.get("1.0", END) != '\n' and dirText.get('1.0', END) != '\n':
mainButton['state'] = NORMAL
mainButton['command'] = lambda: scan_and_display()
else:
mainButton['state'] = DISABLED
def scan_and_display():
threading.Thread(target=bar_start).start()
# get scan directory and log directory from text fields
log_directory = log_text.get("1.0", END)[:-1]
scan_directory = t.get("1.0", END)[:-1]
# store the initial scan directory for later reference
top_dir = scan_directory
# runs the main scan function. Passes scan_directory and log_directory arguments
data, scanDate = main(log_directory, scan_directory)
display(scan_directory, data, scanDate, top_dir)
def bar_start():
print("BAR START CALLED")
progress.start(10)
With this setup (and various other configurations I've trieD), the bar still freezes while main() is doing it's thing, and I need it to move to indicate to the user that something is happening.
You need to thread the long-running function, not the progressbar.
def scan_and_display():
# get scan directory and log directory from text fields
# It's best to do all tkinter calls in the main thread
log_directory = log_text.get("1.0", END)[:-1]
scan_directory = t.get("1.0", END)[:-1]
th = threading.Thread(target=bar_start, args=(log_directory, scan_directory))
th.start()
progress.start()
def bar_start(log_directory, scan_directory):
print("BAR START CALLED")
# store the initial scan directory for later reference
top_dir = scan_directory
# runs the main scan function. Passes scan_directory and log_directory arguments
data, scanDate = main(log_directory, scan_directory)
display(scan_directory, data, scanDate, top_dir)
progress.stop()
EDIT: here's a MCVE:
import tkinter as tk
from tkinter.ttk import Progressbar
import time
from threading import Thread
def long_running_function(arg1, arg2, result_obj):
"""accept some type of object to store the result in"""
time.sleep(3) # long running function
result_obj.append(f'DONE at {int(time.time())}')
root.event_generate("<<LongRunningFunctionDone>>") # trigger GUI event
def long_func_start():
x = 'spam'
y = 'eggs'
t = Thread(target=long_running_function, args=(x,y,data))
t.start()
result.config(text='awaiting result')
progress.start()
def long_func_end(event=None):
progress.stop()
result.config(text=f"process finished with result:\n{data[-1]}")
root = tk.Tk()
root.geometry('200x200')
btn = tk.Button(root, text='start long function', command=long_func_start)
btn.pack()
progress = Progressbar(root, orient=tk.HORIZONTAL, length=100, mode='indeterminate')
progress.pack()
result = tk.Label(root, text='---')
result.pack()
root.bind("<<LongRunningFunctionDone>>", long_func_end) # tell GUI what to do when thread ends
data = [] # something to store data
root.mainloop()
Related
I have made a python program based on Tkinter that analyses a CSV file and returns a PDF file. If there is a big CSV file, the tkinter window will freeze until the process is done. This is not desirable and instead I would like to introduce an indeterminate progress bar that disappears when a question is prompted for the user and then reappears when more processing is followed. I have read that somehow it needs a different thread, but I am quite stuck.
My program looks like:
import threading
from time import sleep
import tkinter as tk
import tkinter.ttk as ttk
root = tk.Tk()
root.title('text')
label = tk.Label(text = 'text')
label.pack()
class myThread(threading.Thread):
def __init__(self, threadID):
threading.Thread.__init__(self)
self.threadID = threadID
def run(self):
func()
def func():
#some processing
#ask the user a question and based on the answer more processing will be done
if True:
#more processing1
sleep(1)
else:
#more processing2
sleep(1)
def check_thread(th):
if not th.isAlive():
root.destroy()
root2.after(100, check_thread, th)
root2 = tk.Tk()
root2.title("New Top Level")
root2.geometry("400x170")
tk.Label(root2, text='Doing some work', justify=tk.CENTER, bg="#CBFDCB").place(x=43, y=30)
progress_bar = ttk.Progressbar(root2, orient="horizontal",
mode="indeterminate", takefocus=True, length=320)
progress_bar.place(x=40, y=80)
progress_bar.start()
thread1 = myThread(1)
thread1.start()
root2.after(100, check_thread, thread1)
root2.mainloop()
Button1 = tk.Button(root, text="sample", padx=10,
pady=5, fg="white", bg="#263D42", command=func)
Button1.pack()
root.mainloop()
Edit1: I have updated it with the version of the progress bar I was trying to use.
I am new to tkinter GUI programming. I tried to search my problem, but I was unable to formulate the correct question without a description of my problem.
I designed a small GUI - for the example here - with a button and a ScrolledText item.
import tkinter as tk
from tkinter import messagebox as msg
from tkinter.ttk import Notebook
from tkinter import filedialog
import tkinter.scrolledtext as tkscrolled
import do_something as ds
import os
import time
class Fatt(tk.Tk):
def __init__(self):
super().__init__()
# window setup
self.title("Test Gui")
self.geometry("1024x768")
self.resizable(0, 0)
# tab
self.notebook = Notebook(self)
# define tabs
res_avg_tab = tk.Frame(self.notebook)
# group nodal averaging
group_avg = tk.LabelFrame(res_avg_tab, text="Perform nodal averaging of all selected DB files")
group_avg.pack(padx=10, pady=10)
# nodal averaging button
self.avg_button = tk.Button(group_avg, text="Perform Nodal Averaging",
command=self.nodal_avg, bg="lightgrey", fg="black", width=50)
self.avg_button.pack(side=tk.TOP, pady=10, padx=10)
# scrolled log-text window
# group LOG
group_log = tk.LabelFrame(res_avg_tab, text="Result Averaging Output (LOG)")
group_log.pack(padx=10, pady=10)
self.avg_log = tkscrolled.ScrolledText(group_log, bg="white", fg="black", height=13, width=110)
self.avg_log.pack(side=tk.TOP, fill=tk.X, padx=10, pady=10)
# status-bar
self.status_text = tk.StringVar(res_avg_tab)
self.status_text.set("---")
self.status = tk.Label(res_avg_tab, textvar=self.status_text,
bd=1, relief=tk.SUNKEN, anchor=tk.W)
self.status.pack(side=tk.BOTTOM, fill=tk.BOTH)
# add everything to tabs
self.notebook.add(res_avg_tab, text="Average Results")
self.notebook.pack(fill=tk.BOTH, expand=True)
def show_cmb_file_creator(self):
pass
def nodal_avg(self):
sel_dbs = ["file1", "file2", "file3"]
# write file-list to log-window
self.avg_log.insert(tk.INSERT, "\nSelected Files for Nodal Averaging:\n")
for i in sel_dbs:
self.avg_log.insert(tk.INSERT, i+'\n')
self.avg_log.see(tk.END)
# if yes --> average results
if msg.askyesno("Nodal Averaging", "Perform nodal averaging with selected db-results?"):
start = time.time()
self.status_text.set("processing...")
self.config(cursor="wait")
self.avg_log.insert(tk.INSERT, "Start nodal averaging - this may take some time...\n")
class_obj = ds.DoSomething(i, self.avg_log)
for i in sel_dbs:
class_obj.do_something()
end = time.time()
overall_time_str = " Overall Averaging RUNTIME: {0:.2f} sec ({1:.1f} min) ".format(end-start, (end-start)/60.0)
self.avg_log.insert(tk.INSERT, "\n{0:*^80}".format(overall_time_str))
self.avg_log.see(tk.END)
self.status_text.set("---")
self.config(cursor="")
def browse_dir(self):
pass
def copy_to_clipboard(self, text=None):
pass
if __name__=="__main__":
fatt = Fatt()
fatt.mainloop()
The button "avg_button" executes the function "nodal_avg" and the main purpose of this function is to instantiate an external class and run a method.
class_obj = ds.DoSomething(i, self.avg_log)
for i in sel_dbs:
class_obj.do_something()
This class contains the main logic of my software and it contains a lot of print outputs.
import tkinter.scrolledtext as tkscrolled
class DoSomething:
def __init__(self, my_file, outp_print=print):
self.my_file = my_file
self.outp_print = outp_print
# my-print function
# for tkinter-log output
def myprint(self, text):
if self.outp_print==print:
print(text)
elif isinstance(self.outp_print, tkscrolled.ScrolledText):
self.outp_print.insert("end", text+'\n')
else:
print("myprint - ERROR: {0}".format(str(self.outp_print)))
def do_something(self):
for i in range(0,100000):
self.myprint("{0:d} - printed output".format(i))
I would like to print the output of the class/method to the ScrolledText window, but I also like to maintain the classic print functionality. Therefore I use the "myprint" method - which is able to use print or ScrolledText.insert for printing (I do not know if this is a smart approach?!).
If I run the code it basically works - but the ScrolledText window does not update on every print, only when the method in the external class is finished - then the output appears.
So my question is - how can I continuously update my ScrolledText window with my output string?
Thank you very much.
Best Regards
Michael
Here's my problem, when keyboard focus is on KeyboardEntry, KeysPressed() should happen, when focus leaves, toggle() should happen.
KeysPressed has a while loop such as:
while Flag:
#Whatever
toggle just toggles the flag
Flag= not Flag
The idea is when focus leaves KeyComboEntry, KeysPressed() stops running
This does not happen, at all matter of fact, toggle is not called
my
# Program by Fares Al Ghazy started 20/5/2017
# Python script to assign key combinations to bash commands, should run in the background at startup
# this program is meant to release bash code, it is obviously non-system agnostic and only works linux systems that use BASH
# is one file which only creates the GUI, another file is needed to use the info taken by this program
FileName = 'BinderData.txt'
import tkinter as tk
from ComboDetect import ComboDetector
from _thread import start_new_thread
# Create a class to get pressed keys and print them
KeyManager = ComboDetector()
# Class that creates GUI and takes info to save in file
class MainFrame(tk.Tk):
# variable to store pressed keys
KeyCombination = ""
testcounter = 1
Flag = True
#function to toggle Flag
def Toggle(self):
print("toggle")
self.Flag = not self.Flag
# function to write to file
def SaveFunction(self, e1, e2, FileName):
file = open(FileName, "a")
combo = e1.get() + '\n'
performed = e2.get() + '\n'
file.write(combo)
file.write(performed)
file.close()
def KeysPressed(self, Entry, KeyCombination):
Entry.config(state="normal")
#Entry.insert(tk.END, "Test")
while self.Flag:
print("test "+str(self.testcounter))
self.testcounter = self.testcounter + 1
KeyCombination = str(KeyManager.getpressedkeys())
Entry.delete(0, tk.END)
Entry.insert(tk.END, KeyCombination)
# constructor
def __init__(self, FileName, **kwargs):
tk.Tk.__init__(self, **kwargs)
# create GUI to take in key combinations and bash codes, then save them in file
root = self # create new window
root.wm_title("Key binder") # set title
# create labels and text boxes
KeyComboLabel = tk.Label(root, text="Key combination = ")
KeyComboEntry = tk.Entry(root)
# Bind function to entry
KeyComboEntry.bind('<FocusIn>', lambda e: start_new_thread(self.KeysPressed, (KeyComboEntry, self.KeyCombination)))
KeyComboEntry.bind('<FocusOut>', lambda f:self.toggle, ())
ActionLabel = tk.Label(root, text="Command to be executed = ")
ActionEntry = tk.Entry(root)
# place widgets in positions
KeyComboLabel.grid(row=0, column=0, sticky=tk.E)
ActionLabel.grid(row=1, column=0, sticky=tk.E)
KeyComboEntry.grid(row=0, column=1)
ActionEntry.grid(row=1, column=1)
# create save button
SaveButton = tk.Button(root, text="save",
command=lambda: self.SaveFunction(KeyComboEntry, ActionEntry, FileName))
SaveButton.grid(row=2, column=2, sticky=tk.E)
app = MainFrame(FileName)
app.mainloop()
I am writing a videoplayer with tkinter/python, so basically I have a GUI that can play a video. Now, I would like to implement a stop button, meaning that I would have a mainloop() for the GUI, and another nested mainloop() to play/stop the video and return to the GUI startup window. Here, it is said:
Event loops can be nested; it's ok to call mainloop from within an event handler.
However, I do not understand how to implement this nesting. Can someone please provide me with a simple example of such a script?
EDIT
Here is a working version of my code, since it seems that I am doing something exotic. Of course, I am a newbie, so it may be that I am mistaken about the necessity to have a nested mainloop.
#!/usr/bin/python
import numpy as np
from multiprocessing import Process, Queue
import cv2
import cv2.cv as cv
from PIL import Image, ImageTk
import Tkinter as tk
def image_capture(queue):
vidFile = cv2.VideoCapture(0)
while True:
flag, frame=vidFile.read()
frame = cv2.cvtColor(frame,cv2.cv.CV_BGR2RGB)
queue.put(frame)
cv2.waitKey(10)
def update_all(root, imagelabel, queue, process, var):
if var.get()==True:
im = queue.get()
a = Image.fromarray(im)
b = ImageTk.PhotoImage(image=a)
imagelabel.configure(image=b)
imagelabel._image_cache = b # avoid garbage collection
root.update()
root.after(0, func=lambda: update_all(root, imagelabel, queue, process, var))
else:
print var.get()
root.quit()
def playvideo(root, imagelabel, queue, var):
print 'beginning'
p = Process(target=image_capture, args=(task,))
p.start()
update_all(root, imagelabel, queue, p, var)
print 'entering nested mainloop'
root.mainloop()
p.terminate()
if var.get()==False:
im = ImageTk.PhotoImage(file='logo.png')
imagelabel.configure(image=im)
imagelabel._image_cache = im # avoid garbage collection
root.update()
var.set(True)
print 'finishing'
if __name__ == '__main__':
#initialize multiprocessing
task = Queue()
#GUI of root window
root = tk.Tk()
#the image container
image_label = tk.Label(master=root)
image_label.grid(column=0, row=0, columnspan=2, rowspan=1)
#fill label with image until video is loaded
bg_im = ImageTk.PhotoImage(file='logo.png')
image_label['image'] = bg_im
#frame for buttons
button_frame = tk.Frame(root)
button_frame.grid(column=0, row=1, columnspan=1)
#load video button and a switch to wait for the videopath to be chosen
load_button = tk.Button(master=button_frame, text='Load video',command=lambda: playvideo(root, image_label, task, switch))
load_button.grid(column=0, row=0, sticky='ew')
#Stop button
switch = tk.BooleanVar(master=root, value=True, name='switch')
stop_button = tk.Button(master=button_frame, text='Stop',command=lambda: switch.set(False))
stop_button.grid(column=0, row=1, sticky='ew')
#quit button
quit_button = tk.Button(master=button_frame, text='Quit',command=root.destroy)
quit_button.grid(column=0, row=2, sticky='ew')
root.mainloop()
This should be an example of a nested mainloop in Tkinter:
import Tkinter
def main():
print 'main'
t.mainloop()
print 'end main'
t = Tkinter.Tk()
b = Tkinter.Button(t, command = main)
b.pack()
t.mainloop()
Whenever you hit the button a new mainloop is executed.
main
main
main
main
main
# now close the window
end main
end main
end main
end main
end main
I have developed a simple app in Python (2.7) with Tkinter. But my status bar is only sort of working. Here's the stripped down code:
from Tkinter import *
import os
import sys
def fixFiles():
inputFilePath= input_dir.get()
#Build a list of files in a directory
fileList = os.listdir(inputFilePath)
#Loop through those files, open the file, do something, close the file
for filename in fileList:
infile = open(inputfilepath + "/" + filename,'r')
#Update the status with the filename
status_string = 'Status: Working on file: ' + str(filename)
status.set(status_string)
for line in infile:
#Do some stuff here
infile.close()
class App:
def __init__(self, master):
i = 0
status.set("Status: Press 'Fix Files!'")
statuslabel = Label(master, textvariable=status, relief = RIDGE, width = 65, pady = 5, anchor=W)
bFixFiles = Button(root, text='Fix Files!', command = fixFiles)
bQuit = Button(root, text='Quit', command = root.destroy)
statuslabel.grid(row=i, column = 0, columnspan = 2)
bFixFiles.grid(row=i, column=2, sticky=E)
bQuit.grid(row=i, column=3, sticky=W)
root = Tk()
root.title("FIX Files")
input_dir = StringVar()
status = StringVar()
choice = IntVar()
app = App(root)
root.mainloop()
Currently what's happening is that the status bar reads "Status: Press 'Fix Files!'" until the program is finished looping through the files, at which point it reads "Status: Working on file: XXXXX.txt" (which is the name of the last file to be opened and closed by the program.
I would like the status bar to update with the file name each time the program opens a new file. Any help is appreciated!
The goofy way is to use root.update_idletasks():
#Update the status with the filename
status_string = 'Status: Working on file: ' + str(filename)
status.set(status_string)
root.update_idletasks()
To its credit, it is simple, but it does not really work -- although the statuslabel gets updated, the Quit button is frozen until fixFiles is completed. That's not very GUI-friendly. Here are some more reasons why update and update_idletasks are considered harmful.
So how should we run a long-running task without freezing the GUI?
The key is to make your callback functions end quickly. Instead of having a long-running for-loop, make a function that runs through the innards of the for-loop once. Hopefully that ends quickly enough for the user to not feel the GUI has been frozen.
Then, to replace the for-loop, you could use calls to root.after to call your quick-running function multiple times.
from Tkinter import *
import tkFileDialog
import os
import sys
import time
def startFixFiles():
inputFilePath = tkFileDialog.askdirectory()
# inputFilePath= input_dir.get()
# Build a list of files in a directory
fileList = os.listdir(inputFilePath)
def fixFiles():
try:
filename = fileList.pop()
except IndexError:
return
try:
with open(os.path.join(inputFilePath, filename), 'r') as infile:
# Update the status with the filename
status_string = 'Status: Working on file: ' + str(filename)
status.set(status_string)
for line in infile:
# Do some stuff here
pass
except IOError:
# You might get here if file is unreadable, you don't have read permission,
# or the file might be a directory...
pass
root.after(250, fixFiles)
root.after(10, fixFiles)
class App:
def __init__(self, master):
i = 0
status.set("Status: Press 'Fix Files!'")
statuslabel = Label(
master, textvariable=status, relief=RIDGE, width=65,
pady=5, anchor=W)
bFixFiles = Button(root, text='Fix Files!', command=startFixFiles)
bQuit = Button(root, text='Quit', command=root.destroy)
statuslabel.grid(row=i, column=0, columnspan=2)
bFixFiles.grid(row=i, column=2, sticky=E)
bQuit.grid(row=i, column=3, sticky=W)
root = Tk()
root.title("FIX Files")
input_dir = StringVar()
status = StringVar()
choice = IntVar()
app = App(root)
root.mainloop()
The above begs the question, What should we do if our long-running task has no loop? or if even one pass through the loop requires a long time?
Here is a way to run the long-running task in a separate process (or thread), and have it communicate information through a queue which the main process can periodically poll (using root.after) to update the GUI status bar. I think this design is more easily applicable to this problem in general since it does not require you to break apart the for-loop.
Note carefully that all Tkinter GUI-related function calls must occur from a single thread. That is why the long-running process simply sends strings through the queue instead of trying to call status.set directly.
import Tkinter as tk
import multiprocessing as mp
import tkFileDialog
import os
import Queue
sentinel = None
def long_running_worker(inputFilePath, outqueue):
# Build a list of files in a directory
fileList = os.listdir(inputFilePath)
for filename in fileList:
try:
with open(os.path.join(inputFilePath, filename), 'r') as infile:
# Update the status with the filename
status_string = 'Status: Working on file: ' + str(filename)
outqueue.put(status_string)
for line in infile:
# Do some stuff here
pass
except IOError:
# You might get here if file is unreadable, you don't have read permission,
# or the file might be a directory...
pass
# Put the sentinel in the queue to tell update_status to end
outqueue.put(sentinel)
class App(object):
def __init__(self, master):
self.status = tk.StringVar()
self.status.set("Status: Press 'Fix Files!'")
self.statuslabel = tk.Label(
master, textvariable=self.status, relief=tk.RIDGE, width=65,
pady=5, anchor='w')
bFixFiles = tk.Button(root, text='Fix Files!', command=self.startFixFiles)
bQuit = tk.Button(root, text='Quit', command=root.destroy)
self.statuslabel.grid(row=1, column=0, columnspan=2)
bFixFiles.grid(row=0, column=0, sticky='e')
bQuit.grid(row=0, column=1, sticky='e')
def update_status(self, outqueue):
try:
status_string = outqueue.get_nowait()
if status_string is not sentinel:
self.status.set(status_string)
root.after(250, self.update_status, outqueue)
else:
# By not calling root.after here, we allow update_status to truly end
pass
except Queue.Empty:
root.after(250, self.update_status, outqueue)
def startFixFiles(self):
inputFilePath = tkFileDialog.askdirectory()
# Start long running process
outqueue = mp.Queue()
proc = mp.Process(target=long_running_worker, args=(inputFilePath, outqueue))
proc.daemon = True
proc.start()
# Start a function to check a queue for GUI-related updates
root.after(250, self.update_status, outqueue)
root = tk.Tk()
root.title("FIX Files")
app = App(root)
root.mainloop()