My little program has a potentially long running process. That's not a problem when doing it from the console, but now I want to add a GUI. Ideally I want to use Tkinter (a) because it's simple, and (b) because it might be easier to implement across platforms. From what I've read and experienced, (almost) all GUIs suffer the same issue anyway.
Through all my reading on the subject of threading and GUI there seem to be two streams. 1 - where the underlying worker process is polling (eg waiting to fetch data), and 2 - where the worker process is doing a lot of work (eg copying files in a for loop). My program falls into the latter.
My code has a "hierarchy" of classes.
The MIGUI class handles the GUI and interacts with the interface class MediaImporter.
The MediaImporter class is the interface between the user interface (console or GUI) and the worker classes.
The Import class is the long-running worker. It does not know that the interface or GUI classes exist.
The problem: After clicking the Start button, the GUI is blocked, so I can't click the Abort button. It is as if I'm not using threading at all. I suspect the issue is with the way I am starting the threading in startCallback method.
I've also tried the approach of threading the entire MediaImporter class. See the commented-out lines.
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog
import threading
import time
class MIGUI():
def __init__(self, master):
self.master = master
self.mediaImporter = MediaImporter()
self.startButton = ttk.Button(self.master, text='Start', command=self.startCallback)
self.startButton.pack()
self.abortButton = ttk.Button(self.master, text='Abort', command=self.abortCallback)
self.abortButton.state(['disabled'])
self.abortButton.pack()
def startCallback(self):
print('startCallback')
self.abortButton.state(['!disabled'])
self.startButton.state(['disabled'])
self.abortButton.update() # forcing the update seems unnecessary
self.startButton.update()
#print(self.startButton.state())
#print(self.abortButton.state())
self.x = threading.Thread(target=self.mediaImporter.startImport)
self.x.start()
self.x.join()
#self.mediaImporter.startImport()
self.startButton.state(['!disabled'])
self.abortButton.state(['disabled'])
self.abortButton.update()
self.startButton.update()
#print(self.startButton.state())
#print(self.abortButton.state())
def abortCallback(self):
print('abortCallback')
self.mediaImporter.abortImport()
self.startButton.state(['!disabled'])
self.abortButton.state(['disabled'])
class MediaImporter():
#class MediaImporter(threading.Thread):
""" Interface between user (GUI / console) and worker classes """
def __init__(self):
#threading.Thread.__init__(self)
self.Import = Import()
#other worker classes exist too
def startImport(self):
print('mediaImporter - startImport')
self.Import.start()
def abortImport(self):
print('mediaImporter - abortImport')
self.Import.abort()
class Import():
""" Worker
Does not know anything about other non-worker classes or UI.
"""
def __init__(self):
self._wantAbort = False
def start(self):
print('import - start')
self._wantAbort = False
self.doImport()
def abort(self):
print('import - abort')
self._wantAbort = True
def doImport(self):
print('doImport')
for i in range(0,10):
#actual code has nested for..loops
print(i)
time.sleep(.25)
if self._wantAbort:
print('doImport - abort')
return
def main():
gui = True
console = False
if gui:
root = tk.Tk()
app = MIGUI(root)
root.mainloop()
if console:
#do simple console output without tkinter - threads not necessary
pass
if __name__ == '__main__':
main()
The reason your GUI is blocked is because you call self.x.join(), which blocks until the doImport function is complete, see the join documentation. Instead I would call join() in your abortCallback() function, since that is what will cause the thread to stop running.
Thank you again XORNAND. The join() was definitely part of the problem. The other part of the problem was that there was no means of the MIGUI class knowing when the long-running process was complete (either because it had run its course, or because it was aborted.) An additional layer of messaging is required between the low-level worker, and the UI layer. I did try to use threading.Event without success, and did consider using Queues.
My solution is to use pubsub. (https://github.com/schollii/pypubsub) The worker layer can sendMessage on various topics, and the UI and interface layers can set up Listeners to perform actions with received data.
In my case, the Import.doImport method sends a STATUS message when it is completed. The MIGUI listener can then flip-flop the Start/Abort buttons accordingly.
To make sure the implementation of pubsub was going to work as planned I also set up a tkinter Progressbar. The doImport method sends a PROGESS message with the percent complete. This is reflected in the on-screen Progressbar.
A side note - in my original issue I had to use .update() on the buttons to get them to display. Now that we're not blocking anymore, this is not necessary.
Posting the complete working solution here, showing the pubsub implementation.
import tkinter as tk
from tkinter import ttk
import threading
import time
from pubsub import pub
class MIGUI():
def __init__(self, master):
self.master = master
self.mediaImporter = MediaImporter()
self.startButton = ttk.Button(self.master, text='Start', command=self.startCallback)
self.startButton.pack()
self.abortButton = ttk.Button(self.master, text='Abort', command=self.abortCallback)
self.abortButton.state(['disabled'])
self.abortButton.pack()
self.progress = ttk.Progressbar(self.master, length=300)
self.progress.pack()
pub.subscribe(self.statusListener, 'STATUS')
pub.subscribe(self.progressListener, 'PROGRESS')
def statusListener(self, status, value):
print('MIGUI', status, value)
if status == 'copying' and (value == 'finished' or value == 'aborted'):
self.startButton.state(['!disabled'])
self.abortButton.state(['disabled'])
def progressListener(self, value):
print('Progress %d' % value)
self.progress['maximum'] = 100
self.progress['value'] = value
def startCallback(self):
print('startCallback')
self.abortButton.state(['!disabled'])
self.startButton.state(['disabled'])
self.x = threading.Thread(target=self.mediaImporter.startImport)
self.x.start()
# original issue had join() here, which was blocking.
def abortCallback(self):
print('abortCallback')
self.mediaImporter.abortImport()
class MediaImporter():
""" Interface between user (GUI / console) and worker classes """
def __init__(self):
self.Import = Import()
#other worker classes exist too
pub.subscribe(self.statusListener, 'STATUS')
def statusListener(self, status, value):
#perhaps do something
pass
def startImport(self):
self.Import.start()
def abortImport(self):
self.Import.abort()
class Import():
""" Worker
Does not know anything about other non-worker classes or UI.
It does use pubsub to publish messages - such as the status and progress.
The UI and interface classes can subsribe to these messages and perform actions. (see listener methods)
"""
def __init__(self):
self._wantAbort = False
def start(self):
self._wantAbort = False
self.doImport()
def abort(self):
pub.sendMessage('STATUS', status='abort', value='requested')
self._wantAbort = True
def doImport(self):
self.max = 13
pub.sendMessage('STATUS', status='copying', value='started')
for i in range(1,self.max):
#actual code has nested for..loops
progress = ((i+1) / self.max * 100.0)
pub.sendMessage('PROGRESS', value=progress)
time.sleep(.1)
if self._wantAbort:
pub.sendMessage('STATUS', status='copying', value='aborted')
return
pub.sendMessage('STATUS', status='copying', value='finished')
def main():
gui = True
console = False
if gui:
root = tk.Tk()
app = MIGUI(root)
root.mainloop()
if console:
#do simple console output without tkinter - threads not necessary
pass
if __name__ == '__main__':
main()
Related
My interface is freezing on pressing the button. I am using threading but I am not sure why is still hanging. Any help will be appreciated. Thanks in advance
class magic:
def __init__(self):
self.mainQueue=queue.Queue()
def addItem(self,q):
self.mainQueue.put(q)
def startConverting(self,funcName):
if(funcName=="test"):
while not self.mainQueue.empty():
t = Thread(target = self.threaded_function)
t.start()
t.join()
def threaded_function(self):
time.sleep(5)
print(self.mainQueue.get())
m=magic()
def helloCallBack():
m.addItem("asd")
m.startConverting("test") //this line of code is freezing
B = tkinter.Button(top, text ="Hello", command = helloCallBack)
B.pack()
top.mainloop()
Here's a recipe for doing an asynchronous task with a tkinter-based GUI. I adapted it from a recipe in the cited book. You should be able to modify it to do what you need.
To keep the GUI responsive requires not interfering with its mainloop() by doing something like join()ing a background thread—which makes the GUI "hang" until the thread is finished. This is accomplished by using the universal after() widget method to poll a Queue at regular intervals.
# from "Python Coobook 2nd Edition", section 11.9, page 439.
# Modified to work in Python 2 & 3.
from __future__ import print_function
try:
import Tkinter as tk, time, threading, random, Queue as queue
except ModuleNotFoundError: # Python 3
import tkinter as tk, time, threading, random, queue
class GuiPart(object):
def __init__(self, master, queue, end_command):
self.queue = queue
# Set up the GUI
tk.Button(master, text='Done', command=end_command).pack()
# Add more GUI stuff here depending on your specific needs
def processIncoming(self):
""" Handle all messages currently in the queue, if any. """
while self.queue.qsize():
try:
msg = self.queue.get_nowait()
# Check contents of message and do whatever is needed. As a
# simple example, let's print it (in real life, you would
# suitably update the GUI's display in a richer fashion).
print(msg)
except queue.Empty:
# just on general principles, although we don't expect this
# branch to be taken in this case, ignore this exception!
pass
class ThreadedClient(object):
"""
Launch the main part of the GUI and the worker thread. periodic_call()
and end_application() could reside in the GUI part, but putting them
here means that you have all the thread controls in a single place.
"""
def __init__(self, master):
"""
Start the GUI and the asynchronous threads. We are in the main
(original) thread of the application, which will later be used by
the GUI as well. We spawn a new thread for the worker (I/O).
"""
self.master = master
# Create the queue
self.queue = queue.Queue()
# Set up the GUI part
self.gui = GuiPart(master, self.queue, self.end_application)
# Set up the thread to do asynchronous I/O
# More threads can also be created and used, if necessary
self.running = True
self.thread1 = threading.Thread(target=self.worker_thread1)
self.thread1.start()
# Start the periodic call in the GUI to check the queue
self.periodic_call()
def periodic_call(self):
""" Check every 200 ms if there is something new in the queue. """
self.master.after(200, self.periodic_call)
self.gui.processIncoming()
if not self.running:
# This is the brutal stop of the system. You may want to do
# some cleanup before actually shutting it down.
import sys
sys.exit(1)
def worker_thread1(self):
"""
This is where we handle the asynchronous I/O. For example, it may be
a 'select()'. One important thing to remember is that the thread has
to yield control pretty regularly, be it by select or otherwise.
"""
while self.running:
# To simulate asynchronous I/O, create a random number at random
# intervals. Replace the following two lines with the real thing.
time.sleep(rand.random() * 1.5)
msg = rand.random()
self.queue.put(msg)
def end_application(self):
self.running = False # Stops worker_thread1 (invoked by "Done" button).
rand = random.Random()
root = tk.Tk()
client = ThreadedClient(root)
root.mainloop()
For anyone having a problem with sys.exit(1) in #martineau's code - if you replace sys.exit(1) with self.master.destroy() the program ends gracefully. I lack the reputation to add a comment, hence the seperate answer.
My interface is freezing on pressing the button. I am using threading but I am not sure why is still hanging. Any help will be appreciated. Thanks in advance
class magic:
def __init__(self):
self.mainQueue=queue.Queue()
def addItem(self,q):
self.mainQueue.put(q)
def startConverting(self,funcName):
if(funcName=="test"):
while not self.mainQueue.empty():
t = Thread(target = self.threaded_function)
t.start()
t.join()
def threaded_function(self):
time.sleep(5)
print(self.mainQueue.get())
m=magic()
def helloCallBack():
m.addItem("asd")
m.startConverting("test") //this line of code is freezing
B = tkinter.Button(top, text ="Hello", command = helloCallBack)
B.pack()
top.mainloop()
Here's a recipe for doing an asynchronous task with a tkinter-based GUI. I adapted it from a recipe in the cited book. You should be able to modify it to do what you need.
To keep the GUI responsive requires not interfering with its mainloop() by doing something like join()ing a background thread—which makes the GUI "hang" until the thread is finished. This is accomplished by using the universal after() widget method to poll a Queue at regular intervals.
# from "Python Coobook 2nd Edition", section 11.9, page 439.
# Modified to work in Python 2 & 3.
from __future__ import print_function
try:
import Tkinter as tk, time, threading, random, Queue as queue
except ModuleNotFoundError: # Python 3
import tkinter as tk, time, threading, random, queue
class GuiPart(object):
def __init__(self, master, queue, end_command):
self.queue = queue
# Set up the GUI
tk.Button(master, text='Done', command=end_command).pack()
# Add more GUI stuff here depending on your specific needs
def processIncoming(self):
""" Handle all messages currently in the queue, if any. """
while self.queue.qsize():
try:
msg = self.queue.get_nowait()
# Check contents of message and do whatever is needed. As a
# simple example, let's print it (in real life, you would
# suitably update the GUI's display in a richer fashion).
print(msg)
except queue.Empty:
# just on general principles, although we don't expect this
# branch to be taken in this case, ignore this exception!
pass
class ThreadedClient(object):
"""
Launch the main part of the GUI and the worker thread. periodic_call()
and end_application() could reside in the GUI part, but putting them
here means that you have all the thread controls in a single place.
"""
def __init__(self, master):
"""
Start the GUI and the asynchronous threads. We are in the main
(original) thread of the application, which will later be used by
the GUI as well. We spawn a new thread for the worker (I/O).
"""
self.master = master
# Create the queue
self.queue = queue.Queue()
# Set up the GUI part
self.gui = GuiPart(master, self.queue, self.end_application)
# Set up the thread to do asynchronous I/O
# More threads can also be created and used, if necessary
self.running = True
self.thread1 = threading.Thread(target=self.worker_thread1)
self.thread1.start()
# Start the periodic call in the GUI to check the queue
self.periodic_call()
def periodic_call(self):
""" Check every 200 ms if there is something new in the queue. """
self.master.after(200, self.periodic_call)
self.gui.processIncoming()
if not self.running:
# This is the brutal stop of the system. You may want to do
# some cleanup before actually shutting it down.
import sys
sys.exit(1)
def worker_thread1(self):
"""
This is where we handle the asynchronous I/O. For example, it may be
a 'select()'. One important thing to remember is that the thread has
to yield control pretty regularly, be it by select or otherwise.
"""
while self.running:
# To simulate asynchronous I/O, create a random number at random
# intervals. Replace the following two lines with the real thing.
time.sleep(rand.random() * 1.5)
msg = rand.random()
self.queue.put(msg)
def end_application(self):
self.running = False # Stops worker_thread1 (invoked by "Done" button).
rand = random.Random()
root = tk.Tk()
client = ThreadedClient(root)
root.mainloop()
For anyone having a problem with sys.exit(1) in #martineau's code - if you replace sys.exit(1) with self.master.destroy() the program ends gracefully. I lack the reputation to add a comment, hence the seperate answer.
I'm writing a program with a GUI using TKinter, in which the user can click a button and a new process is started to perform work using multiprocess.Process. This is necessary so the GUI can still be used while the work is being done, which can take several seconds.
The GUI also has a text box where the status of the program is displayed when things happen. This is often straight forward, with each function calling an add_text() function which just prints text in the text box. However, when add_text() is called in the separate process, the text does not end up in the text box.
I've thought about using a Pipe or Queue, but that would require using some sort of loop to check if anything has been returned from the process and that would also cause the main (GUI) process to be unusable. Is there some way to call a function in one process that will do work in another?
Here's an simple example of what I'm trying to do
import time
import multiprocessing as mp
import tkinter as tk
textbox = tk.Text()
def add_text(text):
# Insert text into textbox
textbox.insert(tk.END, text)
def worker():
x = 0
while x < 10:
add_text('Sleeping for {0} seconds'.format(x)
x += 1
time.sleep(1)
proc = mp.Process(target=worker)
# Usually happens on a button click
proc.start()
# GUI should still be usable here
The asyncronous things actually require loop.
You could attach function to the TkInter's loop by using Tk.after() method.
import Tkinter as tk
class App():
def __init__(self):
self.root = tk.Tk()
self.check_processes()
self.root.mainloop()
def check_processes(self):
if process_finished:
do_something()
else:
do_something_else()
self.after(1000, check_processes)
app=App()
I ended up using a multiprocessing.Pipe by using TKinter's after() method to perform the looping. It loops on an interval and checks the pipe to see if there's any messages from the thread, and if so it inserts them into the text box.
import tkinter
import multiprocessing
def do_something(child_conn):
while True:
child_conn.send('Status text\n')
class Window:
def __init__(self):
self.root = tkinter.Tk()
self.textbox = tkinter.Text()
self.parent_conn, child_conn = multiprocessing.Pipe()
self.process = multiprocessing.Process(target=do_something, args=(child_conn,))
def start(self):
self.get_status_updates()
self.process.start()
self.root.mainloop()
def get_status_updates()
status = self.check_pipe()
if status:
self.textbox.add_text(status)
self.root.after(500, self.get_status_updates) # loop every 500ms
def check_pipe():
if self.parent_conn.poll():
status = self.parent_conn.recv()
return status
return None
I have the following pyqtmain.py:
#!/usr/bin/python3
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from pyqtMeasThread import *
class MainWindow(QMainWindow):
def __init__(self, parent=None):
self.qt_app = QApplication(sys.argv)
QMainWindow.__init__(self, parent)
buttonWidget = QWidget()
rsltLabel = QLabel("Result:")
self.rsltFiled = QLineEdit()
self.buttonStart = QPushButton("Start")
verticalLayout = QVBoxLayout(buttonWidget)
verticalLayout.addWidget(rsltLabel)
verticalLayout.addWidget(self.rsltFiled)
verticalLayout.addWidget(self.buttonStart)
butDW = QDockWidget("Control", self)
butDW.setWidget(buttonWidget)
self.addDockWidget(Qt.LeftDockWidgetArea, butDW)
self.mthread = QThread() # New thread to run the Measurement Engine
self.worker = MeasurementEngine() # Measurement Engine Object
self.worker.moveToThread(self.mthread)
self.mthread.finished.connect(self.worker.deleteLater) # Cleanup after thread finished
self.worker.measure_msg.connect(self.showRslt)
self.buttonStart.clicked.connect(self.worker.run)
# Everything configured, start the worker thread.
self.mthread.start()
def run(self):
""" Show the window and start the event loop """
self.show()
self.qt_app.exec_() # Start event loop
#pyqtSlot(str)
def showRslt(self, mystr):
self.rsltFiled.setText(mystr)
def main():
win = MainWindow()
win.run()
if __name__ == '__main__':
main()
And another thread script performing the actual measurement:
from PyQt4.QtCore import *
import time
class MeasurementEngine(QObject):
measure_msg = pyqtSignal(str)
def __init__(self):
QObject.__init__(self) # Don't forget to call base class constructor
#pyqtSlot()
def run(self):
self.measure_msg.emit('phase1')
time.sleep(2) # here I would like to make it as an interrupt
self.measure_msg.emit('phase2')
What this code does now is that after the Start button is pressed, the function run in the thread will be executed. However, actually in the function run, there are two phases of the measurement. Right now I used an time delay.
But what I would like to implement actually is that after the 'phase1' measurement is done. A message box will be popped up, and at the same time, the thread will be paused/held. Until the user closed the message box, then the thread function will be resumed.
Use a QWaitCondition from the QtCore module. Using a mutex lock, you set the background thread to wait/sleep until the foreground thread wakes it back up. Then it will continue doing its work from there.
#!/usr/bin/python3
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from pyqtMeasThread import *
class MainWindow(QMainWindow):
def __init__(self, parent=None):
self.qt_app = QApplication(sys.argv)
QMainWindow.__init__(self, parent)
buttonWidget = QWidget()
rsltLabel = QLabel("Result:")
self.rsltFiled = QLineEdit()
self.buttonStart = QPushButton("Start")
verticalLayout = QVBoxLayout(buttonWidget)
verticalLayout.addWidget(rsltLabel)
verticalLayout.addWidget(self.rsltFiled)
verticalLayout.addWidget(self.buttonStart)
butDW = QDockWidget("Control", self)
butDW.setWidget(buttonWidget)
self.addDockWidget(Qt.LeftDockWidgetArea, butDW)
self.mutex = QMutex()
self.cond = QWaitCondition()
self.mthread = QThread() # New thread to run the Measurement Engine
self.worker = MeasurementEngine(self.mutex, self.cond) # Measurement Engine Object
self.worker.moveToThread(self.mthread)
self.mthread.finished.connect(self.worker.deleteLater) # Cleanup after thread finished
self.worker.measure_msg.connect(self.showRslt)
self.buttonStart.clicked.connect(self.worker.run)
# Everything configured, start the worker thread.
self.mthread.start()
def run(self):
""" Show the window and start the event loop """
self.show()
self.qt_app.exec_() # Start event loop
# since this is a slot, it will always get run in the event loop in the main thread
#pyqtSlot(str)
def showRslt(self, mystr):
self.rsltFiled.setText(mystr)
msgBox = QMessageBox(parent=self)
msgBox.setText("Close this dialog to continue to Phase 2.")
msgBox.exec_()
self.cond.wakeAll()
def main():
win = MainWindow()
win.run()
if __name__ == '__main__':
main()
And:
from PyQt4.QtCore import *
import time
class MeasurementEngine(QObject):
measure_msg = pyqtSignal(str)
def __init__(self, mutex, cond):
QObject.__init__(self) # Don't forget to call base class constructor
self.mtx = mutex
self.cond = cond
#pyqtSlot()
def run(self):
# NOTE: do work for phase 1 here
self.measure_msg.emit('phase1')
self.mtx.lock()
try:
self.cond.wait(self.mtx)
# NOTE: do work for phase 2 here
self.measure_msg.emit('phase2')
finally:
self.mtx.unlock()
Your timing is a little bit off in all this though. You create the app and start the thread before you even show your window. Thus, the message box will pop up before the main window even pops up. To get the right sequence of events, you should start your thread as part of the run method of your MainWindow, after you have already made the main window visible. If you want the wait condition to be separate from the setting of the messages, you may need a separate signal and slot to deal with that.
You can't display a QDialog from within a QThread. All GUI related stuff must be done in the GUI thread (the one that created the QApplication object). What you could do is to use 2 QThread:
1st: perform phase1. You can connect the finished signal of this QThread to a slot in the QMainWindow that will display the popup (using QDialog.exec_() so it will be modal).
2nd: perform phase2. You create the QThread after the popup shown here above has been closed.
Your thread can emit a signal to the main window to show the dialog.
If you don't want to close the thread while the dialog is open, the thread could enter a while loop for waiting. In the while loop it can continuously check a variable which the main thread can set to true after the dialog is finished.
This might not be the cleanest solution, but it should work.
To clarify my answer a bit, I added some pseudo code. What you have to care about is how you share the dialog_closed variable. You could e.g. use a member variable of the thread class.
Thread:
emit_signal
dialog_closed = False
while not dialog_closed:
pass
go_on_with_processing
MainThread:
def SignalRecieved():
open_dialog
dialog_closed = True
I recently had to solve pretty much this problem, did a little research and discovered an elegant technique that seems to work reliably. I didn't need the full complexity detailed there, so here's an outline of the steps I took.
My GUI class defines, as class attributes, two signals.
oyn_sig = pyqtSignal(str) # Request for operator yes/no
ryn_sig = pyqtSignal(bool) # Response to yes/no request
Inside the method that initialises the GUI components this signal is connected to the GUI instance's signal handler.
self.oyn_sig.connect(self.operator_yes_no)
Here's the code for the handler method of the GUI:
#pyqtSlot(str)
def operator_yes_no(self, msg):
"Asks the user a `yes/no question on receipt of a signal then signal a bool answer.`"
answer = QMessageBox.question(None,
"Confirm Test Sucess",
msg,
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
# Signal the caller that the result was received.
self.ryn_sig.emit(answer==QMessageBox.Yes)
As usual the GUI is running in the main thread, and so it needs to be signalled from the thread doing the work in the background. In turn, once it's received the operator's response it raises a response signal to the originating thread.
The worker thread uses the following function to get an operator response.
def operator_yes_no(self, msg):
loop = LoopSpinner(self.gui, msg)
loop.exec_()
return loop.result
This creates a LoopSpinner object and starts executing its event loop, thereby suspend the current thread's event loop until the "inner thread" terminates. Most of the smarts are hidden inside the LoopSpinner class, which should probably have been better named. Here's its definition.
class LoopSpinner(QEventLoop):
def __init__(self, gui, msg):
"Ask for an answer and communicate the result."
QEventLoop.__init__(self)
gui.ryn_sig.connect(self.get_answer)
gui.oyn_sig.emit(msg)
#pyqtSlot(bool)
def get_answer(self, result):
self.result = result
self.quit()
A LoopSpinner instance connects the response signal to its get_answer method and emits the question signal. When the signal is received the answer is stored as an attribute value and the loop quits. The loop is still referenced by its caller, which can safely access the result attribute before the instance is garbage collected.
I'm writing a little IRC client in python as an exercise. I have a Tkinter.Tk subclass called Main managing the whole application, which creates a socket in its __init__ method. I've played around with sockets in the interactive mode, so I know how to talk to the IRC server with something like this:
>>> s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> s.connect(("irc.foonetic.net", 6667))
>>> s.recv(1000)
":anchor.foonetic.net NOTICE AUTH :*** Looking up your hostname...\r\n:anchor.foonetic.net NOTICE AUTH :*** Couldn't resolve your hostname; using your IP address instead\r\n"
>>> s.send("PASS mypassword\r\n")
That is, I carry on the whole conversation using .send and .recv. Thus to get user input in my Tkinter app, I imagine I'll have an event handler mapped to the Enter key which will call .send. But where do I put the calls to .recv? The only thing I know how to do would be to use a timer to call .recv every few seconds, but that's obviously not a good solution for several reasons. How do I deal with the fact that .recv blocks for several seconds (determined by whatever timeout you set) if there's no data to receive? I realize I could just google "multithreading", but I'd like some guidance on what the best approach is for this specific situation.
In my project, I setup a new thread for long term I/O like socket read/write. To write a practical GUI program, you have to face multithread soon or later. That's because GUI framework has an event queue, and an event loop. The event loop is typically a while loop, in which it get events from event queue and dispatch this events to registered functions. Like the following:
while event is not QUIT:
event = event_queue.get(block=True)
dispatch(event)
In dispatch, all callback functions registered on that event is called directly.
Such code works in the GUI thread, and if you do long term I/O or blocking action in a GUI callback, the thread is blocked in that callback. In terms of event loop, the program is blocked in the dispatch function which called the blocked callback function. Any new event in the event queue will not be processed. As a result, the program looks like dead because the updating event of GUI is blocked.
When you have setup a worker thread to handle time consuming things, don't try to operate GUI widgets directly from that worker thread. Most GUI frameworks are not thread safe, they keep operation sequence by the event queue. And operating a widget in non-GUI threads will break this sequence.
We can add event to event queue from non-GUI thread, and let GUI thread handle that event, to keep the sequence. This is the normal way for some common language, but not for python. In python, function and method are first class object, so we can put then in the queue. Unfortunately, the event queue for tkinter does not support this feature.
In Programming Python by Mark Lutz there is great cover of tkinter programming. In this book, the author introduced a great method to do multithread in tkinter. Here is my demo:
# python3 source code
from tkinter import *
from tkinter.ttk import *
import threading
import time
import queue
root = Tk()
msg = StringVar()
Label(root, textvariable=msg).pack()
# This is our own event queue, each element should be in this form:
# (function_to_be_called_from_gui_thread, function_arguments)
# In python, functions are objects and can be put in a queue.
my_event_queue = queue.Queue()
def worker():
"""
This is a time consuming worker, it takes 1 second for each task.
If you put such a worker in the GUI thread, the GUI will be blocked.
"""
task_counter = 0
while True:
time.sleep(1) # simulate a time consuming task
# show how many tasks finished in the Label. We put this action in my_event_queue instead of handle
# it from this worker thread which is not safe. This action will be handled by my_event_handler which is
# called from GUI thread.
my_event_queue.put((msg.set, '{} tasks finished.'.format(task_counter)))
task_counter += 1
def my_event_handler():
"""
Query my_event_queue, and handle one event per time.
"""
try:
func, *args = my_event_queue.get(block=False)
except queue.Empty:
pass
else:
func(*args)
# At last schedule handling for next time.
# Every 100 ms, my_event_handler will be called
root.after(100, my_event_handler)
threading.Thread(target=worker, daemon=True).start() # start worker in new thread
my_event_handler() # start handler, after root.mainloop(), this method will be called every 100ms. Or you can use root.after(100, my_event_handler)
root.mainloop()
Here is the running picture. You can see I adjust the window size when it is running.(Well I have not enough reputation to post images, so you have to try it yourself)
At last I would suggest you to take a look at Programming Python for tkinter programming.
All Python code are in Python3.
I'm pretty new to Python in general and very new to Tk/ttk. But here's an example of what I've been playing with for event triggering/signaling and worker thread stuff in Tk/ttk. I know some people will hate the singleton decorator and I know there are other ways to call code from other classes but the trigger class is very convenient and the worker class works like a charm. Together they make things super easy.
Credits:
The worker class is a very slightly modified version of the GObject worker found in Pithos and the singleton decorator is a very slightly modified version of something I found here on stackoverflow somewhere.
import sys
import tkinter
from tkinter import ttk
from tkinter import StringVar
import threading
import queue
import traceback
import time
class TkWorkerThreadDemo:
def __init__(self):
self.root = tkinter.Tk()
self.trigger = Trigger.Singleton()
self.trigger.connect_event('enter_main_thread', self.enter_main_thread)
self.worker = Worker()
self.root.title('Worker Thread Demo')
self.root.resizable(width='False', height='False')
self.test_label_text = StringVar()
self.test_label_text.set('')
self.slider_label_text = StringVar()
self.slider_label_text.set('Press either button and try to move the slider around...')
mainframe = ttk.Frame(self.root)
test_label = ttk.Label(mainframe, anchor='center', justify='center', textvariable=self.test_label_text)
test_label.pack(padx=8, pady=8, fill='x')
slider_label = ttk.Label(mainframe, anchor='center', justify='center', textvariable=self.slider_label_text)
slider_label.pack(padx=8, pady=8, expand=True, fill='x')
self.vol_slider = ttk.Scale(mainframe, from_=0, to=100, orient='horizontal', value='100', command=self.change_slider_text)
self.vol_slider.pack(padx=8, pady=8, expand=True, fill='x')
test_button = ttk.Button(mainframe, text='Start Test with a Worker Thread', command=self.with_worker_thread)
test_button.pack(padx=8, pady=8)
test_button = ttk.Button(mainframe, text='Start Test in the Main Thread', command=self.without_worker_thread)
test_button.pack(padx=8, pady=8)
mainframe.pack(padx=8, pady=8, expand=True, fill='both')
self.root.geometry('{}x{}'.format(512, 256))
def enter_main_thread(self, callback, result):
self.root.after_idle(callback, result)
def in_a_worker_thread(self):
msg = 'Hello from the worker thread!!!'
time.sleep(10)
return msg
def in_a_worker_thread_2(self, msg):
self.test_label_text.set(msg)
def with_worker_thread(self):
self.test_label_text.set('Waiting on a message from the worker thread...')
self.worker.send(self.in_a_worker_thread, (), self.in_a_worker_thread_2)
def in_the_main_thread(self):
msg = 'Hello from the main thread!!!'
time.sleep(10)
self.in_the_main_thread_2(msg)
def in_the_main_thread_2(self, msg):
self.test_label_text.set(msg)
def without_worker_thread(self):
self.test_label_text.set('Waiting on a message from the main thread...')
self.root.update_idletasks()#without this the text wil not get set?
self.in_the_main_thread()
def change_slider_text(self, slider_value):
self.slider_label_text.set('Slider value: %s' %round(float(slider_value)))
class Worker:
def __init__(self):
self.trigger = Trigger.Singleton()
self.thread = threading.Thread(target=self._run)
self.thread.daemon = True
self.queue = queue.Queue()
self.thread.start()
def _run(self):
while True:
command, args, callback, errorback = self.queue.get()
try:
result = command(*args)
if callback:
self.trigger.event('enter_main_thread', callback, result)
except Exception as e:
e.traceback = traceback.format_exc()
if errorback:
self.trigger.event('enter_main_thread', errorback, e)
def send(self, command, args=(), callback=None, errorback=None):
if errorback is None: errorback = self._default_errorback
self.queue.put((command, args, callback, errorback))
def _default_errorback(self, error):
print("Unhandled exception in worker thread:\n{}".format(error.traceback))
class singleton:
def __init__(self, decorated):
self._decorated = decorated
self._instance = None
def Singleton(self):
if self._instance:
return self._instance
else:
self._instance = self._decorated()
return self._instance
def __call__(self):
raise TypeError('Singletons must be accessed through `Singleton()`.')
#singleton
class Trigger:
def __init__(self):
self._events = {}
def connect_event(self, event_name, func, *args, **kwargs):
self._events[event_name] = func
def disconnect_event(self, event_name, *args, **kwargs):
if event_name in self._events:
del self._events[event_name]
def event(self, event_name, *args, **kwargs):
if event_name in self._events:
return self._events[event_name](*args, **kwargs)
def main():
demo = TkWorkerThreadDemo()
demo.root.mainloop()
sys.exit(0)
if __name__ == '__main__':
main()