Credit: Jacob Hallén
You need to access sockets, serial ports, and do other asynchronous (but blocking) I/O while running a Tkinter-based GUI.
The solution is to handle a Tkinter interface on one thread and
communicate to it (via Queue
objects) the events
on I/O channels handled by other threads:
import Tkinter
import time
import threading
import random
import Queue
class GuiPart:
def _ _init_ _(self, master, queue, endCommand):
self.queue = queue
# Set up the GUI
console = Tkinter.Button(master, text='Done', command=endCommand)
console.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(0)
# Check contents of message and do whatever is needed. As a
# simple test, 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
pass
class ThreadedClient:
"""
Launch the main part of the GUI and the worker thread. periodicCall and
endApplication 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.endApplication)
# Set up the thread to do asynchronous I/O
# More threads can also be created and used, if necessary
self.running = 1
self.thread1 = threading.Thread(target=self.workerThread1)
self.thread1.start( )
# Start the periodic call in the GUI to check if the queue contains
# anything
self.periodicCall( )
def periodicCall(self):
"""
Check every 200 ms if there is something new in the queue.
"""
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)
self.master.after(200, self.periodicCall)
def workerThread1(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, by select or otherwise.
"""
while self.running:
# To simulate asynchronous I/O, we 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 endApplication(self):
self.running = 0
rand = random.Random( )
root = Tkinter.Tk( )
client = ThreadedClient(root)
root.mainloop( )
This recipe shows the easiest way of handling access to sockets, serial ports, and other asynchronous I/O ports while running a Tkinter-based GUI. Note that the recipe’s principles generalize to other GUI toolkits, since most of them make it preferable to access the GUI itself from a single thread, and all offer a toolkit-dependent way to set up periodic polling as this recipe does.
Tkinter, like most other GUIs, is best used with all graphic commands
in a single thread. On the other hand, it’s far more
efficient to make I/O channels block, then wait for something to
happen, rather than using nonblocking I/O and having to poll at
regular intervals. The latter approach may not even be available in
some cases, since not all data sources support nonblocking I/O.
Therefore, for generality as well as for efficiency, we should handle
I/O with a separate thread, or more than one. The I/O threads can
communicate in a safe way with the main, GUI-handling thread through
one or more Queue
s. In this recipe, the GUI thread still
has to do some polling (on the Queue
s), to check
if something in the Queue
needs to be processed.
Other architectures are possible, but they are much more complex than
the one in this recipe. My advice is to start with this recipe, which
will handle your needs over 90% of the time, and explore the much
more complex alternatives only if it turns out that this approach
cannot meet your performance requirements.
This recipe lets a worker thread block in a select
(simulated by random sleeps in the recipe’s example
worker thread). Whenever something arrives, it is received and
inserted in a Queue
. The main (GUI) thread polls
the Queue
five times per second (often enough that
the end user will not notice any significant delay, but rarely enough
that the computational load on the computer will be
negligible—you may want to fine-tune this, depending on your
exact needs) and processes all messages that have arrived since it
last checked.
This recipe seems to solve a common problem, since there is a
question about how to do it a few times a month in
comp.lang.python. There are other solutions,
involving synchronization between threads, that let you solve such
problems without polling (the root.after
call in
the recipe). Unfortunately, such solutions are generally complicated
and messy, since you tend to raise and wait for semaphores throughout
your code. In any case, a GUI already has several polling mechanisms
built into it (the main event loop), so adding one more
won’t make much difference, especially since it
seldom runs. The code has been tested only under Linux, but it should
work on any platform with working threads, including Windows.
Documentation of the standard library modules
threading
and Queue
in the
Library Reference; information about Tkinter can
be obtained from a variety of sources, such as
Pythonware’s An Introduction to Tkinter, by Fredrik Lundh (http://www.pythonware.com/library), New
Mexico Tech’s Tkinter reference
(http://www.nmt.edu/tcc/help/lang/python/docs.html),
and various books.
Get Python Cookbook now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.