PyQt5 widget Qthread issue when using concurrent.futures.ThreadPoolExecutor() - python

I have been trying to use the concurrent.futures.ThreadPoolExecutor() to run some background tasks in my application, so that I will be able to interact with the GUI while these tasks ("measurements") run. Once these tasks are finished I assign a callback function that updates some fields of the GUI then tries to update the GUI widgets (plots, tables, lists etc.) based on these fields.
Here is an example:
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
*some more code goes here*
self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
def perform_measurement():
future = self.executor.submit(*a function*)
future.add_done_callback(self.update_gui_fields)
def update_gui_fields(self, future):
data = future.result()
self.items_for_list.append(QStandardItem(data['key']))
*more fields are updated here*
self.QListView1.setModel(self.items_for_list)
*more widgets are updated here*
The problem is that the fields are updated normally, but when I try to interact with the widgets the app crashes. This is because the children (here the self.items_for_list) are in a different thread than the parent (here self.QListView1). This is the error that I get:
QObject: Cannot create children for a parent that is in a different thread.
(Parent is QListView(0x555795efbc10), parent's thread is QThread(0x555795296600), current thread is QThread(0x7fd12400a100)
QBasicTimer::start: QBasicTimer can only be used with threads started with QThread
I couldn't find any solution on previous posts. Any idea of how to attack this?
Thanks!

The callback associated with add_done_callback is executed in a secondary thread, and according to your code you are trying to update the GUI from that secondary thread, which is forbidden, so Qt throws that warning. The solution is to implement the logic by creating a QObject that forwards that information through signals:
import concurrent.futures
import sys
import time
from PyQt5 import QtCore, QtGui, QtWidgets
def measure():
time.sleep(5)
return {"key": "value"}
class TaskManager(QtCore.QObject):
finished = QtCore.pyqtSignal(object)
def __init__(self, parent=None, max_workers=None):
super().__init__(parent)
self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=max_workers)
#property
def executor(self):
return self._executor
def submit(self, fn, *args, **kwargs):
future = self.executor.submit(fn, *args, **kwargs)
future.add_done_callback(self._internal_done_callback)
def _internal_done_callback(self, future):
data = future.result()
self.finished.emit(data)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.model = QtGui.QStandardItemModel()
self.view = QtWidgets.QListView()
self.view.setModel(self.model)
self.button = QtWidgets.QPushButton("launch")
self._manager = TaskManager(max_workers=1)
self._manager.finished.connect(self.update_gui_fields)
self.button.clicked.connect(self.perform_measurement)
central_widget = QtWidgets.QWidget()
self.setCentralWidget(central_widget)
lay = QtWidgets.QVBoxLayout(central_widget)
lay.addWidget(self.view)
lay.addWidget(self.button)
def perform_measurement(self):
self._manager.submit(measure)
def update_gui_fields(self, data):
self.model.appendRow(QtGui.QStandardItem(data["key"]))
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())

Related

Python pyqt6 window blocks main loop

I have a small program that does something, but i want to "switch modes", for now i press a key and an input prompts on the console, but to make it easier i want to make a window with pyqt6, the problem is that the window blocks or halts the main loop while it's open, i tried with threading/multiprocessing but i can't make it work.
import threading
from queue import Queue
from PySide6.QtWidgets import *
from PySide6.QtGui import *
from PySide6.QtCore import Qt
queue = Queue()
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
layout = QVBoxLayout()
label = QLabel("Change modes")
btn1 = QPushButton("MODE 1")
btn2 = QPushButton("MODE 2")
layout.addWidget(label)
layout.addWidget(btn1)
layout.addWidget(btn2)
widget = QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)
btn1.clicked.connect(self.mode1)
btn2.clicked.connect(self.mode2)
self.show()
def mode1(self):
queue.put("mode1")
def mode2(self):
queue.put("mode2")
if __name__ == '__main__':
app = QApplication()
window = MainWindow()
app.exec()
mode = "none"
while True:
_mode = queue.get()
if mode != _mode:
mode = _mode;
print(f"mode: {mode}")
# do stuff here
the only way that the while loop executes is when i close the window.
Traditional Python multiprocessing/multithreading libraries such as multiprocessing and threading do not work well with Qt-like (PyQt and PySide) graphical programs. Fortunately, among other solutions, PySide provides the QThread interface, allowing multithreading in PySide graphical interfaces. It can be applied to your program as follows:
import threading
from queue import Queue
from PySide6.QtWidgets import *
from PySide6.QtGui import *
from PySide6.QtCore import Qt, QThread
queue = Queue()
class Worker(QThread):
def __init__(self):
super(Worker, self).__init__()
def run(self):
mode = "none"
while True:
_mode = queue.get()
if mode != _mode:
mode = _mode;
print(f"mode: {mode}")
# do stuff here
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
layout = QVBoxLayout()
label = QLabel("Change modes")
btn1 = QPushButton("MODE 1")
btn2 = QPushButton("MODE 2")
layout.addWidget(label)
layout.addWidget(btn1)
layout.addWidget(btn2)
widget = QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)
btn1.clicked.connect(self.mode1)
btn2.clicked.connect(self.mode2)
self.show()
self.worker = Worker() # Create a Worker instance
self.worker.start() # Start the Worker instance (which calls the run function of the Worker instance)
def mode1(self):
queue.put("mode1")
def mode2(self):
queue.put("mode2")
def closeEvent(self, event):
self.worker.terminate() # When the window closes, stop the thread
if __name__ == '__main__':
app = QApplication()
window = MainWindow()
app.exec()
Please note the changed import statement of PySide6.QtCore (to import QThread), the addition of the self.worker variable in the __init__ function of the MainWindow class (to actually start the thread), as well as the addition of a closeEvent function in the MainWindow class (to terminate the thread when the window closes).

