Using a QTimer within a PyQt worker thread - python

I am working with serial device and set a flag (which is global variable) based on the received data. Now I want to reset the flag after a while (for example one second) by using a timer.
Here is the code:
class Inlet_Worker(QObject):
def __init__(self):
super(Inlet_Worker, self).__init__()
self.timer = QTimer(self)
self.timer.timeout.connect(self.Reset_Register_Barcode)
def run(self):
global Register_Barcode
while True :
if client.read_coils(address = 0x0802).bits[0]:
Register_Barcode = True
self.timer.start(1000)
def Reset_Register_Barcode(self):
global Register_Barcode
Register_Barcode = False
However the timer is not working.

I will assume from your example code that your are using a QThread and that you also use QObject.moveToThread on your worker object. This is the correct procedure, but there are some other things you must do to make your timer work.
Firstly, you should use a single-shot timer so as to avoid re-regsitration whilst the current one is active. Secondly, you must explicitly process any pending events, since your while-loop will block the thread's event-loop. Without this, the timer's timeout signal will never be emitted. Thirdly, you should ensure that the worker and thread shut down cleanly when the program exits (which will also prevent any Qt error messages). Finally, if possible, you should use signals to communicate registration changes to the main GUI thread, rather than global variables.
The demo script below (based on your example) implements all of that. After the Start button is clicked, the thread will start and periodically update the regsitration (indicated by the check-box). Hopefully you shoudld be able to see how to adapt it to your real application:
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class Inlet_Worker(QObject):
barcodeRegistered = pyqtSignal(bool)
def __init__(self):
super().__init__()
self._stopped = False
self._registered = False
self.timer = QTimer(self)
self.timer.setSingleShot(True)
self.timer.timeout.connect(self.updateBarcodeRegistration)
def run(self):
count = 0
self._stopped = False
while not self._stopped:
#if client.read_coils(address = 0x0802).bits[0]:
count += 1
if count % 20 == 0 and not self._registered:
self.updateBarcodeRegistration(True)
self.timer.start(2000)
QCoreApplication.processEvents()
QThread.msleep(100)
self.updateBarcodeRegistration(False)
self.timer.stop()
print('Stopped')
def updateBarcodeRegistration(self, enable=False):
print('Register' if enable else 'Reset')
self._registered = enable
self.barcodeRegistered.emit(enable)
def stop(self):
self._stopped = True
class Window(QWidget):
def __init__(self):
super().__init__()
self.thread = QThread()
self.worker = Inlet_Worker()
self.worker.moveToThread(self.thread)
self.button = QPushButton('Start')
self.check = QCheckBox('Registered')
layout = QHBoxLayout(self)
layout.addWidget(self.button)
layout.addWidget(self.check)
self.thread.started.connect(self.worker.run)
self.button.clicked.connect(self.thread.start)
self.worker.barcodeRegistered.connect(self.check.setChecked)
def closeEvent(self, event):
self.worker.stop()
self.thread.quit()
self.thread.wait()
if __name__ == '__main__':
app = QApplication(['Test'])
window = Window()
window.setGeometry(600, 100, 200, 50)
window.show()
app.exec()

Related

Cancel long running action with multiple dialogs in PyQt6

