PyQt:将 partial() 用于插槽时,moveToThread 不起作用 [英] PyQt: moveToThread does not work when using partial() for slot

查看:61
本文介绍了PyQt:将 partial() 用于插槽时,moveToThread 不起作用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

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

使用部分运行的示例:

gui 线程 ID 140665453623104工作线程 ID:140665453623104

这是我的完整代码:

from PyQt4 import QtCore, QtGui从 PyQt4.QtCore 导入 pyqtSignal将 pyqtgraph 导入为 pg将 numpy 导入为 np从 functools 导入部分从队列导入队列导入数学导入系统导入时间类工人(QtCore.QObject):终端 = pyqtSignal()def __init__(self, q=None, parent=None):super(Worker, self).__init__(parent)self.q = qdef运行(自我,m = 30000):print('worker thread id: {}'.format(QtCore.QThread.currentThreadId()))对于 x 范围内的 x(m):#y = math.sin(x)y = x**2time.sleep(0.001) # 很奇怪,如果它不存在,绘图就会停止...self.q.put((x,y,y))print('工人完成')self.termino.emit()类 MainWindow(QtGui.QWidget):def __init__(self, parent=None):super(MainWindow, self).__init__(parent)self.q = 队列()self.termino = Falseself.worker = Worker(self.q)self.workerThread = 无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 = []布局 = QtGui.QVBoxLayout(self)layout.addWidget(self.pw)layout.addWidget(self.btn)self.resize(400, 400)定义运行(自我):self.workerThread = QtCore.QThread()self.worker.moveToThread(self.workerThread)self.worker.termino.connect(self.setTermino)# moveToThread 在这里不起作用self.btn.clicked.connect(partial(self.worker.run, 30000))# moveToThread 将在这里工作# 假设 def worker.run(self): 而不是 def worker.run(self, m=30000)# self.btn.clicked.connect(self.worker.run)self.btn.clicked.connect(self.graficar)self.workerThread.start()自我展示()def setTermino(self):self.termino = 真def graficar(自我):如果不是 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)如果不是 self.termino:QtCore.QTimer.singleShot(1, self.graficar)如果 __name__ == '__main__':app = QtGui.QApplication([])窗口 = 主窗口()QtCore.QTimer.singleShot(0, window.run);sys.exit(app.exec_())

解决方案

问题是 Qt 试图根据什么来选择连接类型(当你调用 signal.connect(slot) 时)slot 所在的线程.因为您已经用 partial 将插槽包装在 QThread 中,所以您要连接的插槽驻留在 MainThread(GUI 线程)中.您可以覆盖连接类型(作为 connect() 的第二个参数,但这无济于事,因为 partial 创建的方法将始终存在于 MainThread 中,因此通过 Qt.QueuedConnection 没有帮助.

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

代码如下.我在主窗口类中创建了一个带有一个名为relay"的参数(一个 int)的信号.按钮 clicked 信号连接到主窗口类中的一个方法,该方法有一行代码发出我创建的自定义信号.您可以扩展此方法 (relay_signal()) 以将整数作为 m(在本例中为 500)传递给 QThread,从您喜欢的任何位置开始!>

代码如下:

from functools import partial从队列导入队列导入数学导入系统导入时间类工人(QtCore.QObject):终端 = pyqtSignal()def __init__(self, q=None, parent=None):super(Worker, self).__init__(parent)self.q = qdef运行(自我,m = 30000):print('worker thread id: {}'.format(QtCore.QThread.currentThreadId()))对于 x 范围内的 x(m):#y = math.sin(x)y = x**2#time.sleep(0.001) # 奇怪,如果不存在,绘图就会停止...self.q.put((x,y,y))print('工人完成')self.termino.emit()类 MainWindow(QtGui.QWidget):中继 = pyqtSignal(int)def __init__(self, parent=None):super(MainWindow, self).__init__(parent)self.q = 队列()self.termino = Falseself.worker = Worker(self.q)self.workerThread = 无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 = []布局 = QtGui.QVBoxLayout(self)layout.addWidget(self.pw)layout.addWidget(self.btn)self.resize(400, 400)定义运行(自我):self.workerThread = QtCore.QThread()self.worker.termino.connect(self.setTermino)self.worker.moveToThread(self.workerThread)# moveToThread 在这里不起作用# self.btn.clicked.connect(partial(self.worker.run, 30000))# moveToThread 将在这里工作# 假设 def worker.run(self): 而不是 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()自我展示()def relay_signal(self):self.relay.emit(500)def setTermino(self):self.termino = 真def graficar(自我):如果不是 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)如果不是 self.termino 或不是 self.q.empty():QtCore.QTimer.singleShot(1, self.graficar)如果 __name__ == '__main__':app = QtGui.QApplication([])窗口 = 主窗口()QtCore.QTimer.singleShot(0, window.run);sys.exit(app.exec_())

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

另外,关于您在代码中关于放置 moveToThread 的位置的评论,现在的位置是正确的.它应该在将 QThread 插槽连接到信号的调用之前,并且在此堆栈溢出帖子中讨论了其原因:PyQt:将信号连接到插槽以启动后台操作

I am building a small GUI application which runs a producer (worker) and the GUI consumes the output on demand and plots it (using pyqtgraph).

Since the producer is a blocking function (takes a while to run), I (supposedly) moved it to its own thread.

When calling QThread.currentThreadId() from the producer it outputs the same number as the main GUI thread. So, the worker is executed first, and then all the plotting function calls are executed (because they are being queued on the same thread's event queue). How can I fix this?

Example run with partial:

gui thread id 140665453623104
worker thread id: 140665453623104

Here is my full code:

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

解决方案

The problem is that Qt attempts to choose the connection type (when you call signal.connect(slot)) based on the what thread the slot exists in. Because you have wrapped the slot in the QThread with partial, the slot you are connecting to resides in the MainThread (the GUI thread). You can override the connection type (as the second argument to connect() but that doesn't help because the method created by partial will always exist in the MainThread, and so setting the connection type to by Qt.QueuedConnection doesn't help.

The only way around this that I can see is to set up a relay signal, the sole purpose of which is to effectively change an emitted signal with no arguments (eg the clicked signal from a button) to a signal with one argument (your m parameter). This way you don't need to wrap the slot in the QThread with partial().

The code is below. I've created a signal with one argument (an int) called 'relay' in the main windows class. The button clicked signal is connected to a method within the main window class, and this method has a line of code which emits the custom signal I created. You can extend this method (relay_signal()) to get the integer to pass to the QThread as m (500 in this case), from where ever you like!

So here is the code:

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

I also modified the graficar method to continue plotting (even after the thread is terminated) if there is still data in the queue. I think this might be why you needed the time.sleep in the QThread, which is now also removed.

Also regarding your comments in the code on where to place moveToThread, where it is now is correct. It should be before the call that connects the QThread slot to a signal, and the reason for this is discussed in this stack-overflow post: PyQt: Connecting a signal to a slot to start a background operation

这篇关于PyQt:将 partial() 用于插槽时,moveToThread 不起作用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