简体   繁体   中英

Error while changing QObject stylesheet in a Thread [PyQt5]

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?

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

在此处输入图片说明

To achieve the 'red flash' when something wrong is entered, you can use QTimer.singleShot() . Essentially when text is changed in the field, and it triggers your wrong text, you can change the background to the error color. Then after a certain amount of time, say 2 seconds later, you can reset the field color.

from PyQt5.QtWidgets import QDialog, QLineEdit, QVBoxLayout, QApplication
from PyQt5.QtCore import QTimer 
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.invalid_color = 'background-color: #c91d2e'
        self.valid_color = 'background-color: #FFF'
        self.le_test.textChanged.connect(self.redFlashHandler)

    def redFlashHandler(self):
        if self.le_test.text() == 'test':
            self.le_test.setStyleSheet(self.invalid_color)

            # After 2000 ms, reset field color
            QTimer.singleShot(2000, self.resetFieldColor)

    def resetFieldColor(self):
        self.le_test.setStyleSheet(self.valid_color)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    dialog = LoginDialog()
    dialog.show()
    sys.exit(app.exec_())

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM