简体   繁体   English

更改线程[PyQt5]中的QObject样式表时出错

[英]Error while changing QObject stylesheet in a Thread [PyQt5]

Context 上下文

I want to build QObject animations in python. 我想在python中建立QObject动画。 For example, I tried animating the background of a QLineEdit object in order to make a "red flash" when a something wrong is entered. 例如,我尝试对QLineEdit对象的背景进行动画处理,以便在输入错误时产生“红色闪烁”。 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. 这是我为了使您仅用一个文件即可重现此错误而制作的Mwe。 You will notice that the important part of the code is inside LoginDialog class. 您会注意到代码的重要部分在LoginDialog类中。

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 1)您不应直接从另一个线程更新任何GUI元素

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. GUI的绘制是在主线程中完成的,因此GUI在任何情况下都不允许修改涉及从另一个线程进行绘制的任何属性,因此,如果开发人员执行此操作,则不能保证在这种情况下可以正常工作。 For more information read GUI Thread and Worker Thread . 有关更多信息,请阅读GUI线程和辅助线程

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. 对于Qt,如果要从另一个线程更新某些GUI元素,则应通过某种方式(信号,QEvent,QMetaObject :: invokeMethod()等)将信息发送到主线程,并在主线程中线程进行更新。

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. 2)不必使用线程在Qt中制作动画,而是应使用QVariantAnimation,QPropertyAnimation等。

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. 在GUI中,应避免使用线程,因为它带来的问题多于好处(例如,使信号队列饱和),因此请尽量使用它。 In this case you can use QVariantAnimation or QPropertyAnimation : 在这种情况下,您可以使用QVariantAnimationQPropertyAnimation

2.1) QVariantAnimation 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 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() . 要在输入错误时实现“红色闪烁”,可以使用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. 然后经过一定时间(例如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_())

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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