Problems getting QTimer to start when using QThread

I'm trying to implement a webcam using PyQt5 from an example I found (here, but not really relevant).
Getting the example to work wasn't an issue, but I wanted to modify some things and I am stuck on one particular problem.
I have two classes, one QObject Capture which has a QBasicTimer that I want to start, and a QWidget MyWidget with a button that is supposed to start the timer of the Capture object, which is inside a QThread.
If I directly connect the button click to the method that starts the timer, everything works fine.
But I want to do some other things when I click the button, so I connected the button to a method of MyWidget first and call the start method of Capture from there. This, however, doesn't work: the timer doesn't start.
Here is a minimal working example:
from PyQt5 import QtCore, QtWidgets
import sys
class Capture(QtCore.QObject):
def __init__(self, parent=None):
super(Capture, self).__init__(parent)
self.m_timer = QtCore.QBasicTimer()
def start(self):
print("capture start called")
self.m_timer.start(1000, self)
def timerEvent(self, event):
print("time event")
class MyWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(MyWidget, self).__init__(parent)
lay = QtWidgets.QVBoxLayout(self)
self.btn_start = QtWidgets.QPushButton("Start")
lay.addWidget(self.btn_start)
self.capture = Capture()
captureThread = QtCore.QThread(self)
captureThread.start()
self.capture.moveToThread(captureThread)
# self.btn_start.clicked.connect(self.capture.start) # this works
self.btn_start.clicked.connect(self.startCapture) # this doesn't
# self.capture.start() # this doesn't either
self.show()
def startCapture(self):
self.capture.start()
def run_app():
app = QtWidgets.QApplication(sys.argv)
mainWin = MyWidget()
mainWin.show()
app.exec_()
run_app()
It is some problem with the QThread, because if I don't use threading it works. I thought maybe it has something to do with the thread not being in some way available when called from a different method than the one it was created in, but calling self.capture.start() directly from the init does not work either.
I only have a very basic grasp of threads. Can someone tell me how I can properly call self.capture.start() from MyWidget and why it works without problems when directly connecting it to the button click?
If you connect the button's clicked signal to the worker's start slot, Qt will automatically detect that it's a cross-thread connection. When the signal is eventually emitted, it will be queued in the receiving thread's event-queue, which ensures the slot will be called within the worker thread.
However, if you connect the button's clicked signal to the startCapture slot, there's no cross-thread connection, because the slot belongs to MyWidget (which lives in the main thread). When the signal is emitted this time, the slot tries to create the timer from within the main thread, which is not supported. Timers must always be started within the thread that creates them (otherwise Qt will print a message like "QBasicTimer::start: Timers cannot be started from another thread").
A better approach is to connect the started and finished signals of the thread to some start and stop slots in the worker, and then call the thread's start and quit methods to control the worker. Here's a demo based on your script, which shows how to implement that:
from PyQt5 import QtCore, QtWidgets
import sys
class Capture(QtCore.QObject):
def __init__(self, parent=None):
super(Capture, self).__init__(parent)
self.m_timer = QtCore.QBasicTimer()
def start(self):
print("capture start called")
self.m_timer.start(1000, self)
def stop(self):
print("capture stop called")
self.m_timer.stop()
def timerEvent(self, event):
print("time event")
class MyWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(MyWidget, self).__init__(parent)
lay = QtWidgets.QVBoxLayout(self)
self.btn_start = QtWidgets.QPushButton("Start")
lay.addWidget(self.btn_start)
self.capture = Capture()
self.captureThread = QtCore.QThread(self)
self.capture.moveToThread(self.captureThread)
self.captureThread.started.connect(self.capture.start)
self.captureThread.finished.connect(self.capture.stop)
self.btn_start.clicked.connect(self.startCapture)
self.show()
def startCapture(self):
if not self.captureThread.isRunning():
self.btn_start.setText('Stop')
self.captureThread.start()
else:
self.btn_start.setText('Start')
self.stopCapture()
def stopCapture(self):
self.captureThread.quit()
self.captureThread.wait()
def closeEvent(self, event):
self.stopCapture()
def run_app():
app = QtWidgets.QApplication(sys.argv)
mainWin = MyWidget()
mainWin.show()
app.exec_()
run_app()

