I want to build QObject animations in python. For example, I tried animating the background of a QLineEdit object in order to make a "red flash" when a something wrong is entered. The function is working, the thread starts and I see the animation, but when the thread ends, the app collapses without error trace-back. I only get
exit code -1073740940
Which I didn't find on the internet.
Here's a mwe that I made in order for you to reproduce this error with only one file. You will notice that the important part of the code is inside LoginDialog class.
from PyQt5.QtWidgets import QDialog, QLineEdit, QVBoxLayout, QApplication
from threading import Thread
import time
import sys
class Ui_LoginUi(object):
def setupUi(self, Ui_LoginUi):
Ui_LoginUi.setObjectName("LoginUi")
Ui_LoginUi.resize(293, 105)
self.layout = QVBoxLayout(Ui_LoginUi)
self.le_test = QLineEdit(Ui_LoginUi)
self.layout.addWidget(self.le_test)
class LoginDialog(QDialog, Ui_LoginUi):
def __init__(self):
super(LoginDialog, self).__init__()
self.setupUi(self)
self.le_test.textChanged.connect(self.redFlashThreader)
def redFlashThreader(self):
self.redFlashTread1 = Thread(target=self.lineEdit_redFlash, args=[self.le_test])
self.redFlashTread1.start()
def lineEdit_redFlash(self, *args):
inital_r = 255
initial_g = 127
initial_b = 127
for i in range(64):
initial_g += 2
initial_b += 2
time.sleep(0.005)
args[0].setStyleSheet("background-color: rgb(255,{},{})".format(initial_g, initial_b))
args[0].setStyleSheet("background-color: rgb(255,255,255")
if __name__ == '__main__':
app = QApplication(sys.argv)
dialog = LoginDialog()
dialog.show()
sys.exit(app.exec_())
If you click multiple times, the app will freeze and crash. I would like to understand why, but without trace-back, I find that quite hard. Sometimes, it happens after the first click. I thought it would be a thread conflict problem, but since it happens with only the first thread running, I'm not so sure. Anyone could point me in the right direction or explain to me what is happening?
Your question allows to analyze the following aspects:
The painting of the GUI is done in the main thread so the GUI do not allow in any case to modify any property that involves painting from another thread, so if the developer does it there is no guarantee that works as in this case what's wrong. For more information read GUI Thread and Worker Thread.
In the case of Qt if you want to update some GUI element from another thread what you should do is send by some means (signals, QEvent, QMetaObject::invokeMethod(), etc) the information to the main thread, and in the main thread do the update.
So considering the above, a possible solution using signals is the following:
import sys
import time
from threading import Thread
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_LoginUi(object):
def setupUi(self, Ui_LoginUi):
Ui_LoginUi.setObjectName("LoginUi")
Ui_LoginUi.resize(293, 105)
layout = QtWidgets.QVBoxLayout(Ui_LoginUi)
self.le_test = QtWidgets.QLineEdit(Ui_LoginUi)
layout.addWidget(self.le_test)
class LoginDialog(QtWidgets.QDialog, Ui_LoginUi):
colorChanged = QtCore.pyqtSignal(QtGui.QColor)
def __init__(self):
super(LoginDialog, self).__init__()
self.setupUi(self)
self.le_test.textChanged.connect(self.redFlashThreader)
self.colorChanged.connect(self.on_color_change)
@QtCore.pyqtSlot()
def redFlashThreader(self):
self.redFlashTread1 = Thread(
target=self.lineEdit_redFlash, args=[self.le_test]
)
self.redFlashTread1.start()
def lineEdit_redFlash(self, *args):
inital_r = 255
initial_g = 127
initial_b = 127
for i in range(64):
initial_g += 2
initial_b += 2
time.sleep(0.005)
self.colorChanged.emit(QtGui.QColor(255, initial_g, initial_b))
self.colorChanged.emit(QtGui.QColor(255, 255, 255))
@QtCore.pyqtSlot(QtGui.QColor)
def on_color_change(self, color):
self.setStyleSheet("QLineEdit{background-color: %s}" % (color.name(),))
""" or
self.setStyleSheet(
"QLineEdit{ background-color: rgb(%d, %d, %d)}"
% (color.red(), color.green(), color.blue())
)"""
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
dialog = LoginDialog()
dialog.show()
sys.exit(app.exec_())
In a GUI you should avoid using threading since it can bring more problems than benefits (for example saturate the signal queue), so use it as a last resort. In this case you can use QVariantAnimation
or QPropertyAnimation
:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_LoginUi(object):
def setupUi(self, Ui_LoginUi):
Ui_LoginUi.setObjectName("LoginUi")
Ui_LoginUi.resize(293, 105)
layout = QtWidgets.QVBoxLayout(Ui_LoginUi)
self.le_test = QtWidgets.QLineEdit(Ui_LoginUi)
layout.addWidget(self.le_test)
class LoginDialog(QtWidgets.QDialog, Ui_LoginUi):
def __init__(self):
super(LoginDialog, self).__init__()
self.setupUi(self)
self.le_test.textChanged.connect(self.start_animation)
self.m_animation = QtCore.QVariantAnimation(
self,
startValue=QtGui.QColor(255, 127, 127),
endValue=QtGui.QColor(255, 255, 255),
duration=1000,
valueChanged=self.on_color_change,
)
@QtCore.pyqtSlot()
def start_animation(self):
if self.m_animation.state() == QtCore.QAbstractAnimation.Running:
self.m_animation.stop()
self.m_animation.start()
@QtCore.pyqtSlot(QtCore.QVariant)
@QtCore.pyqtSlot(QtGui.QColor)
def on_color_change(self, color):
self.setStyleSheet("QLineEdit{background-color: %s}" % (color.name(),))
""" or
self.setStyleSheet(
"QLineEdit{ background-color: rgb(%d, %d, %d)}"
% (color.red(), color.green(), color.blue())
)"""
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
dialog = LoginDialog()
dialog.show()
sys.exit(app.exec_())
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class LineEdit(QtWidgets.QLineEdit):
backgroundColorChanged = QtCore.pyqtSignal(QtGui.QColor)
def backgroundColor(self):
if not hasattr(self, "_background_color"):
self._background_color = QtGui.QColor()
self.setBackgroundColor(QtGui.QColor(255, 255, 255))
return self._background_color
def setBackgroundColor(self, color):
if self._background_color != color:
self._background_color = color
self.setStyleSheet("background-color: {}".format(color.name()))
self.backgroundColorChanged.emit(color)
backgroundColor = QtCore.pyqtProperty(
QtGui.QColor,
fget=backgroundColor,
fset=setBackgroundColor,
notify=backgroundColorChanged,
)
class Ui_LoginUi(object):
def setupUi(self, Ui_LoginUi):
Ui_LoginUi.setObjectName("LoginUi")
Ui_LoginUi.resize(293, 105)
layout = QtWidgets.QVBoxLayout(Ui_LoginUi)
self.le_test = LineEdit(Ui_LoginUi)
layout.addWidget(self.le_test)
class LoginDialog(QtWidgets.QDialog, Ui_LoginUi):
def __init__(self):
super(LoginDialog, self).__init__()
self.setupUi(self)
self.le_test.textChanged.connect(self.start_animation)
self.m_animation = QtCore.QPropertyAnimation(
self.le_test,
b'backgroundColor',
self,
startValue=QtGui.QColor(255, 127, 127),
endValue=QtGui.QColor(255, 255, 255),
duration=1000,
)
@QtCore.pyqtSlot()
def start_animation(self):
if self.m_animation.state() == QtCore.QAbstractAnimation.Running:
self.m_animation.stop()
self.m_animation.start()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
dialog = LoginDialog()
dialog.show()
sys.exit(app.exec_())