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
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.
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.