I'm having an personal issue with threads in PyQt6. I have used the great example in the solution of How to stop a QThread from the GUI and rebuilt it with a separate dialog and a finished event.
In theory, what I am trying to create is a Connection Dialog which opens a database connection with oracledb. Although, if I click on the connection in the connection dialog, it should not connect silently, but it rather should display a Cancel Dialog, where if I want, I can choose to cancel the current connection attempt.
This functionality should later then be used for every query against the database in my program, the connection dialog was just an example.
My current issue is, that the button on the Cancel Dialog is not displayed. Somehow the Cancel Dialog is frozen even though I use threads for the worker to connect.
For this I have created the following reduced code example:
import time
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
class ConnectionDialog(QDialog):
def __init__(self):
super().__init__()
self.database = -1
self.setFixedSize(QSize(200, 100))
# create CancelDialog but do not display yet
self.cancel_dialog = CancelDialog(self)
# creat a thread
self.thread = QThread()
self.thread.start()
# creat a worker and put it on thread
self.worker = Worker()
self.worker.moveToThread(self.thread)
# create button which triggers the cancel_dialog and therefore the connection attempt
self.btnStart = QPushButton("Connect")
self.btnStart.clicked.connect(self.start_worker)
# if the worker emits finished call helper function end_thread
self.worker.finished.connect(self.end_thread)
# create dummy layout to display the button
self.layout = QHBoxLayout()
self.layout.addWidget(self.btnStart)
self.setLayout(self.layout)
# helper function to quit and wait for the thread
def end_thread(self, connection):
self.worker.stop()
self.thread.quit()
self.thread.wait()
print(f"Connected to {connection}")
self.close()
# push connection to self.database for further use
self.database = connection
# helper function to start worker in thread
def start_worker(self):
self.cancel_dialog.show()
self.worker.task()
class CancelDialog(QDialog):
def __init__(self, parent):
super().__init__(parent)
self.setModal(True)
# create button for the cancel operation for the thread of the parent
self.btnStop = QPushButton("Cancel")
self.btnStop.clicked.connect(lambda: self.cancel_thread())
# create dummy layout to display the button
self.layout = QHBoxLayout()
self.layout.addWidget(self.btnStop)
self.setLayout(self.layout)
# helper function when pressing cancel button
def cancel_thread(self):
# stop worker
self.parent().worker.stop()
# quit thread, this time we dont want to wait for the thread
self.parent().thread.quit()
print("Canceled")
self.close()
# push error value to self.database
self.parent().database = -1
class Worker(QObject):
"Object managing the simulation"
finished = pyqtSignal(int)
def __init__(self):
super().__init__()
self._isRunning = True
self.connection = -1
def task(self):
if not self._isRunning:
self._isRunning = True
# this simulates a database connection
print("connecting...")
print("fake delay before connection")
time.sleep(3)
print("really connecting now")
self.connection = 123
print("fake delay after connection")
time.sleep(3)
print("really connecting now")
self.finished.emit(self.connection)
print("finished connecting")
def stop(self):
self._isRunning = False
if self.connection:
self.connection = 0
print("Canceled")
if __name__ == "__main__":
app = QApplication(sys.argv)
simul = ConnectionDialog()
simul.show()
sys.exit(app.exec())
I did some more research because I had some more errors, the comment of ekhumoro was also VERY helpful. I removed workers altogether and only used a class that inherits from QThread. i added another signal "started" to determine when to show the cancel dialog. maybe this is use to somebody someday :)
this is the final reduced code example:
import sys
import time
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
class ConnectionDialog(QDialog):
def __init__(self):
super().__init__()
self.database = None
self.setFixedSize(QSize(200, 100))
# create CancelDialog but do not display yet
self.cancel_dialog = CancelDialog(self)
# creat a thread
self.thread = BackgroundThread()
# create button which triggers the cancel_dialog and therefore the connection attempt
self.btnStart = QPushButton("Connect")
self.btnStart.clicked.connect(self.thread.start)
# if started signals, show cancel_dialog
self.thread.started.connect(self.cancel_dialog.show)
# if finished signals, end thread and close cancel_dialog
self.thread.finished.connect(self.end_thread)
self.thread.finished.connect(self.cancel_dialog.close)
# create dummy layout to display the button
self.layout = QHBoxLayout()
self.layout.addWidget(self.btnStart)
self.setLayout(self.layout)
# helper function to quit and wait for the thread
def end_thread(self, connection):
self.thread.quit()
self.thread.wait()
print(f"Connected to {connection}")
# push connection to self.database for further use
self.database = connection
self.close()
class CancelDialog(QDialog):
def __init__(self, parent):
super().__init__(parent)
self.setModal(True)
# create button for the cancel operation for the thread of the parent
self.btnStop = QPushButton("Cancel")
self.btnStop.clicked.connect(lambda: self.cancel_thread())
# create dummy layout to display the button
self.layout = QHBoxLayout()
self.layout.addWidget(self.btnStop)
self.setLayout(self.layout)
# helper function when pressing cancel button
def cancel_thread(self):
# terminate thread (Warning, see documentation)
self.parent().thread.terminate()
print("Canceled thread")
# push error value to self.database
self.parent().database = None
self.close()
class BackgroundThread(QThread):
finished = pyqtSignal(int)
started = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
def run(self):
self.started.emit()
print("connecting...")
print("fake delay before connection")
time.sleep(3)
print("really connecting now")
self.connection = 123
print("fake delay after connection")
time.sleep(3)
print("done")
self.finished.emit(self.connection)
print("finished connecting")
if __name__ == "__main__":
app = QApplication(sys.argv)
simul = ConnectionDialog()
simul.show()
ret = app.exec()
if simul.database:
print("Accepted")
else:
print("Rejected")
print(simul.database)

PYQT5: instantiating multiple QThread classes for independent timers

