使用QMessageBox时无法使用QThread设置父错误

时间:2019-08-12 17:19:02

标签: python multithreading pyqt5

我试图在一个线程上运行进度条,并在另一个线程上运行函数。以下是我的方法,并且可以正常工作,直到添加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函数中添加QMessageBox时,我才会收到test错误。我以为这是因为QMessagebox是在主线程(而不是我的External2()类)上运行,我该如何解决?

2 个答案:

答案 0 :(得分:2)

您要做的第一件事就是验证是否满足重型启动的要求(在这种情况下,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

答案 1 :(得分:1)

好吧,这个单方面的功能比您要求的要多,但它的功能也要少得多-但是它确实包含了所有的点点滴滴以及它们如何互连,因此您应该可以从中推断并得到它做你想做的事。请注意,您的程序有几个问题,所以我不能原样复制它-与您的特定问题有关的问题之一是您无法在线程内运行从QWidgets继承的任何东西-我通过创建一个多进程解决了这个问题处理第二窗口的情况。仍然按照我概述线程的方式,您可以看到您不需要在线程中包含该QMessageBox,但是您可以使该线程中的某些内容在QMainWindow中启动QMessageBox-如我所指出的-您需要使用Thread来启动该QMessageBox -尽管您可能需要添加一些内容以显示Threads中正在进行的功能,但是它可以正常工作-我知道这一点,因为我已经对此进行了测试。

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

最后,如果您对此有任何疑问,请务必提出,但我确实尝试在代码中包含解释。此外,我还做了一些额外的跨对象调用,所以您可以看到它是如何完成的-这不是生产版本,而是更多的概念证明,可以证明其中的众多概念