在线程中更改 QObject 样式表时出错

Error while changing QObject stylesheet in a Thread

上下文

我想在 python 中构建 QObject 动画。例如,我尝试为 QLineEdit 对象的背景设置动画,以便在输入错误时制作 "red flash" 。该功能正在运行,线程启动并且我看到了动画,但是当线程结束时,应用程序崩溃而没有错误回溯。我只得到

exit code -1073740940

我在网上找不到。

最小工作示例

这是我为了让您仅使用一个文件重现此错误而制作的一个 mwe。您会注意到代码的重要部分在 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_())

结果

如果您点击多次,应用程序将冻结并崩溃。我想明白为什么,但没有追溯,我觉得很难。有时,它发生在第一次点击之后。我认为这将是一个线程冲突问题,但由于它只发生在第一个线程 运行 上,我不太确定。任何人都可以指出我正确的方向或向我解释发生了什么?

要实现输入错误时的'red flash',可以使用QTimer.singleShot()。本质上,当字段中的文本发生更改并触发错误文本时,您可以将背景更改为错误颜色。然后在一定时间后,比如2秒后,你可以重新设置字段颜色。

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

你的问题可以从以下几个方面来分析:

1) 您不应该直接从另一个线程更新任何 GUI 元素

GUI 的绘制是在主线程中完成的,因此 GUI 在任何情况下都不允许修改涉及从另一个线程绘制的任何 属性,因此如果开发人员这样做,则无法保证这在这种情况下是错误的。有关更多信息,请阅读 GUI Thread and Worker Thread

在 Qt 的情况下,如果你想从另一个线程更新一些 GUI 元素,你应该做的是通过某种方式(信号、QEvent、QMetaObject::invokeMethod() 等)将信息发送到主线程线程,并在主线程中执行更新。

因此考虑到上述情况,使用信号的可能解决方案如下:

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) Qt中制作动画不一定要用线程,应该用QVariantAnimation、QPropertyAnimation等

在 GUI 中,您应该避免使用线程,因为它带来的问题多于好处(例如使信号队列饱和),因此请将其用作最后的手段。在这种情况下,您可以使用 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_())