Search code examples
pythonmultithreadingpyqtpyqt5qtstylesheets

Error while changing QObject stylesheet in a Thread


Context

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.

Minimal Working Example

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

Results

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?


Solution

  • Your question allows to analyze the following aspects:

    1) You should not update directly any GUI element from another thread

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

    2) It is not necessary to use threads to make animation in Qt, instead you should use QVariantAnimation, QPropertyAnimation, etc.

    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:

    2.1) QVariantAnimation

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

    2.2) QPropertyAnimation

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