Terminate QThread when GUI exits

I am using QThreads to run a function in the background, but when I exit the GUI application, the QThread still continues to run.
There are examples about C++ but I do not know how to implement them in python
class PF35Thread(QtCore.QThread):
signalPF35 = pyqtSignal()
def __init__(self, parent = None):
super().__init__(parent)
def run(self):
newcase = newcaseList[-1]
os.system('EAZ{0}(3,5).EAZ{0}(3,5).OUT'.format(newcase))
self.signalPF35.emit()
How do I terminate QThread when GUI closes?
I think that in your case the thread is only necessary because os.system() is blocking, but if you use QProcess it does a similar task and you do not need the thread, it also allows easier management with the Qt eventloop.
from PyQt5 import QtCore, QtGui, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
signalPF35 = QtCore.pyqtSignal()
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
button = QtWidgets.QPushButton("Start task", clicked=self.on_clicked)
self.setCentralWidget(button)
self._process = QtCore.QProcess(self)
self._process.finished.connect(self.on_finished)
#QtCore.pyqtSlot()
def on_clicked(self):
newcase = newcaseList[-1]
self._process.start('EAZ{0}(3,5).EAZ{0}(3,5).OUT'.format(newcase))
#QtCore.pyqtSlot()
def on_finished(self):
self.signalPF35.emit()
def closeEvent(self, event):
self._process.kill()
super(MainWindow, self).closeEvent(event)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())

PyQt5: Timer in a thread

Problem Description
I'm trying to make an application that collects data, processes it, displays it, and some actuation (open/close valves, etc). As a practice for future applications where I have some stricter time constraints, I want to run the data capture and processing in a separate thread.
My current problem is that it's telling me I cannot start a timer from another thread.
Current code progress
import sys
import PyQt5
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QThread, pyqtSignal
# This is our window from QtCreator
import mainwindow_auto
#thread to capture the process data
class DataCaptureThread(QThread):
def collectProcessData():
print ("Collecting Process Data")
#declaring the timer
dataCollectionTimer = PyQt5.QtCore.QTimer()
dataCollectionTimer.timeout.connect(collectProcessData)
def __init__(self):
QThread.__init__(self)
def run(self):
self.dataCollectionTimer.start(1000);
class MainWindow(QMainWindow, mainwindow_auto.Ui_MainWindow):
def __init__(self):
super(self.__class__, self).__init__()
self.setupUi(self) # gets defined in the UI file
self.btnStart.clicked.connect(self.pressedStartBtn)
self.btnStop.clicked.connect(self.pressedStopBtn)
def pressedStartBtn(self):
self.lblAction.setText("STARTED")
self.dataCollectionThread = DataCaptureThread()
self.dataCollectionThread.start()
def pressedStopBtn(self):
self.lblAction.setText("STOPPED")
self.dataCollectionThread.terminate()
def main():
# a new app instance
app = QApplication(sys.argv)
form = MainWindow()
form.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Any advice on how to get this to work would be appreciated!
You have to move the QTimer to the DataCaptureThread thread, in addition to that when the run method ends, the thread is eliminated so the timer is eliminated, so you must avoid running that function without blocking other tasks. QEventLoop is used for this:
class DataCaptureThread(QThread):
def collectProcessData(self):
print ("Collecting Process Data")
def __init__(self, *args, **kwargs):
QThread.__init__(self, *args, **kwargs)
self.dataCollectionTimer = QTimer()
self.dataCollectionTimer.moveToThread(self)
self.dataCollectionTimer.timeout.connect(self.collectProcessData)
def run(self):
self.dataCollectionTimer.start(1000)
loop = QEventLoop()
loop.exec_()

Using Signals for communicating between classes

