Posting Thread Progress back to tkinter Frame - python

I am posting this as informational for folks looking for a way to communicate thread progress back to a tkinter Frame or window. I have seen several approaches detailed in SO and other sites, but none really seemed adequate to me. So here is an approach that displays progress as both a message box update and advancing a Scale Widget. It uses the tkinter variable classes StringVar and DoubleVar rather than trying to use callbacks or continuously poll a queue in the main thread.
Comments are, of course, welcome, but it appears this approach works well.
`
import tkinter as tk
from tkinter import ttk, END, NW, GROOVE
import threading
import queue
import time
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.queue = queue.Queue()
self.msgCt=0
self.listbox = tk.Listbox(self, width=20, height=5)
self.scaleVal=tk.DoubleVar()
self.progressbar = ttk.Scale(self, orient='horizontal',
length=300,
from_=0.0, to=100.0,
variable=self.scaleVal)
self.scaleVal.set(0.0)
self.button = tk.Button(self, text="Start", command=self.spawnthread)
self.msgBtn = tk.Button(self,text="Set Msg", command=self.sendMessage)
self.msgTxt=tk.StringVar(self,"Messages Here...")
self.msgBox=tk.Message(self,textvariable=self.msgTxt,width=200,
anchor=NW,relief=GROOVE)
self.listbox.grid(row=0,column=0,columnspan=2)
self.msgBox.grid(row=1,column=0,columnspan=2)
self.progressbar.grid(row=2,column=0,columnspan=2)
self.button.grid(row=3,column=0)
self.msgBtn.grid(row=3,column=1)
def spawnthread(self):
self.button.config(state="disabled")
self.listbox.delete(0, END)
self.thread = ThreadedClient(self.queue,self.msgTxt,self.scaleVal)
self.thread.start()
self.periodiccall()
def sendMessage(self,msg=None):
if not msg==None:
self.msgTxt.set(msg)
else:
self.msgTxt.set("Message {}".format(self.msgCt))
self.msgCt+=1
def periodiccall(self):
self.checkqueue()
if self.thread.is_alive():
self.after(100, self.periodiccall)
else:
self.button.config(state="active")
def checkqueue(self):
while self.queue.qsize():
try:
msg = self.queue.get(0)
self.listbox.insert('end', msg)
# self.progressbar.step(25)
except queue.Empty:
pass
class ThreadedClient(threading.Thread):
def __init__(self, qu, mtxt,dvar):
threading.Thread.__init__(self)
self.queue = qu
self.msgTxt=mtxt
self.scaleVal=dvar
def run(self):
self.scaleVal.set(0.0)
for x in range(1, 10):
time.sleep(2)
msg = "Function %s finished..." % x
self.msgTxt.set(msg)
self.scaleVal.set(x*10)
self.queue.put(msg)
if __name__ == "__main__":
app = App()
app.mainloop()
`

