Running a heavy QTimer task as a QThread - python

I have a heavy task that constantly runs every 500ms. It consists of updating GUI elements and I need access to its variables at all times.
The task performed: A list that is dynamically updated and every 500ms, a loop goes through that list and performers tasks on the elements contained inside of it. Sometimes I have no elements in it, and sometimes I have plenty.
When the list is loaded, the user starts to encounter a delay in mouse movement, key presses, and such. And that's without a doubt due to the heavy task performed every 500ms.
Would there be a way for me to put this QTimer task into a QThread and constantly have access to it's elements in order update the list contained inside of it?
In other words, I would like it to run in the background at all times but also have the ability to update the list used inside of it at any given moment.
I'm using PySide2; I've seen examples but none that fit what I'm trying to accomplish.
EXAMPLE:
I would like to update the "aList" element from the main thread as I wish. If the list is empty, then the for loop does not do anything. Otherwise, it loops over the elements and adds 1 to them.
The "run" function should have a Qtimer of 500ms set on it.
Sometimes the list may be empty and at times full of elements. It's size is controlled from the GUI thread.
from PySide2 import QtCore
from PySide2 import QtGui
from PySide2 import QtWidgets
import sys
import time
class RxProcess(QtCore.QThread):
output = QtCore.Signal()
def __init__(self, parent = None):
super(RxProcess, self).__init__(parent)
self.aList = list()
def run(self):
# Loop through list
for element in self.aList:
element += 1
# Print to the gui the element that was just updated in the list
self.output.emit(element)

With QThread it is difficult to implement that logic (you would have to use QThread.msleep, mutex, etc). Instead a simple solution is to create a new thread every T seconds and that will be implemented using threading.Thread + QTimer (can also be implemented with QThreadPool + QRunnable + QTimer):
import random
import sys
import threading
import time
from PySide2 import QtCore, QtWidgets
import shiboken2
class Worker(QtCore.QObject):
output = QtCore.Signal(object)
def long_running_function(values, worker):
for element in values:
time.sleep(0.1)
if shiboken2.isValid(worker):
worker.output.emit(element)
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.label = QtWidgets.QLabel(alignment=QtCore.Qt.AlignCenter)
self.button = QtWidgets.QPushButton("Start")
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.button)
lay.addWidget(self.label)
self.timer = QtCore.QTimer(interval=500)
self.button.clicked.connect(self.handle_clicked)
self.timer.timeout.connect(self.launch_task)
def handle_clicked(self):
if self.button.text() == "Start":
self.timer.start()
self.button.setText("Stop")
elif self.button.text() == "Stop":
self.timer.stop()
self.button.setText("Start")
def launch_task(self):
values = random.sample(range(1, 50), 20)
worker = Worker()
worker.output.connect(self.label.setNum)
threading.Thread(
target=long_running_function,
args=(values, worker),
daemon=True,
).start()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())

Related

Creating multiple QThreads and perform action AFTER all QThreads are closed

