Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
188 views
in Technique[技术] by (71.8m points)

python - PyQt: moveToThread does not work when using partial() for slot

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_())
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...