[Tkinter-discuss] Proposal to use dynamic proxy with tkinter to communicate with background thread

Andreas Ostermann andreas.ostermann.11 at googlemail.com
Fri Nov 30 15:45:57 CET 2012


Hi!

I was currently searching for the possibility to communicate between
tkinter and a python background thread. I hoped for something like
java's invokeLater mechanism.

I found a lot of ideas, using a Queue for the communication and then
poll in Tkinter on the Queue, raising the question which interval
should be used. Then there was mentioned to use the after_idle()
method to pass something into the Tk loop, but the method is not
thread_safe as it seems. And then there was the idea of binding events
to methods and to raise the event so that method will be called.

It seems, that this is still a valid question, so I would like to
share my solution and open it for discussion and hope you would review
it and show me problems if there are any.

First I want something like the invokeLater call from java, so I
created to classes which will be "listener", one for the tk loop and
one as a background thread:

class BackgroundListener (threading.Thread) :
    def __init__(self):
        threading.Thread.__init__(self)
        self.queue = Queue.Queue()
        self.cancel = threading.Event()
    def invokeLater(self, delegate):
        self.queue.put(delegate)
    def interrupt(self) :
        self.cancel.set()
    def run(self):
        while not self.cancel.isSet() :
            try :
                delegate = self.queue.get(timeout=0.5)
                if not self.cancel.isSet() : # don't call if already finished !
                    delegate()
            except Queue.Empty :
                pass

class TkListener :
    def __init__(self, tk) :
        self.queue = Queue.Queue()
        self.tk = tk
        self.event = "<<%s>>" % uuid.uuid1()
        tk.bind(self.event, self.invoke)
    def invokeLater(self, delegate) :
        self.queue.put(delegate)
        self.tk.event_generate(self.event, when='tail')
    def invoke(self, event) :
        try :
            while True :
                delegate = self.queue.get(block=False)
                delegate()
        except Queue.Empty :
            pass

So, what happens here?

The Background thread will store the delegate object (see below) into
a Queue and the background thread is reading from this Queue, as Queue
is threadsafe everything is fine.

(I misuse the Event to create something like the interrupt() call from java)

The TkListener will get a reference to the tk object and binds a event
to it which calls a the invoke method. Therefore it is similar to the
other listener. The invokeLater call adds the delegate to the Queue,
then raise the event. The tkloop will then handle the event by calling
the invoke method, where all waiting delegates are removed from the
queue and called.

So whats with the delegate class? Its part of a dynamic proxy
implementation which can be used for any other purpose as long as the
listener you provide has an invokeLater method.

And that's what they look like:

class Delegate :
    def __init__(self, real, name, args, kwargs) :
        self.real = real
        self.name = name
        self.args = args
        self.kwargs = kwargs
    def __call__(self) :
        method = getattr(self.real, self.name)
        apply(method, self.args, self.kwargs)

class Delegator :
    def __init__(self, listener, real, name) :
        self.listener = listener
        self.real = real
        self.name = name
    def __call__(self, *args, **kwargs) :
        delegate = Delegate(self.real, self.name, args, kwargs)
        self.listener.invokeLater(delegate)

class Proxy :
    def __init__(self, listener, real) :
        self.listener = listener
        self.real = real
        self.cache = {}
    def __getattr__(self, name) :
        try :
            delegator = self.cache[name]
        except KeyError :
            delegator = Delegator(self.listener, self.real, name)
            self.cache[name] = delegator
        return delegator

The Proxy class will hold the listener and the real object. When you
call a method on the proxy a delegator object for the method name is
generated and cached. Then the delegator is returned and the caller
call will be handled in the delegator. The delegator then creates a
delegate object holding all the informations and the attributes of the
call and the delegate object will be hand over to the listener via the
invokeLater method.

As you have seen above the listener will then get the delegate object
from the Queue and call it, which leads to a call to the real object
in the context of the listener.

And at last, so that you have something to play around with, some test
classes showing different ways on how to use the proxy object, even
for callbacks over thread boundaries:

import Tkinter
import Queue
import uuid
import threading
import thread

class MyTkinterObject:
    def __init__(self, tk):
        frame = Tkinter.Frame(tk)
        self.quitButton = Tkinter.Button(frame, text="QUIT", fg="red",
command=frame.quit)
        self.quitButton.pack(side=Tkinter.LEFT)
        self.helloButton = Tkinter.Button(frame, text="Hello")
        self.helloButton.pack(side=Tkinter.LEFT)
        self.hello2Button = Tkinter.Button(frame, text="Hello2",
command=self.trigger2)
        self.hello2Button.pack(side=Tkinter.LEFT)
        frame.pack()
    def register(self, myBackgroundObject) :
        self.helloButton.bind("<Button-1>", myBackgroundObject.trigger)
    def trigger(self, callback) :
        print "%s - Front hello" % thread.get_ident()
        callback()
    def trigger2(self) :
        print "%s - Front hello" % thread.get_ident()

class MyBackgroundObject :
    def __init__(self, listener):
        self.proxy = Proxy(listener, self)
    def register(self, myTkinterObject) :
        self.myTkinterObject = myTkinterObject
    def trigger(self, event) :
        print "%s - Back hello - %s" % (thread.get_ident(), event)
        self.myTkinterObject.trigger(self.proxy.callback)
    def callback(self) :
        print "%s - Back bye" % thread.get_ident()

def main() :
    root = Tkinter.Tk()

    tkListener = TkListener(root)
    backgroundListener = BackgroundListener()

    myTkinterObject = MyTkinterObject(root)
    myBackgroundObject = MyBackgroundObject(backgroundListener)

    myTkinterObject.register(myBackgroundObject.proxy)
    myBackgroundObject.register(Proxy(tkListener, myTkinterObject))

    backgroundListener.start()
    root.mainloop()
    backgroundListener.interrupt()

if __name__ == "__main__":
    main()

I would like to hear your comments!

thanks,

AO


More information about the Tkinter-discuss mailing list