简体   繁体   中英

PyQt5 QObject: Cannot create children for a parent that is in a different thread

I am working in a menu system tray with PyQt5. I am very new with PyQt5, and what I want to do is to trigger an action without the menu being blocked (multithreading). After having read in many places, I have come to the conclusion that using Qthread should be the way to go (but if only I could understand how that class works...). However, the use of threading wouldn't be that bad either given that my application is very simple. So, I have tried the following code using import threading :

from PyQt5 import QtCore, QtGui, QtWidgets
import threading

class menubar(object):
    def __init__(self):
    signal.signal(signal.SIGINT, signal.SIG_DFL)
    self.systray = True
    self.stopped = False

    def search_menu(self):
        self.SearchAction = menu.addAction("Search")
        self.SearchAction.triggered.connect(self.search_cast)

    def _search_cast_(self):
        args.select_cc = True
        self.cc.initialize_cast()
        self.cast_list()

    def search_cast(self):
        threading.Thread(target=self._search_cast_).start()

#some more methods here...

def main():

    menubar()
    app = QtWidgets.QApplication(sys.argv)
    tray = QtWidgets.QSystemTrayIcon(icon)

    menu = QtWidgets.QMenu()
    start = menubar()
    start.search_menu()
    start.separator_menu()
    start.populating_menu()
    start.separator_menu()
    start.stop_menu()
    start.resetaudio_menu()
    start.about_menu()
    start.exit_menu()

    tray.setContextMenu(menu)
    tray.show()
    app.exec_()

if __name__ == '__main__':
     main()

When I start my menu, everything is in place as I expect it. Then, when I click on the menu Search the action triggers the self.search_cast method, and my menu gets populated with the list it finds. I can also see my application doing the searching without getting blocked but when it finishes I get the following errors:

QObject: Cannot create children for a parent that is in a different thread.
(Parent is QMenu(0x7fcef497c160), parent's thread is     QThread(0x7fcef2603d10), current thread is QThread(0x7fcef4a89360)
QObject: Cannot create children for a parent that is in a different thread.
(Parent is QMenu(0x7fcef497c160), parent's thread is  QThread(0x7fcef2603d10), current thread is QThread(0x7fcef4a89360)
QObject: Cannot create children for a parent that is in a different thread.

After this, the menu is still "functional" in the sense that it is responsive but no more action can be triggered. Additionally, it seems that no more threads are created. I would be glad if somebody could explain me why is this happening?. I don't see the light...

Update :

I have created now a worker.py that contains:

from PyQt5.QtCore import QThread, QObject, pyqtSignal, pyqtSlot
#some other imports


class Worker(QObject):
    finished = pyqtSignal()


@pyqtSlot()
def _search_cast_(self):
    self.cc = casting()
    self.cc.initialize_cast()
    self.finished.emit()

Then I have added in the class menubar the following:

class menubar(object):
    def __init__(self):
        self.cc = casting()
        signal.signal(signal.SIGINT, signal.SIG_DFL)
        self.cc.cast = None
        self.systray = True
        self.stopped = False

        self.obj = worker.Worker()  # no parent!
        self.thread = QThread()  # no parent!
        self.obj.moveToThread(self.thread)
        self.obj.finished.connect(self.thread.quit)
        self.thread.started.connect(self.obj._search_cast_)

  def search_menu(self):
        self.SearchAction = menu.addAction("Search")
        self.SearchAction.triggered.connect(self.search_cast)

  def search_cast(self):
    self.thread.start()
    self.cast_list()

  def cast_list(self):
     if len(self.cc.availablecc) == 0:
     # some actions here. 

And now I get the following error:

 AttributeError: 'casting' object has no attribute 'availablecc'

I make sure that actually the worker is recovering availablecc from an external class that I called cc . But for some reason is not being received by the menubar class. I am working based on this https://stackoverflow.com/a/33453124/1995261

I will proceed to answer myself. Inspired by https://stackoverflow.com/a/33453124/1995261 , I solved this by implementing the following:

1) I created a worker.py that executes the method _search_cast_ that was blocking the menu. When this method finishes searching, it emits two signals: a) one informing that he recovered the list , and b) that the method has finished.

#worker.py
from PyQt5.QtCore import QThread, QObject, pyqtSignal, pyqtSlot


class Worker(QObject):
    finished = pyqtSignal()
    intReady = pyqtSignal(list)
    def __init__(self):
        QObject.__init__(self)

    @pyqtSlot()
    def _search_cast_(self):
        self.cc = casting()
        self.cc.initialize_cast()
        availablecc = self.cc.availablecc
        self.intReady.emit(availablecc)
        self.finished.emit()

