PyQt5 Threads, Signal, and Slot. Connect Error - python

I'm new to PyQt5 and I can't seem to connect my pyqtSignal and pyqtSlot. The error, "TypeError: connect() failed between worker.newIcon[object] and updateIcon()" pops out. Anyone can guide me to the right path please?
from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QAction, QMenu
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlot
class worker(QThread):
newIcon = pyqtSignal(object)
def run(self):
while True:
self.newIcon.emit(QIcon("shield-off.png"))
QThread.msleep(1000)
class systemTray():
def start_tray(self):
self.app = QApplication([])
self.app.setQuitOnLastWindowClosed(False)
icon = QIcon("shield-on.png")
self.tray = QSystemTrayIcon()
self.tray.setIcon(icon)
self.tray.setVisible(True)
self.menu = QMenu()
self.quit = QAction("Exit")
self.quit.triggered.connect(self.app.quit)
self.menu.addAction(self.quit)
self.tray.setContextMenu(self.menu)
self.thread = QThread()
self.worker = worker()
self.worker.moveToThread(self.thread)
self.worker.newIcon.connect(self.updateIcon)
self.thread.started.connect(self.worker.run)
self.thread.start()
# Run tray
self.app.exec_()
#pyqtSlot(object)
def updateIcon(self, icon):
self.tray.setIcon(icon)
if __name__ == "__main__":
be_tray = systemTray()
be_tray.start_tray()

