I'm trying to extend this example on multithreading in PyQt5: Multithreading PyQt applications with QThreadPool (MWE below) to allow for two different threaded functions, one that uses a callback and the other that does not. In the example above, the progress_callback
is hard-coded in the Worker()
class __init__
, which means that any threaded function must have a signature that accommodates that callback:
def execute_this_fn(self, progress_callback):
which means that if there is a second threaded process, that function's signature would also be required to accommodate the callback in its signature even though the callback is not used.
So instead of hard-coding progress_callback
into the __init__
of Worker()
, I'd like to pass in the callback when instantiating Worker()
.
My MWE is below -- I include two commented-out two lines from the original example for reference. When I run it, and press the "DANGER," button on the GUI: I get:
$ python threadtest.py
Multithreading with maximum 16 threads
progress_callback = <bound PYQT_SIGNAL progress of WorkerSignals object at 0x113bbd160>
n = 0
n = 1
n = 2
n = 3
n = 4
Done.
THREAD COMPLETE!
So the code runs, but the callback function (slot) progress_fn
is never called, and I'm not sure why...
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import time
import traceback, sys
class WorkerSignals(QObject):
''' Defines the signals available from a running worker thread.
Supported signals are:
finished
No data
error
`tuple` (exctype, value, traceback.format_exc() )
result
`object` data returned from processing, anything
progress
`int` indicating % progress
'''
finished = pyqtSignal()
error = pyqtSignal(tuple)
result = pyqtSignal(object)
progress = pyqtSignal(int)
class Worker(QRunnable):
''' Worker thread
Inherits from QRunnable to handler worker thread setup, signals and wrap-up.
:param callback: The function callback to run on this worker thread. Supplied args and
kwargs will be passed through to the runner.
:type callback: function
:param args: Arguments to pass to the callback function
:param kwargs: Keywords to pass to the callback function
'''
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
# Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
# The old way (hard-code the callback to kwargs)
#self.kwargs['progress_callback'] = self.signals.progress
@pyqtSlot()
def run(self):
'''
Initialise the runner function with passed args, kwargs.
'''
# Retrieve args/kwargs here; and fire processing using them
try:
result = self.fn(*self.args, **self.kwargs)
except:
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
else:
self.signals.result.emit(result) # Return the result of the processing
finally:
self.signals.finished.emit() # Done
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.counter = 0
self.signals = WorkerSignals()
layout = QVBoxLayout()
self.l = QLabel("Start")
b = QPushButton("DANGER!")
b.pressed.connect(self.oh_no)
layout.addWidget(self.l)
layout.addWidget(b)
w = QWidget()
w.setLayout(layout)
self.setCentralWidget(w)
self.show()
self.threadpool = QThreadPool()
print("Multithreading with maximum %d threads" % self.threadpool.maxThreadCount())
self.timer = QTimer()
self.timer.setInterval(1000)
self.timer.timeout.connect(self.recurring_timer)
self.timer.start()
def progress_fn(self, n):
print("%d%% done" % n) # this does not execute...
def execute_this_fn(self, progress_callback):
print(f'progress_callback = {progress_callback}')
for n in range(0, 5):
time.sleep(1)
print(f'n = {n}')
progress_callback.emit(int(n*100/4))
return "Done."
def print_output(self, s):
print(s)
def thread_complete(self):
print("THREAD COMPLETE!")
def oh_no(self):
# Pass the function to execute
# the old way (callback hardcoded in __init__ of Worker class)
#worker = Worker(self.execute_this_fn)
# the desired new way
worker = Worker(self.execute_this_fn, progress_callback=self.signals.progress)
worker.signals.result.connect(self.print_output)
worker.signals.finished.connect(self.thread_complete)
worker.signals.progress.connect(self.progress_fn)
# Execute
self.threadpool.start(worker)
def recurring_timer(self):
self.counter +=1
self.l.setText("Counter: %d" % self.counter)
app = QApplication([])
window = MainWindow()
app.exec_()
The problem is simple: You have 2 WorkerSignals objects, and in one of them you make the connection and with the other you emit the signal. The solution is to use the same object for the connection and for the emission:
worker = Worker(self.execute_this_fn, progress_callback=self.signals.progress)
worker.signals.result.connect(self.print_output)
worker.signals.finished.connect(self.thread_complete)
.signals.progress.connect(self.progress_fn)
Although I prefer to create a QObject that implements that logic:
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import time
import traceback, sys
class WorkerSignals(QObject):
"""Defines the signals available from a running worker thread.
Supported signals are:
finished
No data
error
`tuple` (exctype, value, traceback.format_exc() )
result
`object` data returned from processing, anything
progress
`int` indicating % progress
"""
finished = pyqtSignal()
error = pyqtSignal(tuple)
result = pyqtSignal(object)
progress = pyqtSignal(int)
class Worker(QRunnable):
"""Worker thread
Inherits from QRunnable to handler worker thread setup, signals and wrap-up.
:param callback: The function callback to run on this worker thread. Supplied args and
kwargs will be passed through to the runner.
:type callback: function
:param args: Arguments to pass to the callback function
:param kwargs: Keywords to pass to the callback function
"""
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
# Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
# The old way (hard-code the callback to kwargs)
# self.kwargs['progress_callback'] = self.signals.progress
@pyqtSlot()
def run(self):
"""
Initialise the runner function with passed args, kwargs.
"""
# Retrieve args/kwargs here; and fire processing using them
try:
result = self.fn(*self.args, **self.kwargs)
except:
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
else:
self.signals.result.emit(result) # Return the result of the processing
finally:
self.signals.finished.emit() # Done
class ProgressCallback(QObject):
progressChanged = pyqtSignal(int)
def __call__(self, value):
self.progressChanged.emit(value)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.counter = 0
layout = QVBoxLayout()
self.l = QLabel("Start")
b = QPushButton("DANGER!")
b.pressed.connect(self.oh_no)
layout.addWidget(self.l)
layout.addWidget(b)
w = QWidget()
w.setLayout(layout)
self.setCentralWidget(w)
self.show()
self.threadpool = QThreadPool()
print(
"Multithreading with maximum %d threads" % self.threadpool.maxThreadCount()
)
self.timer = QTimer()
self.timer.setInterval(1000)
self.timer.timeout.connect(self.recurring_timer)
self.timer.start()
def progress_fn(self, n):
print("%d%% done" % n) # this does not execute...
def execute_this_fn(self, progress_callback):
print(f"progress_callback = {progress_callback}")
for n in range(0, 5):
time.sleep(1)
print(f"n = {n}")
progress_callback(int(n * 100 / 4))
return "Done."
def print_output(self, s):
print(s)
def thread_complete(self):
print("THREAD COMPLETE!")
def oh_no(self):
callback = ProgressCallback()
worker = Worker(self.execute_this_fn, progress_callback=callback)
worker.signals.result.connect(self.print_output)
worker.signals.finished.connect(self.thread_complete)
callback.progressChanged.connect(self.progress_fn)
# Execute
self.threadpool.start(worker)
def recurring_timer(self):
self.counter += 1
self.l.setText("Counter: %d" % self.counter)
app = QApplication([])
window = MainWindow()
app.exec_()
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.