【问题标题】:PyQt: moveToThread does not work when using partial() for slotPyQt:将 partial() 用于插槽时,moveToThread 不起作用
【发布时间】:2014-06-12 14:08:22
【问题描述】:

我正在构建一个运行生产者(工人)的小型 GUI 应用程序,GUI 按需使用输出并绘制它(使用 pyqtgraph)。

由于生产者是一个阻塞函数(需要一段时间才能运行),所以我(据说)将它移到了自己的线程中。

当从生产者调用 QThread.currentThreadId() 时,它输出与主 GUI 线程相同的数字。因此,首先执行工作程序,然后执行所有绘图函数调用(因为它们在同一个线程的事件队列中排队)。我该如何解决这个问题?

使用部分运行的示例:

gui thread id 140665453623104
worker thread id: 140665453623104

这是我的完整代码:

from PyQt4 import QtCore, QtGui
from PyQt4.QtCore import pyqtSignal
import pyqtgraph as pg
import numpy as np

from functools import partial
from Queue import Queue
import math
import sys
import time


class Worker(QtCore.QObject):

    termino = pyqtSignal()

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

    def run(self, m=30000):
        print('worker thread id: {}'.format(QtCore.QThread.currentThreadId()))
        for x in xrange(m):
            #y = math.sin(x)
            y = x**2
            time.sleep(0.001) # Weird, plotting stops if this is not present...
            self.q.put((x,y,y))
        print('Worker finished')

        self.termino.emit()

class MainWindow(QtGui.QWidget):

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

        self.q = Queue()
        self.termino = False       

        self.worker = Worker(self.q)
        self.workerThread = None
        self.btn = QtGui.QPushButton('Start worker')
        self.pw = pg.PlotWidget(self)
        pi = self.pw.getPlotItem()
        pi.enableAutoRange('x', True)
        pi.enableAutoRange('y', True)
        self.ge1 = pi.plot(pen='y')
        self.xs = []
        self.ys = []

        layout = QtGui.QVBoxLayout(self)
        layout.addWidget(self.pw)
        layout.addWidget(self.btn)

        self.resize(400, 400)

    def run(self):

        self.workerThread = QtCore.QThread()
        self.worker.moveToThread(self.workerThread)
        self.worker.termino.connect(self.setTermino)
        # moveToThread doesn't work here
        self.btn.clicked.connect(partial(self.worker.run, 30000))
        # moveToThread will work here
        # assume def worker.run(self): instead of def worker.run(self, m=30000)
        # self.btn.clicked.connect(self.worker.run)
        self.btn.clicked.connect(self.graficar)

        self.workerThread.start()
        self.show()

    def setTermino(self):
        self.termino = True

    def graficar(self):

        if not self.q.empty():
            e1,e2,ciclos = self.q.get()       
            self.xs.append(ciclos)
            self.ys.append(e1)
            self.ge1.setData(y=self.ys, x=self.xs)

        if not self.termino:
            QtCore.QTimer.singleShot(1, self.graficar)

if __name__ == '__main__':

    app = QtGui.QApplication([])
    window = MainWindow()

    QtCore.QTimer.singleShot(0, window.run);
    sys.exit(app.exec_())

