在线程中更改 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_())
上下文
我想在 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_())