PyQt5: Timer in a thread - python

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

Related

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

How to structure large pyqt5 GUI without subclassing QThread and using QPushButtons to do long-running tasks

I'm looking at creating a program with a PyQt5 GUI. The program will start with a UI with numerous buttons. These buttons will be used to open other programs/completed long running tasks. I know I need to use QThread, but I am unsure how to structure the programs so that it scales properly.
I've been at this for ages and have read numerous posts/tutorials. Most lean down the subclassing route. In the past, I have managed to create a working program subclassing QThread, but I have since read that this metholodogy is not preferred.
I have a feeling I should be creating a generic worker and passing in a function with *args and **kwargs, but that is not in my skillset yet.
I originally created a thread for each button during the GUI init, but that seemed like it was going to get out of hand quickly.
I am currently at the stage of creating a thread under the slot connected to the button.clicked signal. I am not sure if I then have to have a worker for each button or if I can/should make a generic worker and pass in a function. Note: I have tried to do this but have not been able to do it.
#Import standard modules
import sys
#Import third-party modles
from PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QMainWindow, QApplication, QPushButton, QVBoxLayout, QWidget
class Worker(QObject):
#Custom signals?? or built-in QThread signals?
started = pyqtSignal()
finished = pyqtSignal()
def __init__(self):
super().__init__()
self.started.emit()
#pyqtSlot()
def do_something(self):
for _ in range(3):
print('Threading...')
QThread.sleep(1)
self.finished.emit()
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.initUi()
def initUi(self):
#Create GUI
self.centralWidget = QWidget()
self.setCentralWidget(self.centralWidget )
self.vertical_layout = QVBoxLayout(self.centralWidget)
self.setWindowTitle('QThread Test')
self.setGeometry(300, 300, 300, 50)
self.button1=QPushButton("Task 1", self, clicked=self._task1_clicked)
self.button2=QPushButton("Task 2", self, clicked=self._task2_clicked)
self.vertical_layout.addWidget(self.button1)
self.vertical_layout.addWidget(self.button2)
self.vertical_layout.addStretch()
def _task1_clicked(self):
print('task1 clicked')
#Create the worker
self.my_worker = Worker()
#Create thread; needs to be done before connecting signals/slots
self.task1_thread = QThread()
#Move the worker to the thread
self.my_worker.moveToThread(self.task1_thread)
#Connect worker and thread signals to slots
self.task1_thread.started.connect(self._thread_started)
self.task1_thread.started.connect(self.my_worker.do_something)
self.my_worker.finished.connect(self._thread_finished)
#Start thread
self.task1_thread.start()
def _task2_clicked(self):
print('task2 clicked')
def _thread_started(self):
print('thread started')
def _thread_finished(self):
print('thread finished')
self.my_worker.isRunning = False
self.task1_thread.quit()
self.task1_thread.wait()
print('The thread is running: ' + str(self.task1_thread.isRunning()))
if __name__ == '__main__':
app = QApplication(sys.argv)
form = Window()
form.show()
app.exec_()
The above seems to work, but I feel like I have stumbled on to it and it is not the correct way of doing this. I do not want this to be my 'go-to' method if it is completely wrong. I'd like to be able to generate more complicated (more buttons doing things) programs compared to a one button/one task program.
In addition, I can't seem to get the QThread started and finished signals to fire without basically making them custom built signals. This is one reason I think I am going about this wrong.
from PyQt5 import QtCore
class AsyncTask(QtCore.QThread):
taskDone = QtCore.pyqtSignal(dict)
def __init__(self, *, task, callback=None, parent = None):
super().__init__(parent)
self.task = task
if callback != None:
self.taskDone.connect(callback)
if callback == None:
callback = self.callback
self.start()
def run(self):
try:
result = self.task()
print(result)
self.taskDone.emit(result)
except Exception as ex:
print(ex)
def callback(self):
print('callback')
Please try code above, call like this:
AsyncTask(task=yourTaskFunction, callback=yourCallbackFunction)

PyQt5: How to connect() & emit()

