Issue
From a tutorial, I've seen that signals and slots can be used to make callbacks from a worker thread into the main GUI thread, but I'm uncertain how to establish bi-directional communication using signals and slots. The following is what I am working with:
class RespondedToWorkerSignals(QObject):
callback_from_worker = pyqtSignal()
class RespondedToWorker(QRunnable):
def __init__(self, func, *args, **kwargs):
super(RespondedToWorker, self).__init__()
self._func = func
self.args = args
self.kwargs = kwargs
self.signals = RespondedToWorkerSignals()
self.kwargs['signal'] = self.signals.callback_from_worker
print("Created a responded-to worker")
@pyqtSlot()
def run(self):
self._func(*self.args, **self.kwargs)
@pyqtSlot()
def acknowledge_callback_in_worker(self):
print("Acknowledged Callback in Worker")
class MainWindow(QMainWindow):
# Signal meant to connect to a slot present within a worker
mainthread_callback_to_worker = pyqtSignal()
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
# Quick UI setup
w, lay = QWidget(), QVBoxLayout()
w.setLayout(lay)
self.setCentralWidget(w)
self.timer_label = QLabel("Timer Label")
lay.addWidget(self.timer_label)
self.btn_thread_example = QPushButton("Push Me")
self.btn_thread_example.pressed.connect(self.thread_example)
lay.addWidget(self.btn_thread_example)
self.threadpool = QThreadPool()
self.show()
# Set up QTimer to continue in the background to help demonstrate threading advantage
self.counter = 0
self.timer = QTimer()
self.timer.setInterval(1000)
self.timer.timeout.connect(self.recurring_timer)
self.timer.start()
@pyqtSlot()
def do_something(self, signal):
# signal argument will be the callback_from_worker and it will emit to acknowledge_callback_in_mainthread
print("do_something is sleeping briefly. Try to see if you get a locked widget...")
time.sleep(7)
signal.emit()
@pyqtSlot()
def acknowledge_callback_in_mainthread_and_respond(self):
# this function should respond to callback_from_worker and emit a response
print("Acknowledged Callback in Main")
self.mainthread_callback_to_worker.emit()
def thread_example(self):
print("Beginning thread example")
worker = RespondedToWorker(self.do_something)
worker.signals.callback_from_worker.connect(self.acknowledge_callback_in_mainthread_and_respond)
# self.mainthread_callback_to_worker.connect(worker.acknowledge_callback_in_worker) # <-- causes crash
def recurring_timer(self):
self.counter += 1
self.timer_label.setText(f"Counter: {self.counter}")
if __name__ == '__main__':
app = QApplication(sys.argv)
win = MainWindow()
app.setStyle("Fusion")
win.show()
sys.exit(app.exec())
At the current moment, the script can make the second thread and send a signal to the main GUI. I'd like the GUI to send a response signal back to the worker thread. I'm also uncertain why connecting the main/GUI's signal mainthread_callback_to_worker
causes a crash (see the commented-out line).
I understand that one workaround would be for do_something
to return some value and then use it inside the worker as "acknowledgement". But I'd like to know the solution using signals & slots, if possible.
Solution
To understand the cause of the error you must run the code in the terminal and you will get the following error message:
QObject::connect: Cannot connect MainWindow::mainthread_callback_to_worker() to (nullptr)::acknowledge_callback_in_worker()
Traceback (most recent call last):
File "main.py", line 72, in thread_example
self.mainthread_callback_to_worker.connect(worker.acknowledge_callback_in_worker) # <-- causes crash
TypeError: connect() failed between MainWindow.mainthread_callback_to_worker[] and acknowledge_callback_in_worker()
Aborted (core dumped)
And the cause of the error is the abuse of the pyqtSlot
decorator since it should only be used in the QObject
methods but QRunnable is not causing that exception, in addition that in a non-QObject it does not take any advantage so that decorator in the run()
method doesn't make sense.
On the other hand, a QRunnable is only an interface that lives in the main thread and only the run method is executed in another thread, so a QRunnable cannot be a worker since that type of objective must execute its methods in a secondary thread.
So with the above QRunnable is not the appropriate option, so for your purpose I recommend using a QObject that lives in a secondary thread and invoking the methods.
import sys
import time
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer, QThread
from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
QWidget,
QVBoxLayout,
QLabel,
QPushButton,
)
class Worker(QObject):
callback_from_worker = pyqtSignal()
def __init__(self, func, *args, **kwargs):
super(Worker, self).__init__()
self._func = func
self.args = args
self.kwargs = kwargs
self.kwargs["signal"] = self.callback_from_worker
def start_task(self):
QTimer.singleShot(0, self.task)
@pyqtSlot()
def task(self):
self._func(*self.args, **self.kwargs)
@pyqtSlot()
def acknowledge_callback_in_worker(self):
print("Acknowledged Callback in Worker")
print(threading.current_thread())
class MainWindow(QMainWindow):
mainthread_callback_to_worker = pyqtSignal()
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
w, lay = QWidget(), QVBoxLayout()
w.setLayout(lay)
self.setCentralWidget(w)
self.timer_label = QLabel("Timer Label")
lay.addWidget(self.timer_label)
self.btn_thread_example = QPushButton("Push Me")
self.btn_thread_example.pressed.connect(self.thread_example)
lay.addWidget(self.btn_thread_example)
self.counter = 0
self.timer = QTimer(interval=1000, timeout=self.recurring_timer)
self.timer.start()
self._worker = Worker(self.do_something)
self._worker.callback_from_worker.connect(
self.acknowledge_callback_in_mainthread_and_respond
)
self.worker_thread = QThread(self)
self.worker_thread.start()
self._worker.moveToThread(self.worker_thread)
@pyqtSlot()
def do_something(self, signal):
print(
"do_something is sleeping briefly. Try to see if you get a locked widget..."
)
time.sleep(7)
signal.emit()
@pyqtSlot()
def acknowledge_callback_in_mainthread_and_respond(self):
print("Acknowledged Callback in Main")
self.mainthread_callback_to_worker.emit()
def thread_example(self):
print("Beginning thread example")
self._worker.start_task()
def recurring_timer(self):
self.counter += 1
self.timer_label.setText(f"Counter: {self.counter}")
if __name__ == "__main__":
app = QApplication(sys.argv)
win = MainWindow()
app.setStyle("Fusion")
win.show()
ret = app.exec_()
win.worker_thread.quit()
win.worker_thread.wait()
sys.exit(ret)
Answered By - eyllanesc
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.