简体   繁体   中英

Passing a callback to a Worker in PyQt QThreadPool

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM