pyqt threading, appropriate pattern? - python-3.x

I working on a gui application which plots real time data as it comes in. There is a main class which is a plot window. The window shows the plot of the data itself as well as a number of buttons, checkboxes, textboxes, etc. that a user can interact with to modify the plot output. All of these widgets configure the "settings" for the plot. The plotting of new data itself is resource intensive. The loading and plotting of the data may take around a second. I want the user to be able to modify the settings in the GUI without worrying about the lag of loading and plotting so I want to execute the loading and plotting in a separate thread.
I've come up with, what seems to me, to be a weird way to accomplish this but which works. It is something like this:
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import QThread, pyqtSignal
class PlotWorker(QtCore.QObject):
def __init__(self, plotwindow):
self.plotwindow = plotwindow
self.plot_thread = QThread()
self.plot_thread.start()
self.moveToThread(self.plot_thread)
def worker_update:
self.plotwindow.updating = True
self.plotwindow.plot()
self.plotwindow.updating = False
class PlotWindow(Ui_PlotWindow, QtWidgets.QMainWindow):
update_signal = pyqtSignal()
def __init__(self):
self.setupUi(self)
self.settings = self.settings_lineEdit.text()
self.worker = PlotWorker(self)
self.update_signal.connect(self.worker.worker_update)
self.update_settings_pushButton.clicked.connect(self.update_settings)
self.updating = False
self.refresh_timer = QtCore.QTimer(self)
self.refresh_timer.timeout.connect(self.update)
self.update_settings()
self.plot()
self.refresh_time.start(1)
def update_setting(self):
self.settings = self.settings_lineEdit.text()
def update(self)
if self.updating:
print('Already updating, cannot update until previous update complete')
elif not self.updating:
self.update_signal.emit()
def plot(self):
# Plot the data, slow. Uses self.settings
self.load()
...
def load(self):
# Load the data from the appropriate directory/file, slow
...
This gets the rough gist of what I am going for. Ui_PlotWindow implements setupUi. In practices there self.settings stands for a long list of settings variables which can be manipulated by the user. The check on self.updating ensures that requests to update the plot which arrive while the plot is currently updating are terminated rather than added to a queue in the worker thread event loop.
Though my code works, I feel like the pattern I am using with the worker thread is a bit strange. Basically ALL of the information needed is in the PlotWindow class, but since I want to call one of the methods of the PlotWindow class in a separate thread I feel I need a different QObject which can live in a different QThread to house the slot which will run the expensive method.
It just feels a bit roundabout to have a whole seperate class and object just to call a function which already exists in the first class.. However, I am new to threading applications and perhaps this is a normal pattern and not to be worried about?
I would appreciate any advice for how I might be able to make the flow of this function more clear.

The answer is to use a different pattern for threading. Instead of the self.thread = and self.moveToThread for the worker the worker should simply BE a QThread. The thread can be "started" in which case its run method is initiated in a new thread. If a request is made in the main thread to "start" the worker thread again while it is already running nothing will happen. Below is a revision of the above code which implements this pattern.
WARNING: This code is still a bad idea because the plotting involving redrawing GUI objects (the plots). This should not happen in a thread different than the main thread. For posterity I'll state that code in the format in the original question did in fact run but I think that was by luck that the plotting worked in the worker thread. The best pattern would be to have any data loading and processing happen in the worker thread but to then finally emit the data to a plotting method of the main thread to update the GUI with the new data.
class PlotWorker(QtCore.QThread):
def __init__(self, plotwindow):
self.plotwindow = plotwindow
def run(self):
self.plotwindow.plot()
class PlotWindow(Ui_PlotWindow, QtWidgets.QMainWindow):
update_signal = pyqtSignal()
def __init__(self):
self.setupUi(self)
self.settings = self.settings_lineEdit.text()
self.worker = PlotWorker(self)
self.update_settings_pushButton.clicked.connect(self.update_settings)
self.refresh_timer = QtCore.QTimer(self)
self.refresh_timer.timeout.connect(self.update)
self.update_settings()
self.plot()
self.refresh_time.start(1)
def update_setting(self):
self.settings = self.settings_lineEdit.text()
def update(self)
if self.worker.started():
print('Already updating, cannot update until previous update complete')
elif not self.worker.started():
self.worker.start()
def plot(self):
# Plot the data, slow. Uses self.settings
self.load()
...
def load(self):
# Load the data from the appropriate directory/file, slow
...

