PyQt5: How to 'communicate' between QThread and some sub-class? - python

To this question I am referring to the answer from #eyllanesc in PyQt5: How to scroll text in QTextEdit automatically (animational effect)?
There #eyllanesc shows how to make the text auto scrolls smoothly using verticalScrollBar(). It works great.
For this question, I have added some extra lines, to use QThread to get the text.
What I want to achieve here: to let the QThread class 'communicate' with the AnimationTextEdit class, so that the scrolling time can be determined by the text-length. So that the programm stops, when the scrolling process ends.
I must say it is very very tricky task for me. I would like to show the programm flow first, as I imagined.
UPDATE: My code is as follows. It works, but...
Problem with the code: when the text stops to scroll, the time.sleep() still works. The App wait there till time.sleep() stops.
What I want to get: When the text stops to scroll, the time.sleep() runs to its end value.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys
import time
import sqlite3
class AnimationTextEdit(QTextEdit):
# signal_HowLongIsTheText = pyqtSignal(int) # signal to tell the QThread, how long the text is
def __init__(self, *args, **kwargs):
QTextEdit.__init__(self, *args, **kwargs)
self.animation = QVariantAnimation(self)
self.animation.valueChanged.connect(self.moveToLine)
# def sent_Info_to_Thread(self):
# self.obj_Thread = Worker()
# self.signal_HowLongIsTheText.connect(self.obj_Thread.getText_HowLongIsIt)
# self.signal_HowLongIsTheText.emit(self.textLength)
# self.signal_HowLongIsTheText.disconnect(self.obj_Thread.getText_HowLongIsIt)
#pyqtSlot()
def startAnimation(self):
self.animation.stop()
self.animation.setStartValue(0)
self.textLength = self.verticalScrollBar().maximum()
# self.sent_Info_to_Thread()
self.animation.setEndValue(self.textLength)
self.animation.setDuration(self.animation.endValue()*4)
self.animation.start()
#pyqtSlot(QVariant)
def moveToLine(self, i):
self.verticalScrollBar().setValue(i)
class Worker(QObject):
finished = pyqtSignal()
textSignal = pyqtSignal(str)
# #pyqtSlot(int)
# def getText_HowLongIsIt(self, textLength):
# self.textLength = textLength
#pyqtSlot()
def getText(self):
longText = "\n".join(["{}: long text - auto scrolling ".format(i) for i in range(100)])
self.textSignal.emit(longText)
time.sleep(10)
# time.sleep(int(self.textLength / 100))
# My question is about the above line: time.sleep(self.textLength)
# Instead of giving a fixed sleep time value here,
# I want let the Worker Class know,
# how long it will take to scroll all the text to the end.
self.finished.emit()
class MyApp(QWidget):
def __init__(self):
super(MyApp, self).__init__()
self.setFixedSize(600, 400)
self.initUI()
self.startThread()
def initUI(self):
self.txt = AnimationTextEdit(self)
self.btn = QPushButton("Start", self)
self.layout = QHBoxLayout(self)
self.layout.addWidget(self.txt)
self.layout.addWidget(self.btn)
self.btn.clicked.connect(self.txt.startAnimation)
def startThread(self):
self.obj = Worker()
self.thread = QThread()
self.obj.textSignal.connect(self.textUpdate)
self.obj.moveToThread(self.thread)
self.obj.finished.connect(self.thread.quit)
self.thread.started.connect(self.obj.getText)
self.thread.finished.connect(app.exit)
self.thread.start()
def textUpdate(self, longText):
self.txt.append(longText)
self.txt.moveToLine(0)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MyApp()
window.show()
sys.exit(app.exec_())
Thanks for the help and hint. What have I done wrong?

