Abdane
Abdane

Reputation: 79

Cannot set parent error with QThread when using QMessageBox

I am trying to run a progress bar on a thread and a function on another thread. The following is my approach and it is working fine, until I add a QMessageBox. I created two new classes for QThread, one handles the progress bar, another my function. They are being called when the button is pressed using the onButtonClicked function

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

I am getting QObject::setParent: Cannot set parent, new parent is in a different thread Error when doing this, only when I add a QMessageBox in my test function. I am assuming that this is happening because QMessagebox is running on the main thread, not my External2() class, how can I fix this?

Upvotes: 2

Views: 317

Answers (2)

Dennis Jensen
Dennis Jensen

Reputation: 224

Okay this per-sae does a bit more than you asked but it also does a bit less also -- however it does include all the bits and pieces and how they interconnect so you should be able to extrapolate from this and get it to do whatever it is you are wanting to get done. Note your program had a few issues so I could not duplicate it verbatim -- one of those issues pertaining to your specific problem is that you cannot run anything that inherits from QWidgets from within a thread -- I solved this by creating a multi-process to handle the 2nd Window situation. Still with how I have outlined the Threading you can see that you do not need to have that QMessageBox within the Thread but you could cause something from within that Thread to launch the QMessageBox back in the QMainWindow -- as I have pointed out -- if you need the Thread to launch that QMessageBox -- This works although you might need to add a bit to show the functionality going on within the Threads -- I know this because I have already tested that aspect of this.

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

Finally if you have any questions on this do ask but I did try to include explanatories within the code. Also I did a bit of extra cross object calling so you could see how it might be done -- this is not a production version but more a proof of concept that demonstrates numerous concepts within it

Upvotes: 1

eyllanesc
eyllanesc

Reputation: 244132

The first thing you have to do is verify if the requirements are met (in this case the QLineEdit is not empty) for the heavy duty to start. In these cases I prefer to use the worker thread approach. To launch the heavy task, the method must be invoked asynchronously, for example using QTimer.singleShot(), and to pass the additional arguments use functools.partial(), you must also use @pyqtSlot to be sure that the tasks are executed in the thread Right.

On the other hand you should not modify the class generated by Qt Designer(1) but create another class that inherits from the widget and use the first one to fill it.

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

Upvotes: 2

Related Questions