2

I am creating a PyQt GUI for an experimental setup. This will involve computationally heavy operations so I am aiming for an architecture based on the multiprocessing module and inspired from this answer.

The QMainWindow creates

  1. child Processes with an individual "task" Queue to get instructions from the main process and a shared "callback" Queue to send back instructions to the main process
  2. a QThread to poll the "callback" queue and translate the messages into signals that are connected to slots of the QMainWindow

The example uses old style signals with arbitrary signature self.emit(QtCore.SIGNAL(signature), args). My question is: is it possible to replicate this functionality with new-style signals ?.

I am aware of this question and of this one. However, always emitting a valueChanged signal with a general object does not suit my needs since I would like to connect to slots with different names depending on the signature received from one of the child Processes.

Here is a working code (note there is only one child process and one slot in the MainWindow for simplicity, but there will be several in the finished code):

from multiprocessing import Process, Queue
import sys
from PyQt4 import QtGui, QtCore


class CallbackQueueToSignal(QtCore.QThread):

    def __init__(self, queue, parent=None):
        super(CallbackQueueToSignal, self).__init__(parent)
        self.queue = queue

    def _emit(self, signature, args=None):
        if args:
            self.emit(QtCore.SIGNAL(signature), args)
        else:
            self.emit(QtCore.SIGNAL(signature))

    def run(self):
        while True:
            signature = self.queue.get()
            self._emit(*signature)


class WorkerProcess(Process):

    def __init__(self, callback_queue, task_queue, daemon=True):
        super(WorkerProcess, self).__init__()
        self.daemon = daemon
        self.callback_queue = callback_queue
        self.task_queue = task_queue

    def _process_call(self, func_name, args=None):
        func = getattr(self, func_name)
        if args:
            func(args)
        else:
            func()

    def emit_to_mother(self, signature, args=None):
        signature = (signature, )
        if args:
            signature += (args, )
        self.callback_queue.put(signature)

    def run(self):
        while True:
            call = self.task_queue.get()
            # print("received: {}".format(call))
            self._process_call(*call)

    def text_upper(self, text):
        self.emit_to_mother('data(PyQt_PyObject)', (text.upper(),))


class MainWin(QtGui.QMainWindow):

    def __init__(self, parent=None):
        super(MainWin, self).__init__(parent)

        self.data_to_child = Queue()
        self.callback_queue = Queue()

        self.callback_queue_watcher = CallbackQueueToSignal(self.callback_queue)
        self.callback_queue_watcher.start()

        self.child = WorkerProcess(self.callback_queue, self.data_to_child)
        self.child.start()

        self.browser = QtGui.QTextBrowser()
        self.lineedit = QtGui.QLineEdit('Type text and press <Enter>')
        self.lineedit.selectAll()
        layout = QtGui.QVBoxLayout()
        layout.addWidget(self.browser)
        layout.addWidget(self.lineedit)
        self.layout_widget = QtGui.QWidget()
        self.layout_widget.setLayout(layout)
        self.setCentralWidget(self.layout_widget)
        self.lineedit.setFocus()
        self.setWindowTitle('Upper')
        self.connect(self.lineedit, QtCore.SIGNAL('returnPressed()'), self.to_child)
        self.connect(self.callback_queue_watcher, QtCore.SIGNAL('data(PyQt_PyObject)'), self.updateUI)

    def to_child(self):
        self.data_to_child.put(("text_upper", ) + (self.lineedit.text(), ))
        self.lineedit.clear()

    def updateUI(self, text):
        text = text[0]
        self.browser.append(text)

    def closeEvent(self, event):
        result = QtGui.QMessageBox.question(
            self,
            "Confirm Exit...",
            "Are you sure you want to exit ?",
            QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)
        event.ignore()

        if result == QtGui.QMessageBox.Yes:
            # self.pipeWatcher.exit()
            self.child.terminate()
            event.accept()

