使用 QMessageBox 时无法使用 QThread 设置父错误
Posted
技术标签:
【中文标题】使用 QMessageBox 时无法使用 QThread 设置父错误【英文标题】:Cannot set parent error with QThread when using QMessageBox 【发布时间】:2019-08-12 17:19:02 【问题描述】:我试图在一个线程上运行一个进度条,在另一个线程上运行一个函数。以下是我的方法,它工作正常,直到我添加一个 QMessageBox。
我为 QThread 创建了两个新类,一个处理进度条,另一个处理我的函数。当使用 onButtonClicked
函数按下按钮时调用它们
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QMessageBox, QLineEdit, QProgressBar, QLabel, QFileDialog, QCheckBox, QMenuBar, QStatusBar
import time
TIME_LIMIT = 100
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(800, 600)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.msg = QMessageBox()
self.label = QtWidgets.QLabel(self.centralwidget)
self.label.setGeometry(QtCore.QRect(300, 60, 47, 13))
self.label.setObjectName("label")
self.lineEdit = QtWidgets.QLineEdit(self.centralwidget)
self.lineEdit.setGeometry(QtCore.QRect(270, 100, 113, 20))
self.lineEdit.setObjectName("lineEdit")
self.pushButton = QtWidgets.QPushButton(self.centralwidget)
self.pushButton.setGeometry(QtCore.QRect(290, 150, 75, 23))
self.pushButton.setObjectName("pushButton")
self.progressBar = QtWidgets.QProgressBar(self.centralwidget)
self.progressBar.setGeometry(QtCore.QRect(280, 210, 118, 23))
self.progressBar.setProperty("value", 24)
self.progressBar.setObjectName("progressBar")
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 21))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.Actionlistenr()
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.label.setText(_translate("MainWindow", "TextLabel"))
self.pushButton.setText(_translate("MainWindow", "PushButton"))
def Actionlistenr(self):
self.pushButton.clicked.connect(self.onButtonClick)
def test(self):
if self.lineEdit.text() == "":
self.msg.setIcon(QMessageBox.Critical)
self.msg.setText("Please select a document first!")
self.msg.setWindowTitle("Error")
return self.msg.exec()
# If this was just a regular print statement,
# then it would work, or any other statement that
# does not involve a QMessageBox
def onButtonClick(self):
self.calc = External()
self.calc.countChanged.connect(self.onCountChanged)
self.calc.start()
self.calc2 = External2(self)
self.calc2.start()
def onCountChanged(self, value):
self.progressBar.setValue(value)
class External(QThread):
"""
Runs a counter thread.
"""
countChanged = pyqtSignal(int)
def run(self):
count = 0
while count < TIME_LIMIT:
count +=1
time.sleep(1)
self.countChanged.emit(count)
class External2(QThread, object):
"""
Runs a counter thread.
"""
def __init__(self, outer_instance):
super().__init__()
self.outer_instance = outer_instance
def run(self):
self.outer_instance.test()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())
执行此操作时出现QObject::setParent: Cannot set parent, new parent is in a different thread
错误,仅当我在test
函数中添加QMessageBox 时。我假设这是因为 QMessagebox 在主线程上运行,而不是我的 External2()
类,我该如何解决这个问题?
【问题讨论】:
您不能也不应该在另一个线程中运行 QMessageBox,因此 Qt 指出了该错误。为什么要在另一个线程中使用 QMessageBox?我想你有一个XY problem。你的主要目标是什么? 我的主要目标是在各自的线程上运行进度条和test
方法,以避免 GUI 冻结。但是,我的 test
方法有一些 QMessageBox 错误处理,这导致了我现在面临的问题
在我的问题中更具体一点:为什么要在测试方法中使用 QMessageBox?
因为我试图要求用户输入有关 csv 文件的一些信息,例如工作表名称和列名称,如果用户单击“运行”按钮并且任何字段为空,我想显示一个 QMessageBox 来显示他们遇到的错误。
而且我想这些信息(名称和列名)你想用来在同一个线程中执行一个任务。我说的对吗?
【参考方案1】:
您必须做的第一件事是验证是否满足要求(在这种情况下 QLineEdit 不为空)以启动繁重的任务。在这些情况下,我更喜欢使用工作线程方法。要启动繁重的任务,必须异步调用该方法,例如使用 QTimer.singleShot(),并使用 functools.partial() 传递附加参数,还必须使用 @pyqtSlot 确保任务在线程对。
另一方面,您不应该修改 Qt Designer 生成的类(1),而是创建另一个继承自小部件的类并使用第一个类来填充它。
from PyQt5 import QtCore, QtGui, QtWidgets
from functools import partial
import time
TIME_LIMIT = 100
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(800, 600)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.label = QtWidgets.QLabel(self.centralwidget)
self.label.setGeometry(QtCore.QRect(300, 60, 47, 13))
self.label.setObjectName("label")
self.lineEdit = QtWidgets.QLineEdit(self.centralwidget)
self.lineEdit.setGeometry(QtCore.QRect(270, 100, 113, 20))
self.lineEdit.setObjectName("lineEdit")
self.pushButton = QtWidgets.QPushButton(self.centralwidget)
self.pushButton.setGeometry(QtCore.QRect(290, 150, 75, 23))
self.pushButton.setObjectName("pushButton")
self.progressBar = QtWidgets.QProgressBar(self.centralwidget)
self.progressBar.setGeometry(QtCore.QRect(280, 210, 118, 23))
self.progressBar.setProperty("value", 24)
self.progressBar.setObjectName("progressBar")
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 21))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.label.setText(_translate("MainWindow", "TextLabel"))
self.pushButton.setText(_translate("MainWindow", "PushButton"))
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setupUi(self)
self.msg = QtWidgets.QMessageBox()
self.Actionlistenr()
thread = QtCore.QThread(self)
thread.start()
self.m_worker = Worker()
self.m_worker.countChanged.connect(self.progressBar.setValue)
self.m_worker.moveToThread(thread)
def Actionlistenr(self):
self.pushButton.clicked.connect(self.onButtonClick)
def request_information(self):
filename = self.lineEdit.text()
if filename:
wrapper = partial(self.m_worker.task, filename)
QtCore.QTimer.singleShot(0, wrapper)
else:
self.msg.setIcon(QtWidgets.QMessageBox.Critical)
self.msg.setText("Please select a document first!")
self.msg.setWindowTitle("Error")
self.msg.exec_()
@QtCore.pyqtSlot()
def onButtonClick(self):
self.request_information()
class Worker(QtCore.QObject):
countChanged = QtCore.pyqtSignal(int)
@QtCore.pyqtSlot(str)
def task(self, filename):
# execute heavy task here
print("start")
print(filename)
count = 0
while count < TIME_LIMIT:
count += 1
time.sleep(1)
self.countChanged.emit(count)
print("finished")
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
(1)http://pyqt.sourceforge.net/Docs/PyQt5/designer.html
【讨论】:
请问如何在您的解决方案中实现进度条? @Abdane 只需将您的代码添加到我的解决方案中,我已经更新了我的答案。 执行我的函数时,该工具仍然冻结。您可以通过在request_information
函数中运行一个永久循环来测试它。目标是在后台运行该函数并在执行时更新进度条
@Abdane request_information 是 GUI 的一部分,您不能在那里执行繁重的任务(永远循环),而是在 Worker 任务方法中执行。检查我的更新并详细分析我的代码
只是一个额外的小问题,如何在单击按钮时停止 Worker 的执行?谢谢:)【参考方案2】:
好吧,这个 per-sae 比你要求的要多一点,但它也少一点——但是它确实包括所有的点点滴滴以及它们如何相互连接,所以你应该能够从中推断并得到它做任何你想做的事。请注意,您的程序存在一些问题,因此我无法逐字复制它-与您的特定问题有关的问题之一是您无法运行从线程内继承自 QWidgets 的任何内容-我通过创建多进程解决了这个问题处理第二个窗口的情况。仍然按照我概述线程的方式,您可以看到您不需要在线程中拥有该 QMessageBox,但您可能会导致该线程中的某些东西在 QMainWindow 中启动 QMessageBox - 正如我所指出的 - 如果您需要线程来启动该 QMessageBox ——尽管您可能需要添加一些内容以显示线程中正在进行的功能,但这是可行的——我知道这一点,因为我已经测试了这方面的内容。
from sys import exit as sysExit
from time import sleep as tmSleep
from PyQt5.QtCore import Qt, QObject, QThread, QRunnable, pyqtSignal, pyqtSlot
# from PyQt5.QtGui import ??
#Widget Container Objects
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QDockWidget
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QMenuBar, QStatusBar, QLabel
#Widget Action Objects
from PyQt5.QtWidgets import QMessageBox, QFileDialog, QPushButton, QLineEdit
from PyQt5.QtWidgets import QProgressBar, QCheckBox, QAction, QStyleFactory
# Part of Threading
# Note be very careful with Signals/Slots as they are prone to Memory Leaks
class ThreadSignals(QObject):
ObjctSignal = pyqtSignal(object)
IntgrSignal = pyqtSignal(int)
# Part of Threading -- if its a Class that does pretty much the same thing then should only have one
class Processor(QWidget):
def __init__(self, Id):
QWidget.__init__(self)
self.ThreadActive = True
self.RunProcess = False
self.Id = Id
self.Name = '---- Threaded Process ' + str(self.Id)
self.Msg = self.Name
def Connect(self, sigHandle, sigFlag):
self.QueData = queQue()
cnt = 0
self.Flag = sigFlag
sigHandle.emit(self)
tmSleep(0.005) # 5 Milliseconds
# This simulates a continuously running process
# The waits are necessary to allow the OS to do stuff because
# python IS NOT multiprocessing due to the GIL -- look it up
self.lstData = []
while self.ThreadActive:
while self.RunProcess:
cnt += 1
if cnt % 10 == 0:
self.lstData.append(cnt)
if cnt % 100 == 0:
self.Msg = self.Name + ' : Loop ' + str(cnt)
self.QueData.put(self.Msg)
self.QueData.put(self.lstData.copy())
sigFlag.emit(cnt)
self.lstData = []
tmSleep(0.005) # 5 Milliseconds
tmSleep(0.005) # 5 Milliseconds
def GetData(self):
RetData = []
if not self.QueData.empty():
RetData = list(self.QueData.get())
return RetData
def StartProcess(self):
self.RunProcess = True
self.Msg = self.Name + ' Started'
self.Flag.emit(-1)
def StopProcess(self):
self.RunProcess = False
self.Msg = self.Name + ' Stopped'
self.Flag.emit(-1)
def DisConnect(self):
self.RunProcess = False
self.ThreadActive = False
self.Msg = self.Name + ' Disconnected'
# Part of Threading -- if its a Class that does pretty much the same thing then should only have one
class WorkerProcess(QRunnable):
def __init__(self, StartrFunc, Id):
super(WorkerProcess, self).__init__()
self.StartrFunc = StartrFunc
# def StarterFunc(self):
# self.ProcessObject = Processor(#)
# self.ProcessObject.Connect(sigHandle, sigFlag)
self.setAutoDelete(False)
self.Id = Id
self.name = '----- WorkerProcess ' + str(Id)
# Create Signal (aka Sender) Here
self.signals = ThreadSignals()
self.sigHndl = self.signals.ObjctSignal
self.sigFlag = self.signals.IntgrSignal
@pyqtSlot()
def run(self):
print('Inside ',self.name)
self.StartrFunc(self.sigHndl, self.sigFlag)
# def StarterFunc(self):
# self.ProcessObject = Processor(#)
# self.ProcessObject.Connect(sigHandle, sigFlag)
print('******************************')
print('--- Process Completed')
# Note while this process has completed this thread is still active
def DisConnect(self):
# This disconnects all of its Signals
self.signals.disconnect()
# This is your Menu and Tool Bar class it does not handle the Tool Bar
# at this time but it could be expanded to do so fairly easily just
# keep in mind everything on a Tool Bar comes from the Menu Bar
class MenuToolBar(QDockWidget):
def __init__(self, parent):
QDockWidget.__init__(self)
self.Parent = parent
self.MainMenu = parent.menuBar()
# This is used to have a handle to the Menu Items
# should you implement a Tool Bar
self.MenuActRef = 'HelloAct':0,
'ResetAct':0
# ******* Create the World Menu *******
self.WorldMenu = self.MainMenu.addMenu('World')
# ******* Create World Menu Items *******
self.HelloAct = QAction('&Hello', self)
# In case you have or want to include an Icon
# self.HelloAct = QAction(QIcon('Images/hello.ico'), '&Hello', self)
self.HelloAct.setShortcut("Ctrl+H")
self.HelloAct.setStatusTip('Say Hello to the World')
self.HelloAct.triggered.connect(self.SayHello)
self.MenuActRef['HelloAct'] = self.HelloAct
self.ResetAct = QAction('&Reset', self)
# self.ResetAct = QAction(QIcon('Images/reset.ico'), '&Hello', self)
self.ResetAct.setShortcut("Ctrl+H")
self.ResetAct.setStatusTip('Reset the Dialog')
self.ResetAct.triggered.connect(self.ResetWorld)
self.MenuActRef['ResetAct'] = self.ResetAct
# ******* Setup the World Menu *******
self.WorldMenu.addAction(self.HelloAct)
self.WorldMenu.addSeparator()
self.WorldMenu.addAction(self.ResetAct)
self.InitToolBar()
def InitToolBar(self):
# If you create a Tool Bar initialize it here
pass
# These are the Menu/Tool Bar Actions
def SayHello(self):
self.Parent.MenuSubmit()
def ResetWorld(self):
self.Parent.MenuReset()
# Its easiest and cleaner if you Class the Center Pane
# of your MainWindow object
class CenterPanel(QWidget):
def __init__(self, parent):
QWidget.__init__(self)
self.Parent = parent
self.Started = False
#-----
self.lblTextBox = QLabel()
self.lblTextBox.setText('Text Box Label')
#-----
self.lneTextBox = QLineEdit()
#-----
self.btnPush = QPushButton()
self.btnPush.setText('Start')
self.btnPush.clicked.connect(self.Starter)
#-----
self.btnTest = QPushButton()
self.btnTest.setText('Test')
self.btnTest.clicked.connect(self.TestIt)
#-----
HBox = QHBoxLayout()
HBox.addWidget(self.btnPush)
HBox.addWidget(self.btnTest)
HBox.addStretch(1)
#-----
self.pbrThusFar = QProgressBar()
self.pbrThusFar.setProperty('value', 24)
#-----
VBox = QVBoxLayout()
VBox.addWidget(self.lblTextBox)
VBox.addWidget(self.lneTextBox)
VBox.addWidget(QLabel(' ')) # just a spacer
VBox.addLayout(HBox)
VBox.addWidget(QLabel(' ')) # just a spacer
VBox.addWidget(self.pbrThusFar)
VBox.addStretch(1)
#-----
self.setLayout(VBox)
def Starter(self):
if self.Started:
self.btnPush.setText('Start')
self.Started = False
self.Parent.OnStart()
else:
self.btnPush.setText('Reset')
self.Started = True
self.pbrThusFar.setProperty('value', 24)
self.Parent.OnReset()
def TestIt(self):
# Note this cannot be handled within a Thread but a Thread can be
# designed to make a call back to the MainWindow to do so. This
# can be managed by having the MainWindow pass a handle to itself
# to the Thread in question or by using a Signal/Slot call from
# within the Thread back to the MainWindow either works
Continue = True
if self.lneTextBox.text() == '':
DocMsg = QMessageBox()
DocMsg.setIcon(QMessageBox.Critical)
DocMsg.setWindowTitle("Error")
DocMsg.setText("There is no Document. Do you want to Quit?")
DocMsg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
DocMsg.setDefaultButton(QMessageBox.No)
DocMsg.setWindowFlags(Qt.WindowStaysOnTopHint)
MsgReply = DocMsg.exec_()
if MsgReply == QMessageBox.Yes:
sysExit()
def HandleSubmit(self):
self.lneTextBox.setText('Center Panel Menu Submit')
# This is sort of your Main Handler for your interactive stuff as such
# it is best to restrict it to doing just that and let other classes
# handle the other stuff -- it also helps maintain overall perspective
# of what each piece is designed for
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle('Main Window')
# Sometimes its best to place the window where you want but just setting its size works too
# Still I do this in two lines to make it clear what each position is
WinLeft = 150; WinTop = 150; WinWidth = 400; WinHight = 200
# self.setGeometry(WinLeft, WinTop, WinWidth, WinHight)
self.resize(WinWidth, WinHight)
self.CenterPane = CenterPanel(self)
self.setCentralWidget(self.CenterPane)
# The Menu and Tool Bar for your MainWindow should be classed as well
self.MenuBar = MenuToolBar(self)
self.SetStatusBar(self)
# Not exactly sure what all this does yet but it does remove
# oddities from the window so I always include it - for now
self.setStyle(QStyleFactory.create('Cleanlooks'))
# Part of Threading
self.Thread1Connected = False
self.Thread2Connected = False
# Create Handles for the Threads
# I used this methodology as it was best for my program but
# there are other ways to do this it depends on your needs
self.Thread1Hndl = QObject()
self.Thread2Hndl = QObject()
# This is used to start the Thread 1
self.MyThread1 = WorkerProcess(self.Threader1, 1)
# Create Slots (aka Receivers) Here
self.MyThread1.signals.ObjctSignal.connect(self.Thread1_Hndl)
self.MyThread1.signals.IntgrSignal.connect(self.Thread1_Flag)
# This is used to start the Thread 2
self.MyThread2 = WorkerProcess(self.Threader2, 2)
# Create Slots (aka Receivers) Here
self.MyThread2.signals.ObjctSignal.connect(self.Thread2_Hndl)
self.MyThread2.signals.IntgrSignal.connect(self.Thread2_Flag)
def MenuSubmit(self):
self.CenterPane.HandleSubmit()
def MenuReset(self):
self.CenterPane.lineEdit.setText('Main Window Menu Reset')
def SetStatusBar(self, parent):
StatusMsg = ''
parent.StatBar = parent.statusBar()
if len(StatusMsg) < 1:
# This verbiage will disappear when you view menu items
StatusMsg = 'Ready'
parent.StatBar.showMessage(StatusMsg)
def OnStart(self):
if self.Thread1Connected:
self.Thread1Hndl.StartProcess()
if self.Thread2Connected:
self.Thread2Hndl.StartProcess()
def OnReset(self):
pass
# Part of Threading
def Thread1_Hndl(self, sigHandle):
self.Thread1Hndl = sigHandle
print('******************************')
print('--- Thread 1 Handle Sent Back Validation')
print(self.Thread1Hndl.Msg)
self.Thread1Connected = True
def Thread1_Flag(self, sigFlag):
print('******************************')
print('--- Thread 1 Loop Id Sent Back Validation')
print('----- Current Loop : ', sigFlag)
print(self.Thread1Hndl.Msg)
self.DoStuffT1()
if sigFlag > 1000:
self.Thread1Connected = False
self.Thread1Hndl.DisConnect()
print(self.Thread1Hndl.Msg)
def Thread2_Hndl(self, Handle):
self.Thread2Hndl = Handle
print('******************************')
print('--- Thread 2 Handle Sent Back Validation')
print(self.Thread2Hndl.Msg)
self.Thread2Connected = True
def Thread2_Flag(self, sigFlag):
print('******************************')
print('--- Thread 2 Loop Id Sent Back Validation')
print('----- Current Loop : ', sigFlag)
print(self.Thread2Hndl.Msg)
self.DoStuffT2()
if sigFlag > 1000:
self.Thread2Connected = False
self.Thread2Hndl.DisConnect()
print(self.Thread2Hndl.Msg)
def DoStuffT1(self):
# Just a place holder function for demonstration purposes
# Perhaps handle this here for one of the Threads
# self.CenterPane.pbrThusFar.setValue(value)
pass
def DoStuffT2(self):
# Just a place holder function for demonstration purposes
pass
# Part of Threading
# These Functions are being passed into completely Separate Threads
# do not try to print from within as stdout is not available
# Also keep in mind you cannot use anything within a Thread that
# inherits from QWidgets and a few from QGui as well
# Create the entire object within the Thread allowing for complete
# autonomy of its entire functionality from the Main GUI
def Threader1(self, sigHandle, sigFlag):
self.Thrdr1Obj = Processor(1) # Create Threader 1 Object from Class
self.Thrdr1Obj.Connect(sigHandle, sigFlag)
def Threader2(self, sigHandle, sigFlag):
self.Thrdr2Obj = Processor(2) # Create Threader 2 Object from Class
self.Thrdr2Obj.Connect(sigHandle, sigFlag)
if __name__ == "__main__":
# It is best to keep this function to its bare minimum as its
# main purpose is to handle pre-processing stuff
#
# Next you did not appear to be using sys.argv but if you od need
# to use command line arguments I strongly suggest you look into
# argparse its a python library and very helpful for this as such
# also much cleaner than dealing with them via regular means
MainThred = QApplication([])
MainGUI = MainWindow()
MainGUI.show()
sysExit(MainThred.exec_())
最后,如果您对此有任何疑问,请提出,但我确实尝试在代码中包含说明。我还做了一些额外的跨对象调用,这样你就可以看到它是如何完成的——这不是一个生产版本,而是一个概念证明,它展示了其中的许多概念
【讨论】:
以上是关于使用 QMessageBox 时无法使用 QThread 设置父错误的主要内容,如果未能解决你的问题,请参考以下文章
QMessageBox::information 自定义按钮
QMessageBox::information 自定义按钮