15

I have a PySide (Qt) GUI which spawns multiple threads. The threads sometimes need to update the GUI. I have solved this in the following way:

class Signaller(QtCore.QObject) :
    my_signal = QtCore.Signal(QListWidgetItem, QIcon)
signaller = Signaller()

class MyThread(threading.Thread):
    def __init__(self):
        super(IconThread, self).__init__()
        # ...

    def run(self) :
        # ...

        # Need to update the GUI
        signaller.my_signal.emit(self.item, icon)

#
# MAIN WINDOW        
# 
class Main(QtGui.QMainWindow):

    def __init__(self):
        QtGui.QMainWindow.__init__(self)

        # ...

        # Connect signals
        signaller.my_signal.connect(self.my_handler)

    @QtCore.Slot(QListWidgetItem, QIcon)
    def my_handler(self, item, icon):
        item.setIcon(icon)

    def do_something(self, address):
        # ...

        # Start new thread 
        my_thread = MyThread(newItem)
        my_thread.start()

    # ...

Is there an easier way? Creating the signals, handlers and connect them requires a few lines of code.

Petter
  • 37,121
  • 7
  • 47
  • 62
  • Why aren't you using `QThread`? – Avaris Jun 12 '12 at 08:40
  • If it is easier with a `QThread`, I would consider using one. The problem is that existing code often tend to use `threading.Thread`. – Petter Jun 12 '12 at 08:48
  • 1
    It is better, since `QThread` supports signals. You won't need your `Signaller` class. But basically, your way is the way. You need signals and slots to communicate between threads and GUI. – Avaris Jun 12 '12 at 18:08

2 Answers2

21

I started coding with PySide recently and I needed a equivalent of PyGObject's GLib.idle_add behaviour. I based the code off of your answer ( https://stackoverflow.com/a/11005204/1524507 ) but this one uses events instead of using a queue ourselves.

from PySide import QtCore


class InvokeEvent(QtCore.QEvent):
    EVENT_TYPE = QtCore.QEvent.Type(QtCore.QEvent.registerEventType())

    def __init__(self, fn, *args, **kwargs):
        QtCore.QEvent.__init__(self, InvokeEvent.EVENT_TYPE)
        self.fn = fn
        self.args = args
        self.kwargs = kwargs


class Invoker(QtCore.QObject):
    def event(self, event):
        event.fn(*event.args, **event.kwargs)

        return True

_invoker = Invoker()


def invoke_in_main_thread(fn, *args, **kwargs):
    QtCore.QCoreApplication.postEvent(_invoker,
        InvokeEvent(fn, *args, **kwargs))

Which is used the same way in the above answer link.

Community
  • 1
  • 1
chfoo
  • 386
  • 3
  • 13
  • This is great. And even the tiny workaround about wrapping `registerEventType` again in `QEvent.Type` to make it work in PySide opened my eyes. Thanks, will use the code. – NoDataDumpNoContribution May 12 '14 at 14:13
  • @chfoo: thanks very much for this! Would you agree to let the code in this answer to be used under open source licensing terms? BSD or MIT would be ideal, since they are compatible with Numpy, Pandas, Scipy, etc. – naitsirhc Feb 19 '19 at 20:50
  • @naitsirhc I would, but the code is based off the other answer so I'm not sure if I can do that. Also, I'm not a lawyer, but the code snippet is very small and I think the practise of including a link to this answer as a source code comment is sufficient. – chfoo Feb 22 '19 at 02:26
  • Thanks @chfoo. Unfortunately, the default license of responses is [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) (see bottom right of every page). Without explicit release under an [OSI-approved open source license](https://opensource.org/licenses/BSD-2-Clause), usage terms (even of short snippets) [are ambiguous](https://opensource.stackexchange.com/a/1718). Your answer is still certainly useful to point to the right parts of the Qt API! :D – naitsirhc Feb 22 '19 at 14:13
  • This is amazing! Really helped me understand the event handling and threading process with multithreading in pyside6. – Nicholas Stommel Jun 23 '23 at 13:41
7

This is what I have so far. I wrote the following code somewhere in a helper module:

from Queue import Queue
class Invoker(QObject):
    def __init__(self):
        super(Invoker, self).__init__()
        self.queue = Queue()

    def invoke(self, func, *args):
        f = lambda: func(*args)
        self.queue.put(f)
        QMetaObject.invokeMethod(self, "handler", QtCore.Qt.QueuedConnection)

    @Slot()
    def handler(self):
        f = self.queue.get()
        f()
invoker = Invoker()

def invoke_in_main_thread(func, *args):
    invoker.invoke(func,*args)

Then my threads can very easily run code to update the GUI in the main thread. There is no need to create and connect signals for every operation.

class MyThread(threading.Thread):
    def __init__(self):
        super(IconThread, self).__init__()
        # ...

    def run(self) :
        # ...

        # Need to update the GUI
        invoke_in_main_thread(self.item.setIcon, icon)

I think something like this is quite nice.

Petter
  • 37,121
  • 7
  • 47
  • 62