In response to the several comments, here is a new version of the code:
import tkinter as tk
from tkinter import ttk, END, NW, GROOVE
import threading
import queue
import time
from pickle import FALSE
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.msgCt=0
self.thrdCt=0;
self.scaleVal=tk.DoubleVar()
self.doneSignal=tk.BooleanVar()
self.doneSignal.set(False)
self.doneSignal.trace("w",self.on_doneSignal_set)
self.progressbar = ttk.Progressbar(self, orient='horizontal',
length=300,
maximum=100.0,
variable=self.scaleVal)
self.scaleVal.set(0.0)
self.startBtn = tk.Button(self, text="Start", command=self.spawnthread)
self.stopBtn=tk.Button(self,text="Stop", command=self.stopthread)
self.stopBtn.config(state="disabled")
self.msgBtn = tk.Button(self,text="Set Msg", command=self.sendMessage)
self.msgTxt=tk.StringVar(self,"Messages Here...")
self.msgBox=tk.Message(self,textvariable=self.msgTxt,width=200,
anchor=NW,relief=GROOVE)
self.msgBox.grid(row=0,column=0,columnspan=3)
self.progressbar.grid(row=1,column=0,columnspan=3)
self.startBtn.grid(row=2,column=0)
self.stopBtn.grid(row=2,column=1)
self.msgBtn.grid(row=2,column=2)
def on_doneSignal_set(self,*kwargs):
self.sendMessage("Thread is DONE")
self.startBtn.config(state="active")
self.stopBtn.config(state="disabled")
def stopthread(self):
if self.thread.is_alive():
self.thread.stopNow()
def spawnthread(self):
self.thrdCt=0
self.startBtn.config(state="disabled")
self.stopBtn.config(state="active")
self.thread = ThreadedClient(self.msgTxt,self.scaleVal,self.doneSignal)
self.thread.start()
# self.periodiccall()
def sendMessage(self,msg=None):
if not msg==None:
self.msgTxt.set(msg)
else:
self.msgTxt.set("Message {}".format(self.msgCt))
self.msgCt+=1
class ThreadedClient(threading.Thread):
def __init__(self, mtxt,dvar,dsig):
threading.Thread.__init__(self)
self.msgTxt=mtxt
self.scaleVal=dvar
self._stopNow=False
self._doneSignal=dsig
self._lock=threading.Lock()
def run(self):
self._stopNow=False
self.scaleVal.set(0.0)
for x in range(1, 10):
if not self.checkStopNow():
time.sleep(2)
msg = "Function %s finished..." % x
self.msgTxt.set(msg)
self.scaleVal.set(x*10)
else:
break
self._doneSignal.set(True)
def stopNow(self):
with self._lock:
self._stopNow=True
def checkStopNow(self):
rtrn=False
with self._lock:
rtrn=self._stopNow
return rtrn
if __name__ == "__main__":
app = App()
app.mainloop()
First, the motivation for this exercise in the first place: I have a large python application that uses Scipy.optimize to find a solution to a modeling problem. This can often take a long time, so I wanted a way to have it run, but post messages to the user periodically to let them know things are happening, allow user to abort in the middle, and finally to post a message to the main thread that the modeling is now done. My original code was partly based on a threading example that assumed the producer/consumer model of threading, whereby a producer creates data (put into a Queue) and the consumer consumes it. This is the WRONG model for my problem, so this new code has no Queue. It just simulates a long modeling process using the run() method, in which there are calls to sleep() which could obviously be replaced by steps in the SciPy.minimize function calls.
This new example uses DoubleVar to allow the thread to update a progressBar (thanks to stovfl for that suggestion), a StringVar to update a message box in the main thread from the modeling thread, and finally a BooleanVar to signal the main thread that things are done. This version has no polling in the main thread. To me, that does not seem a very elegant solution!
How do I know the changes to the DoubleVar, StringVar and BooleanVar get through to the main thread? Only that this program works!! Note that the message box can be updated from either the modeling thread or using a button in the main GUI thread.
Again, comments welcome -- Give me reasons this should NOT work, then tell me why it does given those reasons! Does this violate some basic design of Python, or would there be a situation where this would not work for some reason??

Related

Threading and Tkinter - How to use the threading module with my simple example?