I am trying to port code from PyQt4 to PyQt5 and am not understanding why the following does not work. The slot is not being called. I see a bunch of ticks and no tocks. What am I missing?
from PyQt5 import QtCore
import time
# expect to see ticks & tocks
class Alarm(QtCore.QThread, QtCore.QObject):
signal = QtCore.pyqtSignal()
def __init__(self, parent=None):
super(Alarm, self).__init__(parent)
self.signal.connect(self.eventp)
self.start()
def run(self):
while True:
print('tick')
self.signal.emit()
time.sleep(1)
#QtCore.pyqtSlot()
def eventp(self):
print('Tock')
# main
alarm = Alarm()
time.sleep(6) # wait for countdown, then terminate
First QThread already inherits from QObject so it is not necessary to use it as an interface. On the other hand the QThread must live in an eventloop since that is what allows the transmission of the signals, for example in your case you are blocking the eventloop with the time.sleep(6), instead if you want to finish the loop after 6 seconds use a QTimer:
import time
from PyQt5 import QtCore
# expect to see ticks & tocks
class Alarm(QtCore.QThread):
signal = QtCore.pyqtSignal()
def __init__(self, parent=None):
super(Alarm, self).__init__(parent)
self.signal.connect(self.eventp)
self.start()
def run(self):
while True:
print('tick')
self.signal.emit()
time.sleep(1)
#QtCore.pyqtSlot()
def eventp(self):
print('Tock')
if __name__ == '__main__':
import sys
app = QtCore.QCoreApplication(sys.argv)
alarm = Alarm()
QtCore.QTimer.singleShot(6*1000, QtCore.QCoreApplication.quit)
sys.exit(app.exec_())

Redirecting stdout from a secondary thread (multithreading with a function instead of class?)

I am trying to get my stdout displayed on a QTextEdit made via Qt Designer (PyQt5). Actually I made it work yet it doesn't show the info at the same time it was made. Instead it waits for the process to completely end and only then it shows all the information at once. I understand that this should be solved via threading. Also since the QTextEdit (itself) is a GUI element i need a different approach. I found the answer I was looking for here:
This question is referenced to:
Redirecting stdout and stderr to a PyQt4 QTextEdit from a secondary thread
#three_pineapples provided the answer.
My question is pretty much exactly the same, thus the answer is also correct. But my scenario is a little different and I'm having trouble making it work.
In all the threading answers I only see them using Classes. But the thing is in my main class I have a function that does all the stuff that would be printed on the QTextEdit. Sometimes it takes minutes to complete. I am looking for a way for the example code below to work using the answer provided by #three_pineapples.
Here is the example code:
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtGui import QTextCursor
from ui_form import Ui_Form
class EmittingStream(QObject): # test
textWritten = pyqtSignal(str)
def write(self, text):
self.textWritten.emit(str(text))
class Form(QMainWindow):
def __init__(self, parent=None):
super(Form, self).__init__(parent)
# Install the custom output stream
sys.stdout = EmittingStream(textWritten=self.normalOutputWritten) # test
self.ui = Ui_Form()
self.ui.setupUi(self)
self.ui.pushButton_text.clicked.connect(self.test_write)
def __del__(self): # test
# Restore sys.stdout
sys.stdout = sys.__stdout__
def normalOutputWritten(self, text): # test
"""Append text to the QTextEdit."""
cursor = self.ui.textEdit.textCursor()
cursor.movePosition(QTextCursor.End)
cursor.insertText(text)
self.ui.textEdit.setTextCursor(cursor)
self.ui.textEdit.ensureCursorVisible()
def test_write(self): # this is a long, complicated function. its nested in this class. I don't have a way to get it out as a different class.
print("something written")
def main():
app = QApplication(sys.argv)
form = Form()
form.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Is there a way to get the provided solution to work -directly- on the (test_write) function in my main class? How would I implement it?
To make it more clear, from the reference link "LongRunningThing" class is not available for me. The function that needs to run on a separate thread is within the main class (named Form() in the example code). Perhaps a nested class could be used that encapsulates the test_write function inside my main class? Is that even possible?
For this case you can use the native threading of python:
import sys
import threading
import time
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtGui import QTextCursor
from ui_form import Ui_Form
class EmittingStream(QObject): # test
textWritten = pyqtSignal(str)
def write(self, text):
self.textWritten.emit(str(text))
class Form(QMainWindow):
finished = pyqtSignal()
def __init__(self, parent=None):
super(Form, self).__init__(parent)
# Install the custom output stream
sys.stdout = EmittingStream(textWritten=self.normalOutputWritten) # test
self.ui = Ui_Form()
self.ui.setupUi(self)
self.ui.pushButton_text.clicked.connect(self.start_task)
self.finished.connect(lambda: self.ui.pushButton_text.setEnabled(True))
def start_task(self):
var = self.ui.lineEdit.text()
self.thread = threading.Thread(target=self.test_write, args=(args, ))
self.thread.start()
self.ui.pushButton_text.setEnabled(False)
def __del__(self): # test
# Restore sys.stdout
sys.stdout = sys.__stdout__
def normalOutputWritten(self, text): # test
"""Append text to the QTextEdit."""
cursor = self.ui.textEdit.textCursor()
cursor.movePosition(QTextCursor.End)
cursor.insertText(text)
self.ui.textEdit.setTextCursor(cursor)
self.ui.textEdit.ensureCursorVisible()
def test_write(self, *args):
var1 = args[0]
print("something written")
time.sleep(5) # simulate expensive task
print("something written ----")
self.finished.emit()
def main():
app = QApplication(sys.argv)
form = Form()
form.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

