I have a large module from a separate project, which I wanted to integrate into a GUI. The module does some calculations that take a couple of minutes, and I want to keep the GUI responsive during this time, and preferrably be able to cancel the process at any time.
The best solution would probably be to rewrite the module using signals and threading, but I wanted to try to do it without that to start. So my idea was to run myLongFunction
in a separate thread.
In the GUI I have made a text box (a QPlainTextEdit
) where I want to display messages via the logging facilities of Python. I also have a "Start" button.
The program seems to work as intended for a little while, but it usually crashes within 10 seconds. Sometimes it crashes right away, sometimes it takes a bit longer. And I get no exceptions or other errors, I'm just returned to the terminal prompt. A minimal example is below.
import sys
import time
import logging
from PySide2 import QtWidgets, QtCore
import numpy as np
def longFunction():
logging.info("Start long running function")
i = 0
while True:
for j in range(10000):
t = np.arange(256)
sp = np.fft.fft(np.sin(t))
freq = np.fft.fftfreq(t.shape[-1])
sp = sp + freq
logging.info("%d" % i)
i += 1
# I added a sleep here, but it doesn't seem to help
time.sleep(0.001)
# since I don't really need an event thread, I subclass QThread, as per
# https://woboq.com/blog/qthread-you-were-not-doing-so-wrong.html
class Worker(QtCore.QThread):
def __init__(self, parent=None):
super().__init__(parent)
def run(self):
longFunction()
# custom logging handler
class QTextEditLogger(logging.Handler):
def __init__(self, parent=None):
super().__init__()
self.widget = QtWidgets.QPlainTextEdit(parent)
self.widget.setReadOnly(True)
def emit(self, record):
msg = self.format(record)
self.widget.appendPlainText(msg)
self.widget.centerCursor() # scroll to the bottom
class MyWidget(QtWidgets.QDialog):
def __init__(self, parent=None):
super().__init__(parent)
logTextBox = QTextEditLogger(self)
# format what is printed to text box
logTextBox.setFormatter(
logging.Formatter('%(asctime)s - %(levelname)s - %(threadName)s - %(message)s'))
logging.getLogger().addHandler(logTextBox)
# set the logging level
logging.getLogger().setLevel(logging.DEBUG)
self.resize(400, 500)
# start button
self.startButton = QtWidgets.QPushButton(self)
self.startButton.setText('Start')
# connect start button
self.startButton.clicked.connect(self.start)
# set up layout
layout = QtWidgets.QVBoxLayout()
layout.addWidget(logTextBox.widget)
layout.addWidget(self.startButton)
self.setLayout(layout)
def start(self):
logging.info('Start button pressed')
self.thread = Worker()
# regardless of whether the thread finishes or the user terminates it
# we want to show the notification to the user that it is done
# and regardless of whether it was terminated or finished by itself
# the finished signal will go off. So we don't need to catch the
# terminated one specifically, but we could if we wanted.
self.thread.finished.connect(self.threadFinished) # new-style signal
self.thread.start()
# we don't want to enable user to start another thread while this one
# is running so we disable the start button.
self.startButton.setEnabled(False)
def threadFinished(self):
logging.info('Thread finished!')
self.startButton.setEnabled(True)
app = QtWidgets.QApplication(sys.argv)
w = MyWidget()
w.show()
app.exec_()
The strangest thing is that if I remove the text box (comment out line 51-56 and line 72), the program runs just fine (I stopped it manually after 5 minutes).
Any idea what could cause this?
QTextEditLogger
which lives on the main GUI thread directly from your secondary thread (i.e. theWorker
). That's not supported. Your initial idea of using signals/slots with queued connections is the way to go. – Multiply