Terminating QThread gracefully on QDialog reject() - python

I have a QDialog which creates a QThread to do some work while keeping the UI responsive, based on the structure given here: How To Really, Truly Use QThreads; The Full Explanation. However, if reject() is called (due to the user pressing cancel or closing the dialog) while the thread is still running I get an error:
QThread: Destroyed while thread is still running
What I'd like to happen is for the loop in the worker to break early, then do some cleanup in the background (e.g. clear some queues, emit a signal). I've managed to do this with my own "cancel" function, but how do I get it to play nicely with reject() (and all the many ways it could be called)? I don't want the dialog to block waiting for the cleanup - it should just keep running in the background, then exit gracefully.
See sample code below which exhibits the problem. Any help would be greatly appreciated.
#!/usr/bin/env python
from PyQt4 import QtCore, QtGui
import sys
import time
class Worker(QtCore.QObject):
def __init__(self):
QtCore.QObject.__init__(self)
def process(self):
# dummy worker process
for n in range(0, 10):
print 'process {}'.format(n)
time.sleep(0.5)
self.finished.emit()
finished = QtCore.pyqtSignal()
class Dialog(QtGui.QDialog):
def __init__(self):
QtGui.QDialog.__init__(self)
self.init_ui()
def init_ui(self):
self.layout = QtGui.QVBoxLayout(self)
self.btn_run = QtGui.QPushButton('Run', self)
self.layout.addWidget(self.btn_run)
self.btn_cancel = QtGui.QPushButton('Cancel', self)
self.layout.addWidget(self.btn_cancel)
QtCore.QObject.connect(self.btn_run, QtCore.SIGNAL('clicked()'), self.run)
QtCore.QObject.connect(self.btn_cancel, QtCore.SIGNAL('clicked()'), self.reject)
self.show()
self.raise_()
def run(self):
# start the worker thread
self.thread = QtCore.QThread()
self.worker = Worker()
self.worker.moveToThread(self.thread)
QtCore.QObject.connect(self.thread, QtCore.SIGNAL('started()'), self.worker.process)
QtCore.QObject.connect(self.worker, QtCore.SIGNAL('finished()'), self.thread.quit)
QtCore.QObject.connect(self.worker, QtCore.SIGNAL('finished()'), self.worker.deleteLater)
QtCore.QObject.connect(self.thread, QtCore.SIGNAL('finished()'), self.thread.deleteLater)
self.thread.start()
def main():
app = QtGui.QApplication(sys.argv)
dlg = Dialog()
ret = dlg.exec_()
if __name__ == '__main__':
main()

Your problem is: self.thread is freed by Python after the dialog is closed or the cancel button is pressed, while Qt thread is still running.
To avoid such situation, you can designate a parent to that thread. For example,
def run(self):
# start the worker thread
self.thread = QtCore.QThread(self)
self.worker = Worker()
self.worker.moveToThread(self.thread)
QtCore.QObject.connect(self.thread, QtCore.SIGNAL('started()'), self.worker.process)
QtCore.QObject.connect(self.worker, QtCore.SIGNAL('finished()'), self.thread.quit)
QtCore.QObject.connect(self.worker, QtCore.SIGNAL('finished()'), self.worker.deleteLater)
QtCore.QObject.connect(self.thread, QtCore.SIGNAL('finished()'), self.thread.deleteLater)
self.thread.start()
Then it will be owned by Qt instead of PyQt and hence won't be collected by GC before it is terminated by Qt gracefully.
Actually, this method just lets Qt not complain and doesn't solve the problem completely.
To terminate a thread gracefully, the common approach is using a flag to inform the worker function to stop.
For example:
class Worker(QtCore.QObject):
def __init__(self):
QtCore.QObject.__init__(self)
def process(self):
# dummy worker process
self.flag = False
for n in range(0, 10):
if self.flag:
print 'stop'
break
print 'process {}'.format(n)
time.sleep(0.5)
self.finished.emit()
finished = QtCore.pyqtSignal()
class Dialog(QtGui.QDialog):
def __init__(self, parent=None):
QtGui.QDialog.__init__(self, parent)
self.init_ui()
def init_ui(self):
self.layout = QtGui.QVBoxLayout(self)
self.btn_run = QtGui.QPushButton('Run', self)
self.layout.addWidget(self.btn_run)
self.btn_cancel = QtGui.QPushButton('Cancel', self)
self.layout.addWidget(self.btn_cancel)
QtCore.QObject.connect(self.btn_run, QtCore.SIGNAL('clicked()'), self.run)
QtCore.QObject.connect(self.btn_cancel, QtCore.SIGNAL('clicked()'), self.reject)
QtCore.QObject.connect(self, QtCore.SIGNAL('rejected()'), self.stop_worker)
self.show()
self.raise_()
def stop_worker(self):
print 'stop'
self.worker.flag = True
def run(self):
# start the worker thread
self.thread = QtCore.QThread(self)
self.worker = Worker()
self.worker.moveToThread(self.thread)
QtCore.QObject.connect(self.thread, QtCore.SIGNAL('started()'), self.worker.process)
QtCore.QObject.connect(self.worker, QtCore.SIGNAL('finished()'), self.thread.quit)
QtCore.QObject.connect(self.worker, QtCore.SIGNAL('finished()'), self.worker.deleteLater)
QtCore.QObject.connect(self.thread, QtCore.SIGNAL('finished()'), self.thread.deleteLater)
self.thread.start()

