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

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

提问人:NovoRei 提问时间:11/5/2014 最后编辑:NoDataDumpNoContributionNovoRei 更新时间:12/6/2018 访问量:13346

问:

上下文: 在 Python 中,主线程生成第二个进程(使用多处理模块),然后启动 GUI(使用 PyQt4)。此时,主线程会阻塞,直到 GUI 关闭。第二个进程始终是处理,理想情况下应以异步方式向 GUI 中的特定插槽发送信号。

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

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

Python PyQt 多处理 信号 Pyside

评论

0赞 smitkpatel 5/6/2015
你可能应该读这个 stackoverflow.com/questions/15685695/......
0赞 NoDataDumpNoContribution 5/7/2015
@smitkpatel 实际上,QProcess 仅用于执行其他应用程序。相当于 Python 中的子进程,而不是多处理。过程。但是 C 根本没有这个问题,因为无论如何 QThreads 确实并行运行,而在 CPython 中,您需要进程来规避 GIL。

答:

3赞 NoDataDumpNoContribution 4/29/2015 #1

首先应该看看 Signals/Slots 如何在一个 Python 进程中工作:

如果只有一个正在运行的 QThread,它们只需直接调用插槽即可。

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

因此,内部总是涉及某种轮询,重要的是轮询是非阻塞的。

多处理创建的进程可以通过管道进行通信,管道为每端提供两个连接

的功能是非阻塞的,因此我会定期用 a 轮询它,然后相应地发出信号。pollConnectionQTimer

另一种解决方案可能是让线程模块(或QThread)专门等待来自具有队列功能的新消息。有关更多信息,请参阅多处理的管道和队列部分。ThreadQueueget

下面是一个示例,在另一个Qt GUI中启动一个Qt GUI,以及一个监听特定消息的人,关闭GUI,然后终止该过程。ProcessThreadConnection

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赞 Paul Cornelius 5/4/2015
您的示例有效,但有一个严重的限制:该方法在不是主 GUI 线程的线程中运行。在那里你能做的很少,因为大多数Qt函数都是不可重入的。将 LineEdit 对象添加到窗口并尝试在函数中更新它 - 应用程序将崩溃。您在文本中提到了一个强大的解决方案(在计时器事件中对 Pipe 进行非阻塞轮询)。鉴于Qt的架构,我认为没有实际的逃避轮询。listenlisten
0赞 NoDataDumpNoContribution 5/4/2015
@PaulCornelius 谢谢你的评论。我不太喜欢轮询,但如果这是唯一实际可行的解决方案,我想必须这样做。但是,也许我会之前测试 QApplication.postEvent()。也许这可以从外面插入一些东西。
1赞 NoDataDumpNoContribution 5/4/2015
QCoreApplication::p ostEvent 是线程安全的。所以那里可能有些事情。
0赞 Paul Cornelius 5/6/2015
良好的观察力。可能还需要对 QEvent 进行子类化。作为记录,我最近在一个应用程序中使用了这个想法,它似乎在极少数情况下会导致崩溃(每天一次)。我不能肯定地说postEvent是原因,但是当我重新设计程序以消除postEvent时,它运行完美。有一些迹象表明,在任务切换时,PySide 的 python GIL 存在问题。我找不到任何人用PyQt重新解决此类问题,但我没有使用它。
0赞 three_pineapples 5/7/2015
我用我的模块qtutils在线程之间做一些事情。显然这很糟糕,因为我是从 Python 线程中完成的(见这里),但它似乎有效。此外,PySide 存在内存泄漏(请参阅此处),但 PyQt4 没有。也许您可以使用它来告知您在多进程应用程序中实现信号/时隙。postEvent()postEvent()
1赞 bosnjak 5/1/2015 #2

一个非常有趣的话题。我想有一个在线程之间工作的信号是一件非常有用的事情。如何创建基于套接字的自定义信号? 我还没有测试过这个,但这是我通过一些快速调查收集到的:

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

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

评论

0赞 ekhumoro 5/1/2015
Qt4/5 已经有在线程之间工作的信号。问题在于要求在进程之间起作用的信号 - 即它主要是关于多处理,而不是多线程。
0赞 bosnjak 5/1/2015
嗯,套接字方法也应该适用于多处理。但是,实现可能需要一些调整。
0赞 NoDataDumpNoContribution 5/4/2015
是否可以扩展示例,使其与流程一起使用?我想这是回答这个问题所必需的。
23赞 Nizam Mohamed 5/5/2015 #3

这是一个示例Qt应用程序,演示了将信号从子进程发送到母进程中的插槽。我不确定这是正确的方法,但它有效。

我将过程区分为母亲,因为在Qt上下文中使用了parent这个词。
母进程有两个线程。母进程的主线程通过 向子进程发送数据。子进程通过 发送要发送到母进程的第二个线程的信号的处理数据和签名。母进程的第二个线程实际上发出了信号。
multiprocessing.Queuemultiprocessing.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_()

评论

0赞 NoDataDumpNoContribution 5/5/2015
我收到“RuntimeError:从未调用过 ChildProc 类型的超类 __init__()”,尽管您的代码看起来不错。如果删除“ch.start()”,它就会消失吗?
0赞 NoDataDumpNoContribution 5/5/2015
我在 Windows 7(均为 x64)上的 Python 2.7.8 和 PyQt 4.11.3 上得到了它,它似乎与酸洗有关。不知道为什么会这样。也许其他人可以说它是如何在他们的系统上运行的。
0赞 Nizam Mohamed 5/5/2015
class ChildProc(QObject)不必继承自 。尝试更改为并删除 .QObjectclass ChildProc(object)super(ChildProc, self).__init__(parent)
0赞 NoDataDumpNoContribution 5/7/2015
为了方便起见,我添加了一个 Python3.X,PySide 版本并获得了赏金。谢谢你的出色工作。我希望这对其他人有所帮助。
0赞 Lorem Ipsum 6/24/2021
为什么设置在?daemonEmitter
1赞 madduci 5/7/2015 #4

我在 C++ 中遇到了同样的问题。在 QApplication 中,我生成了一个 Service 对象。该对象创建 Gui Widget,但它不是它的父级(父级是 QApplication)。为了从服务小部件控制 GuiWidget,我只像往常一样使用信号和插槽,它按预期工作。 注意:GuiWidget 的线程和服务线程是不同的。该服务是 QObject 的子类。

如果你需要多进程信号/时隙机制,那么尝试使用Apache Thrift或使用Qt监控进程,它会产生2个QProcess对象。

评论

0赞 NoDataDumpNoContribution 5/7/2015
Qt4/5 中的信号在线程之间工作。但问题是要求在进程之间起作用的信号 - 即它主要是关于多处理,而不是多线程。
17赞 anonymous_ghoster 12/6/2018 #5

大家好,

我希望这不会被认为是一个死灵转储,但我认为通过向 PyQt5 添加更新他的示例、添加一些注释、删除一些 python2 语法以及最重要的是通过使用 PyQt 中可用的新样式的信号来更新 Nizam 的答案会很好。希望有人觉得它有用。

"""
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赞 shashashamti2008 10/22/2020
PyQt5 的优秀答案。谢谢。
0赞 Chris P 8/7/2021
很好的答案,我将使用此代码来防止冻结我的应用程序。