Although in the animation the duration is established it is necessary to understand that this is not exact, this could vary for several reasons, so calculating using the sleep to wait for it to end in a certain time and just closing the application may fail.
If your main objective is that when the animation is finished the program execution is finished then you must use the finished QVariantAnimation signal to finish the execution of the thread, this signal is emited when it finishes executing.
class AnimationTextEdit(QTextEdit):
def __init__(self, *args, **kwargs):
QTextEdit.__init__(self, *args, **kwargs)
self.animation = QVariantAnimation(self)
self.animation.valueChanged.connect(self.moveToLine)
#pyqtSlot()
def startAnimation(self):
self.animation.stop()
self.animation.setStartValue(0)
self.textLength = self.verticalScrollBar().maximum()
self.animation.setEndValue(self.textLength)
self.animation.setDuration(self.animation.endValue()*4)
self.animation.start()
#pyqtSlot(QVariant)
def moveToLine(self, i):
self.verticalScrollBar().setValue(i)
class Worker(QObject):
textSignal = pyqtSignal(str)
#pyqtSlot()
def getText(self):
longText = "\n".join(["{}: long text - auto scrolling ".format(i) for i in range(100)])
self.textSignal.emit(longText)
class MyApp(QWidget):
def __init__(self):
super(MyApp, self).__init__()
self.setFixedSize(600, 400)
self.initUI()
self.startThread()
def initUI(self):
self.txt = AnimationTextEdit(self)
self.btn = QPushButton("Start", self)
self.layout = QHBoxLayout(self)
self.layout.addWidget(self.txt)
self.layout.addWidget(self.btn)
self.btn.clicked.connect(self.txt.startAnimation)
def startThread(self):
self.obj = Worker()
self.thread = QThread()
self.obj.textSignal.connect(self.textUpdate)
self.obj.moveToThread(self.thread)
self.txt.animation.finished.connect(self.thread.quit)
self.thread.started.connect(self.obj.getText)
self.thread.finished.connect(app.exit)
self.thread.start()
def textUpdate(self, longText):
self.txt.append(longText)
self.txt.moveToLine(0)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MyApp()
window.show()
sys.exit(app.exec_())

Related

How to use Threading with variables inside a function? PyQt5