Related

Running Infinite loops in PyQT [duplicate]

I have a GUI in PyQt with a function addImage(image_path). Easy to imagine, it is called when a new image should be added into a QListWidget. For the detection of new images in a folder, I use a threading.Thread with watchdog to detect file changes in the folder, and this thread then calls addImage directly.
This yields the warning that QPixmap shouldn't be called outside the gui thread, for reasons of thread safety.
What is the best and most simple way to make this threadsafe? QThread? Signal / Slot? QMetaObject.invokeMethod? I only need to pass a string from the thread to addImage.
You should use the built in QThread provided by Qt. You can place your file monitoring code inside a worker class that inherits from QObject so that it can use the Qt Signal/Slot system to pass messages between threads.
class FileMonitor(QObject):
image_signal = QtCore.pyqtSignal(str)
#QtCore.pyqtSlot()
def monitor_images(self):
# I'm guessing this is an infinite while loop that monitors files
while True:
if file_has_changed:
self.image_signal.emit('/path/to/image/file.jpg')
class MyWidget(QtGui.QWidget):
def __init__(self, ...)
...
self.file_monitor = FileMonitor()
self.thread = QtCore.QThread(self)
self.file_monitor.image_signal.connect(self.image_callback)
self.file_monitor.moveToThread(self.thread)
self.thread.started.connect(self.file_monitor.monitor_images)
self.thread.start()
#QtCore.pyqtSlot(str)
def image_callback(self, filepath):
pixmap = QtGui.QPixmap(filepath)
...
I believe the best approach is using the signal/slot mechanism. Here is an example. (Note: see the EDIT below that points out a possible weakness in my approach).
from PyQt4 import QtGui
from PyQt4 import QtCore
# Create the class 'Communicate'. The instance
# from this class shall be used later on for the
# signal/slot mechanism.
class Communicate(QtCore.QObject):
myGUI_signal = QtCore.pyqtSignal(str)
''' End class '''
# Define the function 'myThread'. This function is the so-called
# 'target function' when you create and start your new Thread.
# In other words, this is the function that will run in your new thread.
# 'myThread' expects one argument: the callback function name. That should
# be a function inside your GUI.
def myThread(callbackFunc):
# Setup the signal-slot mechanism.
mySrc = Communicate()
mySrc.myGUI_signal.connect(callbackFunc)
# Endless loop. You typically want the thread
# to run forever.
while(True):
# Do something useful here.
msgForGui = 'This is a message to send to the GUI'
mySrc.myGUI_signal.emit(msgForGui)
# So now the 'callbackFunc' is called, and is fed with 'msgForGui'
# as parameter. That is what you want. You just sent a message to
# your GUI application! - Note: I suppose here that 'callbackFunc'
# is one of the functions in your GUI.
# This procedure is thread safe.
''' End while '''
''' End myThread '''
In your GUI application code, you should create the new Thread, give it the right callback function, and make it run.
from PyQt4 import QtGui
from PyQt4 import QtCore
import sys
import os
# This is the main window from my GUI
class CustomMainWindow(QtGui.QMainWindow):
def __init__(self):
super(CustomMainWindow, self).__init__()
self.setGeometry(300, 300, 2500, 1500)
self.setWindowTitle("my first window")
# ...
self.startTheThread()
''''''
def theCallbackFunc(self, msg):
print('the thread has sent this message to the GUI:')
print(msg)
print('---------')
''''''
def startTheThread(self):
# Create the new thread. The target function is 'myThread'. The
# function we created in the beginning.
t = threading.Thread(name = 'myThread', target = myThread, args = (self.theCallbackFunc))
t.start()
''''''
''' End CustomMainWindow '''
# This is the startup code.
if __name__== '__main__':
app = QtGui.QApplication(sys.argv)
QtGui.QApplication.setStyle(QtGui.QStyleFactory.create('Plastique'))
myGUI = CustomMainWindow()
sys.exit(app.exec_())
''' End Main '''
EDIT
Mr. three_pineapples and Mr. Brendan Abel pointed out a weakness in my approach. Indeed, the approach works fine for this particular case, because you generate / emit the signal directly. When you deal with built-in Qt signals on buttons and widgets, you should take another approach (as specified in the answer of Mr. Brendan Abel).
Mr. three_pineapples adviced me to start a new topic in StackOverflow to make a comparison between the several approaches of thread-safe communication with a GUI. I will dig into the matter, and do that tomorrow :-)