I have an application that keeps track of the time with a 1-second countdown timer that works perfectly:
class CDWorker(QtCore.QThread):
sinout= pyqtSignal()
def __init__(self, ctrl):
super(CDWorker,self).__init__()
self.working = True
self.num = 0
################################ Trigger the controller callback on timer cycle
def timerEvent(self, ctrl, event):
if len(ctrl.activeDF) != 0:
ctrl.activeDF.apply(ctrl.timerPipeline, axis = 1)
ctrl.getNearest()
super().timerEvent(event)
######################## Spin up the 1-second timer to constantly update time
#QtCore.pyqtSlot()
def run(self, ctrl):
while self.working == True:
loop = QtCore.QEventLoop()
ctrl.DF.apply(ctrl.callback1, axis = 1)
QtCore.QTimer.singleShot(1000, loop.quit)
loop.exec()
and I have a controller class that handles most of the heavy lifting in the app
class Controller:
def __init__(self):
self._app = QtWidgets.QApplication(sys.argv)
self._model = Model()
self._view = View(ctrl = self)
self._view.show()
cdTimer = QtCore.QThread()
cdWorker = CDWorker(ctrl = self)
cdWorker.moveToThread(cdTimer)
cdWorker.run(ctrl = self)
cdTimer.started.connect(CDWorker.run)
This all works perfectly and everything is good. However, now I need another class responsible for scheduling an http request on a 5 minute timer. Every attempt ive made so far to make another Qthread, QTimer, Qthreadpool, etc has either blocked the main eventloop, overridden the original timer, or was just ignored. Ive tried messing with the signal/slots and all too.
How can I get two classes with independent timers to trigger callbacks in my controller class?

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

PySide Signals not being sent to Slot, from QThread object

I'm working with a multi-threading application, where a worker thread gets created, that emits a signal.
After creating the thread, I connect the signal with an object slot, that will perform some action.
The problem, is the object slot, is not called, can someone help to figure out what's wrong with this code ?
import time
from PySide import QtCore
from PySide.QtCore import Slot, Signal
class Worker1(QtCore.QThread):
task_done_signal = Signal(int)
def __init__(self):
super(Worker1, self).__init__()
self._run = False
def run(self):
self._loop()
def _loop(self):
count = 0
while self._run:
print("running")
count += 1
self.task_done_signal.emit(count)
def start(self):
self._run = True
super(Worker1, self).start()
def stop(self):
self._run = False
class Worker1Listener(QtCore.QObject):
def __init__(self):
super(Worker1Listener, self).__init__()
#Slot()
def print_task(self, val):
print("listener: {}".format(val))
def test_signals_and_threads():
# create the thread
worker = Worker1()
# create the listener
listener = Worker1Listener()
# connect the thread signal with the slot
worker.task_done_signal.connect(listener.print_task)
worker.start()
time.sleep(5)
worker.stop()
time.sleep(5)
if __name__ == '__main__':
test_signals_and_threads()
Your code has several errors:
You must have an event loop so that Qt handles communications between the various objects of the application, in your case you must use QCoreApplication.
The decorator Slot must have as parameter the type of data of the arguments of the function, in your case: Slot(int)
You should not use time.sleep since it is blocking and does not let the event loop do its work, a possible solution is to use QEventLoop next to a QTimer.
It is always advisable to give a short time for communications to be given, for this we use QThread.msleep.
When you connect between signals that are in different threads, the correct option is to use the Qt.QueuedConnection option.
import sys
from PySide import QtCore
class Worker1(QtCore.QThread):
task_done_signal = QtCore.Signal(int)
def __init__(self):
super(Worker1, self).__init__()
self._run = False
def run(self):
self._loop()
def _loop(self):
count = 0
while self._run:
print("running")
count += 1
self.task_done_signal.emit(count)
QtCore.QThread.msleep(1)
def start(self):
self._run = True
super(Worker1, self).start()
def stop(self):
self._run = False
class Worker1Listener(QtCore.QObject):
#QtCore.Slot(int)
def print_task(self, val):
print("listener: {}".format(val))
def test_signals_and_threads():
app = QtCore.QCoreApplication(sys.argv)
# create the thread
worker = Worker1()
# create the listener
listener = Worker1Listener()
# connect the thread signal with the slot
worker.task_done_signal.connect(listener.print_task, QtCore.Qt.QueuedConnection)
worker.start()
loop = QtCore.QEventLoop()
QtCore.QTimer.singleShot(5000, loop.quit)
loop.exec_()
worker.stop()
loop = QtCore.QEventLoop()
QtCore.QTimer.singleShot(5000, loop.quit)
loop.exec_()
#sys.exit(app.exec_())
if __name__ == '__main__':
test_signals_and_threads()

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