I don't understand how to use the threading module properly. In this example I have two tkinter widgets, a button and a progress bar. The progress bar (configured in indeterminate mode) has to be active when the user pushes the button, and when the task is completed, the progress bar has to be stopped.
import tkinter as tk
from tkinter import ttk
import threading, ipaddress
class MainWindow:
def __init__(self):
self.parent=tk.Tk()
self.parent.geometry("786x524+370+100")
self.parent.title("Test")
self.parent.configure(background="#f0f0f0")
self.parent.minsize(786, 524)
self.ProBar=ttk.Progressbar(self.parent, mode="indeterminate")
self.ProBar.pack(padx=(40, 40), pady=(40, 40), fill=tk.BOTH)
self.StartButton=ttk.Button(self.parent, text="Start", command=self.MyHeavyTask)
self.StartButton.pack(padx=(40, 40), pady=(40, 40), fill=tk.BOTH)
self.parent.mainloop()
# my start function:
def Start(self):
self.ProBar.start(4)
self.MyHeavyTask()
self.ProBar.stop()
# my real start function. it's just an example, it needs time to be completed:
def MyHeavyTask(self):
ls=[]
obj=ipaddress.ip_network("10.0.0.0/8")
for obj in list(obj.hosts()):
print(obj.exploded)
# start my test:
if __name__=="__main__":
app=MainWindow()
This code has an issue, it can't run the function "MyHeavyTask" and at the same time keep active the progress bar widget. to solve it, I tried to put "MyHeavyTask" in an indipendent thread changing the line 17 with this one:
self.StartButton=ttk.Button(self.parent, text="Start",
command=threading.Thread(target=self.MyHeavyTask).start())
unfortunately this solution doesn't work. when I press the button, nothig happens…why? What is the right way to use the threading module in my example?
You can add a method to the class
def Get_Input(self):
message = input(">")
if message:
send_message(message)
and add in init class
threading.Thread(target=self.Get_Input, args=(,)).start()
Please note :
If you passing one argument, you need to use
threading.Thread(target=self.Get_Input, args=(var1,)).start()
Unlike common sense :)
Here a runnable example similar to the code in your question, that shows a way to run a background task and keep a ttk.Progressbar active simultaneously. It does this by using the universal after() widget method to repeatedly schedule calls to a method that checks whether the background task is running and updates the progress bar if it is. It also disables and re-enables the Start button appropriately so the task can't be start again while it's running.
Note I strongly suggest you read and start following the PEP 8 - Style Guide for Python Code.
from random import randint
import tkinter as tk
from tkinter import ttk
import threading
from time import sleep
class MainWindow:
def __init__(self):
self.parent = tk.Tk()
self.parent.geometry("786x524+370+100")
self.parent.title("Test")
self.parent.configure(background="#f0f0f0")
self.parent.minsize(786, 524)
self.task = threading.Thread(target=self.my_heavy_task)
self.pro_bar = ttk.Progressbar(self.parent, mode="indeterminate")
self.pro_bar.pack(padx=(40, 40), pady=(40, 40), fill=tk.BOTH)
self.start_btn = ttk.Button(self.parent, text="Start", command=self.start)
self.start_btn.pack(padx=(40, 40), pady=(40, 40), fill=tk.BOTH)
self.parent.mainloop()
def check_thread(self):
if self.task.is_alive():
self.pro_bar.step() # Update progressbar.
self.parent.after(20, self.check_thread) # Call again after delay.
else:
self.pro_bar.stop()
self.start_btn.config(state=tk.ACTIVE)
def start(self):
"""Start heavy background task."""
self.start_btn.config(state=tk.DISABLED)
self.task.start()
self.pro_bar.start()
self.check_thread() # Start checking thread.
def my_heavy_task(self):
"""Slow background task."""
for obj in (randint(0, 99) for _ in range(6)):
print(obj)
sleep(.5)
if __name__=="__main__":
app = MainWindow()
Does this help?
start_thread = MainWindow()
run_test = threading.Thread(None, start_thread.start)
run_test.start()
# start my test:
if __name__=="__main__":
app=MainWindow()

Python Multithreading - not running simultaneously