2) In the main.py I dumped the following and I try to explain inside the code with comments:

#main.py
from PyQt5.QtCore import QThread, QObject, pyqtSignal, pyqtSlot
import worker # This is to import worker.py
class menubar(object):
    def __init__(self):
        signal.signal(signal.SIGINT, signal.SIG_DFL)
        self.cc.cast = None
        self.systray = True
        self.stopped = False

        self.obj = worker.Worker()  # The worker is started with no parent!
        self.thread = QThread()  # We initialise the Qthread class with no parent!
        self.obj.intReady.connect(self.onIntReady) # We receive the signal that the list is ready
        self.obj.moveToThread(self.thread) # Moving the object to the thread
        self.obj.finished.connect(self.thread.quit) # When the method is finished we receive the signal that it is finished
        self.thread.started.connect(self.obj._search_cast_) # We need to connect the above with the desired method inside the work.py

        self.app = QtWidgets.QApplication(sys.argv)

        def search_menu(self):
            self.SearchAction = self.menu.addAction("Search")
            self.SearchAction.triggered.connect(self.search_cast)

        def onIntReady(self, availablecc):     # This method receives the list from the worker
            print ('availablecc', availablecc)  # This is for debugging reasons to verify that I receive the list with the correct content
            self.availablecc = availablecc

        def search_cast(self):   #This method starts the thread when  self.SearchAction is triggered
            args.select_cc = True
            self.thread.start()

In this way, when searching for the list the menu does not get blocked, no errors are shown on the screen and the number of threads when monitoring them in activity monitor stay correct.

I hope this helps people. For more precise information (I am still learning PyQt and my wording may not be very good), I suggest you to check the link that I posted above.

Since this is the google top answer for this error and it took me longer than expected to work this out properly, I will share my very simple solution for Python 3 and PyQt 5 (if you change some imports it should work in PyQt4 too I guess).

The situation I had was a systray icon with a right-click menu, that should be re-built when a different thread requests it. You can of course apply this to other problems where you want to communicate through thread limits.

import time
import sys
import threading
from PyQt5 import QtGui
from PyQt5 import QtWidgets
from PyQt5 import QtCore



class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
    def __init__(self, icon=None, parent=None):
        icon = QtGui.QIcon(QtWidgets.QApplication.style().standardPixmap(QtWidgets.QStyle.SP_MediaPlay))
        QtWidgets.QSystemTrayIcon.__init__(self, icon, parent)

        self.menu = QtWidgets.QMenu(parent)
        self.setContextMenu(self.menu)

        self.build_menu()
        self.show()

        # see http://pyqt.sourceforge.net/Docs/PyQt5/signals_slots.html for more information
        self.signal = MySignal()
        self.signal.sig_no_args.connect(self.build_menu)
        self.signal.sig_with_str.connect(self.print_string)


    def build_menu(self):
        ''' This function should be called in order to rebuild 
        the right-click menu for the systray icon'''
        global list_dict_streams
        self.menu.clear()

        exitAction = self.menu.addAction("Exit")
        exitAction.triggered.connect(self._exit)

        for x in list_dict_streams :
            self.menu.addAction(x)


    def print_string(self, str):
        print(str)


    def _exit(self):
        QtCore.QCoreApplication.exit()



class MySignal(QtCore.QObject):
    ''' Why a whole new class? See here: 
    https://stackoverflow.com/a/25930966/2441026 '''
    sig_no_args = QtCore.pyqtSignal()
    sig_with_str = QtCore.pyqtSignal(str)


list_dict_streams = ["1"]
def work_thread(trayIcon):
    ''' Will add one menu item to the systray menu every 5 seconds
    and will send a signal with a string '''
    global list_dict_streams

    while True:
        trayIcon.signal.sig_no_args.emit()
        trayIcon.signal.sig_with_str.emit("String emitted")
        list_dict_streams.append(str(len(list_dict_streams)+1))
        time.sleep(5)


def main():
    app = QtWidgets.QApplication(sys.argv)
    trayIcon = SystemTrayIcon()

    t = threading.Thread(target=work_thread, args=(trayIcon,))
    t.daemon = True     # otherwise the 'Exit' from the systray menu will not work
    t.start()

    sys.exit(app.exec_())


if __name__ == '__main__':
    main()

Basically you have to create a new class MySignal(QtCore.QObject) why . I created a class with two examples - one that sends no arguments along another one that you can pass a string. You can of course define other arguments . Then in your target thread you create a new instance of this class and connect the functions from that class, to the functions inside your target (the systray icon in my case). After that you can now call the emit(...) functions like I do in the while-loop.
Now Qt is happy as you just emit a signal compared to when you would call trayIcon.build_menu() directly from a different thread.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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