I have this simple program where I want to have a modal, non-blocking progress window (using a QProgressDialog) that is remotely updated. SIZE simply controls the maximum value of the QProgressDialog. However, if I set it to have a value of 4 or less, the window looks like this during the entire duration of the action:
In other words, the window is completely white and displays no text nor progress bar. If I set the value of SIZE to 5 or more, the display works correctly, but only after the 2-3 first iterations:
and later
import sys, time
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
SIZE = 5
def doGenerate(setValue):
for x2 in range(SIZE):
time.sleep(1)
setValue(x2 + 1)
print('Done')
class MainMenu(QMainWindow):
def __init__(self):
super().__init__()
self.genAudioButton = QPushButton('Generate', self)
self.genAudioButton.clicked.connect(self.generate)
self.setCentralWidget(self.genAudioButton)
self.show()
def generate(self):
try:
progress = QProgressDialog('Work in progress', '', 0, SIZE, self)
progress.setWindowTitle("Generating files...")
progress.setWindowModality(Qt.WindowModal)
progress.show()
progress.setValue(0)
doGenerate(progress.setValue)
except Exception as e:
errBox = QMessageBox()
errBox.setWindowTitle('Error')
errBox.setText('Error: ' + str(e))
errBox.addButton(QMessageBox.Ok)
errBox.exec()
return
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = MainMenu()
ret = app.exec_()
sys.exit(ret)
What is causing this and how can I fix it?
Additionally, is there a way to completely remove the cancel button, instead of having an empty button that still cancels the action? The PyQt4 docs (I am using PyQt5) indicate that an empty string should achieve this result, and the C++ docs for Qt5 indicate the same, but that clearly doesn't work here. I haven't found standalone documentation for PyQt5.
The GUI implements a mainloop through app.exec_(), this loop is used to perform tasks such as checking events, signals, calling some functions, etc. so if we interrupt the loop we can get unexpected behavior like the one you observe. in your case sleep() is a blocking function that should not be used, Qt offers alternatives to it, and one of them is to use a QEventLoop with a QTimer:
def doGenerate(setValue):
for x2 in range(SIZE):
loop = QEventLoop()
QTimer.singleShot(1000, loop.quit)
loop.exec_()
setValue(x2 + 1)
print('Done')
If you want the cancel button not to show, you must pass None:
progress = QProgressDialog('Work in progress', None, 0, SIZE, self)
If you want to use gTTS you must do it through threads, Qt offers several ways to implement it, in this case I will use QThreadPool with QRunnable. We will use the QMetaObject.invokeMethod to update the values of the GUI since Qt prohibits the update of the GUI from another thread that is not from the main thread.
import sys, time
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from gtts import gTTS
class GTTSRunnable(QRunnable):
def __init__(self, data, progress):
QRunnable.__init__(self)
self.data = data
self.w = progress
def run(self):
for i, val in enumerate(self.data):
text, filename = val
tts = gTTS(text=text, lang='en')
tts.save(filename)
QMetaObject.invokeMethod(self.w, "setValue",
Qt.QueuedConnection, Q_ARG(int, i+1))
QThread.msleep(10)
class MainMenu(QMainWindow):
def __init__(self):
super().__init__()
self.genAudioButton = QPushButton('Generate', self)
self.genAudioButton.clicked.connect(self.generate)
self.setCentralWidget(self.genAudioButton)
self.show()
def generate(self):
try:
info = [("hello", "1.mp4"), ("how are you?", "2.mp4"), ("StackOverFlow", "3.mp4")]
self.progress = QProgressDialog('Work in progress', '', 0, len(info), self)
self.progress.setWindowTitle("Generating files...")
self.progress.setWindowModality(Qt.WindowModal)
self.progress.show()
self.progress.setValue(0)
self.doGenerate(info)
except Exception as e:
errBox = QMessageBox()
errBox.setWindowTitle('Error')
errBox.setText('Error: ' + str(e))
errBox.addButton(QMessageBox.Ok)
errBox.exec()
return
def doGenerate(self, data):
self.runnable = GTTSRunnable(data, self.progress)
QThreadPool.globalInstance().start(self.runnable)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = MainMenu()
ret = app.exec_()
sys.exit(ret)
This might be useful to anyone using Quamash/asyncio for async applications.
It takes #eyllanesc example and dispatches a CPU bound task in an executor and removes the dependency on Gtts.
Also for my purpose, I don't know how long the CPU bound will take, so I have set the min and max value of the progress dialog both to zero. This has the nice effect of just animating the progress bar until the task is completed. However, one has to manually call the cancel() method when doing this because the progress dialog cannot know when it is completed. This is done in a callback attached to the future.
def main():
import sys
import time
import quamash
import asyncio
import concurrent
import logging
import random
import PyQt5
# Integrate event loops
app = PyQt5.QtWidgets.QApplication(sys.argv)
loop = quamash.QEventLoop(app)
asyncio.set_event_loop(loop)
loop.set_debug(False) # optional
# Config logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger('quamash').setLevel(logging.ERROR)
# Print exception before crash!
def except_hook(cls, exception, traceback):
sys.__excepthook__(cls, exception, traceback)
sys.excepthook = except_hook
class MainWindow(PyQt5.QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.exitRequest = asyncio.Event()
self.genAudioButton = PyQt5.QtWidgets.QPushButton('Generate', self)
self.genAudioButton.clicked.connect(self.generate)
self.setCentralWidget(self.genAudioButton)
self.show()
def generate(self):
self.progress = PyQt5.QtWidgets.QProgressDialog('Work in progress...', None, 0, 0, self)
self.progress.setWindowTitle("Calculation")
self.progress.setWindowModality(PyQt5.QtCore.Qt.WindowModal)
self.progress.show()
self.progress.setValue(0)
# As the loop to run the coroutine
loop = asyncio.get_event_loop()
loop.create_task(self.doGenerate())
def closeEvent(self, event):
""" Called when the windows closes.
"""
self.exitRequest.set()
def cpuBound(self):
""" Just wait 2s or raise an exception 50% of the time to test error handling.
"""
# %50 change of raising an exception
time.sleep(1.0)
if random.random() < 0.5:
time.sleep(1.0)
else:
raise RuntimeError(
("If the CPU bound task fails you can raise "
"an exception that can be caught and displayed"
" like this!")
)
def onComplete(self, future):
""" Callback which contains the future that has completed.
"""
# Dismiss the progress popup widget before we (possibly)
# display a popup with an error message.
self.progress.cancel()
# Check if we got a result or an exception!
try:
result = future.result()
except Exception as e:
errBox = PyQt5.QtWidgets.QMessageBox()
errBox.setWindowTitle('Error')
errBox.setText('Error: ' + str(e))
errBox.addButton(PyQt5.QtWidgets.QMessageBox.Ok)
errBox.exec()
async def doGenerate(self):
""" The coroutine that is added to the event loop when the button is pressed.
"""
loop = asyncio.get_event_loop()
with concurrent.futures.ThreadPoolExecutor() as pool:
future = loop.run_in_executor(pool, self.cpuBound)
# This call back handles the result or possible exception
future.add_done_callback(self.onComplete)
# Block here until complete
result = await future
# Startup application
_window = MainWindow()
_window.show()
with loop:
loop.run_until_complete(_window.exitRequest.wait())
if __name__ == '__main__':
main()
I was able to solve the same problem by calling QtGui.QApplication.processEvents() after i set the value to refresh the QProgressDialog
progress.setValue(i)
QApplication.processEvents()
Almost exactly the problem that led me here. Blank, white dialog, then suddenly it displays correctly but as if 2 or 3 iterations have taken place.
The solution for me makes little sense...
progress = QProgressDialog('Work in progress', '', 0, SIZE, self)
progress.setWindowTitle("Generating files...")
progress.setWindowModality(Qt.WindowModal)
progress.setValue(0)
progress.setValue(1)
progress.setValue(0)
It is almost as if the first setValue gives the blank dialog, and the next two perform the first two iterations so the first real iteration has a correctly displaying dialog to update...
Related
I am trying to figure out why this code crashes if I try to run the threads for a second time once they are completed.
The first time I click "Start 5 Threads" It runs just fine and finishes. But if I click it again. The entire program crashes and I get the QThread: Destroyed while thread is still running Error
This code was found on the web. I am trying to learn from it.
import time
import sys
from PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QApplication, QPushButton, QTextEdit, QVBoxLayout, QWidget
def trap_exc_during_debug(*args):
# when app raises uncaught exception, print info
print(args)
# install exception hook: without this, uncaught exception would cause application to exit
sys.excepthook = trap_exc_during_debug
class Worker(QObject):
"""
Must derive from QObject in order to emit signals, connect slots to other signals, and operate in a QThread.
"""
sig_step = pyqtSignal(int, str) # worker id, step description: emitted every step through work() loop
sig_done = pyqtSignal(int) # worker id: emitted at end of work()
sig_msg = pyqtSignal(str) # message to be shown to user
def __init__(self, id: int):
super().__init__()
self.__id = id
self.__abort = False
#pyqtSlot()
def work(self):
"""
Pretend this worker method does work that takes a long time. During this time, the thread's
event loop is blocked, except if the application's processEvents() is called: this gives every
thread (incl. main) a chance to process events, which in this sample means processing signals
received from GUI (such as abort).
"""
thread_name = QThread.currentThread().objectName()
thread_id = int(QThread.currentThreadId()) # cast to int() is necessary
self.sig_msg.emit('Running worker #{} from thread "{}" (#{})'.format(self.__id, thread_name, thread_id))
for step in range(100):
time.sleep(0.1)
self.sig_step.emit(self.__id, 'step ' + str(step))
# check if we need to abort the loop; need to process events to receive signals;
app.processEvents() # this could cause change to self.__abort
if self.__abort:
# note that "step" value will not necessarily be same for every thread
self.sig_msg.emit('Worker #{} aborting work at step {}'.format(self.__id, step))
break
self.sig_done.emit(self.__id)
def abort(self):
self.sig_msg.emit('Worker #{} notified to abort'.format(self.__id))
self.__abort = True
class MyWidget(QWidget):
NUM_THREADS = 5
# sig_start = pyqtSignal() # needed only due to PyCharm debugger bug (!)
sig_abort_workers = pyqtSignal()
def __init__(self):
super().__init__()
self.setWindowTitle("Thread Example")
form_layout = QVBoxLayout()
self.setLayout(form_layout)
self.resize(400, 800)
self.button_start_threads = QPushButton()
self.button_start_threads.clicked.connect(self.start_threads)
self.button_start_threads.setText("Start {} threads".format(self.NUM_THREADS))
form_layout.addWidget(self.button_start_threads)
self.button_stop_threads = QPushButton()
self.button_stop_threads.clicked.connect(self.abort_workers)
self.button_stop_threads.setText("Stop threads")
self.button_stop_threads.setDisabled(True)
form_layout.addWidget(self.button_stop_threads)
self.log = QTextEdit()
form_layout.addWidget(self.log)
self.progress = QTextEdit()
form_layout.addWidget(self.progress)
QThread.currentThread().setObjectName('main') # threads can be named, useful for log output
self.__workers_done = None
self.__threads = None
def start_threads(self):
self.log.append('starting {} threads'.format(self.NUM_THREADS))
self.button_start_threads.setDisabled(True)
self.button_stop_threads.setEnabled(True)
self.__workers_done = 0
self.__threads = []
for idx in range(self.NUM_THREADS):
worker = Worker(idx)
thread = QThread()
thread.setObjectName('thread_' + str(idx))
self.__threads.append((thread, worker)) # need to store worker too otherwise will be gc'd
worker.moveToThread(thread)
# get progress messages from worker:
worker.sig_step.connect(self.on_worker_step)
worker.sig_done.connect(self.on_worker_done)
worker.sig_msg.connect(self.log.append)
# control worker:
self.sig_abort_workers.connect(worker.abort)
# get read to start worker:
# self.sig_start.connect(worker.work) # needed due to PyCharm debugger bug (!); comment out next line
thread.started.connect(worker.work)
thread.start() # this will emit 'started' and start thread's event loop
# self.sig_start.emit() # needed due to PyCharm debugger bug (!)
#pyqtSlot(int, str)
def on_worker_step(self, worker_id: int, data: str):
self.log.append('Worker #{}: {}'.format(worker_id, data))
self.progress.append('{}: {}'.format(worker_id, data))
#pyqtSlot(int)
def on_worker_done(self, worker_id):
self.log.append('worker #{} done'.format(worker_id))
self.progress.append('-- Worker {} DONE'.format(worker_id))
self.__workers_done += 1
if self.__workers_done == self.NUM_THREADS:
self.log.append('No more workers active')
self.button_start_threads.setEnabled(True)
self.button_stop_threads.setDisabled(True)
# self.__threads = None
#pyqtSlot()
def abort_workers(self):
self.sig_abort_workers.emit()
self.log.append('Asking each worker to abort')
for thread, worker in self.__threads: # note nice unpacking by Python, avoids indexing
thread.quit() # this will quit **as soon as thread event loop unblocks**
thread.wait() # <- so you need to wait for it to *actually* quit
# even though threads have exited, there may still be messages on the main thread's
# queue (messages that threads emitted before the abort):
self.log.append('All threads exited')
if __name__ == "__main__":
app = QApplication([])
form = MyWidget()
form.show()
sys.exit(app.exec_())
The problem is solved by passing him as a parent to self. You must change:
thread = QThread()
to:
thread = QThread(parent=self)
I have a thread where I need to execute a heavy function.
First, I have a function that takes two QDateTime values from the GUI and convert them to UNIX timestamps. Secondly, the "heavy" function uses these values to perform a task.
Both functions (function_task, time_converter_to_unix) do not belong to any class, so as far as I know I can use them in the thread.
But not the parameters, since I cannot access the QDateTime values.
Error: AttributeError: 'TaskThread' object has no attribute 'startTime'
How can I access the QDateTime and read the content from the thread? Thank you.
EDIT: Complete Code. You can find also the link to the GUI in the following link: interface.ui
import sys
import datetime
import time
from PyQt4 import QtCore, QtGui, uic
from PyQt4.QtCore import *
from PyQt4.QtGui import *
# Link to GUI
qtCreatorFile = "interface.ui"
Ui_MainWindow, QtBaseClass = uic.loadUiType(qtCreatorFile)
def time_converter_to_unix(start_datetime, end_datetime):
# Convert QTimeEdit to UNIX Timestamp (int, msec included), and then to float
start_datetime_unix_int = start_datetime.toMSecsSinceEpoch ()
start_datetime_unix = (float(start_datetime_unix_int) / 1000)
end_datetime_unix_int = end_datetime.toMSecsSinceEpoch ()
end_datetime_unix = (float(end_datetime_unix_int) / 1000)
return start_datetime_unix, end_datetime_unix
def dummy_function(self, start_datetime_unix, end_datetime_unix):
# Dummy function, just to simulate a task
result = start_datetime_unix + end_datetime_unix
print result
class Tool(QtGui.QMainWindow, Ui_MainWindow):
def __init__(self, parent = None):
# Setting-ip UI
QtGui.QMainWindow.__init__(self)
Ui_MainWindow.__init__(self)
self.setupUi(self)
# Button Action
self.runButton.clicked.connect(self.onStart)
# Progress Bar and Label. At the begining, the bar is at 0
self.progressBar.setValue(0)
self.progressBar.setRange(0,100)
self.resultLabel.setText("Waiting...")
#Thread
self.myLongTask = TaskThread()
self.myLongTask.taskFinished.connect(self.onFinished)
def onStart(self):
# Before running the thread, we set the progress bar in waiting mode
self.progressBar.setRange(0,0)
self.resultLabel.setText("In progress...")
print "Starting thread..."
self.myLongTask.start()
def onFinished(self):
# Stop the pulsation when the thread has finished
self.progressBar.setRange(0,1)
self.progressBar.setValue(1)
self.resultLabel.setText("Done")
class TaskThread(QtCore.QThread):
taskFinished = QtCore.pyqtSignal()
def __init__(self):
QtCore.QThread.__init__(self)
def __del__(self):
self.wait()
def run(self):
# First, we read the times from the QDateTime elements in the interface
print "Getting times..."
start_datetime_unix, end_datetime_unix = time_converter_to_unix(self.startTime.dateTime(), self.endTime.dateTime())
# Then, we put these values in my_function
print "Executing function..."
dummy_function(self, start_datetime_unix, end_datetime_unix)
# To finish, we execute onFinished.
print "Finishing thread..."
self.taskFinished.emit()
def main():
app = QtGui.QApplication(sys.argv)
window = Tool()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
If you execute it along with the .ui, you will see, that once we click on "Run", the Progress bar stays in waiting mode, and the error commented above appears.
startTime and endTime belong to the GUI, and not to the thread, that's why you get that error.
On the other hand it is advisable not to access the GUI from the other thread, it is best to obtain the values before starting the thread and set it as property of the thread as shown below:
class Tool(QtGui.QMainWindow, Ui_MainWindow):
...
def onStart(self):
# Before running the thread, we set the progress bar in waiting mode
self.progressBar.setRange(0,0)
self.resultLabel.setText("In progress...")
self.myLongTask.start_dt = self.startTime.dateTime() # <----
self.myLongTask.end_dt = self.endTime.dateTime() # <----
print "Starting thread..."
self.myLongTask.start()
...
class TaskThread(QtCore.QThread):
...
def run(self):
# First, we read the times from the QDateTime elements in the interface
print "Getting times..."
start_datetime_unix, end_datetime_unix = time_converter_to_unix(self.start_dt, self.end_dt) # <----
# Then, we put these values in my_function
print "Executing function..."
dummy_function(self, start_datetime_unix, end_datetime_unix)
# To finish, we execute onFinished.
print "Finishing thread..."
self.taskFinished.emit()
I'm trying to understand how thread works, and i'm stuck with this problem. That's my program explained:
i made a simple GUI in pyqt that use a QObject as a worker class. When i press the botton start the gui read a random value from a list and pass it to the thread, that print the
next five number. When the thread finish the work, it pass the data to the gui. Now i want the GUI to restart automatically a new thread with a new start value. I can restart the thread by pressing start again, but i need to start it without human interaction. Are there
any method?
thanks in advance
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import time
import sys
import numpy as np
class SomeObject(QObject):
finished = pyqtSignal(object)
valore = pyqtSignal(object)
vector = pyqtSignal(object)
def __init():
super(SomeObject, self).__init__()
def longRunning(self):
vec = []
end = self.count + 5
while self.count < end:
time.sleep(1)
vec.append(self.count)
self.valore.emit(self.count)
self.count += 1
self.finished.emit(vec)
#self.vector.emit()
def setCount(self, num):
self.count = num
class GUI(QDialog):
def __init__(self, parent = None):
super(GUI, self).__init__(parent)
#declare QThread object
self.objThread = QThread()
#declare SomeObject type, and move it to thread
self.obj = SomeObject()
self.obj.moveToThread(self.objThread)
#connect finished signal to nextVector method
self.obj.finished.connect(self.nextVector)
#connect valore to self.prova method
self.obj.valore.connect(self.prova)
#self.obj.vector.connect(self.nextVector)
#Connect thread.start to the method long running
self.objThread.started.connect(self.obj.longRunning)
botton = QPushButton("start")
self.connect(botton, SIGNAL("clicked()"), self.showcount)
box = QHBoxLayout()
box.addWidget(botton)
self.setLayout(box)
#a list of random number
a = np.random.randint(10, size = 5)
self.iter = iter(a)
def showcount(self):
"""
When botton clicked, read the next value from iter, pass it to
setCount and when start the thread
"""
try:
a = self.iter.next()
print a
self.obj.setCount(a)
self.objThread.start()
except StopIteration:
print "finito"
#self.obj.setCount(a)
#self.objThread.start()
#print self.objThread.currentThreadId()
def prova(self, value):
"""
Connected to signal valore, print the value
"""
print value
def nextVector(self, vec):
"""
Print the whole vector
"""
print vec
self.objThread.quit()
try:
a = self.iter.next()
print a
self.obj.setCount(a)
self.objThread.start()
except StopIteration:
print "finito"
app = QApplication(sys.argv)
form = GUI()
form.show()
app.exec_()
You already have it set up. When your thread is finished it emits the finished signal which calls the nextVector method, so just call the start method at the end of nextVector.
def nextVector(self, vec):
...
self.showcount()
# end nextVector
You may also want to change to the new signal connection for your QPushButton
button.clicked.connect(self.showcount)
I'm trying to build a PyQt app which (among other things) has the ability via a QTextEdit Box to function like a serial terminal program (HyperTerminal, TeraTerm, etc.) I've read through a few examples from the PySerial page and I think I've managed to get the receive data thread working properly but maybe not as efficiently as possible.
My problem is how do I take the last typed character in the QTextEdit box and send that out the serial connection? I've tried using the textChanged signal that QTextEdit emits, but that then sends everything that I type AND that it receives. I've tried setting up an eventFilter in my main GUI class, but I can't figure out how to get that over to the serial function in another file. Do I want to have a separate thread that listens for a signal emitted from the eventFilter? How do I do that? Is there a more elegant way to do this?
I'm sure I've just managed to overthink this and the solution is simple, but I'm somewhat struggling with it. I'll attach the relevant code snippets (not a full code set) and perhaps somebody can point me in the right direction. If anybody also thinks that the threading that I'm doing could be done in a more efficient manner, then please relay that to me as well!
Thanks for any help that anybody can provide!
Main File:
import sys
from PyQt4 import QtGui
from MainGUI import TestGUI
from SerialClasses import *
from SerialMiniterm import *
class StartMainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(StartMainWindow, self).__init__(parent)
self.ui = TestGUI()
self.ui.setupUi(self)
self.ui.serialTextEditBox.installEventFilter(self)
def eventFilter(self, source, event):
if (event.type() == QtCore.QEvent.KeyPress and source is self.ui.serialTextEditBox):
# print some debug statements to console
if (event.key() == QtCore.Qt.Key_Tab):
print ('Tab pressed')
print ('key pressed: %s' % event.text())
print ('code pressed: %d' % event.key())
# do i emit a signal here? how do i catch it in thread?
self.emit(QtCore.SIGNAL('transmitSerialData(QString)'), event.key())
return True
return QtGui.QTextEdit.eventFilter(self, source, event)
def serialConnectCallback(self):
self.miniterm = SerialMiniterm(self.ui, self.SerialSettings)
self.miniterm.start()
temp = self.SerialSettings.Port + 1
self.ui.serialLabel.setText("<font color = green>Serial Terminal Connected on COM%d" % temp)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
app.setStyle("Cleanlooks")
myapp = StartMainWindow()
myapp.show()
sys.exit(app.exec_())
SerialMiniterm.py:
import serial
from PyQt4 import QtGui, QtCore
def character(b):
return b
class SerialMiniterm(object):
def __init__(self, ui, SerialSettings):
self.SerialSettings = SerialSettings
self.ui = ui
self.serial = serial.Serial(self.SerialSettings.Port, self.SerialSettings.BaudRate, parity=self.SerialSettings.Parity, rtscts=self.SerialSettings.RTS_CTS, xonxoff=self.SerialSettings.Xon_Xoff, timeout=1)
self.repr_mode = self.SerialSettings.RxMode
self.convert_outgoing = self.SerialSettings.NewlineMode
self.newline = NEWLINE_CONVERISON_MAP[self.convert_outgoing]
self.dtr_state = True
self.rts_state = True
self.break_state = False
def _start_reader(self):
"""Start reader thread"""
self._reader_alive = True
self.receiver_thread = ReaderThread(self.alive, self._reader_alive, self.repr_mode, self.convert_outgoing, self.serial)
self.receiver_thread.connect(self.receiver_thread, QtCore.SIGNAL("updateSerialTextBox(QString)"), self.updateTextBox)
self.receiver_thread.start()
def _stop_reader(self):
"""Stop reader thread only, wait for clean exit of thread"""
self._reader_alive = False
self.receiver_thread.join()
def updateTextBox(self, q):
self.ui.serialTextEditBox.insertPlainText(q)
self.ui.serialTextEditBox.moveCursor(QtGui.QTextCursor.End)
#print "got here with value %s..." % q
def start(self):
self.alive = True
self._start_reader()
# how do i handle transmitter thread?
def stop(self):
self.alive = False
def join(self, transmit_only=False):
self.transmitter_thread.join()
if not transmit_only:
self.receiver_thread.join()
class ReaderThread(QtCore.QThread):
def __init__(self, alive, _reader_alive, repr_mode, convert_outgoing, serial, parent=None):
QtCore.QThread.__init__(self, parent)
self.alive = alive
self._reader_alive = _reader_alive
self.repr_mode = repr_mode
self.convert_outgoing = convert_outgoing
self.serial = serial
def __del__(self):
self.wait()
def run(self):
"""loop and copy serial->console"""
while self.alive and self._reader_alive:
data = self.serial.read(self.serial.inWaiting())
if data: #check if not timeout
q = data
self.emit(QtCore.SIGNAL('updateSerialTextBox(QString)'), q)
Something like this?
from PyQt4 import QtCore, QtGui
app = QtGui.QApplication([])
class Terminal(QtGui.QPlainTextEdit):
def keyPressEvent(self, event):
print event.text()
return QtGui.QPlainTextEdit.keyPressEvent(self, event)
term = Terminal()
term.show()
I am totally confused by the QThread behavior. My idea is to acquire some audio signal in a qthread, save it in a python queue object and with a QTimer I read the queue and plot it using pyqtgraph. It works, however, only at around 6-7 fps. However, when I use .terminate() to terminate the thread, the thread does actually NOT terminate, but rather speeds up to > 100 fps, exactly what I actually wanted.
My issues:
why does the QThread not terminate/is aborted/closed...?
what is .terminate() actually doing?
what is slowing down the normal thread.start()?
On a side note, I know that I am not using a Signal/Slot for checking if it should still run or not, I just want to understand this strange behavior, and why the thread is not fast from the very beginning! Something is maybe blocking the proper function and is turned off (?!) by the .terminate() function...
My minimal working example (hope you guys have a soundcard/mic somewhere):
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QPushButton
from PyQt5.QtCore import QThread, QTimer
import sounddevice as sd
import queue
import pyqtgraph as pg
import numpy as np
import time
class Record(QThread):
def __init__(self):
super().__init__()
self.q = queue.Queue()
def callback(self, indata, frames, time, status):
self.q.put(indata.copy())
def run(self):
with sd.InputStream(samplerate=48000, device=1, channels=2, callback=self.callback, blocksize=4096):
print('Stream started...')
while True:
pass
print(self.isRunning(), 'Done?') # never called
class Main(QWidget):
def __init__(self):
super().__init__()
self.recording = False
self.r = None
self.x = 0
self.times = list(range(10))
self.setWindowTitle("Record Audio Tester")
self.l = QGridLayout()
self.setLayout(self.l)
self.pl = pg.PlotWidget(autoRange=False)
self.curve1 = self.pl.plot(np.zeros(8000))
self.curve2 = self.pl.plot(np.zeros(8000)-1, pen=pg.mkPen("y"))
self.l.addWidget(self.pl)
self.button_record = QPushButton("Start recording")
self.button_record.clicked.connect(self.record)
self.l.addWidget(self.button_record)
def record(self):
if self.recording and self.r is not None:
self.button_record.setText("Start recording")
self.recording = False
self.r.terminate()
else:
self.button_record.setText("Stop recording")
self.recording = True
self.r = Record()
self.r.start()
self.t = QTimer()
self.t.timeout.connect(self.plotData)
self.t.start(0)
def plotData(self):
self.times = self.times[1:]
self.times.append(time.time())
fps = 1 / (np.diff(np.array(self.times)).mean() + 1e-5)
self.setWindowTitle("{:d} fps...".format(int(fps)))
if self.r.q.empty():
return
d = self.r.q.get()
self.curve1.setData(d[:, 0])
self.curve2.setData(d[:, 1]-3)
if __name__ == '__main__':
app = QApplication([])
w = Main()
w.show()
app.exec_()
edit 1
The first suggestion #Dennis Jensen was to not subclass QThread, but use rather QObject/QThread/moveToThread. I did this, see code below, and one can see that the issue is gone with either using while and just app.processEvents() or while with time.sleep(0.1), but to make it response you have to use anyway app.processEvents(), so this is sufficient. The pass statement alone eats up alot of CPU processing power, resulting in 7-10 fps, but if you thread.terminate() this thread, everything still runs.
I added additionally a trace, what happens on which thread, and the callback is always on a separate thread, regardless which callback you use (outside any class, in QObject or in the main thread), indicating that the answer from #three_pineapples is correct.
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QPushButton, QCheckBox
from PyQt5.QtCore import QThread, QTimer, QObject, pyqtSignal, pyqtSlot
import threading
import sounddevice as sd
import queue
import pyqtgraph as pg
import numpy as np
import time
q = queue.Queue()
# It does not matter at all where the callback is,
# it is always on its own thread...
def callback(indata, frames, time, status):
print("callback", threading.get_ident())
# print()
q.put(indata.copy())
class Record(QObject):
start = pyqtSignal(str)
stop = pyqtSignal()
data = pyqtSignal(np.ndarray)
def __init__(self, do_pass=False, use_terminate=False):
super().__init__()
self.q = queue.Queue()
self.r = None
self.do_pass = do_pass
self.stop_while = False
self.use_terminate = use_terminate
print("QObject -> __init__", threading.get_ident())
def callback(self, indata, frames, time, status):
print("QObject -> callback", threading.get_ident())
self.q.put(indata.copy())
#pyqtSlot()
def stopWhileLoop(self):
self.stop_while = True
#pyqtSlot()
def run(self, m='sth'):
print('QObject -> run', threading.get_ident())
# Currently uses a callback outside this QObject
with sd.InputStream(device=1, channels=2, callback=callback) as stream:
# Test the while pass function
if self.do_pass:
while not self.stop_while:
if self.use_terminate: # see the effect of thread.terminate()...
pass # 7-10 fps
else:
app.processEvents() # makes it real time, and responsive
print("Exited while..")
stream.stop()
else:
while not self.stop_while:
app.processEvents() # makes it responsive to slots
time.sleep(.01) # makes it real time
stream.stop()
print('QObject -> run ended. Finally.')
class Main(QWidget):
def __init__(self):
super().__init__()
self.recording = False
self.r = None
self.x = 0
self.times = list(range(10))
self.q = queue.Queue()
self.setWindowTitle("Record Audio Tester")
self.l = QGridLayout()
self.setLayout(self.l)
self.pl = pg.PlotWidget(autoRange=False)
self.curve1 = self.pl.plot(np.zeros(8000))
self.curve2 = self.pl.plot(np.zeros(8000)-1, pen=pg.mkPen("y"))
self.l.addWidget(self.pl)
self.button_record = QPushButton("Start recording")
self.button_record.clicked.connect(self.record)
self.l.addWidget(self.button_record)
self.pass_or_sleep = QCheckBox("While True: pass")
self.l.addWidget(self.pass_or_sleep)
self.use_terminate = QCheckBox("Use QThread terminate")
self.l.addWidget(self.use_terminate)
print("Main thread", threading.get_ident())
def streamData(self):
self.r = sd.InputStream(device=1, channels=2, callback=self.callback)
def record(self):
if self.recording and self.r is not None:
self.button_record.setText("Start recording")
self.recording = False
self.r.stop.emit()
# And this is where the magic happens:
if self.use_terminate.isChecked():
self.thr.terminate()
else:
self.button_record.setText("Stop recording")
self.recording = True
self.t = QTimer()
self.t.timeout.connect(self.plotData)
self.t.start(0)
self.thr = QThread()
self.thr.start()
self.r = Record(self.pass_or_sleep.isChecked(), self.use_terminate.isChecked())
self.r.moveToThread(self.thr)
self.r.stop.connect(self.r.stopWhileLoop)
self.r.start.connect(self.r.run)
self.r.start.emit('go!')
def addData(self, data):
# print('got data...')
self.q.put(data)
def callback(self, indata, frames, time, status):
self.q.put(indata.copy())
print("Main thread -> callback", threading.get_ident())
def plotData(self):
self.times = self.times[1:]
self.times.append(time.time())
fps = 1 / (np.diff(np.array(self.times)).mean() + 1e-5)
self.setWindowTitle("{:d} fps...".format(int(fps)))
if q.empty():
return
d = q.get()
# print("got data ! ...")
self.curve1.setData(d[:, 0])
self.curve2.setData(d[:, 1]-1)
if __name__ == '__main__':
app = QApplication([])
w = Main()
w.show()
app.exec_()
edit 2
Here the code that uses no QThread environment, and this works as expected!
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QPushButton, QCheckBox
from PyQt5.QtCore import QTimer
import threading
import sounddevice as sd
import queue
import pyqtgraph as pg
import numpy as np
import time
class Main(QWidget):
def __init__(self):
super().__init__()
self.recording = False
self.r = None
self.x = 0
self.times = list(range(10))
self.q = queue.Queue()
self.setWindowTitle("Record Audio Tester")
self.l = QGridLayout()
self.setLayout(self.l)
self.pl = pg.PlotWidget(autoRange=False)
self.curve1 = self.pl.plot(np.zeros(8000))
self.curve2 = self.pl.plot(np.zeros(8000)-1, pen=pg.mkPen("y"))
self.l.addWidget(self.pl)
self.button_record = QPushButton("Start recording")
self.button_record.clicked.connect(self.record)
self.l.addWidget(self.button_record)
print("Main thread", threading.get_ident())
def streamData(self):
self.r = sd.InputStream(device=1, channels=2, callback=self.callback)
self.r.start()
def record(self):
if self.recording and self.r is not None:
self.button_record.setText("Start recording")
self.recording = False
self.r.stop()
else:
self.button_record.setText("Stop recording")
self.recording = True
self.t = QTimer()
self.t.timeout.connect(self.plotData)
self.t.start(0)
self.streamData()
def callback(self, indata, frames, time, status):
self.q.put(indata.copy())
print("Main thread -> callback", threading.get_ident())
def plotData(self):
self.times = self.times[1:]
self.times.append(time.time())
fps = 1 / (np.diff(np.array(self.times)).mean() + 1e-5)
self.setWindowTitle("{:d} fps...".format(int(fps)))
if self.q.empty():
return
d = self.q.get()
# print("got data ! ...")
self.curve1.setData(d[:, 0])
self.curve2.setData(d[:, 1]-1)
if __name__ == '__main__':
app = QApplication([])
w = Main()
w.show()
app.exec_()
The problem is due to the while True: pass line in your thread. To understand why, you need to understand how PortAudio (the library wrapped by sounddevice) works.
Anything passed a callback like you are doing with InputStream is likely calling the provided method from a separate thread (not the main thread or your QThread). Now from what I can tell, whether the callback is called from a separate thread or some sort of interrupt is platform dependent, but either way it is operating somewhat independently of your QThread even though the method exists inside that class.
The while True: pass is going to consume close to 100% of your CPU, limiting what any other thread can do. That is until you terminate it! Which frees up resources for whatever is actually calling the callback to work faster. While you might expect the audio capture to be killed along with your thread, chances are it hasn't been garbage collected yet (and garbage collection gets complicated when dealing with C/C++ wrapped libraries, nevermind when you have two of them! [PortAudio and Qt] - And there is a good chance that garbage collection in Python might not actually free the resources in your case anyway!)
So this explains why things get faster when you terminate the thread.
The solution is to change your loop to while True: time.sleep(.1) which will ensure it doesn't consume resources unnecessarily! You could also look into whether you actually need that thread at all (depending on how PortAudio works on your platform). If you move to the signal/slot architecture and do away with the with statement (managing the open/close of the resource in separate slots) that would also work as you wouldn't need the problematic loop at all.
There are some small issues in your code, all concurring to the "low" framerate, mostly due to the fact that you're using a blocksize of 4096 (which is too high if you want frequent updates) and are also trying to update the GUI too fast while also processing data.
On a slightly old computer as mine, your code completely hangs the interface as soon as I start recording, and stopping is almost impossible if not by killing the app at all.
What you see is not the thread "speeding" up, but rather the QTimer that has much more "time" (cycles) to call its timeout more frequently.
First of all, you shouldn't use terminate, but maybe use a Queue to send a "quit" command to the while loop, allowing it to exit gracefully.
Then, it's better to use signals/slots to retrieve and process the output data, as it's more intuitive and improves the overall cpu load.
Finally, if you want to get the fps of the received data, there's no use in a QTimer set to 0 (which will make it only run as fast as possible even if unnecessary, making the cpu spike needlessly).
class Record(QThread):
audioData = pyqtSignal(object)
def __init__(self):
super().__init__()
self.stopper = queue.Queue()
def callback(self, indata, frames, time, status):
self.audioData.emit(indata.copy())
def run(self):
with sd.InputStream(samplerate=48000, channels=2, callback=self.callback, blocksize=1024):
print('Stream started...')
while True:
try:
if self.stopper.get(timeout=.1):
break
except:
pass
print(self.isRunning(), 'Done?') # never called
def stop(self):
self.stopper.put(True)
class Main(QWidget):
# ...
def record(self):
if self.recording and self.r is not None:
self.button_record.setText("Start recording")
self.recording = False
self.r.stop()
else:
self.button_record.setText("Stop recording")
self.recording = True
self.r = Record()
self.r.audioData.connect(self.plotData)
self.r.start()
def plotData(self, data):
self.curve1.setData(data[:, 0])
self.curve2.setData(data[:, 1]-3)
self.times = self.times[1:]
self.times.append(time.time())
fps = 1 / (np.diff(np.array(self.times)).mean() + 1e-5)
self.setWindowTitle("{:d} fps...".format(int(fps)))