im trying to learn python. But i have problems with the threading. First i failed in the "Proces" class because i putted the loop on the wrong place and my program newer returned from the other class.
But now i think all is correct and it still does not work. I need to have a GUI where i want to be able to write my conditions via text entries and i need another class "Proces" that will do stuff, checking status ower internet and so on constantly or in a specified interval...
The Problem is that my tkinter GUI is freezing after pressing something
here is my GUI.py file:
import tkinter as tk
from Proces import Proces
root = tk.Tk()
frame = tk.Frame(root)
frame.pack()
button = tk.Button(frame, text="QUIT", fg="red",command=quit).pack(side=tk.LEFT)
pr = Proces()
print("\nGUI: proces init...")
pr.start()
print("\nGUI: Start ended")
root.mainloop()
here is the Proces.py file:
import time, threading
class Proces(threading.Thread):
def loop(self):
while True:
time.sleep(2)
print("\nProces: looping")
def __init__(self):
threading.Thread.__init__(self)
print("\nProces: Starting proces")
time.sleep(2)
def run(self):
self.deamon = True
print("\nProces: Starting loop")
self.loop()
*This is the output: *
Proces: Starting proces
GUI: proces init...
Proces: Starting loop
GUI: Start ended
Proces: looping
Proces: looping
Proces: looping
Proces: looping
*But the GUI of the tkinter does not react.*
How should i do this kind of task?
Thank you for your help, advice and answer
I think you don't have problem about starting thread etc. However, you should be able to control your thread, meaning your thread should return based on some condition. It seems you have a button to quit. I assume you want to finish the process by clicking the button.
To do that, when you click to button, your main thread should pass a variable to Process in order to break your while loop, which is your thread basically.
Here is a code that you can work on.
import tkinter as tk
import time
import threading
class MainWindow:
def __init__(self, master):
self.master = master
self.quit_button = tk.Button(self.master, text="QUIT", command=lambda:self.quit(), width=20)
self.quit_button.pack(side=tk.LEFT)
self.process = None
print("\nGUI: process init...")
self.start_task()
def quit(self):
self.process.stop_process = True
print("\nGUI: Start ended")
def start_task(self):
self.process = Process()
self.process.start()
class Process(threading.Thread):
def loop(self):
while True:
if not self.stop_process:
time.sleep(2)
print("\nProcess: looping")
else:
print("\nProcess: looping ended")
return
def __init__(self):
threading.Thread.__init__(self)
self.stop_process = False
print("\nProcess: Starting proces")
time.sleep(2)
def run(self):
self.deamon = True
print("\nProcess: Starting loop")
self.loop()
if __name__ == '__main__':
root = tk.Tk()
app = MainWindow(master=root)
root.mainloop()
So you start yout tkinter, which is your main thread. Then iniating another class within your main thread, which is inherits the thread. So you have two seperate thread that working. When you clicked to "quit" button, you pass variable stop_process, which breaks the loop and return, meaning ending your thread. Your main thread is still alive for running your tkinter window.
I hope it will help

Python match progress bar to long process

I have a Python script that has some processes that take some time such as creating a .shp file and opening an .mxd. I've read Python Progress Bar but I would like to avoid using a external package. So far I have;
from tkinter import *
from tkinter.ttk import *
import os, arcpy
tk=Tk()
progress=Progressbar(tk,orient=HORIZONTAL,length=100,mode='determinate')
def bar():
import time
os.startfile("Assignment.mxd")
progress['value']=20
tk.update_idletasks()
time.sleep(8)
progress['value']=50
tk.update_idletasks()
time.sleep(8)
progress['value']=80
tk.update_idletasks()
time.sleep(8)
progress['value']=100
progress.pack()
Button(tk,text='Open',command=bar).pack()
mainloop()
It seems to be working but I don't think the process to open the .mxd is paying attention to what stage the progress bar is at. I would like to get it so that the process finishing coincides with the progress bar reaching 100%. Is there away to get the process to 'wait' for the progress bar? Could anyone offer some pointers on how I might get this to work?
Thanks
The problem is that the sleep function and possibly any other blocking function you call in your bar function blocks the GUI. A solution could be pushing the actions to a worker thread. A very quick solution I can come up with is this:
from random import randint
from threading import Thread
from tkinter import Tk, DoubleVar, HORIZONTAL
from tkinter.ttk import Frame, Button, Progressbar
from time import sleep
class Worker(Thread):
def __init__(self, reference):
super().__init__()
self.reference = reference
def run(self):
print("running...")
# replace this dummy loop with actual processing tasks
while self.reference.get_progress() < 100:
duration = randint(1, 3)
sleep(duration)
self.reference.add_progress(10 * duration)
print("finished.")
class Example(Frame):
def __init__(self, root):
super().__init__(master=root)
self.progress = DoubleVar(value=0.0)
self.worker = Worker(reference=self)
self.startbutton = Button(master=self, text="Start", command=self.start)
self.startbutton.pack()
self.progressbar = Progressbar(master=self, orient=HORIZONTAL, length=100, mode='determinate', variable=self.progress)
self.progressbar.pack()
self.infobutton = Button(master=self, text="Info", command=self.info)
self.infobutton.pack()
self.pack()
def get_progress(self):
return self.progress.get()
def set_progress(self, value):
self.progress.set(value)
def add_progress(self, value):
self.progress.set(self.progress.get() + value)
def start(self):
# print("sleeping...")
# sleep(10) # uncomment this to block the GUI
print("starting...")
self.worker.start()
def info(self):
print(self.worker.is_alive())
def main():
root = Tk()
app = Example(root)
app.mainloop()
if __name__ == '__main__':
main()
Note that the info button works before, while and after the thread runs. One problem with this quick solution is that the thread can only be run once. To run it multiple times, some kind of resetting needs to be implemented.

