简体   繁体   English

如何从不同的进程发出 GUI 中的插槽信号?

[英]How to signal slots in a GUI from a different process?

Context: In Python a main thread spawns a 2nd process (using multiprocessing module) and then launches a GUI (using PyQt4).上下文:在 Python 中,主线程产生第二个进程(使用多处理模块),然后启动一个 GUI(使用 PyQt4)。 At this point the main thread blocks until the GUI is closed.此时主线程会阻塞,直到 GUI 关闭。 The 2nd process is always processing and ideally should emit signal(s) to specific slot(s) in the GUI in an asynchronous manner.第二个进程总是在处理,理想情况下应该以异步方式向 GUI 中的特定插槽发出信号。

Question: Which approach/tools are available in Python and PyQt4 to achieve that and how?问题:在 Python 和 PyQt4 中可以使用哪些方法/工具来实现这一点以及如何实现? Preferably in a soft-interrupt manner rather than polling.最好以软中断方式而不是轮询方式。

Abstractly speaking, the solution I can think of is a "tool/handler" instantiated in the main thread that grabs the available slots from the GUI instance and connects with the grabbed signals from the 2nd process, assuming I provide this tool some information of what to expect or hard coded.抽象地说,我能想到的解决方案是在主线程中实例化的“工具/处理程序”,它从 GUI 实例中获取可用插槽并与从第二个进程中获取的信号连接,假设我向这个工具提供了一些关于什么的信息期望或硬编码。 This could be instantiated to a 3rd process/thread.这可以实例化为第三个进程/线程。

This is an example Qt application demonstrating sending signals from a child process to slots in the mother process.这是一个示例 Qt 应用程序,演示了从子进程向母进程中的插槽发送信号。 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.我将 process 区分为Motherchild ,因为在 Qt 上下文中已经使用了parent一词。
The mother process has two threads.母进程有两个线程。 Main thread of mother process sends data to child process via multiprocessing.Queue .母进程的主线程通过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 .子进程通过multiprocessing.Pipe将处理后的数据和信号签名发送到母进程的第二个线程。 The second thread of mother process actually emits the signal.母进程的第二个线程实际发出信号。

Python 2.X, PyQt4: 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:为了方便起见,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_()

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.我希望这不会被认为是一个死灵转储,但是我认为通过将更新他的示例添加到 PyQt5、添加一些注释、删除一些 python2 语法以及最重要的是通过使用新样式来更新 Nizam 的答案会很好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_()

One should first look how Signals/Slots work within only one Python process:首先应该看看信号/槽是如何在一个 Python 进程中工作的:

If there is only one running QThread, they just call the slots directly.如果只有一个运行 QThread,它们就直接调用插槽。

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. Connectionpoll功能是非阻塞的,因此我会定期使用QTimer轮询它,然后相应地发出信号。

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.另一种解决方案可能是从线程模块(或 QThread)中获得一个Thread ,专门等待来自具有Queueget函数的队列的新消息。 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.这是在另一个Process启动 Qt GUI 以及侦听Connection和特定消息的Thread的示例,关闭 GUI,然后终止进程。

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()

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.可能只是让你走上正轨。

I had the same problem in C++.我在 C++ 中遇到了同样的问题。 From a QApplication, I spawn a Service object.从 QApplication 中,我生成了一个 Service 对象。 The object creates the Gui Widget but it's not its parent (the parent is QApplication then).该对象创建了 Gui 小部件,但它不是它的父级(然后父级是 QApplication)。 To control the GuiWidget from the service widget, I just use signals and slots as usual and it works as expected.为了从服务小部件控制 GuiWidget,我像往常一样使用信号和插槽,它按预期工作。 Note: The thread of GuiWidget and the one of the service are different.注意:GuiWidget 的线程和服务的线程是不同的。 The service is a subclass of QObject.该服务是 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.如果您需要多进程信号/槽机制,那么尝试使用 Apache Thrift 或使用产生 2 个 QProcess 对象的 Qt 监控进程。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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