[Tkinter-discuss] tk problem !!!

Jeff Epler jepler at unpythonic.net
Thu Oct 21 20:07:04 CEST 2004


I'm not in your situation, so I've never actually had to address this
problem.  However, I thought about how I'd solve it.

First, one way to do it is to re-structure your code so that you no
longer use threads.  Any long-running calculations are re-structured so
that they can run in pieces from Tk's "after", blocking the user
interface for no more than a few ms at a time.  This is often far from
trivial, even when using the "generators as limited coroutines"
approach.

If you can't do that, then you have to arrange for GUI calls in a
non-main thread to actually be sent to the main thread somehow.  The
obvious way is through a Queue.Queue.  In the main thread, an "after"
will fire every 100ms or so, service any items in the queue of calls,
and then return to normal event handling.

There are a few more wrinkles:  Some calls will want the return value,
others won't.  Some calls will need to be done in a bundle, without
handling events in the meantime.

Here's the class I wrote, called TkThreadingGizmo:
#------------------------------------------------------------------------
# Copyright (C) 2004 Jeff Epler
# You may use this code according to the terms of the Python License
import Queue

class TkThreadingGizmo:
    def __init__(self, csize=100, rsize=1):
        self.calls = Queue.Queue(csize)
        self.results = Queue.Queue(rsize)
        self.block = False

    def install(self, window=None, interval=100):
        if window is None: window = Tkinter._default_root
        self.window = window
        self.interval = interval
        self.handle_calls_callback()

    def unuinstall(self):
        self.window = None

    def queue_call(self, func, *args, **kw):
        self.calls.put((True, func, args, kw))
        state, value = self.results.get()
        if state:
            raise value
        else:
            return value

    def queue_call_async(self, func, *args, **kw):
        self.calls.put((False, func, args, kw))

    def handle_calls(self):
        while self.block or not self.calls.empty():
            wantresult, func, args, kw = self.calls.get()
            if wantresult:
                try:
                    self.results.put((False, func(*args, **kw)))
                except (KeyborardInterrupt, SystemExit): raise
                except Exception, detail:
                    self.results.put((True, detail))
            else:
                try:
                    func(*args, **kw)
                except (KeyborardInterrupt, SystemExit): raise
                except Exception, detail:
                    exception.print_last()

    def handle_calls_callback(self):
        self.handle_calls()
        if self.window:
            self.window.after(self.interval, self.handle_calls_callback)
#------------------------------------------------------------------------

The important details:
    queue_call sends a call from a thread to the main thread, and waits
    for the result.  Most exceptions are intended to be reraised in the
    caller, but this is untested.

    queue_call_async sends a call from a thread to the main thread, but
    doesn't wait for a result.  Most exceptions are intended to be
    printed (untested, again) but some exceptions are allowed to
    propagate in the main thread.

    Set the "block" flag *before* the first call that must happen in a
    bundle, then unset it *before* the last call (not *after* the last
    call).

Here's a test program:

#------------------------------------------------------------------------
# Copyright (C) 2004 Jeff Epler
# You may use this code according to the terms of the Python License
from Tkinter import *
from tktg import TkThreadingGizmo
import threading
import time

class App(threading.Thread):
    def run(self):
        i = 0
        while not self.shutdown:
            self.block = True
            value = self.gizmo.queue_call(self.var.get)
            self.block = False
            self.gizmo.queue_call_async(self.var.set, value + 1)
            time.sleep(1)

t = Tk()
v = IntVar(t)
l = Label(t, text="Value:", width=8)
l.pack(side="left")
l = Label(t, textvariable=v, width=5)
l.pack(side="left")
b = Button(t, text="Reset", width=8, command=lambda: v.set(0), default="active")
t.bind("<Return>", lambda event: b.invoke())
b.pack(side="left")
g = TkThreadingGizmo()
a = App()
a.gizmo = g; a.var = v; a.shutdown = 0
a.start()
g.install(t)
try:
    t.mainloop()
finally:
    a.shutdown = 1
#------------------------------------------------------------------------
The thread just increments the value once a second or so.  Because it
reads the old value, and then sets the new value, the calls must be done
as a bundle.  Otherwise, you would be able to hit the Reset button
between the read and the write, so that the value would jump to 0 for a
moment but then return to the previous value plus one.

Feel free to use this code if it's helpful to you.

Jeff
-------------- next part --------------
A non-text attachment was scrubbed...
Name: not available
Type: application/pgp-signature
Size: 189 bytes
Desc: not available
Url : http://mail.python.org/pipermail/tkinter-discuss/attachments/20041021/0467685a/attachment.pgp


More information about the Tkinter-discuss mailing list