I want to use signals for communicating between my view and my application controller. I have following approach but since I'm beginner in PyQt I don't know if that is the right one. Can anyone tell me If I am on the right path or are there better solutions?
EDIT: I have changed the example to a fully working example.
import sys
from PyQt4 import QtGui, QtCore
class View(QtGui.QMainWindow):
sigFooChanged = QtCore.pyqtSignal()
sigBarChanged = QtCore.pyqtSignal()
def __init__(self):
QtGui.QMainWindow.__init__(self)
central_widget = QtGui.QWidget()
central_layout = QtGui.QHBoxLayout()
self.__cbFoo = QtGui.QComboBox()
self.__cbBar = QtGui.QComboBox()
self.__cbFoo.currentIndexChanged[str].connect(lambda x: self.sigFooChanged.emit())
self.__cbBar.currentIndexChanged[str].connect(lambda x: self.sigBarChanged.emit())
central_layout.addWidget(QtGui.QLabel("Foo:"))
central_layout.addWidget(self.__cbFoo)
central_layout.addWidget(QtGui.QLabel("Bar:"))
central_layout.addWidget(self.__cbBar)
central_widget.setLayout(central_layout)
self.setCentralWidget(central_widget)
def setFooModel(self, model):
self.__cbFoo.setModel(model)
def setBarModel(self, model):
self.__cbBar.setModel(model)
class Controller:
def __init__(self, view):
self.__view = view
# Connect all signals from view with according handlers
self.__view.sigFooChanged.connect(self.handleFooChanged)
self.__view.sigBarChanged.connect(self.handleBarChanged)
self.__fooModel = QtGui.QStringListModel(["Foo1", "Foo2", "Foo3"])
self.__barModel = QtGui.QStringListModel(["Bar1", "Bar2", "Bar3"])
self.__view.setFooModel(self.__fooModel)
self.__view.setBarModel(self.__barModel)
def handleFooChanged(self):
print("Foo Changed")
def handleBarChanged(self):
print("Bar Changed")
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
view = View()
controller = Controller(view)
view.show()
sys.exit(app.exec_())
Personally, I don't create a separate generic controller class like that. It could be my own preference, but I tend to consider the actual QWidget class my controller, and the view is usually the GUI-only definitions that I generate from QtDesigner (Ui_Dialog for example), or create manually. And I make all my connections in the relevant QWidget.
Now on to your code, I don't know if you are just considering this snippet a general pseudocode example of the direction you are taking...but it has errors... I would normally suggest posting working code so people don't get confused as to whether you are having errors because of it, or just asking if its generally a correct direction to laying out code.
You are forgetting to call __init__() on the QMainWindow superclass.
I'm not sure what controller.show() would do (fail as of right now) because I don't see an example of how you intend to forward that show() command to your main window object? Again I don't really see why its even necessary to have that separate class.
Here is how I would see a more realistic example, again considering the QWidget classes themselves to be the controllers:
View
## mainUI.py ##
from PyQt4 import QtCore, QtGui
class Ui_MyWidget(object):
def setupUi(self, obj):
obj.layout = QtGui.QVBoxLayout(obj)
obj.cbFoo = QtGui.QComboBox()
obj.cbBar = QtGui.QComboBox()
obj.layout.addWidget(obj.cbFoo)
obj.layout.addWidget(obj.cbBar)
Non-Gui Library Module (Controller)
## nonGuiModule.py ##
class LibModule(object):
def handleBarChanged(self, *args):
print("Bar Changed: %s" % args)
Controller (any entry point)
## main.py ##
import sys
from PyQt4 import QtCore, QtGui
from mainUI import Ui_MyWidget
from nonGuiModule import LibModule
class Main(QtGui.QMainWindow):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
self.resize(640,480)
self._lib = LibModule()
self.myWidget = MyWidget(self)
self.setCentralWidget(self.myWidget)
self.myWidget.sigFooChanged.connect(self.handleFooChanged)
self.myWidget.sigBarChanged.connect(self._lib.handleBarChanged)
def handleFooChanged(self, *args):
print("Foo Changed: %s" % args)
class MyWidget(QtGui.QFrame, Ui_MyWidget):
sigFooChanged = QtCore.pyqtSignal(str)
sigBarChanged = QtCore.pyqtSignal(str)
def __init__(self, parent=None):
super(MyWidget, self).__init__(parent)
# this is where you set up from the view
self.setupUi(self)
self.cbFoo.addItems(['Foo1', 'Foo2'])
self.cbBar.addItems(['Bar1', 'Bar2'])
self.layout.addWidget(self.cbFoo)
self.layout.addWidget(self.cbBar)
# going to forward private signals to public signals
self.cbFoo.currentIndexChanged[str].connect(self.sigFooChanged)
self.cbBar.currentIndexChanged[str].connect(self.sigBarChanged)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv[1:])
view = Main()
view.show()
sys.exit(app.exec_())

Categories

Resources