I have a big function which freezes my PyQt5 program, I tried to use a different thread for it (I use QThread for that). Problem is my function needs some variables to work properly.
How to make this works? I show what I did.
Original code:
class AnalysisWindow(QtWidgets.QMainWindow):
def __init__(self, firstWindow):
super(AnalysisWindow, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.ui.pushButton.clicked.connect(self.letsgo)
def letsgo(self):
#here some code , not big
#and at some point i have a heavy one which make it freeze until it's over:
self.x1, self.x2, self.y1,self.y2, self.z, = self.analyze(self.i1, self.i2, self.i3)
def analyze(self,i1,i2,i3):
#big function
return(x1,x2,y1,y2,z)
what I tried :
from PyQt5.QtCore import Qt, QThread, pyqtSignal
class AnalysisWindow(QtWidgets.QMainWindow):
class MyThread(QThread):
_signal =pyqtSignal()
def __init__(self):
super().__init__()
def run(self,i1,i2,i3): # here I obviously can't put variables
#I copied here my analyze function
return(x1,x2,y1,y2,z)
self._signal.emit()
def __init__(self, firstWindow):
super(AnalysisWindow, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.ui.pushButton.clicked.connect(self.letsgo)
def letsgo(self):
self.thread = MyThread()
self.thread.start()
#here I dont see how to send the variables self.i1, self.i2, self.i3 and how to get the result: x1,x2,y1,y2,z
I created the thread class inside the QMainWindow class because i need to pass some variables (self.i1, self.i2, self.i3) from QMainWindow to the function which will use the new thread. Maybe that's bad, but it doesn't work in any way. Thanks everyone.
Here is a minimal working example that you can adapt it to your code.
Few things to note:
You should not inherit from QThread. Instead, you should create a worker and move it into your thread.
In worker, instead of trying to return a result, emit the signal that holds the result and process that signal in your application.
Similarly, instead of trying to call your worker normally, communicate it through its slots via QtCore.QMetaObject.invokeMethod. Once your thread is started, you can call this method as much as you want.
Refer to this answer for more
import sys
import random
from PyQt5.QtCore import QThread, pyqtSignal, QObject, pyqtSlot, Qt
from PyQt5 import QtWidgets
from PyQt5 import QtCore
class Analyzer(QObject):
analyze_completed = pyqtSignal(bool)
analyze_result = pyqtSignal(list, int)
#pyqtSlot(str, list)
def analyze(self, foo, analyze_args):
print(foo, analyze_args)
self.analyze_completed.emit(False)
# do your heavy calculations
for i in range(10000000):
x = i ** 0.5
result = sum(analyze_args)
self.analyze_result.emit(analyze_args, result)
self.analyze_completed.emit(True)
class AnalysisWindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.label = QtWidgets.QLabel("")
self.i = 0
self.label_i = QtWidgets.QLabel("Value of i: {}".format(self.i))
self.increment_button = QtWidgets.QPushButton("increment i")
self.pushbutton = QtWidgets.QPushButton("Analyze")
super(AnalysisWindow, self).__init__()
self.analyze_args = []
self.analyzer = Analyzer()
self.thread = QThread()
self.analyzer.analyze_result.connect(self.on_analyze_result_ready)
self.analyzer.analyze_completed.connect(self.on_analyze_completed)
self.analyzer.moveToThread(self.thread)
self.thread.start()
self.init_UI()
def init_UI(self):
grid = QtWidgets.QGridLayout()
grid.addWidget(self.label, 0, 0)
grid.addWidget(self.pushbutton)
grid.addWidget(self.label_i)
grid.addWidget(self.increment_button)
self.increment_button.clicked.connect(self.increment_i)
self.pushbutton.clicked.connect(self.start_analyze)
self.setLayout(grid)
self.move(300, 150)
self.setMinimumSize(300, 100)
self.setWindowTitle('Thread Test')
self.show()
def start_analyze(self):
self.analyze_args.clear()
self.analyze_args.extend(random.choices(range(100), k=5))
QtCore.QMetaObject.invokeMethod(self.analyzer, 'analyze', Qt.QueuedConnection,
QtCore.Q_ARG(str, "Hello World!"),
QtCore.Q_ARG(list, self.analyze_args))
def increment_i(self):
self.i += 1
self.label_i.setText("Value of i: {}".format(self.i))
def on_analyze_result_ready(self, args, result):
t = "+".join(str(i) for i in args)
self.label.setText(f"{t} = {result}")
def on_analyze_completed(self, completed):
if completed:
self.label.setStyleSheet('color: blue')
else:
self.label.setText(
"Analyzing... {}".format(", ".join(str(i) for i in self.analyze_args)))
self.label.setStyleSheet('color: yellow')
app = QtWidgets.QApplication(sys.argv)
widget = AnalysisWindow()
sys.exit(app.exec_())
Hope this helps!

How to update UI with output from QProcess loop without the UI freezing?

I am wanting to have a list of commands being processed through a QProcess and have its output be appended to a textfield I have. I've found a these two pages that seems to do each of the things i need (updating the UI, and not freezing the UI via QThread):
Printing QProcess Stdout only if it contains a Substring
https://nikolak.com/pyqt-threading-tutorial/
So i tried to combine these two....
import sys
from PySide import QtGui, QtCore
class commandThread(QtCore.QThread):
def __init__(self):
QtCore.QThread.__init__(self)
self.cmdList = None
self.process = QtCore.QProcess()
def __del__(self):
self.wait()
def command(self):
# print 'something'
self.process.start('ping', ['127.0.0.1'])
processStdout = str(self.process.readAll())
return processStdout
def run(self):
for i in range(3):
messages = self.command()
self.emit(QtCore.SIGNAL('dataReady(QString)'), messages)
# self.sleep(1)
class MainWindow(QtGui.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.initUI()
def dataReady(self,outputMessage):
cursorOutput = self.output.textCursor()
cursorSummary = self.summary.textCursor()
cursorOutput.movePosition(cursorOutput.End)
cursorSummary.movePosition(cursorSummary.End)
# Update self.output
cursorOutput.insertText(outputMessage)
# Update self.summary
for line in outputMessage.split("\n"):
if 'TTL' in line:
cursorSummary.insertText(line)
self.output.ensureCursorVisible()
self.summary.ensureCursorVisible()
def initUI(self):
layout = QtGui.QHBoxLayout()
self.runBtn = QtGui.QPushButton('Run')
self.runBtn.clicked.connect(self.callThread)
self.output = QtGui.QTextEdit()
self.summary = QtGui.QTextEdit()
layout.addWidget(self.runBtn)
layout.addWidget(self.output)
layout.addWidget(self.summary)
centralWidget = QtGui.QWidget()
centralWidget.setLayout(layout)
self.setCentralWidget(centralWidget)
# self.process.started.connect(lambda: self.runBtn.setEnabled(False))
# self.process.finished.connect(lambda: self.runBtn.setEnabled(True))
def callThread(self):
self.runBtn.setEnabled(False)
self.get_thread = commandThread()
# print 'this this running?'
self.connect(self.get_thread, QtCore.SIGNAL("dataReady(QString)"), self.dataReady)
self.connect(self.get_thread, QtCore.SIGNAL("finished()"), self.done)
def done(self):
self.runBtn.setEnabled(True)
def main():
app = QtGui.QApplication(sys.argv)
mainWindow = MainWindow()
mainWindow.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
The problem is that once I click the "Run" button the textfield on the right doesn't seem to populate, and i am no longer getting any errors so I am not sure what is happening.
I tried referring to this page as well but I think i am already emulating what it is describing...?
https://www.qtcentre.org/threads/46056-QProcess-in-a-loop-works-but
Ultimately what I want to build is for a main window to submit a series of commands via subprocess/QProcess, and open up a little log window that constantly updates it on the progress via displaying the console output. Similar to what you kind of see in like Installer packages...
I feel like i am so close to an answer, yet so far away. Is anyone able to chime in on this?
EDIT: so to answer eyllanesc's question, the list of commands has to be run one after the previous one has completed, as the command i plan to use will be very CPU intensive, and i cannot have more than one process of it running. also the time of each command completing will completely vary so I can't just have a arbitrary hold like with time.sleep() as some may complete quicker/slower than others. so ideally figuring out when the process has finished should kickstart another command (which is why i have a for loop in this example to represent that).
i also decided to use threads because apparently that was one way of preventing the UI to freeze while the process was running,so i assumed i needed to utilize this to have a sort of live feed/update in the text field.
the other thing is in the UI i would ideally in addition to updating a text field with console logs, i would want it to have some sort of label that gets updated that says something like "2 of 10 jobs completed". so something like this:
It would be nice too when before a new command is being processed a custom message can be appended to the text field indicating what command is being run...
UPDATE: apologies for taking so long to post an update on this, but based on eyllanesc's answer, I was able to figure out how to make this open a separate window and run the "ping" commands. here is the example code I have made to achieve my results in my main application:
from PySide import QtCore, QtGui
class Task:
def __init__(self, program, args=None):
self._program = program
self._args = args or []
#property
def program(self):
return self._program
#property
def args(self):
return self._args
class SequentialManager(QtCore.QObject):
started = QtCore.Signal()
finished = QtCore.Signal()
progressChanged = QtCore.Signal(int)
dataChanged = QtCore.Signal(str)
#^ this is how we can send a signal and can declare what type
# of information we want to pass with this signal
def __init__(self, parent=None):
super(SequentialManager, self).__init__(parent)
self._progress = 0
self._tasks = []
self._process = QtCore.QProcess(self)
self._process.setProcessChannelMode(QtCore.QProcess.MergedChannels)
self._process.finished.connect(self._on_finished)
self._process.readyReadStandardOutput.connect(self._on_readyReadStandardOutput)
def execute(self, tasks):
self._tasks = iter(tasks)
#this 'iter()' method creates an iterator object
self.started.emit()
self._progress = 0
self.progressChanged.emit(self._progress)
self._execute_next()
def _execute_next(self):
try:
task = next(self._tasks)
except StopIteration:
return False
else:
self._process.start(task.program, task.args)
return True
# QtCore.Slot()
#^ we don't need this line here
def _on_finished(self):
self._process_task()
if not self._execute_next():
self.finished.emit()
# #QtCore.Slot()
def _on_readyReadStandardOutput(self):
output = self._process.readAllStandardOutput()
result = output.data().decode()
self.dataChanged.emit(result)
def _process_task(self):
self._progress += 1
self.progressChanged.emit(self._progress)
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.outputWindow = outputLog(parentWindow=self)
self._button = QtGui.QPushButton("Start")
central_widget = QtGui.QWidget()
lay = QtGui.QVBoxLayout(central_widget)
lay.addWidget(self._button)
self.setCentralWidget(central_widget)
self._button.clicked.connect(self.showOutput)
def showOutput(self):
self.outputWindow.show()
self.outputWindow.startProcess()
#property
def startButton(self):
return self._button
class outputLog(QtGui.QWidget):
def __init__(self, parent=None, parentWindow=None):
QtGui.QWidget.__init__(self,parent)
self.parentWindow = parentWindow
self.setWindowTitle('Render Log')
self.setMinimumSize(225, 150)
self.renderLogWidget = QtGui.QWidget()
lay = QtGui.QVBoxLayout(self.renderLogWidget)
self._textedit = QtGui.QTextEdit(readOnly=True)
self._progressbar = QtGui.QProgressBar()
self._button = QtGui.QPushButton("Close")
self._button.clicked.connect(self.windowClose)
lay.addWidget(self._textedit)
lay.addWidget(self._progressbar)
lay.addWidget(self._button)
self._manager = SequentialManager(self)
self.setLayout(lay)
def startProcess(self):
self._manager.progressChanged.connect(self._progressbar.setValue)
self._manager.dataChanged.connect(self.on_dataChanged)
self._manager.started.connect(self.on_started)
self._manager.finished.connect(self.on_finished)
self._progressbar.setFormat("%v/%m")
self._progressbar.setValue(0)
tasks = [
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
]
self._progressbar.setMaximum(len(tasks))
self._manager.execute(tasks)
#QtCore.Slot()
def on_started(self):
self._button.setEnabled(False)
self.parentWindow.startButton.setEnabled(False)
#QtCore.Slot()
def on_finished(self):
self._button.setEnabled(True)
#QtCore.Slot(str)
def on_dataChanged(self, message):
if message:
cursor = self._textedit.textCursor()
cursor.movePosition(QtGui.QTextCursor.End)
cursor.insertText(message)
self._textedit.ensureCursorVisible()
def windowClose(self):
self.parentWindow.startButton.setEnabled(True)
self.close()
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
i still don't really understand the use of the QtCore.Slot() decorators as when I commented them out it didn't really seem to change the result. But i kept them in just to be safe.
It is not necessary to use threads in this case since QProcess is executed using the event loop. The procedure is to launch a task, wait for the finishes signal, get the result, send the result, and execute the next task until all the tasks are finished. The key to the solution is to use the signals and distribute the tasks with an iterator.
Considering the above, the solution is:
from PySide import QtCore, QtGui
class Task:
def __init__(self, program, args=None):
self._program = program
self._args = args or []
#property
def program(self):
return self._program
#property
def args(self):
return self._args
class SequentialManager(QtCore.QObject):
started = QtCore.Signal()
finished = QtCore.Signal()
progressChanged = QtCore.Signal(int)
dataChanged = QtCore.Signal(str)
def __init__(self, parent=None):
super(SequentialManager, self).__init__(parent)
self._progress = 0
self._tasks = []
self._process = QtCore.QProcess(self)
self._process.setProcessChannelMode(QtCore.QProcess.MergedChannels)
self._process.finished.connect(self._on_finished)
self._process.readyReadStandardOutput.connect(self._on_readyReadStandardOutput)
def execute(self, tasks):
self._tasks = iter(tasks)
self.started.emit()
self._progress = 0
self.progressChanged.emit(self._progress)
self._execute_next()
def _execute_next(self):
try:
task = next(self._tasks)
except StopIteration:
return False
else:
self._process.start(task.program, task.args)
return True
QtCore.Slot()
def _on_finished(self):
self._process_task()
if not self._execute_next():
self.finished.emit()
#QtCore.Slot()
def _on_readyReadStandardOutput(self):
output = self._process.readAllStandardOutput()
result = output.data().decode()
self.dataChanged.emit(result)
def _process_task(self):
self._progress += 1
self.progressChanged.emit(self._progress)
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self._button = QtGui.QPushButton("Start")
self._textedit = QtGui.QTextEdit(readOnly=True)
self._progressbar = QtGui.QProgressBar()
central_widget = QtGui.QWidget()
lay = QtGui.QVBoxLayout(central_widget)
lay.addWidget(self._button)
lay.addWidget(self._textedit)
lay.addWidget(self._progressbar)
self.setCentralWidget(central_widget)
self._manager = SequentialManager(self)
self._manager.progressChanged.connect(self._progressbar.setValue)
self._manager.dataChanged.connect(self.on_dataChanged)
self._manager.started.connect(self.on_started)
self._manager.finished.connect(self.on_finished)
self._button.clicked.connect(self.on_clicked)
#QtCore.Slot()
def on_clicked(self):
self._progressbar.setFormat("%v/%m")
self._progressbar.setValue(0)
tasks = [
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
]
self._progressbar.setMaximum(len(tasks))
self._manager.execute(tasks)
#QtCore.Slot()
def on_started(self):
self._button.setEnabled(False)
#QtCore.Slot()
def on_finished(self):
self._button.setEnabled(True)
#QtCore.Slot(str)
def on_dataChanged(self, message):
if message:
cursor = self._textedit.textCursor()
cursor.movePosition(QtGui.QTextCursor.End)
cursor.insertText(message)
self._textedit.ensureCursorVisible()
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())

QThread does not update view with events

On menu I can trigger:
def on_git_update(self):
update_widget = UpdateView()
self.gui.setCentralWidget(update_widget)
updateGit = UpdateGit()
updateGit.progress.connect(update_widget.on_progress)
updateGit.start()
then I have:
class UpdateView(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
vbox = QVBoxLayout()
self.pbar = QProgressBar()
vbox.addWidget(self.pbar)
vbox.addStretch(1)
self.setLayout(vbox)
def on_progress(self, value):
self.pbar.setValue(int(value * 100))
class UpdateGit(QThread):
progress = pyqtSignal(float)
def __del__(self):
self.wait()
def run(self):
for i in range(10):
self.progress.emit(i / 10)
sleep(.5)
The app freezes during the processing, afaik it should work as it is in a thread using signals.
Also, it works as expected with the app updating every step when I run it in debug mode via pycharm.
How is my thread set up incorrectly?
A variable created in a function only exists until the function exists, and this is what happens with updateGit, in the case of update_widget when it is set as centralwidget it has a greater scope since Qt handles it. The solution is to extend the scope of the thread by making it a member of the class.
def on_git_update(self):
update_widget = UpdateView()
self.gui.setCentralWidget(update_widget)
self.updateGit = UpdateGit()
self.updateGit.progress.connect(update_widget.on_progress)
self.updateGit.start()

Multithreading with PySide : is my structure viable?

I'm running into a very strange error while using QThread in PySide. As I run my code, when I click on the button that is supposed to launch the thread and send Signal, I get :
AttributeError: 'builtin_function_or_method' object has no attribute 'displayText'
at line :
self.displayMessage.connect(self.window.displayText)
NB : displayText is a method that I defined in my MainWindow(QWidget) class, while displayMessage is the signal to be emitted.
This error seems, according to the Internet, to occur in various situations, and I couldn't find one that suits my case yet. Therefore I have 2 questions :
Have you guys ever met this error before in a similar situation, and could you (if yes) give me some tips ?
Am I doing it right ? I can't post my code directly down here, so I created a short verifiable example in which I used the same process. Unfortunately, it seems to work perfectly. Please tell me if at least my construction is correct.
Thank you very much.
EDIT : I eventually figured out that I have forgotten a self somewhere in my big code, but the actual error was hidden to me behind this one that I didn't know. I'm still interested in whether or not this code is reliable, and am fully open to suggestions of improvement.
Without thread
When you click "GO !" and try to move the window, you can see that it freezes for a second.
#!/usr/bin/env python
# -*- encoding : utf-8 -*-
import sys
import time
from PySide.QtCore import *
from PySide.QtGui import *
class MainWindow(QWidget):
def __init__(self, qt_app):
QWidget.__init__(self)
worker = Worker(self)
self.qt_app = qt_app
self.setGeometry(100, 100, 220, 40)
self.infoPanel = QTextEdit(self)
self.infoPanel.setReadOnly(True)
self.goButton = QPushButton('GO !', self)
self.goButton.clicked.connect(lambda: worker.displayOnWindow())
self.layout = QVBoxLayout()
self.layout.addWidget(self.infoPanel)
self.layout.addWidget(self.goButton)
self.setLayout(self.layout)
def launch(self):
self.show()
self.qt_app.exec_()
class Worker():
def __init__(self, window):
self.counter = 0
self.window = window
def displayOnWindow(self):
time.sleep(1)
self.window.infoPanel.append(str(self.counter))
self.counter += 1
if __name__=='__main__':
qt_app = QApplication(sys.argv)
mw = MainWindow(qt_app)
mw.launch()
With thread
Now you can move the window without trouble, since the sleeping thread is not the one that displays the window. I've written a sub-class for my thread because in the original code there are some functions that will be called by several buttons.
#!/usr/bin/env python
# -*- encoding : utf-8 -*-
import sys
import time
from PySide.QtCore import *
from PySide.QtGui import *
class MainWindow(QWidget):
def __init__(self, qt_app):
QWidget.__init__(self)
worker = SubWorker(self)
self.qt_app = qt_app
self.setGeometry(100, 100, 220, 40)
self.infoPanel = QTextEdit(self)
self.infoPanel.setReadOnly(True)
self.goButton = QPushButton('GO !', self)
self.goButton.clicked.connect(lambda: worker.start())
self.layout = QVBoxLayout()
self.layout.addWidget(self.infoPanel)
self.layout.addWidget(self.goButton)
self.setLayout(self.layout)
def launch(self):
self.show()
self.qt_app.exec_()
#Slot(int)
def displayOnWindow(self, i):
self.infoPanel.append(str(i))
class Worker(QThread):
displayMessage = Signal(int)
def __init__(self, window):
QThread.__init__(self)
self.counter = 0
self.window = window
self.displayMessage.connect(self.window.displayOnWindow)
def run(self, *args, **kwargs):
return QThread.run(self, *args, **kwargs)
class SubWorker(Worker):
def __init__(self, window):
Worker.__init__(self, window)
def run(self):
time.sleep(1)
self.displayMessage.emit(self.counter)
self.counter += 1
if __name__=='__main__':
qt_app = QApplication(sys.argv)
mw = MainWindow(qt_app)
mw.launch()

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

Categories