How to schedule a function to run on the main UI thread in Qt for python?

Question:

I am porting a python GTK application to use Qt for python(PySide2).
It implements worker threads with python standard threading module and worker threads use Gdk.threads_add_idle() to interact with the main UI thread.

There are plenty of articles on QThread, but I couldn’t find a simple way to do this with Qt.

I hacked and came to an ugly solution like the following.
(For the core logic only, see the IdleRunner class and run_on_idle() function.)

import sys
import time
import threading
from PySide2.QtCore import *
from PySide2.QtWidgets import *

class IdleRunner(QObject):
    run = Signal(object, tuple, float)
    def __init__(self):
        super().__init__()
        self.run.connect(self.on_run)
    def on_run(self, func, args, delay):
        if delay: QTimer.singleShot(delay * 1000, lambda: func(*args))
        else: func(*args)
_idle_runner = IdleRunner()
def run_on_idle(func, *args, delay = 0):
    _idle_runner.run.emit(func, args, delay)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__(windowTitle = sys.argv[0])
        self.resize(400, 300)
        self.setAttribute(Qt.WA_DeleteOnClose, True)

        self.label = label = QLabel('aaa', alignment = Qt.AlignCenter)
        self.setCentralWidget(label)

        def thread_entry():
            time.sleep(1)
            run_on_idle(lambda: self.label.setText('bbb'))
            run_on_idle(self.close, delay = 1)
        self.thread = thread = threading.Thread(target = thread_entry)
        thread.start()
    def close(self):
        self.thread.join()
        super().close()

app = QApplication()
main_window = MainWindow()
main_window.show()
app.exec_()

I have two questions.

  1. What is the best solution to this?
  2. What are the possible problems of this solution?
    (Such as a memory leak).
Asked By: relent95

||

Answers:

I am answering my own question for others because there has been no answer for a long time.(I posted the same question on the Qt forum but still got no answer. See this.)

  1. A good solution seems to be like these. Although it’s not so neat, it handles two fundamental problems using proper Qt APIs.
  • Posting an event from a background thread is done by Signal.emit().
  • Running a code on idle time is done by QTimer.singleshot().
from PySide2.QtCore import QObject, Signal, QTimer

class IdleRunner(QObject):
    run = Signal(object, tuple, float)
    def __init__(self):
        super().__init__()
        self.run.connect(self.on_run)
    def on_run(self, func, args, delay):
        QTimer.singleShot(delay * 1000, lambda: func(*args))

_idle_runner = IdleRunner()
def run_on_idle(func, *args, delay = 0):
    _idle_runner.run.emit(func, args, delay)

  1. We are using the above solution around 6 months. Up until now, there are no known problems such as a memory leak or a performance bottleneck.

  2. After 9 months, I found a more efficient solution, posting an event from a background thread by QCoreApplication.postEvent(). (Now it feels less hacky.) The following is more extended example which supports keyword arguments. Also, you can easily apply this to PyQt5 by changeing PySide2 in the import statement into PyQt5.

from PySide2.QtCore import QObject, QEvent, QTimer, QCoreApplication

class RunEvent(QEvent):
    TYPE = QEvent.Type(QEvent.registerEventType())
    def __init__(self, *args):
        super().__init__(RunEvent.TYPE)
        self.args = args

class IdleRunner(QObject):
    def event(self, e):
        delay, func, args, kwargs = e.args
        if delay == 0:
            func(*args, **kwargs)
        else:
            QTimer.singleShot(int(delay * 1000), lambda: func(*args, **kwargs))
        return True

_idle_runner = IdleRunner()
def run_on_idle(func, *args, delay = 0, **kwargs):
    QCoreApplication.postEvent(_idle_runner, RunEvent(delay, func, args, kwargs))

Answered By: relent95
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.