How to signal slots in a GUI from a different process?

前端 未结 5 2027
囚心锁ツ
囚心锁ツ 2021-01-31 19:52

Context: In Python a main thread spawns a 2nd process (using multiprocessing module) and then launches a GUI (using PyQt4). At this point the main thread blocks until the GUI is

相关标签:
5条回答
  • 2021-01-31 20:26

    One should first look how Signals/Slots work within only one Python process:

    If there is only one running QThread, they just call the slots directly.

    If the signal is emitted on a different thread it has to find the target thread of the signal and put a message/ post an event in the thread queue of this thread. This thread will then, in due time, process the message/event and call the signal.

    So, there is always some kind of polling involved internally and the important thing is that the polling is non-blocking.

    Processes created by multiprocessing can communicate via Pipes which gives you two connections for each side.

    The poll function of Connection is non-blocking, therefore I would regularly poll it with a QTimer and then emit signals accordingly.

    Another solution might be to have a Thread from the threading module (or a QThread) specifically just waiting for new messages from a Queue with the get function of the queue. See the Pipes and Queues part of multiprocessing for more information..

    Here is an example starting a Qt GUI in another Process together with a Thread who listens on a Connection and upon a certain message, closes the GUI which then terminates the process.

    from multiprocessing import Process, Pipe
    from threading import Thread
    import time
    from PySide import QtGui
    
    class MyProcess(Process):
    
        def __init__(self, child_conn):
            super().__init__()
            self.child_conn = child_conn
    
        def run(self):
            # start a qt application
            app = QtGui.QApplication([])
            window = QtGui.QWidget()
            layout = QtGui.QVBoxLayout(window)
            button = QtGui.QPushButton('Test')
            button.clicked.connect(self.print_something)
            layout.addWidget(button)
            window.show()
    
            # start thread which listens on the child_connection
            t = Thread(target=self.listen, args = (app,))
            t.start()
    
            app.exec_() # this will block this process until somebody calls app.quit
    
        def listen(self, app):
            while True:
                message = self.child_conn.recv()
                if message == 'stop now':
                    app.quit()
                    return
    
        def print_something(self):
            print("button pressed")
    
    if __name__ == '__main__':
        parent_conn, child_conn = Pipe()
        s = MyProcess(child_conn)
        s.start()
        time.sleep(5)
        parent_conn.send('stop now')
        s.join()
    
    0 讨论(0)
  • 2021-01-31 20:31

    A quite interesting topic. I guess having a signal that works between threads is a very useful thing. How about creating a custom signal based on sockets? I haven't tested this yet, but this is what I gathered up with some quick investigation:

    class CrossThreadSignal(QObject):
        signal = pyqtSignal(object)
        def __init__(self, parent=None):
            super(QObject, self).__init__(parent)
            self.msgq = deque()
            self.read_sck, self.write_sck = socket.socketpair()
            self.notifier = QSocketNotifier(
                               self.read_sck.fileno(), 
                               QtCore.QSocketNotifier.Read
                            )
            self.notifier.activated.connect(self.recv)
    
        def recv(self):
            self.read_sck.recv(1)
            self.signal.emit(self.msgq.popleft())
    
        def input(self, message):
            self.msgq.append(message)
            self.write_sck.send('s')
    

    Might just put you on the right track.

    0 讨论(0)
  • 2021-01-31 20:32

    Hy all,

    I hope this is not considered to much of a necro-dump however I thought it would be good to update Nizam's answer by adding updating his example to PyQt5, adding some comments, removing some python2 syntax and most of all by using the new style of signals available in PyQt. Hope someone finds it useful.

    """
    Demo to show how to use PyQt5 and qt signals in combination with threads and
    processes.
    
    Description:
    Text is entered in the main dialog, this is send over a queue to a process that 
    performs a "computation" (i.e. capitalization) on the data. Next the process sends 
    the data over a pipe to the Emitter which will emit a signal that will trigger 
    the UI to update.
    
    Note:
    At first glance it seems more logical to have the process emit the signal that 
    the UI can be updated. I tried this but ran into the error 
    "TypeError: can't pickle ChildProc objects" which I am unable to fix.
    """
    
    import sys
    from multiprocessing import Process, Queue, Pipe
    
    from PyQt5.QtCore import pyqtSignal, QThread
    from PyQt5.QtWidgets import QApplication, QLineEdit, QTextBrowser, QVBoxLayout, QDialog
    
    
    class Emitter(QThread):
        """ Emitter waits for data from the capitalization process and emits a signal for the UI to update its text. """
        ui_data_available = pyqtSignal(str)  # Signal indicating new UI data is available.
    
        def __init__(self, from_process: Pipe):
            super().__init__()
            self.data_from_process = from_process
    
        def run(self):
            while True:
                try:
                    text = self.data_from_process.recv()
                except EOFError:
                    break
                else:
                    self.ui_data_available.emit(text.decode('utf-8'))
    
    
    class ChildProc(Process):
        """ Process to capitalize a received string and return this over the pipe. """
    
        def __init__(self, to_emitter: Pipe, from_mother: Queue, daemon=True):
            super().__init__()
            self.daemon = daemon
            self.to_emitter = to_emitter
            self.data_from_mother = from_mother
    
        def run(self):
            """ Wait for a ui_data_available on the queue and send a capitalized version of the received string to the pipe. """
            while True:
                text = self.data_from_mother.get()
                self.to_emitter.send(text.upper())
    
    
    class Form(QDialog):
        def __init__(self, child_process_queue: Queue, emitter: Emitter):
            super().__init__()
            self.process_queue = child_process_queue
            self.emitter = emitter
            self.emitter.daemon = True
            self.emitter.start()
    
            # ------------------------------------------------------------------------------------------------------------
            # Create the UI
            # -------------------------------------------------------------------------------------------------------------
            self.browser = QTextBrowser()
            self.lineedit = QLineEdit('Type text and press <Enter>')
            self.lineedit.selectAll()
            layout = QVBoxLayout()
            layout.addWidget(self.browser)
            layout.addWidget(self.lineedit)
            self.setLayout(layout)
            self.lineedit.setFocus()
            self.setWindowTitle('Upper')
    
            # -------------------------------------------------------------------------------------------------------------
            # Connect signals
            # -------------------------------------------------------------------------------------------------------------
            # When enter is pressed on the lineedit call self.to_child
            self.lineedit.returnPressed.connect(self.to_child)
    
            # When the emitter has data available for the UI call the updateUI function
            self.emitter.ui_data_available.connect(self.updateUI)
    
        def to_child(self):
            """ Send the text of the lineedit to the process and clear the lineedit box. """
            self.process_queue.put(self.lineedit.text().encode('utf-8'))
            self.lineedit.clear()
    
        def updateUI(self, text):
            """ Add text to the lineedit box. """
            self.browser.append(text)
    
    
    if __name__ == '__main__':
        # Some setup for qt
        app = QApplication(sys.argv)
    
        # Create the communication lines.
        mother_pipe, child_pipe = Pipe()
        queue = Queue()
    
        # Instantiate (i.e. create instances of) our classes.
        emitter = Emitter(mother_pipe)
        child_process = ChildProc(child_pipe, queue)
        form = Form(queue, emitter)
    
        # Start our process.
        child_process.start()
    
        # Show the qt GUI and wait for it to exit.
        form.show()
        app.exec_()
    
    0 讨论(0)
  • 2021-01-31 20:36

    I had the same problem in C++. From a QApplication, I spawn a Service object. The object creates the Gui Widget but it's not its parent (the parent is QApplication then). To control the GuiWidget from the service widget, I just use signals and slots as usual and it works as expected. Note: The thread of GuiWidget and the one of the service are different. The service is a subclass of QObject.

    If you need multi process signal/slot mechanism, then try to use Apache Thrift or use a Qt-monitoring process which spawns 2 QProcess objects.

    0 讨论(0)
  • 2021-01-31 20:45

    This is an example Qt application demonstrating sending signals from a child process to slots in the mother process. I'm not sure this is right approach but it works.

    I differentiate between process as mother and child, because the word parent is alread used in the Qt context.
    The mother process has two threads. Main thread of mother process sends data to child process via multiprocessing.Queue. Child process sends processed data and signature of the signal to be sent to the second thread of mother process via multiprocessing.Pipe. The second thread of mother process actually emits the signal.

    Python 2.X, PyQt4:

    from multiprocessing import Process, Queue, Pipe
    from threading import Thread
    import sys
    from PyQt4.QtCore import *
    from PyQt4.QtGui import *
    
    class Emitter(QObject, Thread):
    
        def __init__(self, transport, parent=None):
            QObject.__init__(self,parent)
            Thread.__init__(self)
            self.transport = transport
    
        def _emit(self, signature, args=None):
            if args:
                self.emit(SIGNAL(signature), args)
            else:
                self.emit(SIGNAL(signature))
    
        def run(self):
            while True:
                try:
                    signature = self.transport.recv()
                except EOFError:
                    break
                else:
                    self._emit(*signature)
    
    class Form(QDialog):
    
        def __init__(self, queue, emitter, parent=None):
            super(Form,self).__init__(parent)
            self.data_to_child = queue
            self.emitter = emitter
            self.emitter.daemon = True
            self.emitter.start()
            self.browser = QTextBrowser()
            self.lineedit = QLineEdit('Type text and press <Enter>')
            self.lineedit.selectAll()
            layout = QVBoxLayout()
            layout.addWidget(self.browser)
            layout.addWidget(self.lineedit)
            self.setLayout(layout)
            self.lineedit.setFocus()
            self.setWindowTitle('Upper')
            self.connect(self.lineedit,SIGNAL('returnPressed()'),self.to_child)
            self.connect(self.emitter,SIGNAL('data(PyQt_PyObject)'), self.updateUI)
    
        def to_child(self):
            self.data_to_child.put(unicode(self.lineedit.text()))
            self.lineedit.clear()
    
        def updateUI(self, text):
            text = text[0]
            self.browser.append(text)
    
    class ChildProc(Process):
    
        def __init__(self, transport, queue, daemon=True):
            Process.__init__(self)
            self.daemon = daemon
            self.transport = transport
            self.data_from_mother = queue
    
        def emit_to_mother(self, signature, args=None):
            signature = (signature, )
            if args:
                signature += (args, )
            self.transport.send(signature)
    
        def run(self):
            while True:
                text = self.data_from_mother.get()
                self.emit_to_mother('data(PyQt_PyObject)', (text.upper(),))
    
    if __name__ == '__main__':
    
        app = QApplication(sys.argv)
        mother_pipe, child_pipe = Pipe()
        queue = Queue()
        emitter = Emitter(mother_pipe)
        form = Form(queue, emitter)
        ChildProc(child_pipe, queue).start()
        form.show()
        app.exec_()
    

    And as convenience also Python 3.X, PySide:

    from multiprocessing import Process, Queue, Pipe
    from threading import Thread
    
    from PySide import QtGui, QtCore
    
    class Emitter(QtCore.QObject, Thread):
    
        def __init__(self, transport, parent=None):
            QtCore.QObject.__init__(self, parent)
            Thread.__init__(self)
            self.transport = transport
    
        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:
                try:
                    signature = self.transport.recv()
                except EOFError:
                    break
                else:
                    self._emit(*signature)
    
    class Form(QtGui.QDialog):
    
        def __init__(self, queue, emitter, parent=None):
            super().__init__(parent)
            self.data_to_child = queue
            self.emitter = emitter
            self.emitter.daemon = True
            self.emitter.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.setLayout(layout)
            self.lineedit.setFocus()
            self.setWindowTitle('Upper')
            self.lineedit.returnPressed.connect(self.to_child)
            self.connect(self.emitter, QtCore.SIGNAL('data(PyObject)'), self.updateUI)
    
        def to_child(self):
            self.data_to_child.put(self.lineedit.text())
            self.lineedit.clear()
    
        def updateUI(self, text):
            self.browser.append(text[0])
    
    class ChildProc(Process):
    
        def __init__(self, transport, queue, daemon=True):
            Process.__init__(self)
            self.daemon = daemon
            self.transport = transport
            self.data_from_mother = queue
    
        def emit_to_mother(self, signature, args=None):
            signature = (signature, )
            if args:
                signature += (args, )
            self.transport.send(signature)
    
        def run(self):
            while True:
                text = self.data_from_mother.get()
                self.emit_to_mother('data(PyQt_PyObject)', (text.upper(),))
    
    if __name__ == '__main__':
    
        app = QApplication(sys.argv)
        mother_pipe, child_pipe = Pipe()
        queue = Queue()
        emitter = Emitter(mother_pipe)
        form = Form(queue, emitter)
        ChildProc(child_pipe, queue).start()
        form.show()
        app.exec_()
    
    0 讨论(0)
提交回复
热议问题