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

后端 未结 2 1969
轮回少年
轮回少年 2021-01-07 04:29

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

相关标签:
2条回答
  • 2021-01-07 05:22

    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.

    0 讨论(0)
  • 2021-01-07 05:31

    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.

    0 讨论(0)
提交回复
热议问题