here I have a simple GUI. With the creation of this GUI I am starting a thread (THREAD1) which is running while the GUI is running. The task of this thread (THREAD1) is, to perform a specific action in a specific interval.
It is important, that this specific action is performed as fast as possible. So I am creating new thread objects. This is accomplished by THREAD1
Until here everything is working fine. I get the THREAD1 to work. I am also able to get threads created by THREAD1 to work.
here the code.
from PyQt6.QtCore import QThread, QObject
from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget
import time
class GetMarketDataV2(QThread):
def __init__(self, index):
super(GetMarketDataV2, self).__init__()
self.index = index
def run(self):
time.sleep(5)
print(f"Thread: {self.index}\n")
class DataCollectionLoop(QObject):
"""
Runs as long as the GUI is running. Terminated with the Main window close event.
The task of this class is, to start a specific amount of threads with a specific interval.
"""
def __init__(self):
super(DataCollectionLoop, self).__init__()
self.thread = {}
self.first_startup = True
def run(self):
while True:
# In this example 10 threads are created.
for i in range(10):
self.thread[i] = GetMarketDataV2(index=i)
self.thread[i].start()
# I want this line below to execute after all threads above are done with their job.
print("print this statement after all threads are finished")
# Here the specific interval is 10 seconds.
time.sleep(10)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Setting up thread which controls the data collection.
self.thread = QThread()
# Instantiating the DataCollectionLoop object and moving it to a thread.
data_collection = DataCollectionLoop()
data_collection.moveToThread(self.thread)
# calling the run method when the thread is started.
self.thread.started.connect(data_collection.run)
# Starting the thread
self.thread.start()
# Minimal GUI
self.centralWidget = QWidget()
self.setCentralWidget(self.centralWidget)
self.show()
# Terminating the thread when window is closed.
def closeEvent(self, event):
self.thread.terminate()
app = QApplication([])
window = MainWindow()
I have searched a decent amount of time, but couldn't find a solution to my problem.
Which is:
I want to wait for all threads, which are created by THREAD1, to finish, before continuing with my code.
I think I should catch the states of each thread (if they are finished or not) but I don't know exactly how..
Thank you very much!
As #musicamante suggested I got the desired result using the isFinished() method.
Here is how I made it:
# Waits for all threads to finnish. Breaks out of the loop afterwards
while True:
for thread in self.thread.values():
if not thread.isFinished():
break
else:
break
Another solution I stumpled uppon which does the trick for me, is to work with QRunnables and a Threadpool.
for market in forex_markets:
market = GetMarketData() # Subclassed QRunnable
self.threadpool.start(market)
self.threadpool.waitForDone()
Thank you!

PySide2 UI stops responding when entering a while loop after showing it

I'm having trouble where my QtWidget stops responding if I try to use a while loop after calling the widget.show() method for the QtWidget object. I initially thought the issue was in how I was using the signals and slots. I had created my own signal with new_data = Signal(float) and I was sampling data at an interval established by a time.sleep() call within that while loop and emitting the new_data signal each time the data was sampled. That was connected to a method in my QtWidget that just set the text of a label in the QtWidget to display the new data.
However, after some testing I found that if I ONLY try to print("in loop") inside that while loop, I get the same behavior. The QtWidget object stops responding. What is the proper way to update PySide2 interface at a periodic interval from outside the interface object? Can I perhaps run the interface as a process and feed it with updated data with a queue? I imaging that is possible, but am having trouble finding an example. The interface is only one piece of this application primarily made in Python and I will have multiple processes and multiple queues besides the Qt interface. Here is the code:
import sys
from PySide2.QtUiTools import QUiLoader
from PySide2.QtWidgets import QApplication, QWidget
from PySide2.QtCore import QFile, QObject, Signal, Slot
import time
import NI9213
class MyDaq(QObject):
new_daq_data = Signal(float)
def __init__(self):
QObject.__init__(self)
daq_channels = "cDAQ1Mod2/ai0"
self.daq = NI9213.NI9213(channels=daq_channels)
def sample_daq(self):
data = self.daq.read_all()
self.new_daq_data.emit(data)
class DigitalDisplay(QWidget):
def __init__(self):
#Initialize the QWidget object used to create the user interface
QWidget.__init__(self)
#Load the user interface
designer_file = QFile("signal_digital_display.ui")
designer_file.open(QFile.ReadOnly)
loader = QUiLoader()
self.ui = loader.load(designer_file, self)
designer_file.close()
#Add title to the UI window
self.setWindowTitle("Digital Display")
self.mode = 'run'
self.ui.stopButton.clicked.connect(self.stopMode)
self.sampling_period = 0.1
#Slot(float)
def refresh_data(self, data):
self.ui.label.setText(str(data))
def stopMode(self):
self.mode = 'stop'
if __name__ == "__main__":
app = QApplication(sys.argv)
digital_display = DigitalDisplay()
digital_display.show()
## data = MyDaq()
## data.new_daq_data.connect(digital_display.refresh_data)
##
while(digital_display.mode=='run'):
print("after display.show")
## data.sample_daq()
time.sleep(digital_display.sampling_period)
sys.exit(app.exec_())
The problem here is that you have an infinite loop before you start the Qt event loop.
if __name__ == "__main__":
app = QApplication(sys.argv) # [1]
digital_display = DigitalDisplay()
digital_display.show() # [2]
while(digital_display.mode=='run'):
print("after display.show")
time.sleep(digital_display.sampling_period) # [3]
sys.exit(app.exec_()) # [4]
[1] This creates the QApplication object. It does not start the event loop yet.
[2] Create your widget and open a new window to display it. The operating system will create events for the window, but they have no effect on the application itself until we start the event loop.
[3] digital_display.mode will never change. This is an infinite loop and Python will never advance past this point.
[4] Here we start the application event loop and close the process once the application has finished. But we never get here.
What you should do instead is to create a QTimer on the DigitalDisplay widget that periodically fires a signal that can be connected to data.sample_daq.

