如何从不同的进程中向GUI信号槽发送信号?

22

背景信息: 在Python中,主线程生成第二个进程(使用multiprocessing模块),然后启动GUI(使用PyQt4)。此时,主线程将阻塞,直到GUI关闭。第二个进程始终在处理,并且应该以异步方式向GUI的特定插槽发射信号。

问题: Python和PyQt4中有哪些方法/工具可用于实现这一点?如何实现?最好是以软件中断的方式而不是轮询。

抽象地说,我能想到的解决方案是在主线程中实例化一个“工具/处理程序”,该程序从GUI实例中获取可用插槽,并连接来自第二个进程的抓取信号,假设我提供这个工具一些期望的信息或硬编码。可以将其实例化为第三个进程/线程。


你可能需要阅读这篇文章:http://stackoverflow.com/questions/15685695/qthread-signals-slots-to-qprocesses - smitkpatel
@smitkpatel 实际上,QProcess 只用于执行其他应用程序。它相当于 Python 中的 subprocess,而不是 multiprocessing.Process。但是在 C 中根本没有这个问题,因为 QThreads 无论如何都可以并行运行,而在 CPython 中,您需要使用 Processes 来规避 GIL。 - Trilarion
5个回答

23
这是一个Qt应用程序示例,演示了从子进程发送信号到母进程的插槽。我不确定这是否是正确的方法,但它可以工作。
我将进程区分为母体子代,因为Qt上下文中已经使用了父亲这个词。母进程有两个线程。母进程的主线程通过multiprocessing.Queue将数据发送到子进程。子进程通过multiprocessing.Pipe将处理后的数据和要发送的信号签名发送到母进程的第二个线程。母进程的第二个线程实际上发出信号。
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_()

同时方便的是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_()

虽然您的代码看起来很好,但我遇到了“RuntimeError:未调用ChildProc类型的超类__init __()”的问题。如果删除“ch.start()”,这个问题就会消失? - Trilarion
我在 Windows 7(x64)上使用 Python 2.7.8 和 PyQt 4.11.3 进行编程,似乎与 pickling 有关。不确定为什么会出现这种情况,或许其他人可以分享他们的系统运行情况。 - Trilarion
class ChildProc(QObject) 不必继承 QObject。试着改成 class ChildProc(object) 并移除 super(ChildProc, self).__init__(parent) - Nizam Mohamed
我为了方便添加了Python3.X和PySide版本,并授予了奖励。感谢你的出色工作,希望这能帮助到其他人。 - Trilarion
感谢您的慷慨奖励! - Nizam Mohamed
为什么在“Emitter”上设置了“daemon”? - Lorem Ipsum

17

大家好,

我希望这不算是太长时间未更新的内容,然而我认为将Nizam的答案更新到PyQt5是个好主意。我加入了一些注释、移除了Python2的语法,并且更重要的是使用了PyQt中新式信号的方式。希望有人会觉得有用。

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

1
PyQt5的回答非常出色。谢谢。 - afp_2008
非常好的答案,我会使用这段代码来防止我的应用程序冻结。 - Chris P

3

首先,我们需要了解信号/槽在Python进程中的工作方式:

如果只有一个运行中的QThread,则直接调用槽。

如果信号在不同的线程上发出,则必须找到信号的目标线程,并将消息/事件放入该线程的队列中。该线程将在适当的时间处理消息/事件并调用信号。

因此,在内部始终涉及某种轮询,并且重要的是轮询是非阻塞的。

multiprocessing创建的进程可以通过管道进行通信,每个方向都有两个连接

Connectionpoll函数是非阻塞的,因此我会定期使用QTimer对其进行轮询,然后相应地发出信号。

另一种解决方案可能是使用来自线程模块(或QThread)的Thread,专门等待来自Queue的新消息,并使用队列的get函数。有关更多信息,请参见multiprocessing中的Pipes和Queues部分。

以下是一个示例,在另一个Process中启动Qt GUI,并在一个Thread上侦听Connection,一旦收到特定消息,则关闭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()

1
你的示例可以工作,但有一个严重的限制:listen 方法在一个不是主 GUI 线程的线程中运行。由于大多数 Qt 函数都不可重入,因此你几乎无法在其中执行任何操作。将 LineEdit 对象添加到窗口中,并尝试在 listen 函数中更新它 - 应用程序将崩溃。你在文本中提到了一个强大的解决方案(在定时器事件中进行非阻塞管道轮询)。考虑到 Qt 的架构,我认为没有任何实际的逃脱轮询的方法。 - Paul Cornelius
@PaulCornelius 感谢您的评论。我不太喜欢轮询,但如果它是唯一实际可行的解决方案,我想必须这样做。然而,也许我会在此之前测试QApplication.postEvent()。也许它可以从外部插入一些东西。 - Trilarion
1
QCoreApplication::postEvent 是线程安全的。因此在那里可能会有一些可能性。 - Trilarion
好的观察。可能也需要子类化 QEvent。记录一下,最近我在一个应用中使用了这个想法,它似乎偶尔会导致崩溃(大约每天一次)。我不能确定 postEvent 是原因,但当我重新设计程序以消除 postEvent 时,它运行得非常完美。有一些提示表明 PySide 在任务切换时存在与 Python GIL 的问题。我找不到任何人使用 PyQt 报告这样的问题,但我没有使用过 PyQt。 - Paul Cornelius
我在我的模块qtutils中在线程之间使用postEvent()执行某些操作。显然这样做是不好的,因为我是从Python线程中进行操作的(参见这里),但它似乎能够正常工作。此外,PySide在使用postEvent()时存在内存泄漏问题(参见这里),但PyQt4则没有。也许您可以利用这一点来改进多进程应用程序中的信号/槽实现方式。 - three_pineapples

1
一个非常有趣的主题。我想在线程之间使用的信号是一件非常有用的事情。那么基于套接字创建自定义信号如何? 我还没有测试过这个,但这是我通过一些快速调查收集到的信息:
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')

可能只是让你走上正确的轨道。

Qt4/5已经具备在线程之间工作的信号。问题是要求在进程之间工作的信号 - 即主要涉及多处理而不是多线程。 - ekhumoro
嗯,套接字方法在多进程中也应该可以工作。但是实现可能需要进行一些微调。 - bosnjak
能否扩展示例以便与进程一起使用?我猜这是回答问题所必需的。 - Trilarion

1
我在C++中遇到了同样的问题。从QApplication中,我生成了一个Service对象。该对象创建了Gui Widget,但它不是其父对象(父对象是QApplication)。为了从服务小部件控制GuiWidget,我只需像往常一样使用信号和槽即可,它按预期工作。 注意:GuiWidget的线程和服务线程是不同的。服务是QObject的子类。
如果您需要多进程信号/槽机制,请尝试使用Apache Thrift或使用一个Qt监视进程,该进程生成2个QProcess对象。

Qt4/5中的信号是在线程之间工作的。但问题要求在进程之间工作的信号 - 即主要涉及多处理而不是多线程。 - Trilarion

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接