QObject and QThread relations

I have a pyqt4 gui which allows me to import multiple .csv files. I've created a loop that goes through this list of tuples that have the following parameters (filename + location of file, filename, bool,bool, set of dates in file)=tup.
I've created several classes that my gui frequently refers to in order to pull parameters off a projects profile. Let's call this class profile(). I also have another class that has a lot of functions based on formatting, such as datetime, text edits,etc...let's call this classMyFormatting(). Then I created a QThread class that is created for each file in the list, and this one is called Import_File(QThread). And lets say this class takes in a few parameters for the __init__(self,tup).
My ideal goal is to be able to make an independent instance of MyFormatting() and profile() for the Import_File(QThread). I am trying to get my head around on how to utilize the QObject capabilities to solve this..but I keep getting the error that the thread is being destroyed while still running.
for tup in importedlist:
importfile = Import_File(tup)
self.connect(importfile,QtCore.SIGNAL('savedfile(PyQt_PyObject()'),self.printstuffpassed)
importfile.start()
I was thinking of having the two classes be declared as
MyFormatting(QObject):
def __init__(self):
QObject.__init__(self)
def func1(self,stuff):
dostuff
def func2(self):
morestuff
profile(QObject):
def __init__(self):
QObject.__init__(self)
def func11(self,stuff):
dostuff
def func22(self):
morestuff
AND for the QThread:
Import_File(QThread):
def __init__(self,tup):
QThread.__init(self)
common_calc_stuff = self.calc(tup[4])
f = open(tup[0] + '.csv', 'w')
self.tup = tup
# this is where I thought of pulling an instance just for this thread
self.MF = MyFormatting()
self.MF_thread = QtCore.QThread()
self.MF.moveToThread(self.MF_thread)
self.MF_thread.start()
self.prof = profile()
self.prof_thread = QtCore.QThread()
self.prof.moveToThread(self.prof_thread)
self.prof_thread.start()
def func33(self,stuff):
dostuff
self.prof.func11(tup[4])
def func44(self):
morestuff
def run(self):
if self.tup[3] == True:
self.func33
self.MF.func2
elif self.tup[3] ==False:
self.func44
if self.tup[2] == True:
self.prof.func22
self.emit(QtCore.SIGNAL('savedfile()',)
Am I totally thinking of it the wrong way? How can I keep to somewhat of the same structure that I have for the coding and still be able to implement the multithreading and not have the same resource tapped at the same time, which I think is the reason why my qui keeps crashing? Or how can I make sure that each instance of those objects get turned off that they don't interfere with the other instances?

Implementation of QThread in PyQt5 Application

I wrote a PyQt5 GUI (Python 3.5 on Win7). While it is running, its GUI is not responsive. To still be able to use the GUI, I tried to use QThread from this answer: https://stackoverflow.com/a/6789205/5877928
The module is now designed like this:
class MeasureThread(QThread):
def __init(self):
super().__init__()
def get_data(self):
data = auto.data
def run(self):
time.sleep(600)
# measure some things
with open(outputfile) as f:
write(things)
class Automation(QMainWindow):
[...]
def measure(self):
thread = MeasureThread()
thread.finished.connect(app.exit)
for line in open(inputfile):
thread.get_data()
thread.start()
measure() gets called once per measurement but starts the thread once per line in inputfile. The module now exits almost immediately after starting it (I guess it runs all thread at once and does not sleep) but I only want it to do all the measurements in another single thread so the GUI can still be accessed.
I also tried to apply this to my module, but could not connect the methods used there to my methods: http://www.xyzlang.com/python/PyQT5/pyqt_multithreading.html
The way I used to use the module:
class Automation(QMainWindow):
[...]
def measure(self):
param1, param2 = (1,2)
for line in open(inputfile):
self.measureValues(param1, param2)
def measureValues(self, param1, param2):
time.sleep(600)
# measure some things
with open(outputfile) as f:
write(things)
But that obviously used only one thread. Can you help me to find the right method to use here( QThread, QRunnable) or to map the example of the second link to my module?
Thanks!

Gifs shown inside a label doesn't update itself regulary in Pyqt5

I have a loading widget that consists of two labels, one is the status label and the other one is the label that the animated gif will be shown in. If I call show() method before heavy stuff gets processed, the gif at the loading widget doesn't update itself at all. There's nothing wrong with the gif btw(looping problems etc.). The main code(caller) looks like this:
self.loadingwidget = LoadingWidgetForm()
self.setCentralWidget(self.loadingwidget)
self.loadingwidget.show()
...
...
heavy stuff
...
...
self.loadingwidget.hide()
The widget class:
class LoadingWidgetForm(QWidget, LoadingWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setupUi(self)
self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
pince_directory = SysUtils.get_current_script_directory() # returns current working directory
self.movie = QMovie(pince_directory + "/media/loading_widget_gondola.gif", QByteArray())
self.label_Animated.setMovie(self.movie)
self.movie.setScaledSize(QSize(50, 50))
self.movie.setCacheMode(QMovie.CacheAll)
self.movie.setSpeed(100)
self.movie.start()
self.not_finished=True
self.update_thread = Thread(target=self.update_widget)
self.update_thread.daemon = True
def showEvent(self, QShowEvent):
QApplication.processEvents()
self.update_thread.start()
def hideEvent(self, QHideEvent):
self.not_finished = False
def update_widget(self):
while self.not_finished:
QApplication.processEvents()
As you see I tried to create a seperate thread to avoid workload but it didn't make any difference. Then I tried my luck with the QThread class by overriding the run() method but it also didn't work. But executing QApplication.processEvents() method inside of the heavy stuff works well. I also think I shouldn't be using seperate threads, I feel like there should be a more elegant way to do this. The widget looks like this btw:
Processing...
Full version of the gif:
Thanks in advance! Have a good day.
Edit: I can't move the heavy stuff to a different thread due to bugs in pexpect. Pexpect's spawn() method requires spawned object and any operations related with the spawned object to be in the same thread. I don't want to change the working flow of the whole program
In order to update GUI animations, the main Qt loop (located in the main GUI thread) has to be running and processing events. The Qt event loop can only process a single event at a time, however because handling these events typically takes a very short time control is returned rapidly to the loop. This allows the GUI updates (repaints, including animation etc.) to appear smooth.
A common example is having a button to initiate loading of a file. The button press creates an event which is handled, and passed off to your code (either via events directly, or via signals). Now the main thread is in your long-running code, and the event loop is stalled — and will stay stalled until the long-running job (e.g. file load) is complete.
You're correct that you can solve this with threads, but you've gone about it backwards. You want to put your long-running code in a thread (not your call to processEvents). In fact, calling (or interacting with) the GUI from another thread is a recipe for a crash.
The simplest way to work with threads is to use QRunner and QThreadPool. This allows for multiple execution threads. The following wall of code gives you a custom Worker class that makes it simple to handle this. I normally put this in a file threads.py to keep it out of the way:
import sys
from PyQt5.QtCore import QObject, QRunnable
class WorkerSignals(QObject):
'''
Defines the signals available from a running worker thread.
error
`tuple` (exctype, value, traceback.format_exc() )
result
`dict` data returned from processing
'''
finished = pyqtSignal()
error = pyqtSignal(tuple)
result = pyqtSignal(dict)
class Worker(QRunnable):
'''
Worker thread
Inherits from QRunnable to handler worker thread setup, signals and wrap-up.
:param callback: The function callback to run on this worker thread. Supplied args and
kwargs will be passed through to the runner.
:type callback: function
:param args: Arguments to pass to the callback function
:param kwargs: Keywords to pass to the callback function
'''
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
# Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
#pyqtSlot()
def run(self):
'''
Initialise the runner function with passed args, kwargs.
'''
# Retrieve args/kwargs here; and fire processing using them
try:
result = self.fn(*self.args, **self.kwargs)
except:
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
else:
self.signals.result.emit(result) # Return the result of the processing
finally:
self.signals.finished.emit() # Done
To use the above, you need a QThreadPool to handle the threads. You only need to create this once, for example during application initialisation.
threadpool = QThreadPool()
Now, create a worker by passing in the Python function to execute:
from .threads import Worker # our custom worker Class
worker = Worker(fn=<Python function>) # create a Worker object
Now attach signals to get back the result, or be notified of an error:
worker.signals.error.connect(<Python function error handler>)
worker.signals.result.connect(<Python function result handler>)
Then, to execute this Worker, you can just pass it to the QThreadPool.
threadpool.start(worker)
Everything will take care of itself, with the result of the work returned to the connected signal... and the main GUI loop will be free to do it's thing!

Signal/Slot help-- setting a signal to a slot outside of the current class

I'm trying to populate a table (present in the main window) from a slider that's located in a widget in a separate class. I can't seem to get it to work...what's the best way to go about doing this?
Here's my current code:
class Widget(QWidget):
def __init__(self,filename,parent=None):
super(Widget,self).__init__(parent)
self.resize(900,900)
self.layout=QVBoxLayout(self)
frame=Frame(filename)
self.image=pg.ImageView()
self.image.setImage(frame.data)
self.image.setCurrentIndex(0)
fileheader=FileHeader(filename)
self.slider=QSlider(self)
self.slider.setOrientation(Qt.Horizontal)
self.slider.setMinimum(1)
self.slider.setMaximum(fileheader.numframes)
self.slider.sliderMoved.connect(self.sliderMoved)
self.layout.addWidget(self.image)
self.layout.addWidget(self.slider)
def sliderMoved(self,val):
print "slider moved to:", val
fileheader=FileHeader(filename)
idx=val
frame=fileheader.frameAtIndex(idx)
self.image.setImage(frame.data)
class MainWindow(QMainWindow):
def __init__(self, filename, parent=None):
super(MainWindow,self).__init__(parent)
self.initUI(filename)
def initUI(self,filename):
self.filetable=QTableWidget()
self.frametable=QTableWidget()
self.imageBrowser=Widget(filename)
self.imagesplitter=QSplitter(Qt.Horizontal)
self.tablesplitter=QSplitter(Qt.Horizontal)
self.imagesplitter.addWidget(self.imageBrowser)
self.tablesplitter.addWidget(self.imagesplitter)
self.tablesplitter.addWidget(self.filetable)
self.tablesplitter.addWidget(self.frametable)
self.setCentralWidget(self.tablesplitter)
exitAction=QAction(QIcon('exit.png'),'&Exit',self)
exitAction.setShortcut('Ctrl+Q')
exitAction.triggered.connect(qApp.quit)
openAction=QAction(QIcon('open.png'),'&Open',self)
openAction.setShortcut('Ctrl+O')
menubar=self.menuBar()
fileMenu=menubar.addMenu('&File')
fileMenu.addAction(exitAction)
fileMenu.addAction(openAction)
self.fileheader=FileHeader(filename)
self.connect(self.frametable,
SIGNAL("Widget.sliderMoved(idx)"),
self.fileheader.frameAtIndex(idx))
self.frameheader=self.fileheader.frameAtIndex(0)
self.populate()
def populate(self):
self.filetable.setRowCount(len(self.fileheader.fileheader_fields))
self.filetable.setColumnCount(2)
self.filetable.setHorizontalHeaderLabels(['File Header','value'])
for i,field in enumerate(self.fileheader.fileheader_fields):
name=QTableWidgetItem(field)
value=QTableWidgetItem(unicode(getattr(self.fileheader,field)))
self.filetable.setItem(i,0,name)
self.filetable.setItem(i,1,value)
self.frametable.setRowCount(len(self.frameheader.frameheader_fields))
self.frametable.setColumnCount(2)
self.frametable.setHorizontalHeaderLabels(['Frame Header','Value'])
for i,fields in enumerate(self.frameheader.frameheader_fields):
Name=QTableWidgetItem(fields)
Value=QTableWidgetItem(unicode(getattr(self.frameheader,fields)))
self.frametable.setItem(i,0,Name)
self.frametable.setItem(i,1,Value)
I know the "connect" is wrong-- I'm very new to PyQt and Python in general, so I'm not quite sure where to start.
Since self.imageBrowser is your Widget class, it will have the slider attribute which has the sliderMoved signal. You just need a few more dots.
self.imageBrowser.slider.sliderMoved.connect(self.fileheader.frameAtIndex)
The way you have it organized is correct though. Your main window composes your custom widgets and binds the connections together.
Though because you have a data source, and also a QTableWidget that will need to be updated, you probably need to wrap the steps up into a little method:
def initUI(self,filename):
...
self.imageBrowser.slider.sliderMoved.connect(self._handle_slider_moved)
# initialize it the first time during the window set up
self._handle_slider_moved(0)
def _handle_slider_moved(self, val):
# update the data source
self.fileheader.frameAtIndex(val)
# update the second data source
self.frameheader=self.fileheader.frameAtIndex(0)
# now refresh the tables
self.populate()

Resources