【问题讨论】:

    标签: python multithreading qt pyqt pyqt4


    【解决方案1】:

    问题是Qt尝试根据slot所在的线程来选择连接类型(当你调用signal.connect(slot)时)。因为你已经用partial包裹了QThread中的槽,所以槽您要连接的对象位于 MainThread(GUI 线程)中。您可以覆盖连接类型(作为connect() 的第二个参数,但这无济于事,因为partial 创建的方法将始终存在于MainThread 中,因此将连接类型设置为Qt.QueuedConnection 不会帮助。

    我能看到的唯一解决方法是设置一个中继信号,其唯一目的是有效地将不带参数的发射信号(例如来自按钮的单击信号)更改为带一个参数的信号(您的 m 参数)。这样你就不需要用partial() 包裹QThread 中的槽了。

    代码如下。我在主窗口类中创建了一个带有一个称为“中继”的参数(一个 int)的信号。按钮clicked 信号连接到主窗口类中的一个方法,该方法有一行代码发出我创建的自定义信号。您可以扩展此方法 (relay_signal()) 以获取整数作为 m(在本例中为 500)传递给 QThread,从您喜欢的任何地方!

    代码如下:

    from functools import partial
    from Queue import Queue
    import math
    import sys
    import time
    
    class Worker(QtCore.QObject):
    
        termino = pyqtSignal()
    
        def __init__(self, q=None, parent=None):
            super(Worker, self).__init__(parent) 
            self.q = q
    
        def run(self, m=30000):
            print('worker thread id: {}'.format(QtCore.QThread.currentThreadId()))
            for x in xrange(m):
                #y = math.sin(x)
                y = x**2
                #time.sleep(0.001) # Weird, plotting stops if this is not present...
                self.q.put((x,y,y))
            print('Worker finished')
    
            self.termino.emit()
    
    class MainWindow(QtGui.QWidget):
    
        relay = pyqtSignal(int)
    
        def __init__(self, parent=None):
            super(MainWindow, self).__init__(parent)
    
            self.q = Queue()
            self.termino = False       
    
            self.worker = Worker(self.q)
    
            self.workerThread = None
            self.btn = QtGui.QPushButton('Start worker')
            self.pw = pg.PlotWidget(self)
            pi = self.pw.getPlotItem()
            pi.enableAutoRange('x', True)
            pi.enableAutoRange('y', True)
            self.ge1 = pi.plot(pen='y')
            self.xs = []
            self.ys = []
    
            layout = QtGui.QVBoxLayout(self)
            layout.addWidget(self.pw)
            layout.addWidget(self.btn)
    
            self.resize(400, 400)
    
        def run(self):
            self.workerThread = QtCore.QThread()
            self.worker.termino.connect(self.setTermino)
            self.worker.moveToThread(self.workerThread)
            # moveToThread doesn't work here
            # self.btn.clicked.connect(partial(self.worker.run, 30000))
            # moveToThread will work here
            # assume def worker.run(self): instead of def worker.run(self, m=30000)
            #self.btn.clicked.connect(self.worker.run)        
            self.relay.connect(self.worker.run)
            self.btn.clicked.connect(self.relay_signal)
            self.btn.clicked.connect(self.graficar)
    
            self.workerThread.start()
            self.show()
    
        def relay_signal(self):
            self.relay.emit(500)
    
        def setTermino(self):
            self.termino = True
    
        def graficar(self):
            if not self.q.empty():
                e1,e2,ciclos = self.q.get()       
                self.xs.append(ciclos)
                self.ys.append(e1)
                self.ge1.setData(y=self.ys, x=self.xs)
    
            if not self.termino or not self.q.empty():
                QtCore.QTimer.singleShot(1, self.graficar)
    
    if __name__ == '__main__':
    
        app = QtGui.QApplication([])
        window = MainWindow()
    
        QtCore.QTimer.singleShot(0, window.run);
        sys.exit(app.exec_())
    

    如果队列中仍有数据,我还修改了graficar 方法以继续绘图(即使在线程终止后)。我认为这可能是您需要 QThread 中的 time.sleep 的原因,现在它也被删除了。

    另外关于你的 cmets 在代码中关于在哪里放置 moveToThread,它现在的位置是正确的。它应该在将 QThread 槽连接到信号的调用之前,其原因在此堆栈溢出帖子中讨论:PyQt: Connecting a signal to a slot to start a background operation

    【讨论】:

    • 非常感谢您的详细解释!
    猜你喜欢
    • 1970-01-01
    • 2019-06-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-27
    • 1970-01-01
    • 2017-09-08
    相关资源
    最近更新 更多