Related

Passing a function to a Worker (QObject) class in Python GUI application to prevent freezing/blocking

I am trying to find a way to successfully pass a function to a Worker class in Python using PyQT5. Instead of using the pre-defined run function (or Long-running task) in the sample Worker class code, I would like to be able to pass a custom function to the worker class. Below I've pasted the sample code I'm working with, followed by an adjustment I've tried.
from time import sleep
from PyQt5.QtCore import QObject, QThread, pyqtSignal,Qt
from PyQt5.QtWidgets import QApplication,QMainWindow,QLabel,QPushButton,QVBoxLayout,QWidget
import sys
# Step 1: Create a worker class
class Worker(QObject):
finished = pyqtSignal()
progress = pyqtSignal(int)
def run(self):
"""Long-running task."""
for i in range(5):
sleep(1)
self.progress.emit(i + 1)
self.finished.emit()
class Window(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.clicksCount = 0
self.setupUi()
def setupUi(self):
self.setWindowTitle("Freezing GUI")
self.resize(300, 150)
self.centralWidget = QWidget()
self.setCentralWidget(self.centralWidget)
# Create and connect widgets
self.clicksLabel = QLabel("Counting: 0 clicks", self)
self.clicksLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
self.stepLabel = QLabel("Long-Running Step: 0")
self.stepLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
self.countBtn = QPushButton("Click me!", self)
self.countBtn.clicked.connect(self.countClicks)
self.longRunningBtn = QPushButton("Long-Running Task!", self)
self.longRunningBtn.clicked.connect(self.runLongTask)
# Set the layout
layout = QVBoxLayout()
layout.addWidget(self.clicksLabel)
layout.addWidget(self.countBtn)
layout.addStretch()
layout.addWidget(self.stepLabel)
layout.addWidget(self.longRunningBtn)
self.centralWidget.setLayout(layout)
def countClicks(self):
self.clicksCount += 1
self.clicksLabel.setText(f"Counting: {self.clicksCount} clicks")
def reportProgress(self, n):
self.stepLabel.setText(f"Long-Running Step: {n}")
def runLongTask(self):
# Step 2: Create a QThread object
self.thread = QThread()
# Step 3: Create a worker object
self.worker = Worker()
# Step 4: Move worker to the thread
self.worker.moveToThread(self.thread)
# Step 5: Connect signals and slots
self.thread.started.connect(self.worker.run)
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.worker.progress.connect(self.reportProgress)
# Step 6: Start the thread
self.thread.start()
# Final resets
self.longRunningBtn.setEnabled(False)
self.thread.finished.connect(
lambda: self.longRunningBtn.setEnabled(True)
)
self.thread.finished.connect(
lambda: self.stepLabel.setText("Long-Running Step: 0")
)
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec())
class Worker(QObject):
finished = pyqtSignal(str)
def __init__(self, *init_args, **init_kwargs):
QObject.__init__(self, *init_args, **init_kwargs)
self._return = None
def run(self):
"""Long-running task."""
self._return = self._target(*self._args, **self._kwargs)
self.finished.emit(self._return)
As one of solutions, you can pass a function and an argument to the Worker's __init__ method, like:
class Worker(QObject):
finished = pyqtSignal()
progress = pyqtSignal(int)
result = pyqtSignal('QVariant')
def __init__(self, function, args):
super().__init__()
self.function = function
self.args = args
def run(self):
res = self.function(self.args)
self.result.emit(res)
self.finished.emit()
So you can pass a method and an argument when creating a worker and connect the method to handle the result:
self.thread = QThread()
self.worker = Worker(Proxy.GetInfo, code)
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.run)
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.thread.deleteLater)
self.worker.result.connect(self.receiveResult)
self.thread.finished.connect(self.thread.deleteLater)
self.thread.start()