Why is processEvents() needed to get QThread to work?

Below is my code for listing all the sub-directories of a directory. I'm using it to understand QThread and signal and slots in PySide. The problem is, when I'm not using Qtcore.QApplication.processEvents() in the scan() method of the Main class, the code does not work. Is the event-loop not already running?
import sys
import os
import time
from PySide import QtGui, QtCore
class Scanner(QtCore.QObject):
folderFound = QtCore.Signal(str)
done = QtCore.Signal()
def __init__(self, path):
super(Scanner, self).__init__()
self.path = path
#QtCore.Slot()
def scan(self):
for folder in os.listdir(self.path):
time.sleep(1)
self.folderFound.emit(folder)
class Main(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.resize(420,130)
self.setWindowTitle('Threads')
self.lyt = QtGui.QVBoxLayout()
self.setLayout(self.lyt)
self.topLyt = QtGui.QHBoxLayout()
self.lyt.addLayout(self.topLyt)
self.scanBtn = QtGui.QPushButton('Scan')
self.scanBtn.clicked.connect(self.scan)
self.clearBtn = QtGui.QPushButton('Clear')
self.topLyt.addWidget(self.scanBtn)
self.topLyt.addWidget(self.clearBtn)
self.folders = list()
self.show()
def scan(self):
self.th = QtCore.QThread()
scanner = Scanner(r"D:\\")
scanner.moveToThread(self.th)
scanner.folderFound.connect(self.addFolder)
scanner.done.connect(scanner.deleteLater)
scanner.done.connect(self.quit)
self.th.started.connect(scanner.scan)
self.th.start()
QtCore.QApplication.processEvents()
#QtCore.Slot()
def addFolder(self, folder):
lbl = QtGui.QLabel(folder)
self.folders.append(lbl)
self.lyt.addWidget(lbl)
#QtCore.Slot()
def quit(self):
self.th.quit()
self.th.wait()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
main = Main()
app.exec_()
It is a pure fluke that your example works at all.
When you attempt to call QtCore.QApplication.processEvents(), a NameError will be raised, because the QApplication class is actually in the QtGui module, not the QtCore module. However, raising the exception has the side-effect of preventing your scanner object from being garbage-collected, and so the thread appears to run normally.
The correct way to fix your code is to keep a reference to the scanner object, and get rid of the processEvents line, which is not needed:
def scan(self):
self.th = QtCore.QThread()
# keep a reference to the scanner
self.scanner = Scanner(r"D:\\")
self.scanner.moveToThread(self.th)
self.scanner.folderFound.connect(self.addFolder)
self.scanner.done.connect(self.scanner.deleteLater)
self.scanner.done.connect(self.quit)
self.th.started.connect(self.scanner.scan)
self.th.start()
The rest of your code is okay, and the example will now work as expected.
Event loop is not something that runs behind your back. You're always the one who has to run it. A thread cannot be doing two things at once: if your code is the locus of control, then obviously the event loop isn't! You need to return from your code to the event loop, and make sure that your code was called from the event loop. Any sort of a UI signal, networking event, or timeout is invoked from the event loop, so most likely your code already has the event loop on the call stack. To keep the loop spinning, you have to return to it, though.
Never use processEvents - instead, invert the control, so that the event loop calls into your code, and then you perform a chunk of work, and finally return back to the event loop.
The idiom for "keep my code working from event loop" is a zero-duration timer. The callable that performs the work is attached to the timeout signal.

How to prevent the GUI from freezing using QThread?

I have a GUI which needs to perform work that takes some time and I want to show the progress of this work, similar to the following:
import sys
import time
from PyQt4 import QtGui, QtCore
class MyProgress(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
# start loop with signal
self.button = QtGui.QPushButton('loop', self)
self.connect(self.button, QtCore.SIGNAL('clicked()'), self.loop)
# test button
self.test_button = QtGui.QPushButton('test')
self.connect(self.test_button, QtCore.SIGNAL('clicked()'), self.test)
self.pbar = QtGui.QProgressBar(self)
self.pbar.setMinimum(0)
self.pbar.setMaximum(100)
# layout
vbox = QtGui.QVBoxLayout()
vbox.addWidget(self.test_button)
vbox.addWidget(self.button)
vbox.addWidget(self.pbar)
self.setLayout(vbox)
self.show()
def update(self):
self.pbar.setValue(self.pbar.value() + 1)
def loop(self):
for step in range(100):
self.update()
print step
time.sleep(1)
def test(self):
if self.test_button.text() == 'test':
self.test_button.setText('ok')
else:
self.test_button.setText('test')
app = QtGui.QApplication(sys.argv)
view = MyProgress()
view.loop() # call loop directly to check whether view is displayed
sys.exit(app.exec_())
When I execute the code the loop method is called and it prints out the values as well as updates the progress bar. However the view widget will be blocked during the execution of loop and although this is fine for my application it doesn't look nice with Ubuntu. So I decided to move the work to a separate thread like this:
import sys
import time
from PyQt4 import QtGui, QtCore
class Worker(QtCore.QObject):
def __init__(self, parent=None):
QtCore.QObject.__init__(self, parent)
def loop(self):
for step in range(10):
print step
time.sleep(1)
class MyProgress(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
# test button
self.test_button = QtGui.QPushButton('test')
self.connect(self.test_button, QtCore.SIGNAL('clicked()'), self.test)
self.pbar = QtGui.QProgressBar(self)
self.pbar.setMinimum(0)
self.pbar.setMaximum(100)
# layout
vbox = QtGui.QVBoxLayout()
vbox.addWidget(self.test_button)
vbox.addWidget(self.pbar)
self.setLayout(vbox)
self.show()
def test(self):
if self.test_button.text() == 'test':
self.test_button.setText('ok')
else:
self.test_button.setText('test')
app = QtGui.QApplication(sys.argv)
view = MyProgress()
work = Worker()
thread = QtCore.QThread()
work.moveToThread(thread)
# app.connect(thread, QtCore.SIGNAL('started()'), work.loop) # alternative
thread.start()
work.loop() # not called if thread started() connected to loop
sys.exit(app.exec_())
When I run this version of the script the loop starts running (the steps are displayed in the terminal) but the view widget is not shown. This is the first thing I can't quite follow. Because the only difference from the previous version here is that the loop runs in a different object however the view widget is created before and therefore should be shown (as it was the case for the previous script).
However when I connected the signal started() from thread to the loop function of worker then loop is never executed although I start the thread (in this case I didn't call loop on worker). On the other hand view is shown which makes me think that it depends whether app.exec_() is called or not. However in the 1st version of the script where loop was called on view it showed the widget although it couldn't reach app.exec_().
Does anyone know what happens here and can explain how to execute loop (in a separate thread) without freezing view?
EDIT: If I add a thread.finished.connect(app.exit) the application exits immediately without executing loop. I checked out the 2nd version of this answer which is basically the same what I do. But in both cases it finishes the job immediately without executing the desired method and I can't really spot why.
The example doesn't work because communication between the worker thread and the GUI thread is all one way.
Cross-thread commnunication is usually done with signals, because it is an easy way to ensure that everything is done asynchronously and in a thread-safe manner. Qt does this by wrapping the signals as events, which means that an event-loop must be running for everything to work properly.
To fix your example, use the thread's started signal to tell the worker to start working, and then periodically emit a custom signal from the worker to tell the GUI to update the progress bar:
class Worker(QtCore.QObject):
valueChanged = QtCore.pyqtSignal(int)
def loop(self):
for step in range(0, 10):
print step
time.sleep(1)
self.valueChanged.emit((step + 1) * 10)
...
thread = QtCore.QThread()
work.moveToThread(thread)
thread.started.connect(work.loop)
work.valueChanged.connect(view.pbar.setValue)
thread.start()
sys.exit(app.exec_())

pyqt timer does not return a value or an error

I am trying to use a timer with pyqt. The code is below, but it does not print anything and I do not get an error. Does anyone know what is wrong?
Thanks
import functools
from PyQt4.QtCore import QTimer
def onTimer(initParams):
print initParams
print "HERE"
# your code here...
def update():
print "Upd"
myInitParams = "Init!"
timerCallback = functools.partial(onTimer, myInitParams)
myTimer = QTimer()
myTimer.timeout.connect(timerCallback)
myTimer.start(1000) #once a sec
t = QTimer()
t.start(500)
t.timeout.connect(update)
EDIT UPDATE:
So this is a snip of the full code im using, which is a bit more complicated and maybe is a different problem, but the result is the same.
The structure is like this:
from PyQt4 import QtCore, QtGui
# Import the UI Classes from the ui directory in this package
from ui.MoAnGUI import Ui_MoAnWindow
class MoAnWin(QtGui.QDialog, Ui_MoAnWindow):
def __init__(self, parent=None):
super(MoAnWin, self).__init__(parent=parent)
self.setupUi(self)
self.playMarkers.clicked.connect(self._PlayMarkers)
self.connect(self,QtCore.SIGNAL("PlayMarker"),PlayMarkerCall)
#Marker Stop
self.stopMarkers.clicked.connect(self._StopMarkers)
self.connect(self,QtCore.SIGNAL("StopMarker"),StopMarkerCall)
def _StopMarkers(self):
arg = "Stop"
self.emit(QtCore.SIGNAL("StopMarker"),arg)
def _PlayMarkers(self):
fps = self.fpsBox.value()
starttime = self.fromTime.value()
endtime = self.ToTime.value()
arg = [fps,starttime,endtime]
self.emit(QtCore.SIGNAL("PlayMarker"),arg)
def StopMarkerCall(arg):
#I want to stop the timer here
def PlayMarkerCall(arg):
#I tried this, but again, nothing happens, no error
myInitParams = "Init!"
timerCallback = functools.partial(TimerCall, myInitParams)
myTimer = QtCore.QTimer()
myTimer.timeout.connect(timerCallback)
myTimer.start(1000) #once a sec
#I Imagine something like this:
myTimer = QtCore.QTimer()
for i in range(start,end):
# I want to connect to my view marker function but i need to pass a variable
myTimer.timeout.connect(ViewMarkerCall(i))
myTimer.start(1000)
def TimerCall(args):
print "HERE", args
def show():
global globalMoAnWindow
app = QtGui.QApplication(sys.argv)
globalMoAnWindow = MoAnWin()
globalMoAnWindow.show()
sys.exit(app.exec_())
return globalMoAnWindow
show()
My goal is to have a button click play, and stuff happens in a qtgraphic widget at a certain time interval, then the stop button stops the playing. I found the functools from another question on here, but im not sure if its the correct thing to do.
To correctly use Qt, you need to set up a QtGui.QApplication or QtCore.QCoreApplication and use it's event loop to process events.
This should work:
#from utils import sigint
import functools
from PyQt4.QtCore import QTimer, QCoreApplication
app = QCoreApplication([])
def onTimer(initParams):
print initParams
print "HERE"
# your code here...
def update():
print "Upd"
myInitParams = "Init!"
timerCallback = functools.partial(onTimer, myInitParams)
myTimer = QTimer()
myTimer.timeout.connect(timerCallback)
myTimer.start(1000) #once a sec
t = QTimer()
t.start(500)
t.timeout.connect(update)
# use a timer to stop the event loop after some time
stopTimer = QTimer(timeout=app.quit, singleShot=True)
stopTimer.start(4000)
app.exec_()
Edit:
In your updated version you don't keep any reference to your timer object you create in the PlayMarkerCall method. When myTimer goes out of scope, it is destroyed (together with the underlying C++ Qt object), that's why your timer never fires.
And you can't pass parameters when you connect a signal to a slots, at that time you only set up the connection. Parameters can only be passed when the signal is emitted, to do that you could create a class derived from QTimer and override it's timerEvent method to emit a signal with arguments specified when instantiating the Timer.

Categories