PyQt5 GUI only updates when screen is clicked off and back on

I am running into an issue when I am taking in values over serial and then attempting to update my Gui with those values. Unfortunately, even though the values update correctly, I am unable to get to the screen to refresh unless I click off of it and then back on to it. I have tried repaint, update, and processEvents() but have been unable to solve the problem.
Here is the code I am working with:
import sys
import serial
import time
import requests
import PyQt5
from PyQt5.QtWidgets import *
from PyQt5.QtCore import*
from PyQt5.QtGui import *
import mainwindow_auto
CUSTOM_EVENT = 1000
ser = serial.Serial('/dev/ttyACM0', 9600)
class TestThread(QThread):
def __init__(self, target):
QThread.__init__(self)
self.target = target
def run(self):
while True:
QApplication.postEvent(self.target, QEvent(QEvent.Type(CUSTOM_EVENT)))
QApplication.processEvents()
QThread.sleep(15)
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.thread = TestThread(self)
self.thread.start()
def event(s, e):
if(e.type() == CUSTOM_EVENT):
print("Readline: ",int(ser.readline()))
SOC = int(ser.readline())
s.lcdNumber.display(SOC)
s.progressBar.setValue(SOC)
print("SOC: ",SOC)
print(s.lcdNumber.value())
return True
def main():
app = QApplication(sys.argv)
form = MainWindow()
form.lcdNumber.display(30)
form.progressBar.setValue(30)
form.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Thanks in advance!
Since you already have an I/O thread, let it handle the I/O and sent the received value to the main thread via a signal.
No need for a custom event, no I/O on the main thread.
Just adding a signal to the thread subclass and connecting a slot to that before starting the thread.
Rather than rewriting the code that I had above, I ended up fixing it by force redrawing using s.hide() and s.show() after updating the values in the event code. It forced a redraw that otherwise refused to work.
s.lcdNumber.display(SOC)
s.progressBar.setValue(SOC)
s.hide()
s.show()
As suggested by #KevinKrammer, this is simple to do with a custom signal:
class TestThread(QThread):
serialUpdate = pyqtSignal(int)
def run(self):
while True:
QThread.sleep(1)
value = int(ser.readline())
self.serialUpdate.emit(value)
class MainWindow(QMainWindow, mainwindow_auto.Ui_MainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setupUi(self)
self.thread = TestThread(self)
self.thread.serialUpdate.connect(self.handleSerialUpdate)
self.thread.start()
def handleSerialUpdate(self, value):
print("Readline: ", value)
self.lcdNumber.display(value)
self.progressBar.setValue(value)

Categories

Resources