My application has some time-consuming background processes to be run occasionally. I want to show that the application is working using a simple indeterminate ttk.Progressbar.
However, my implementation only shows static progress bar.
Anyway, here is how I implemented it.
class MyApp(ttk.Frame):
def __init__(self, master):
ttk.Frame.__init__(self, master)
fname = 'test.txt'
self.build_ui()
self.process_file(fname)
def process_file(self, fname):
self.show_progress(True)
fdata = self.read_file(fname)
fdata = self.spellcheck(fdata)
self.show_progress(False)
return fdata
def show_progress(self, start)
if start:
self.prog_win = tk.Toplevel()
self.prog_win.title('Working...')
self.prog_win.resizable(0, 0)
self.progress_bar = ttk.Progressbar(self.prog_win,
orient=tk.HORIZONTAL,
mode='indeterminate',
takefocus=True)
self.progress_bar.grid()
self.progress_bar.start()
else:
self.progress_bar.stop()
self.prog_win.destroy()
root = tk.Tk()
root.update()
gui = MyApp(root)
gui.mainloop()
The code above does not work as intended. The Progressbar appears static, it does not move and just hangs there forever. I tried to use threading but if I start show_progress in a separate Thread it always gets executed after the processing has been done.
What am I doing wrong?
The problem is that self.read_file() and self.spellcheck() are blocking which prevents Tkinter from updating your progress bar in its main loop. A simple way to solve this is to do your processing in a separate thread, and periodically check to see if the thread finished its work.
import threading
class MyApp(ttk.Frame):
def __init__(self, master):
ttk.Frame.__init__(self, master)
fname = 'test.txt'
self.build_ui()
self.process_file(fname)
def process_file(self, fname):
self.show_progress(True)
# Start thread to process file.
self.thread = threading.Thread(target=self.process_file_worker, args=(fname,))
self.thread.daemon = True # Allow the program to terminate without waiting for the thread to finish.
self.thread.start()
# Start checking the thread.
self.process_file_check()
def process_file_check(self):
if self.thread.is_alive():
# Thread is still running, check thread again in 10 milliseconds.
self.after(10, self.process_file_check)
else:
# Thread finished, handle processed results.
# Do something with `self.fdata`.
self.show_progress(False)
def process_file_worker(self, fname):
# This is run inside the thread.
fdata = self.read_file(fname)
fdata = self.spellcheck(fdata)
self.fdata = fdata
def show_progress(self, start):
if start:
self.prog_win = tk.Toplevel()
self.prog_win.title('Working...')
self.prog_win.resizable(0, 0)
self.progress_bar = ttk.Progressbar(self.prog_win,
orient=tk.HORIZONTAL,
mode='indeterminate',
takefocus=True)
self.progress_bar.grid()
self.progress_bar.start()
else:
self.progress_bar.stop()
self.prog_win.destroy()
root = tk.Tk()
root.update()
gui = MyApp(root)
gui.mainloop()
Related
I'm creating a tkinter application and I want to disable certain GUI elements while a particular set of threads are running. Basically one button starts up all the threads and they get added to a list which is a member of a class named Form. I have a separate thread which is a class member and is constantly checking the list of threads to see if it is empty; as long as there is at least one living thread in the list, the GUI elements should be disabled.
class Form()
def __init__(self):
...
self.check_running_threads_thread = threading.Thread(target = self.check_running_threads)
self.check_running_threads_thread.start()
The issue is that I need this thread to quit running on application exit. The way I have been handling that is like so in main:
root = tk.Tk()
myForm = Form(root)
root.mainloop()
myForm.kill_threads = True
and the thread function that is started on the call of __init()__ of the Form class is:
def check_running_threads(self):
while not self.kill_threads:
for key, val in self.items.items():
if len(self.running_threads) > 0:
if key.endswith("entry"):
val.config(state='readonly')
elif key.endswith('button'):
val['state'] = tk.DISABLED
else:
if key.endswith("entry"):
val.config(state='normal')
elif key.endswith('button'):
val['state'] = tk.NORMAL
self.running_threads = [th for th in self.running_threads if th.is_alive()]
time.sleep(0.1)
where self.items is just a dictionary containing all the tk gui elements, indexed by a unique name.
The problem is that sometimes when the mainloop ends, such as by me exiting out of the application, the check_running_threads() function could be anywhere in the loop before kill_threads gets set to True, meaning it could try to change the state of the tk gui elements when they no longer exist. This leads to the program freezing and never exiting. The tk window is destroyed, but the command line window hangs forever.
What is the proper way to handle this? How can I ensure a thread running independently of the tkinter mainloop doesn't access an element which no longer exists?
2022-09-05 Edit:
Here is a complete minimal example:
import tkinter as tk
from tkinter import ttk
import threading
import time
import random
class Form():
def __init__(self, root):
self.kill_threads = False
self.running_threads = []
self.items = {}
self.btn = ttk.Button(root, text='Press me, wait for num threads to be zero, then click "X" button', command=self.run_threads)
self.btn.pack(side=tk.LEFT)
self.items['my_button'] = self.btn
for jj in range(60):
new = ttk.Button(root, text='', width=0)
self.items[f'my_{jj}_button'] = new
new.pack(side=tk.LEFT)
self.check_running_threads_thread = threading.Thread(target = self.check_running_threads)
self.check_running_threads_thread.start()
def thread_func(self):
time.sleep(random.uniform(0.5, 2.5))
def run_threads(self):
for ii in range(30):
mythread = threading.Thread(target = self.thread_func)
mythread.start()
self.running_threads.append(mythread)
def check_running_threads(self):
while not self.kill_threads:
for key, val in self.items.items():
if key.endswith("button"):
if len(self.running_threads) > 0:
val['state'] = tk.DISABLED
else:
val['state'] = tk.NORMAL
print("Num threads running:", len(self.running_threads))
self.running_threads = [th for th in self.running_threads if th.is_alive()]
time.sleep(0.2)
root = tk.Tk()
myForm = Form(root)
root.mainloop()
myForm.kill_threads = True
All the excess buttons are there to represent other GUI elements that are in my actual program. The more there are, the more likely it seems the program hangs on exit.
Here's a video of me running it. You can see the behavior is sometimes inconsistent (it doesn't always hang, such as the third time I ran it):
https://youtu.be/JhKxRYVujQg
You can create a new method in your Form class that will be called on close:
root = tk.Tk()
myForm = Form(root)
root.protocol("WM_DELETE_WINDOW", myForm.wait_till_threads_killed)
root.mainloop()
Where you method would look something like this:
def wait_till_threads_killed(self):
self.kill_threads = True
self.check_running_threads_thread.join()
for t in self.running_threads:
t.join()
self.parent.destroy()
After having added root as parent in the __init__ method:
def __init__(self, root):
self.parent = root
Edit: adding destroy() otherwise the window won't close + waiting for all threads
Edit2: note that this is only needed if you want to wait for the threads to finish. If want to kill them on exit, just set them as daemons in your initial code (then you don't necessarily need protocol):
mythread = threading.Thread(target = self.thread_func, daemon=True)
I am currently trying to program a robot arm which is controlled by a Raspberry Pi.
Everything works fine so far, except for one thing and I already googled and tried everything for many hours but can't find a working solution.
For the movement of the robot arm it is necessary to run all motors "simultaneously" with threads (works fine).
The problem I have is that I need to update a label which shows the current angle of an axis (motor) as soon as it finished its movement but other motors are still running (threads).
After a lot of research I thought I found the solution by using a queue and Tkinters after-method. But it still doesn't work as the labels text only gets updated after all threads terminated.
I wrote an example code where I want to get a label update for motor "one" which will finish its for-loop (100 iterations) before motor "two" (500 iterations). I expected the label to get updated as soon as motor one reached its target while motor two is still runing.
But although I used the after-method it still waits till motor two finished before updating the label.
Hope you can help me!
from tkinter import *
import threading
import time
from queue import *
class StepperMotors:
def __init__(self, root):
self.root = root
self.start_btn = Button(root, text="Start", command=lambda:self.start_movement())
self.start_btn.config(width = 10)
self.start_btn.grid(row=1,column=1)
self.label_one = Label(root, text='')
self.label_one.config(width = 10)
self.label_one.grid(row=2, column=1)
self.label_two = Label(root, text='')
self.label_two.config(width = 10)
self.label_two.grid(row=3, column=1)
def start_movement(self):
self.thread_queue = Queue()
self.root.after(100, self.wait_for_finish)
thread_one = threading.Thread(target=self.motor_actuation, args=(1,100))
thread_two = threading.Thread(target=self.motor_actuation, args=(2,500))
thread_one.start()
thread_two.start()
thread_one.join()
thread_two.join()
def motor_actuation(self, motor, iterations):
for i in range(iterations):
i = i+1
update_text = str(motor) + " " + str(i) + "\n"
print(update_text)
time.sleep(0.01)
self.thread_queue.put(update_text)
def wait_for_finish(self):
try:
self.text = self.thread_queue.get()
self.label_one.config(text=self.text)
except self.thread_queue.empty():
self.root.after(100, self.wait_for_finish)
if __name__ == "__main__":
root = Tk()
root.title("test")
stepper = StepperMotors(root)
root.mainloop()
It’s better to use daemon thread which are non-blocking.
Also, It’s better to have a separation of concerns: a robot (or robot arm) can be a object which has its own life time: a daemon thread. Idem, you can define a "LabelUpdater" which read the state of a robot and update a label.
Let’s define a robot:
it is created on application initialisation and is run when the user click the "Start" button,
The robot moves and reports its angle in a app-level multithreading-queue,
class Robot(threading.Thread):
def __init__(self, name: str, label_queue: queue.Queue, end_pos: int):
super().__init__(name=name)
self.daemon = True
self.label_queue = label_queue
self.end_pos = end_pos
def run(self) -> None:
for angle in range(self.end_pos):
self.label_queue.put(angle)
time.sleep(0.01)
Let’s define a LabelUpdater:
It is created on application initialisation and run forever (it can observe a robot even if it is not running).
It reads the robot queue (for instance each second to avoid blinking) and update the label
class LabelUpdater(threading.Thread):
def __init__(self, name: str, label_queue: queue.Queue, root_app: tkinter.Tk, variable: tkinter.Variable):
super().__init__(name=name)
self.daemon = True
self.label_queue = label_queue
self.root_app = root_app
self.variable = variable
def run(self) -> None:
# run forever
while True:
# wait a second please
time.sleep(1)
# consume all the queue and keep only the last message
last_msg = None
while True:
try:
msg = self.label_queue.get(block=False)
except queue.Empty:
break
last_msg = msg
self.label_queue.task_done()
if last_msg:
self.variable.set(last_msg)
Then, the main application should define:
2 multithreading queues: one for each label,
2 tkinter.StringVar variable which will be updated,
the robots and the label updaters,
the two updaters are started, and will run forever.
class StepperMotors:
def __init__(self, root):
self.root = root
self.label_one_queue = queue.Queue()
self.label_two_queue = queue.Queue()
self.start_btn = tkinter.Button(root, text="Start", command=lambda: self.start_movement())
self.start_btn.config(width=10)
self.start_btn.grid(row=1, column=1)
self.text_one = tkinter.StringVar()
self.text_one.set("one")
self.label_one = tkinter.Label(root, textvariable=self.text_one)
self.label_one.config(width=10)
self.label_one.grid(row=2, column=1)
self.text_two = tkinter.StringVar()
self.text_two.set("two")
self.label_two = tkinter.Label(root, textvariable=self.text_two)
self.label_two.config(width=10)
self.label_two.grid(row=3, column=1)
self.robot_one = Robot("robot_one", self.label_one_queue, 100)
self.robot_two = Robot("robot_two", self.label_two_queue, 500)
self.updater_one = LabelUpdater("updater_one", self.label_one_queue, self.root, self.text_one)
self.updater_two = LabelUpdater("updater_two", self.label_two_queue, self.root, self.text_two)
self.updater_one.start()
self.updater_two.start()
def start_movement(self):
self.robot_one.start()
self.robot_two.start()
Of course, you need a flag or something to check that each robot is not already running.
I have a GUI application. It has more than one windows. Main windows is updating every second with new values. I am using threading and queue here. There is a button in the main window. Pressing it will open a new window. What I want is, when new window is shown, stop the process in the main window (updating the values process) and some new process will be shown in the new window. Closing the new window should revoke the main window again and continue the process. Do i need to use multi threading or multi processing?
Unfortunately, I am asking this question for the second time. sorry for that.
import tkinter
import time
import threading
import random
import queue
class GuiPart:
def __init__(self, master, queue, endCommand,newWindow):
self.queue = queue
self.pause = False
# Set up the GUI
console = tkinter.Button(master, text='Done', command=endCommand)
console.pack()
console2 = tkinter.Button(master, text='New', command=newWindow)
console2.pack()
self.output = tkinter.StringVar()
#output.set(1)
output_1_label = tkinter.Label(master, textvariable= self.output, height=2, width=12)
output_1_label.pack()
# Add more GUI stuff here
self.temp_process()
def temp_process(self):
if not self.pause:
print ("handling messages")
else:
print ("Not handling messages")
root.after(1000,self.temp_process)
def processIncoming(self):
while self.queue.qsize():
try:
msg = self.queue.get(0)
print (msg)
self.output.set(msg)
except queue.Empty:
pass
class ThreadedClient:
def __init__(self, master):
self.master = master
# Create the queue
self.queue = queue.Queue()
# Set up the GUI part
self.gui = GuiPart(master, self.queue, self.endApplication,self.create_window)
self.running = 1
self.thread1 = threading.Thread(target=self.workerThread1) #this is for sending data to queue.
# what about second window?
self.thread1.start()
self.periodicCall()
def on_quit(self):
self.gui.pause = False
self.window.destroy()
def create_window(self):
self.window = tkinter.Toplevel(root)
self.gui.pause = True
self.window.protocol("WM_DELETE_WINDOW",self.on_quit)
def periodicCall(self):
self.gui.processIncoming()
if not self.running:
import sys
sys.exit(1)
self.master.after(1000, self.periodicCall)
def workerThread1(self):
while self.running:
time.sleep(rand.random() * 1)
msg = rand.random()
self.queue.put(msg)
def endApplication(self):
self.running = 0
rand = random.Random()
root = tkinter.Tk()
client = ThreadedClient(root)
root.mainloop()
You can have your function processIncoming check for a flag, and pause/resume when required.
class GuiPart:
def __init__(self, master, queue, endCommand,newWindow):
self.queue = queue
self.pause = False
....
def processIncoming(self):
while self.queue.qsize() and not self.pause:
try:
msg = self.queue.get(0)
print (msg)
self.output.set(msg)
except queue.Empty:
pass
I'm currently developing a program with a GUI to gather data from a sensor and visualise it within the GUI.
The data from the sensor is stored in a list for further calculations.
What I'm currently trying to do, is starting the logging process in a new thread.
This should stop the GUI from freezing.
My current code:
import tkinter as tk
import time
import threading
class GUI:
def __init__(self, tk_object):
self.gui = tk_object
self.gui.title("Logger")
self.gui.resizable(width=True, height=True)
self.testlabel = tk.Label(self.gui, text="Recording in process")
self.testlabel.grid(row = 7, column = 0)
self.testlabel.config(bg='white')
btn1 = tk.Button(master, text="Start Recording", width=16, height=5, command=lambda: self.start_functions())
btn1.grid(row=2,column=0)
btn2 = tk.Button(master, text="Stop Recording", width=16, height=5, command=lambda: self.stop_functions())
btn2.grid(row=3,column=0)
def start_functions(self):
"""Calls the method get_sample in a new thread and changes the bg-color of the label to red"""
Thread_1 = threading.Thread(target=self.get_sample(), name='Thread1')
Thread_1.start()
self.testlabel.config(bg='red')
def stop_functions(self):
"""Stops all active threads and changes bg-color of the label back to white"""
#stop_threads = threading.Event()
#stop_threads.set()
threading.Event().set()
self.testlabel.config(bg='white')
def get_sample(self):
"""get data and append it to a list"""
while not threading.Event().is_set():
time.sleep(1)
res_cel.append(res_cel[-1]+1)
x_value.append(x_value[-1]+1)
print(res_cel)
print(x_value)
master = tk.Tk()
master_object = GUI(master)
master.mainloop()
Currently, the get_sample method contains a placeholder.
I'm trying to stop the Thread1 (where the get_sample method is running in) via the Event() handler.
while not threading.Event().is_set():
This doesn't seem to work in a proper way.
Are there better ways to do this?
Before this approach I tried using a class to handle the threads (This was found on stackoverflow, but I can't seem to find it anymore, sorry):
class Controller(object):
def __init__(self):
self.thread1 = None
self.stop_threads = Event()
def run(self):
.. do somehting
and starting / stopping the threads via:
def starting_thread(self):
self.stop_threads.clear()
self.thread1 = Thread(target = self.run)
self.thread1.start()
def stopping_thread(self):
self.stop_threads.set()
self.thread1 = None
Note: These functions are within the Controller Class.
With this solution I wasn't able to alter the lables background color in the GUI Class, since it doesn't know what object it is referencing to.
Im fairly new to programming python so I'd be glad if someone could explain to me what I'm missing here.
Thanks
xSaturn
You'd better create your own Thread object with a method to stop it. A thread stop running when it goes of its run method. Look at the example bellow
import time
import tkinter as tk
from threading import Thread
class MyThread(Thread):
thread_running = False
def run(self):
k = 0
self.thread_running = True
while self.thread_running:
print(k)
k +=1
time.sleep(1)
print("thread ended")
def stop(self):
self.thread_running = False
class Window(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.thread = None
tk.Button(self, text="launch thread", command=self.launch_thread)\
.grid(row=1, column=0)
tk.Button(self, text="stop thread", command=self.stop_thread)\
.grid(row=2, column=0)
def launch_thread(self):
if self.thread:
print("thread already launched")
else:
print("thread launched")
self.thread = MyThread()
self.thread.start()
def stop_thread(self):
if self.thread:
self.thread.stop()
self.thread = None
else:
print("no thread running")
if __name__ == '__main__':
win = Window()
win.mainloop()
See this topic for more information
Is there any way to kill a Thread?
I have a tkinter application (runs as main thread), within it I open new top-level window - it is a log windows printing result of tests (the test are performed with selenium webdriver). This dialog is also a caller of all the tests.
So I want to display the dialog (as top-level, there is one more window for whole application), run a test, wait until the test is done and print the result, then do the same for another test unit. But I don't want to make the window freeze during the tests.
I've tried to use threads, but it obviously it can work just like that. In this case the dialog doesn't even start till tests are done.
Here's the code of the dialog window.
class TestDialog(tkinter.Toplevel):
def __init__(self, parent, tester, url):
super().__init__(parent)
self.parent = parent
self.webtester = tester;
self.__initComponents()
self.run(url)
self.wait_window(self)
def __initComponents(self):
self.transient(self.parent)
frame = tkinter.Frame(self)
self._tarea = tkinter.Text(frame, state='disabled',wrap='none', width=55, height=25)
vsb = tkinter.Scrollbar(frame, orient=tkinter.VERTICAL, command=self._tarea.yview)
self._tarea.configure(yscrollcommand=vsb.set)
self._tarea.grid(row=1, column=0, columnspan=4, sticky="NSEW", padx=3, pady=3)
vsb.grid(row=1, column=4, sticky='NS',pady=3)
frame.grid(row=0, column=0, sticky=tkinter.NSEW)
frame.columnconfigure(0, weight=2)
frame.rowconfigure(1, weight=1)
window = self.winfo_toplevel()
window.columnconfigure(0, weight=1)
window.rowconfigure(0, weight=1)
self.bind("<Escape>", self.close)
self.protocol("WM_DELETE_WINDOW", self.close)
self.grab_set()
def appendLine(self, msg):
self._tarea['state'] = 'normal'
self._tarea.insert("end", msg+'\n')
self._tarea['state'] = 'disabled'
def run(self, url):
self.appendLine("Runneing test #1...")
try:
thr = threading.Thread(target=self.webtester.urlopen, args=(url,))
thr.start()
except:
pass
thr.join()
self.webtester.urlopen(url)
self.appendLine("Running test #2")
try:
thr = threading.Thread(target=self.webtester.test2)
thr.start()
except:
pass
def close(self, event=None):
self.parent.setBackgroundScheme(DataTreeView.S_DEFAULT)
self.parent.focus_set()
self.destroy()
This dialog is opened from parent window simply by:
testDialog = TestDialog(self.parent, self._webtester, url)
Thank you for any advice.
To prevent the GUI from freezing you need self.run() to end quickly.
It needs to spawn a thread, start the thread, then end:
import Queue
sentinel = object()
root = tkinter.Tk()
...
def run(self, url):
outqueue = Queue.Queue()
thr = threading.Thread(target=self.run_tests, args=(url, outqueue))
thr.start()
root.after(250, self.update, outqueue)
Now the function this thread runs can run for a long time:
def run_tests(self, url, outqueue):
outqueue.put("Running test #1...")
self.webtester.urlopen(url)
outqueue.put("Running test #2")
self.webtester.test2()
outqueue.put(sentinel)
But because Tkinter expects all GUI calls to originate from a single thread, this spawned thread must not make any GUI calls. For it to interact with the GUI, you can send output (such as a status update message) through a Queue.Queue and concurrently let the main Tkinter thread monitor this Queue.Queue periodically (through calls to root.after):
def update(self, outqueue):
try:
msg = outqueue.get_nowait()
if msg is not sentinel:
self.appendLine(msg)
root.after(250, self.update, outqueue)
else:
# By not calling root.after here, we allow update to
# truly end
pass
except Queue.Empty:
root.after(250, self.update, outqueue)