Welcome, guest | Sign In | My Account | Store | Cart

This class forms a bridge between the main Qt event loop and python-based threads. This was a thorny problem to figure out, so I'm posting for others to benefit. Basically, you need to invoke all Qt GUI calls and (ActiveX calls on Windows) from the main Qt thread. But if you're using Python threads, you need to manage the interaction yourself.

Python, 83 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
from qt import *

class BaseQObject(QObject):

    MAIN_THREAD_ID = 0

    def __init__(self):
        QObject.__init__(self)
        self.installEventFilter(self)
        self.event = None

    def eventFilter(self,obj,event):
        # FIXME:  This is a workaround for an unexplained bug
        # The events were getting posted through postEVentWithCallback()
        # But the event() method wasn't getting called.  But the eventFilter()
        # method is getting called.  
        if event.type()==QEvent.User:
            cb = event.__dict__.get('callback')
            if cb: self._doEvent(event)
            return False
        return QObject.eventFilter(self,obj,event)

    def _doEvent(self,event):
        cb = event.__dict__.get('callback')
        if not cb: return
        data = event.__dict__.get('data')
        if data or type(data)==type(False): cb(data)
        else: cb()
        del event

    def event(self, event):
        if event.type()==QEvent.User:
            self._doEvent(event)
            return True
        return QObject.event(self, event)

    def postEventWithCallback(self, callback, data=None):
        # if we're in main thread, just fire off callback
        if get_ident()==BaseQObject.MAIN_THREAD_ID:
            if data or type(data)==type(False): callback(data)
            else: callback()
        # send callback to main thread 
        else:
            event = QEvent(QEvent.User)
            event.callback = callback
            if data or type(data)==type(False): event.data = data
            qApp.postEvent(self, event)


class ThreadLock:

    def __init__(self):
        if sys.platform == 'win32':
            from qt import QMutex
            self.mutex = QMutex(True)
        elif sys.platform == 'darwin':
            from Foundation import NSRecursiveLock
            self.mutex = NSRecursiveLock.alloc().init()

    def lock(self): self.mutex.lock()
    def unlock(self): self.mutex.unlock()
        

import thread

class Foo(BaseQObject):

    def doMainFoo(self):
        print 'doMainFoo(): thread id = '%thread.get_ident()
    
    def doFoo(self):
        print 'doFoo(): thread id = '%thread.get_ident()
        self.postEventWithCallback(self.doMainFoo)

if __name__=="__main__":
    foofoo = Foo()
    
    import threading
    threading.Timer(1.0, foofoo.doFoo)

    from qt import QApplication
    app = QApplication([])
    app.exec_loop()

Some of the code may be superfluous. For instance, you shouldn't really need qApp.sendPostedEvent(), but I did. Also eventFilter() isn't the place to intercept events, but its the only thing that worked.

The code is above is just a starter. Its not very mature, but provides an example of dealing with the potentially thorny issues of python threads and Qt event loop.

UPDATE:

After working with the code more, I realized the extra call to sendPostedEvents() is actually a bug. Also, added a shortcut where if the call was coming from the main thread, the callback would be invoked directly. Assign the value of MAIN_THREAD_ID using the thread.get_ident() function, this should be done when the app starts up (from the main thread, of course).

Also added is the ThreadLock wrapper class that you'll need for thread locking within your python threads. An example of using this with Mac OS X Foundation kit locks is provided as an illustration.

An example of how to use the class is provided as well. The example runs a python threading.timer, which is triggered in a python thread. The timer calls a method, which calls another one from within the main Qt event loop.

6 comments

Yair Chuchem 16 years, 6 months ago  # | flag

Why not add a usage example? A usage example would make the recipe much easier to quickly comprehend. Thanks :)

Jonathan Kolyer (author) 16 years, 2 months ago  # | flag

Example Provided. I added a __main__ call to show how this is used in my situation.

Daniel Miller 15 years ago  # | flag

Updated version. Here's a slightly shorter version that supports passing arbitrary positional arguments to the callback.

from qt import QEvent, QObject, qApp
from thread import get_ident

class CallbackEventHandler(QObject):

    MAIN_THREAD_ID = 0

    def __init__(self):
        QObject.__init__(self)
        self.installEventFilter(self)

    def eventFilter(self, obj, event):
        # FIXME:  This is a workaround for an unexplained bug
        # The events were getting posted through postEVentWithCallback()
        # But the event() method wasn't getting called.  But the eventFilter()
        # method is getting called.
        if event.type() == QEvent.User:
            callback = event.__dict__.get('callback')
            if callback:
                self._doEvent(event)
            return False
        return QObject.eventFilter(self, obj, event)

    def _doEvent(self, event):
        callback = event.__dict__.get('callback')
        args = event.__dict__.get('args')
        if callback is not None and args is not None:
            callback(*args)

    def event(self, event):
        if event.type() == QEvent.User:
            self._doEvent(event)
            return True
        return QObject.event(self, event)

    def postEventWithCallback(self, callback, *args):
        if get_ident() == CallbackEventHandler.MAIN_THREAD_ID:
            # if we're in main thread, just fire off callback
            callback(*args)
        else:
            # send callback to main thread
            event = QEvent(QEvent.User)
            event.callback = callback
            event.args = args
            qApp.postEvent(self, event)
Stephen Tether 14 years, 7 months ago  # | flag

How about customEvent()? Try using QCustomEvent (or a subclass) and a customEvent() handler. Using QEvent and event() together with a user-defined event ID may be confusing Qt3, requiring you to meddle with eventFilter().

Daniel Miller 12 years, 7 months ago  # | flag

A much simpler, shorter version using a Queue and customEvent. This version has a slight semantic difference: all pending callbacks are called when an event is posted. This only has an effect if multiple events are posted in short succession before the main thread has time to process events. If this "feature" becomes problematic, the wile loop in customEvent() can be removed so a single event is processed for each event that is posted.

from qt import QEvent, QObject, qApp
from Queue import Queue, Empty

class CallbackEventHandler(QObject):

    def __init__(self):
        QObject.__init__(self)
        self.queue = Queue()

    def customEvent(self, event):
        while True:
            try:
                callback, args = self.queue.get_nowait()
            except Empty:
                break
            try:
                callback(*args)
            except Exception:
                log.warn("callback event failed: %r %r", self.callback, self.args, exc_info=True)

    def postEventWithCallback(self, callback, *args):
        self.queue.put((callback, args))
        qApp.postEvent(self, qt.QCustomEvent(QEvent.User))
a 10 years, 10 months ago  # | flag

event.__dict__.get('callback') should be written as getattr(event, 'callback', None)