if __name__ == '__main__':

    app = QtGui.QApplication(sys.argv)

    form = MainWin()
    form.show()

    app.aboutToQuit.connect(app.deleteLater)
    sys.exit(app.exec_())
Community
  • 1
  • 1
Thibaud Ruelle
  • 303
  • 1
  • 16
  • Why does the connection have to depend on the signature? Just use one pre-defined signal as suggested in the questions you linked to, and send an identifier as the first parameter. – ekhumoro Aug 17 '16 at 18:36
  • In that case, you would lose the advantage of using signals. For instance you could not connect and disconnect specific slots from a specific signal during the execution of the program. But maybe I am missing your point? – Thibaud Ruelle Aug 18 '16 at 05:52
  • There is no way to replicate dynamically emitting custom signals with new-style signals. Custom signals must be pre-defined as class attributes. But I don't see any reason why this would affect connecting and disconnecting signals at runtime. And I also don't see how this is relevant to your actual question - your code example doesn't disconnect any signals. – ekhumoro Aug 19 '16 at 02:11
  • @ekumoro The code does not include such connection/disconnection too keep it minimal, as mentioned in the question. Then do you mean creating several predefined signals in the `CallbackQueueToSignal`class and choosing which one to send based on the message received ? If not please clarify, maybe in an answer if you have the time ? – Thibaud Ruelle Aug 19 '16 at 06:28

1 Answers1

2

The new-style signal and slot syntax requires that signals are pre-defined as class attributes on a class that inherits from QObject. When the class is instantiated, a bound-signal object is automatically created for the instance. The bound-signal object has connect/disconnect/emit methods, and a __getitem__ syntax which allows different overloads to be selected.

Since bound-signals are objects, it no longer makes sense to allow the dynamic emission of arbitrary signals that was possible with the old-style syntax. This is simply because an arbitrary signal (i.e. one that is not pre-defined) could not have a corresponding bound-signal object for slots to connect to.

The example code in the question can still be ported to the new-style syntax, though:

class CallbackQueueToSignal(QtCore.QThread):
    dataSignal = QtCore.pyqtSignal([], [object], [object, object])   
    ...

    def _emit(self, signal, *args):
        getattr(self, signal)[(object,) * len(args)].emit(*args)

    def run(self):
        while True:
            args = self.queue.get()
            self._emit(*args)


class WorkerProcess(Process):
    ...

    def emit_to_mother(self, *args):
        self.callback_queue.put(args)

    def text_upper(self, text):
        self.emit_to_mother('dataSignal', (text.upper(),))


class MainWin(QtGui.QMainWindow):
    def __init__(self, parent=None):
        ...

        self.lineedit.returnPressed.connect(self.to_child)
        self.callback_queue_watcher.dataSignal[object].connect(self.updateUI)
ekhumoro
  • 115,249
  • 20
  • 229
  • 336
  • Sorry for the long response time. I now understand what you mean. This is a really clever way to port to the new-style syntax, thank you ! I would never have thought about overloading the signal like that. – Thibaud Ruelle Aug 25 '16 at 10:01
  • However this is less general that the code with old-style signals since you now have to copy paste `signal_name = QtCore.pyqtSignal([], [object], [object, object])` for each different signal name you want to use. So a follow-up question would be: are there significant advantages of new-style signals that would justify the port ? – Thibaud Ruelle Aug 25 '16 at 10:04
  • @ThibaudRuelle. I suppose the major advantage is forwards compatibility - the old-syntax is not supported at all in PyQt5. But more generally, the new-style syntax is much more pythonic and less error-prone. It's very easy to get the signature wrong with old-style syntax (especially for newbies and those unfamiliar with C++). Even worse, it fails silently. rather than raising an error like the new-style signals do. I also think it's worth saying that "readability matters": most people find the old-style syntax to be very ugly and verbose. I don't miss it at all. – ekhumoro Aug 25 '16 at 16:49