Access to Tkinter's Text widget by background thread cause crashes

I created a simple tkinter app, where have used two threads. Their task is to write numbers to widgets such as label and text. One thread is triggered by button (click event) and second is executed as a background thread.
import Tkinter as tk
from ttk import *
from Tkconstants import *
import threading, thread, time
def tl1(text,counter):
while True:
text.insert(END,counter)
counter += 1
time.sleep(2)
def tl2(label,counter):
while True:
label['text'] = counter
counter += 1
time.sleep(1)
class mainWindow():
def __init__(self, master):
self.master = master
self._initLayout()
def _initLayout(self):
#button
self.button = tk.Button(self.master, text="thread1_start", command = self._task1)
self.button.pack()
#label
self.label = tk.Label(self.master)
self.label.pack()
#text
self.text = tk.Text(self.master, width=30)
self.text.pack()
def _task1(self):
t1 = thread.start_new_thread(tl1,(self.text,1))
def _task2(self):
t2 = thread.start_new_thread(tl2,(self.label,1000))
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.mainWindow = mainWindow(self)
self.mainWindow._task2() #background_thread
app = App()
app.mainloop()
In this manner everything works fine, but if we change the background thread to display results on text widget, the whole app freezes.
Why background thread works fine communicating with label but causes problems with text widget? Is there any way to run it properly?
Tkinter isn't thread safe. You can only access widgets from the thread that created them. Your threads will need to put data on a thread-safe queue, and your GUI thread will need to poll the queue.
In your particular case you don't need threads at all. You can use the tkinter after method to run code periodically.

Getting data from a long function to update a GUI

I've got a GUI which gives the user the option to run a selection of tests.
These tests run in threads as they use MIDI data in.
I have a checkQueue() function that is run using after() but once the user selects a test, the checkQueue() function is no longer called until the function finishes.
How do I get the checkQueue to continue running during my buttonTest() function so I can use data from the test to update the GUI?
Here is a simplified version of my code:
import Tkinter as tk
import Queue
class Program(tk.Frame):
def __init__(self,parent):
tk.Frame.__init__(self, parent)
self.parent = parent
self.initUI()
self.q = Queue.Queue()
self.after(200, checkQueue, self.q, self)
def initUI(self):
start = tk.Button(self.parent, command=self.runTest, text="Run Test 1")
start.pack()
self.instruction = tk.Label(self.parent, text="Press Button 1")
self.instruction.pack()
def runTest(self):
buttonTest(self)
def checkQueue(q,app):
print "Calling checkQueue"
while not q.empty():
#HandleData (update a label/canvas etc.)
app.update()
app.after(200, checkQueue,q,app)
def buttonTest(gui):
#Does lots of functions but is just a while for this example
x=1
while x==1:
if x == 100:
gui.q.put("Some Data")
def main():
root = tk.Tk()
root.configure(background="black")
app = Program(root)
root.mainloop()
root.destroy()
if __name__ == "__main__":
main()
I'm assuming buttonTest is the function that's calling your tests that are running in other threads. Even though the actual work is done in child threads, buttonTest is still running in the main thread, and its while loop is hogging all the main thread's processing cycles. Try starting buttonTest itself in its own thread. This will give the main thread the breathing room necessary to process your checkQueue calls.
def runTest(self):
Thread(target=buttonTest, args=(self,)).start()

Categories

Resources