Terminate a QThread after QTimer Singleshot

I've got an issue. I'm running a PyQt5 form that runs a worker called Task() (I won't get into the details of its code, but it basically just returns a value to a QLabel) in a QThread like so:
class Menu(QMainWindow):
def __init__(self, workers):
super().__init__()
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
lay = QVBoxLayout(self.central_widget)
self.setFixedSize(500, 350)
Pic = QLabel(self)
self.Total = QLabel("Total: <font color='orange'>%s</font>" % (to_check()), alignment=QtCore.Qt.AlignHCenter)
lay.addWidget(self.Total)
thread = QtCore.QThread(self)
thread.start()
self.worker = Task()
self.worker.moveToThread(thread)
self.worker.totalChanged.connect(self.updateTotal)
QtCore.QTimer.singleShot(0, self.worker.dostuff)
thread.finished.connect(self.terminate)
#QtCore.pyqtSlot(int)
def updateTotal(self, total):
self.Total.setText("Total: <font color='orange'>%s</font>" % (total))
def terminate(self):
print("FINISHED")
self.worker.quit()
self.worker.wait()
self.close()
What I'd like is for the program to call the terminate slot (and basically terminate the thread and function) once the Task().dostuff() function is finished - but I can't seem to make it work.
I'm not sure how I can return to the main function through QTimer.singleshot.
A timer shouldn't be needed. Use the thread's started signal to start the worker, and add a finished signal to the worker class to quit the thread:
class Task(QtCore.QObject):
totalChanged = QtCore.pyqtSignal(int)
finished = QtCore.pyqtSignal()
def dostuff(self):
# do stuff ...
self.finished.emit()
class Menu(QtWidgets.QMainWindow):
def __init__(self, workers):
super().__init__()
...
self.thread = QtCore.QThread()
self.worker = Task()
self.worker.moveToThread(self.thread)
self.worker.totalChanged.connect(self.updateTotal)
self.worker.finished.connect(self.thread.quit)
self.thread.started.connect(self.worker.dostuff)
self.thread.start()

Setup signal and slot before moving Worker object to QThread in pyqt

In Qt/PyQt, I used to make threading using a Worker class and a QThread.
self.worker = Worker()
self.thread = QThread()
worker.moveToThread(thread)
setup_signal_slot_with_main_object()
// start
thread.start()
I must place setup_signal_slot_with_main_object() after moveToThread(). But I have a complex worker. In Worker.__ init__(), it creates many QObjects and connects internal signals and slots. I don't want to create a method which makes all connections and calls worker.setup_signal_slot() after worker->moveToThread(&thread) because Worker contains many child QObjects and each QObject can make signal/slot in its constructor.
In Qt/C++, I can make signal/slot connection in worker's constructor. But in PyQt, slot will not run in new thread.
This is an example with a Worker contains a QTimer
import sys
import signal
import threading
from PyQt5.QtCore import QObject, pyqtSignal, QTimer, QCoreApplication, QThread
import datetime
class Worker(QObject):
timeChanged = pyqtSignal(object)
def __init__(self, parent=None):
QObject.__init__(self, parent)
self.timer = QTimer(self)
self.timer.setInterval(1000)
# I want to make connection at here
self.timer.timeout.connect(self.main_process)
def start(self):
# self.timer.timeout.connect(self.main_process)
self.timer.start()
print('Worker thread {}: Start timer'.format(threading.get_ident()))
# this method still run in main thread
def main_process(self):
timestamp = datetime.datetime.now()
print('Worker thread {}: {}'.format(threading.get_ident(), timestamp.strftime('%d-%m-%Y %H-%M-%S')))
self.timeChanged.emit(timestamp)
class WorkerThread(QObject):
def __init__(self, parent=None):
QObject.__init__(self, parent)
self.emitter = Worker()
self.thread = QThread(self)
self.emitter.moveToThread(self.thread)
self.thread.started.connect(self.emitter.start)
self.thread.finished.connect(self.emitter.deleteLater)
self.emitter.timeChanged.connect(self.show_time)
def start(self):
self.thread.start()
def stop(self):
if self.thread.isRunning():
self.thread.quit()
self.thread.wait()
print('Exit thread')
def show_time(self, timestamp):
print('Main thread {}: {}'.format(threading.get_ident(), timestamp.strftime('%d-%m-%Y %H-%M-%S')))
def signal_handler(sig, frame):
print('Quit')
app.quit()
if __name__ == '__main__':
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
app = QCoreApplication(sys.argv)
timer = QTimer()
timer.timeout.connect(lambda: None)
timer.start(500)
print('Main thread {}'.format(threading.get_ident()))
emitter = WorkerThread()
emitter.start()
sys.exit(app.exec_())
In Worker, timer timeout will call main_process in main thread. I can move self.timer.timeout.connect(self.main_process) into method worker.start(). But as I have said above, I still want to place internal signal/slot in its constructor.
Could anyone suggest me a solution ? Thanks !
If you want the methods to be invoked in the same thread where the receiver uses the pyqtSlot() decorator, if you don't do it then it will be called in the sender's thread.
import sys
import signal
import threading
import datetime
from PyQt5.QtCore import QObject, pyqtSignal, QTimer, QCoreApplication, QThread, pyqtSlot
class Worker(QObject):
timeChanged = pyqtSignal(object)
def __init__(self, parent=None):
QObject.__init__(self, parent)
self.timer = QTimer(self)
self.timer.setInterval(1000)
self.timer.timeout.connect(self.main_process)
#pyqtSlot()
def start(self):
self.timer.start()
print("Worker thread {}: Start timer".format(threading.get_ident()))
#pyqtSlot()
def main_process(self):
timestamp = datetime.datetime.now()
print(
"Worker thread {}: {}".format(
threading.get_ident(), timestamp.strftime("%d-%m-%Y %H-%M-%S")
)
)
self.timeChanged.emit(timestamp)
class WorkerThread(QObject):
def __init__(self, parent=None):
QObject.__init__(self, parent)
self.emitter = Worker()
self.thread = QThread(self)
self.emitter.moveToThread(self.thread)
self.thread.started.connect(self.emitter.start)
self.thread.finished.connect(self.emitter.deleteLater)
self.emitter.timeChanged.connect(self.show_time)
#pyqtSlot()
def start(self):
self.thread.start()
def stop(self):
if self.thread.isRunning():
self.thread.quit()
self.thread.wait()
print("Exit thread")
#pyqtSlot(object)
def show_time(self, timestamp):
print(
"Main thread {}: {}".format(
threading.get_ident(), timestamp.strftime("%d-%m-%Y %H-%M-%S")
)
)
def signal_handler(sig, frame):
print("Quit")
app.quit()
if __name__ == "__main__":
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
app = QCoreApplication(sys.argv)
timer = QTimer()
timer.timeout.connect(lambda: None)
timer.start(500)
print("Main thread {}".format(threading.get_ident()))
emitter = WorkerThread()
emitter.start()
sys.exit(app.exec_())
Output:
Main thread 140175719339648
Worker thread 140175659480832: Start timer
Worker thread 140175659480832: 26-07-2019 04-39-42
Main thread 140175719339648: 26-07-2019 04-39-42
Worker thread 140175659480832: 26-07-2019 04-39-43
Main thread 140175719339648: 26-07-2019 04-39-43
Worker thread 140175659480832: 26-07-2019 04-39-44
Main thread 140175719339648: 26-07-2019 04-39-44
Worker thread 140175659480832: 26-07-2019 04-39-45
Main thread 140175719339648: 26-07-2019 04-39-45

QTimer in worker thread blocking GUI

I am trying to create a worker thread whose job is to monitor the status bit of a positioning platform.
To do this I connect a QTimer timeout signal to a function that queries the platform.
class expSignals(QtCore.QObject):
pause=QtCore.pyqtSignal()
class motorpositioner(QtCore.QObject):
def __init__(self):
QtCore.QThread.__init__(self)
self.timer = QtCore.QTimer()
self.timer.start(100)
self.timer.timeout.connect(self.do_it)
self.lock=QtCore.QMutex()
self.running=True
self.stat=0
def do_it(self):
with QtCore.QMutexLocker(self.lock):
#self.stat = self.motors.get_status()
print(self.stat)
time.sleep(5)
#QtCore.pyqtSlot()
def stop1(self):
self.timer.stop()
print('stop heard')
The GUI stuff looks like this:
class MyApp(QtWidgets.QMainWindow):
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
self.thread=QtCore.QThread(self)
#worker
self.mot=motorpositioner()
# =============================================================================
# Putting buttons and GUI stuff in place
# =============================================================================
self.button=QtWidgets.QPushButton('Derp',self)
layout = QtWidgets.QHBoxLayout()
layout.addWidget(self.button)
self.setLayout(layout)
self.setGeometry( 300, 300, 350, 300 )
# =============================================================================
# Connecting signals
# =============================================================================
self.sig=expSignals()
self.sig2=expSignals()
self.button.clicked.connect(self.stop)
self.sig.pause.connect(self.mot.stop1)
self.sig2.pause.connect(self.thread.quit)
self.mot.moveToThread(self.thread)
self.thread.start()
def stop(self):
self.sig.pause.emit()
def closeEvent(self,event):
self.sig2.pause.emit()
event.accept()
However the way it is written now the GUI is unresponsive. However if I comment out self.timer.timeout.connect(self.do_it) and put do_it in a while(True) loop, the GUI isn't being blocked.
Why is the main thread being blocked when using QTimer?
I do not know what is expSignals() and I think it is not relevant, and neither is the button.
Your code has the following errors:
You are starting the timer before the thread starts, so the task will run on the GUI thread.
QTimer is not a child of motorpositioner so if motorpositioner moves to the new thread QTimer will not. For him to move he must be a son so you must pass him as a parent to self.
I do not know if it is a real error, but you are firing the QTimer every 100 ms but the task takes 5 seconds, although the QMutex helps to have no problems because it is blocked.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
import time
class motorpositioner(QtCore.QObject):
def __init__(self):
QtCore.QThread.__init__(self)
self.timer = QtCore.QTimer(self)
self.lock = QtCore.QMutex()
self.running = True
self.stat = 0
def start_process(self):
self.timer.timeout.connect(self.do_it)
self.timer.start(100)
def do_it(self):
with QtCore.QMutexLocker(self.lock):
#self.stat = self.motors.get_status()
print(self.stat)
time.sleep(5)
#QtCore.pyqtSlot()
def stop1(self):
self.timer.stop()
print('stop heard')
class MyApp(QtWidgets.QMainWindow):
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
self.thread = QtCore.QThread(self)
self.mot = motorpositioner()
self.mot.moveToThread(self.thread)
self.thread.started.connect(self.mot.start_process)
self.thread.start()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
ex = MyApp()
ex.show()
sys.exit(app.exec_())

Python: How can I refresh QLCDNumbers + emiting again after stopping

I want to ask how is it possible to refresh QLCDNumbers after I have started some measures.
I created an GUI thread to connect the signals to QLCDNumbers like that:
class BtDialog(QtGui.QDialog, Dlg):
def __init__(self):
QtGui.QDialog.__init__(self)
self.setupUi(self)
self.thread = WorkerThread()
#Configure slots
self.connect(self.startButton, QtCore.SIGNAL("clicked()"), self.onStart)
self.connect(self.stopButton, QtCore.SIGNAL("clicked()"), self.onStop)
#QLCDNumber Slot
self.connect(self.thread, self.thread.voltage, self.lcdVoltage.display)
def onStart(self):
self.thread.start()
def onStop(self):
self.emit(self.thread.voltage, 0) #Trying to refresh
abort()
Here I connect two buttons, one for starting the worker thread and the other to stop the process. When I stop the process I want to refresh the QLCDNumber by displaying '0' but it doesn't work.
In worker thread i initialize the signal like that:
def __init__(self, parent = None):
QtCore.QThread.__init__(self, parent)
self.voltage = QtCore.SIGNAL("voltage")
And when the process runs I emit the signal with
self.emit(self.voltage, volt_act)
after measuring. That works so far. But after stopping when I want to start the worker process again the signal doesn't emit to QLCDNumber again. For that I have to restart the GUI. How can I fix the two problems of mine for that I want to refresh QLCDNumber and over that after stopping and refreshing emitting the signal again?
Can't tell where the issue is from the code you posted, but this should help you modify it, also checkout the docs for new-style signal/slot connections and further reference (modal dialogs, timers, etc):
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import time
from PyQt4 import QtGui, QtCore
class MyThread(QtCore.QThread):
countChange = QtCore.pyqtSignal(int)
countReset = QtCore.pyqtSignal(int)
def __init__(self, parent=None):
super(MyThread, self).__init__(parent)
self.stopped = QtCore.QEvent(QtCore.QEvent.User)
def start(self):
self.stopped.setAccepted(False)
self.count = 0
super(MyThread, self).start()
def run(self):
while not self.stopped.isAccepted():
self.count += 1
self.countChange.emit(self.count)
time.sleep(1)
self.countReset.emit(0)
def stop(self):
self.stopped.setAccepted(True)
class MyWindow(QtGui.QDialog):
def __init__(self, parent=None):
super(MyWindow, self).__init__(parent)
self.lcdNumber = QtGui.QLCDNumber(self)
self.pushButtonStart = QtGui.QPushButton(self)
self.pushButtonStart.setText("Start")
self.pushButtonStart.clicked.connect(self.on_pushButtonStart_clicked)
self.pushButtonStop = QtGui.QPushButton(self)
self.pushButtonStop.setText("Stop")
self.pushButtonStop.clicked.connect(self.on_pushButtonStop_clicked)
self.pushButtonDone = QtGui.QPushButton(self)
self.pushButtonDone.setText("Done")
self.pushButtonDone.clicked.connect(self.on_pushButtonDone_clicked)
self.layoutHorizontal = QtGui.QHBoxLayout(self)
self.layoutHorizontal.addWidget(self.lcdNumber)
self.layoutHorizontal.addWidget(self.pushButtonStart)
self.layoutHorizontal.addWidget(self.pushButtonStop)
self.layoutHorizontal.addWidget(self.pushButtonDone)
self.thread = MyThread(self)
self.thread.countChange.connect(self.lcdNumber.display)
self.thread.countReset.connect(self.lcdNumber.display)
#QtCore.pyqtSlot()
def on_pushButtonStart_clicked(self):
self.thread.start()
#QtCore.pyqtSlot()
def on_pushButtonStop_clicked(self):
self.thread.stop()
#QtCore.pyqtSlot()
def on_pushButtonDone_clicked(self):
sys.exit()
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
app.setApplicationName('MyWindow')
main = MyWindow()
main.exec_()
sys.exit(app.exec_())

Categories