The pyqtSlot decorator is intended to be used for QObject subclasses, otherwise it will not work correctly.
So, the solution is pretty simple, as you only need to inherit from QObject:
class systemTray(QObject):
def start_tray(self):
# ...
On the other hand, using a pyqtSlot decorator is rarely needed and only for specific situations (see this related answer. In your case, it doesn't seem necessary.
Also, since you're already subclassing from QThread, you can just use its own start(), without using moveToThread() on a new one.

Related

Python Qt - do signals need to be created in the worker thread?

I am using Python Qt (PySide2) which has a GUI in the main thread and a library that does IO, data-crunching, etc in a second thread.
I use signals & slots to update the GUI. In the examples I have seen on SO, the signal is always created in the worker (non-GUI) thread. Is it necessary to do this?
Reason: my library can be used with a GUI or could be used in another Python script. So, it might output data to the GUI or maybe to console/log file. To make the code in the library generic, I thought that whatever calls the library can register a callback. That callback can be to emit to Qt or output to file, etc.
Here's an example where the Signal is created in the GUI thread. It works, but could it cause thread-related issues?
import threading
import time
import sys
from PySide2.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QPushButton, QApplication
from PySide2 import QtCore
class aio_connection(QtCore.QObject):
data_recvd = QtCore.Signal(object)
class TextEditDemo(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("TEST")
self.resize(600,540)
self.textEdit = QTextEdit()
self.btnPress1 = QPushButton("Run")
layout = QVBoxLayout()
layout.addWidget(self.textEdit)
layout.addWidget(self.btnPress1)
self.setLayout(layout)
self.btnPress1.clicked.connect(self.btnPress1_Clicked)
self.aio_conn = aio_connection() # Signal is created in main (GUI) thread
# Connect signals (data_recvd) and slots (on_data_ready)
self.aio_conn.data_recvd.connect(self.on_data_ready)
def btnPress1_Clicked(self):
threading.Thread(target=bg_task, args=(self.cb,)).start()
#QtCore.Slot()
def on_data_ready(self, msg):
self.textEdit.append(msg)
def cb(self, info):
self.aio_conn.data_recvd.emit(info)
# Simulate the library that runs in a second thread
def bg_task(callback):
for i in range(100):
callback(str(i))
time.sleep(0.1)
def main():
app = QApplication(sys.argv)
win = TextEditDemo()
win.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
By default, signal connections are made using the AutoConnection type. This means the eventual type of connection will be automatically made at runtime, when the signal is actually emitted. If the sender and receiver are in different threads, Qt will use a queued connection, which will post an event to the event-queue of the receiving thread. Thus, when control returns to the receiver, the event will be processed and any connected slots will be called at that time.
This is an important consideration when updating GUI elements from worker threads, because Qt does not support GUI-related operations of any kind outside the main thread. However, so long as you always use signals to communicate between threads using the default connection-type, you should not have any problems - Qt will automatically guarantee they are done in a thread-safe way.
Below is a version of your scripts that verifies everything is working as desired. When I run it, I get the following output, which shows the current thread-id within each function call:
main: 4973
bg_task: 4976
cb: 4976
on_data_ready: 4973
cb: 4976
on_data_ready: 4973
cb: 4976
...
import threading, time, sys
from PySide2.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QPushButton, QApplication
from PySide2 import QtCore
class aio_connection(QtCore.QObject):
data_recvd = QtCore.Signal(object)
class TextEditDemo(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("TEST")
self.resize(600,540)
self.textEdit = QTextEdit()
self.btnPress1 = QPushButton("Run")
layout = QVBoxLayout()
layout.addWidget(self.textEdit)
layout.addWidget(self.btnPress1)
self.setLayout(layout)
self.btnPress1.clicked.connect(self.btnPress1_Clicked)
self.aio_conn = aio_connection()
self.aio_conn.data_recvd.connect(self.on_data_ready)
def btnPress1_Clicked(self):
threading.Thread(target=bg_task, args=(self.cb,)).start()
#QtCore.Slot()
def on_data_ready(self, msg):
print('on_data_ready:', threading.get_native_id())
self.textEdit.append(msg)
def cb(self, info):
print('cb:', threading.get_native_id())
self.aio_conn.data_recvd.emit(info)
def bg_task(callback):
print('bg_task:', threading.get_native_id())
for i in range(5):
callback(str(i))
time.sleep(0.1)
def main():
print('main:', threading.get_native_id())
app = QApplication(sys.argv)
win = TextEditDemo()
win.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

When using a QThread, how do i pass data to the function that is called with .connect()?

I am using a QThread and, when my worker class is dont with the big work, i want to call a function that does changes to my UI (in this case with .finished.connect(), but i dont fully understand how the .connect() step works and how i can pass variables to the function called.
As an example: A button that, when pressed, opens a Thread in which it counts up, and changes the button text to the number its on. When the counter is finished, it prints something. Very simple and it works just fine.
main.py:
from PyQt5 import QtWidgets
import sys
import worker
from PyQt5.QtCore import QThread
from start import test
class App(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(App, self).__init__(parent)
self.init_ui()
self.initiate_buttons()
self.obj = worker.Worker()
self.thread = QThread()
self.obj.intReady.connect(self.onIntReady)
self.obj.moveToThread(self.thread)
self.obj.finished.connect(self.thread.quit)
self.obj.finished.connect(test)
self.thread.started.connect(self.obj.procCounter)
def init_ui(self):
self.pushButton = QtWidgets.QPushButton(self)
def onIntReady(self, i):
self.pushButton.setText("{}".format(i))
def initiate_buttons(self):
print("clicked")
self.pushButton.clicked.connect(lambda: self.thread.start())
def run():
application = QtWidgets.QApplication(sys.argv)
main_app = App()
main_app.show()
application.exec()
if __name__ == '__main__':
run()
worker.py:
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot
import time
class Worker(QObject):
finished = pyqtSignal()
intReady = pyqtSignal(int)
#pyqtSlot()
def procCounter(self):
for i in range(1, 5):
time.sleep(1)
self.intReady.emit(i)
self.finished.emit()
start.py:
def test(x="bar"):
print("test"+x)
So the thing i am talking about is start.py - i can put the function in its own module just fine and it worked. But what if i want to pass a parameter to it? I tried the obvious attempt, changing self.obj.finished.connect(test) to self.obj.finished.connect(test(x="foo")) - but that doesnt work. But i dont understand why it does not work, and how else i am supposed to do this. In the end, i want the module to do changes to my UI, just like onIntReady. I found this and this but did not understand how i can apply it.

Why isn't this signal/slot code working

I have a basic signal/slot code where I want to emit a signal in one widget, and connect that signal in another widget.
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from PyQt5.QtWidgets import QMainWindow, QAction, QApplication, QWidget, QPushButton, qApp, QLabel, QHBoxLayout, QVBoxLayout, QSplitter, QFileDialog
from PyQt5.QtGui import QIcon, QPixmap, QPainter, QImage
from PyQt5.QtCore import QSize, Qt, pyqtSignal, QObject
import sys, random
import qdarkstyle
from os import path
class SignalFactory(QObject):
selectedTextureToLoad = pyqtSignal()
class Application(QMainWindow):
def __init__(self):
super().__init__()
self.signals = SignalFactory()
self.mainArea = MainArea()
# Option 1 - Uncomment below / Works but strong coupling between widgets
# self.signals.selectedTextureToLoad.connect(self.mainArea.doSomeStuff)
self.setCentralWidget(self.mainArea)
self.setGeometry(300, 300, 800, 400)
self.show()
def emitStuff(self):
print("Emitting...")
self.signals.selectedTextureToLoad.emit()
class MainArea(QWidget):
def __init__(self):
super().__init__()
self.signals = SignalFactory()
# Option 2 - Uncomment below / Does not work
#self.signals.selectedTextureToLoad.connect(self.doSomeStuff)
def doSomeStuff(self):
print("Receiving...")
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Application()
ex.emitStuff()
sys.exit(app.exec_())
If I uncomment Option 1, the code works, and the signal is received. However, there is a coupling between the two widgets. In this case it's fine, because one widget is the parent of the other and naturally keeps track of its child. But in a more complex scenario, that means keeping track of many widgets just to configure the signals, which is not great.
If I uncomment Option 2, the code does not work, and the console only displays "Emitting...". It's kind of annoying, since this is in my opinion the cleanest way to configure signal in one place and emit it from another place, without introducing coupling.
Am I missing something fundamental here ?
EDIT:
If you modify the code like this, by adding the returnASignal function
from PyQt5.QtWidgets import QMainWindow, QAction, QApplication, QWidget, QPushButton, qApp, QLabel, QHBoxLayout, QVBoxLayout, QSplitter, QFileDialog
from PyQt5.QtGui import QIcon, QPixmap, QPainter, QImage
from PyQt5.QtCore import QSize, Qt, pyqtSignal, QObject
import sys, random
import qdarkstyle
from os import path
def returnASignal():
print('Returning a signal')
return pyqtSignal()
class SignalFactory(QObject):
selectedTextureToLoad = returnASignal()
class Application(QMainWindow):
def __init__(self):
super().__init__()
self.signals = SignalFactory()
self.mainArea = MainArea()
# Option 2 - Uncomment below / Works but strong coupling between widgets
# self.signals.selectedTextureToLoad.connect(self.mainArea.doSomeStuff)
self.setCentralWidget(self.mainArea)
self.setGeometry(300, 300, 800, 400)
self.show()
def emitStuff(self):
print("Emitting...")
self.signals.selectedTextureToLoad.emit()
class MainArea(QWidget):
def __init__(self):
super().__init__()
self.signals = SignalFactory()
# Option 1 - Uncomment below / Does not work
self.signals.selectedTextureToLoad.connect(self.doSomeStuff)
def doSomeStuff(self):
print("Receiving...")
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Application()
ex.emitStuff()
sys.exit(app.exec_())
When running it the console displays this:
Returning a signal
Emitting...
"Returning a signal" is printed only once and not twice, which shows that although there are multiple SignalFactory instances, they all share the same selectedTextureToLoad object. This is definitely a proper static class member and not an instance variable. Therefore, the signal object is the same everywhere, and I still don't understand why Option 2 does not work.
There is no real mystery here. Signal objects behave in exactly the same way as methods defined in classes.
If you put in some debugging prints like this:
class SignalFactory(QObject):
selectedTextureToLoad = returnASignal()
print(selectedTextureToLoad)
def foo(self): pass
print(foo)
class Application(QMainWindow):
def __init__(self):
super().__init__()
self.signals = SignalFactory()
print(self.signals.selectedTextureToLoad)
print(self.signals.foo)
...
class MainArea(QWidget):
def __init__(self):
super().__init__()
self.signals = SignalFactory()
print(self.signals.selectedTextureToLoad)
print(self.signals.foo)
and run your example, it will produce output like this:
Returning a signal
<unbound PYQT_SIGNAL []>
<function SignalFactory.foo at 0x7f2a57b1c268>
<bound PYQT_SIGNAL selectedTextureToLoad of SignalFactory object at 0x7f2a57b96828>
<bound method SignalFactory.foo of <__main__.SignalFactory object at 0x7f2a57b96828>>
<bound PYQT_SIGNAL selectedTextureToLoad of SignalFactory object at 0x7f2a57b96948>
<bound method SignalFactory.foo of <__main__.SignalFactory object at 0x7f2a57b96948>>
Emitting...
As you can see, both signals and methods are bound objects when accessed from instances, and unbound objects when accessed from the class. Methods must be bound to the instance so that self can be passed as the first argument. Likewise, a signal object must be bound to the instance to ensure that connected slots only receive signals from the specific instance that sent it.
So there are two signals named selectedTextureToLoad in your example - one for each instance of SignalFactory that is created. Your example doesn't work because the doSomeStuff slot isn't connected to the specific bound signal object that emitted the selectedTextureToLoad signal.
The signal-slot mechanism is designed for communication between objects. There is no facility for broadcasting messages without a known sender, so explicit connections must always be made between the emitter and the receiver. If you want to send a more generic signal, create a global instance of SignalFactory.

How to can I add threading to PyQt5 GUI?

So I have created a GUI using QT Designer. It works pretty well, but on more complex calls it doesn't update the main window and locks up. I want to run my CustomComplexFunction() while updating a textEdit in the main window from constantly changing backend information, and I wanted it to run every 2 seconds. The following code seems right and runs without errors, but doesn't update the textEdit. Please note i'm importing a .ui file designed from QT Designer with a pushButton and textEdit and the code won't run without it.
Main.py
import sys
from PyQt5.QtWidgets import QDialog, QApplication, QPushButton, QVBoxLayout, QMainWindow
from PyQt5.QtCore import QCoreApplication, QObject, QRunnable, QThread, QThreadPool, pyqtSignal, pyqtSlot
from PyQt5 import uic, QtGui
class Worker(QObject):
newParams = pyqtSignal()
#pyqtSlot()
def paramUp(self):
x=1
for x in range(100):
time.sleep(2)
self.newParams.emit()
print ("sent new params")
x +=1
Ui_somewindow, _ = uic.loadUiType("mainwindow.ui") #the path to UI
class SomeWindow(QMainWindow, Ui_somewindow, QDialog):
def __init__(self):
QMainWindow.__init__(self)
Ui_somewindow.__init__(self)
self.setupUi(self)
# Start custom functions
self.params = {}
self.pushButton.clicked.connect(self.complex) #NumEvent
def complex(self):
self.work = Worker()
self.thread = QThread()
self.work.newParams.connect(self.changeTextEdit)
self.work.moveToThread(self.thread)
self.thread.start()
self.CustomComplexFunction()
def CustomComplexFunction(self):
self.params['City'] = 'Test'
def changeTextEdit(self):
try:
City = self.params['City']
self.textEdit_14.setPlainText(City)
except KeyError:
City = None
if __name__ == "__main__":
app = QApplication(sys.argv)
window = SomeWindow()
window.show()
sys.exit(app.exec_())
You can see the official docs for Signals and Slots here and this SO post was also very helpful, but it seems like I built it correctly. According to the docs, the emitter doesn't care if the signal is used. This might be why the code doesn't have errors but doesn't work either.
Any ideas on how to make it work? Or atleast some way to test the emitter and signals??
You have forgot to connect the thread to the worker object.
self.work = Worker()
self.thread = QThread()
self.thread.started.connect(self.worker.work) # <--new line, make sure work starts.
self.thread.start()
Good luck with the application :-)

How to call a method asynchronously in PyQt5 using Python3?

How to call a method asynchronously in PyQt5 using Python3?
I have tried to use a signal to do it.
import sys
from time import sleep
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel
class MainWindow(QMainWindow):
asyncFuncSignal = pyqtSignal()
def __init__(self):
super().__init__()
self.initUi()
def initUi(self):
self.label = QLabel(self)
self.label.setText("loading...")
# Trying to call 'self.asyncFunc' asynchronously
self.asyncFuncSignal.connect(self.asyncFunc)
self.asyncFuncSignal.emit()
print("loaded")
def asyncFunc(self):
# Doing something hard that takes time
# I have used 'sleep' to implement the delay
sleep(2)
self.label.setText("done")
print("asyncFunc finished")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
This program tries to finish asyncFunc before writing "loaded". But I would like the program to finish initUi immediately with shown loading... in the label and after that the text done appears in 2 seconds.
What is the best and the shortest way to do it?
It's said here http://pyqt.sourceforge.net/Docs/PyQt5/signals_slots.html I can use queued connections but I haven't found an example how to implement it.
PyQt5.QtCore.Qt.QueuedConnection may help. Just replace
self.asyncFuncSignal.connect(self.asyncFunc)
with
from PyQt5.QtCore import Qt
...
self.asyncFuncSignal.connect(self.asyncFunc, Qt.QueuedConnection)

Categories