PDA

View Full Version : Proper PyQt5 signal & slot syntax



illuzioner1
1st May 2020, 02:35
Hi,

I've read conflicting blog and StackOverflow (SO) posts about PyQt5's signal & slot syntax. This site is supposed to be the reference:

https://www.riverbankcomputing.com/static/Docs/PyQt4/new_style_signals_slots.html

but their examples don't work, at least not in the way I'm trying them. I'm working in PyQt5, but the syntax should be exactly the same according to several posts on SO. I'm just trying to connect a bunch of items to a single signal, but the syntax just doesn't work as it keeps telling me there is no 'connect'. Some posts mention connectNotify, but I've tried that too with no success. Here is a small example of what I'd like to do.

I have a signal 'increase' as a class variable in class 'ChangeSignals', just like in the reference above. However, I want a different class, 'SignalWatchers' to connect to the 'increase' signal and call the SignalWatchers callback 'signal_handler' when 'increase' is emitted.

What is the proper way to construct this?


import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import *
from PyQt5.QtGui import QPen, QPainter, QColor
import string as st
#import time
from threading import Timer
from itertools import count

def print_dir(key, alphabetize=True):
cnt = 0
for i in sorted(dir(key)):
print(i, end=", ")
cnt += 1
if cnt % 10 == 0:
print()
print()

class GlobalTimer(object):
def __init__(self, interval=1.0):
self._registered = set()
self._interval = interval
self._timer = None
self.isActive = False

def register_callback(self, callback):
self._registered.add(callback)

def unregister_callback(self, callback):
self._registered.remove(callback)

def setInterval(self, interval=2.5):
if interval<0 or interval>10:
return
self._interval = interval

def start(self):
self._timer = Timer(self._interval, self._callback)
self._timer.start()
self.isActive = True

def stop(self):
self._timer.cancel()
self.isActive = False

def _callback(self):
self._timer.cancel()
self.isActive = False
for callback in self._registered:
callback()
#self._start_timer()

class ChangeSignals(QObject):

increase = pyqtSignal()
trigger = pyqtSignal()

# This defines a signal called 'rangeChanged' that takes two
# integer arguments.
range_changed = pyqtSignal(int, int, name='rangeChanged')

# This defines a signal called 'valueChanged' that has two overloads,
# one that takes an integer argument and one that takes a QString
# argument. Note that because we use a string to specify the type of
# the QString argument then this code will run under Python v2 and v3.
valueChanged = pyqtSignal([int], ['QString'])

#@staticmethod
def connect_trigger(self):
# Connect the trigger signal to a slot.
self.trigger.connect(self.handle_trigger)

#@staticmethod
def emit_trigger(self):
# Emit the signal.
self.trigger.emit()

sig = ChangeSignals()
sig.connectNotify()

class SignalWatchers(QtWidgets.QGraphicsRectItem):
_instance_count = count(0)

def __init__(self, chr_num, parent=None):
super(SignalWatchers, self).__init__(parent)
self.chr_num = chr_num
self.count = self._instance_count
self._instance_count = next(self._instance_count)
#print(dir(self._instance_count))

self.timer = GlobalTimer(4)
self.timer.register_callback(self.time_callback)

self.font_size = 9
print("count = {}".format(self.count))

#ChangeSignals.increase.connect(self.signal_handle r)
#ChangeSignals.increase.connectNotify(self.signal_ handler)
if self._instance_count == 1:
print_dir(ChangeSignals.increase)


def connect_and_emit_trigger(self):
# Connect the trigger signal to a slot.
self.trigger.connect(self.handle_trigger)

# Emit the signal.
self.trigger.emit()

@pyqtSlot()
def signal_handler(self):
self.chr_num += 1

def time_callback(self):
pass

def paint(self, p, opts, widget):
self.paint_me(p)

def paint_me(self, p):
p.setRenderHint(QtGui.QPainter.Antialiasing)
txt = chr(self.chr_num)
penWidth = 1
font = QtGui.QFont("Helvetica", 9, QtGui.QFont.Bold)
pen = QtGui.QPen(Qt.black, 1)
p.setPen(pen)
p.setFont(font)
p.drawRoundedRect(self.rect(), 5, 5)
p.drawText(12*(self._instance_count -2), 10, txt)


class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.app_width = 605
self.keyboard_height = 220
self.iteration_delay = 50
self.press_cnt = 0
self.setWindowTitle("Signal Test")
self.scene = QtWidgets.QGraphicsScene(self)
self.scene.setItemIndexMethod(QtWidgets.QGraphicsS cene.NoIndex)

self.create_central_widget()
self.sigs = {}
for i in range(65,70):
self.sigs[i] = SignalWatchers(i)
self.scene.addItem(self.sigs[i])

def handle_signal(self, signal_val):
print("signal_val = {}".format(signal_val))
pass


def create_layout(self):
self.layout = QtWidgets.QGraphicsLinearLayout()
self.widget = QtWidgets.QGraphicsWidget()
self.widget.setLayout(self.layout)
self.scene.addItem(self.widget)
width = self.app_width
height = self.keyboard_height
self.setMinimumSize(width, height)
self.scene.setSceneRect(0, 0, width, height)

def create_central_widget(self):
self.view = QtWidgets.QGraphicsView(self.scene)
self.view.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing)
self.view.setBackgroundBrush((QColor("bisque")))
self.setCentralWidget(self.view)
scale_val = 1
self.view.scale(scale_val, scale_val)

def main():
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
ret = app.exec_()
sys.exit(ret)

if __name__ == "__main__":

main()

d_stranz
1st May 2020, 22:53
#ChangeSignals.increase.connect(self.signal_handle r)

I am no PyQt expert, but in C++, you would need an instance of the ChangeSignals class as an argument of the connect() statement. Otherwise, how does connect() know which instance of the signalling class to connect to? Signals aren't broadcast, there must be explicit signal-slot or signal-signal connections between instances of the classes on each end. The only argument you supply is your signal_handler slot (and I assume that in PyQt connect() defaults the SignalWatchers instance on the receiving end (slot) to "self").

illuzioner1
2nd May 2020, 15:47
Thank you for the response. I had also tried using an instantiated version of the class, but was still getting errors on connect.

It turns out that the line 110 above:
@pyqtSlot

was causing the issue. I don't know why, but when I commented out this line everything worked! It looks like that decorator is only for non-class functions.

Thanks!

d_stranz
2nd May 2020, 16:24
Maybe it is my ignorance of PyQt and python in general, but where is the instance of ChangeSignals that forms the sending (signal) end of the increase() signal? I see you have defined a global variable named sig that is a ChangeSignals instance, but where is that used in the connect() statement?

The only thing I can imagine is that ChangeSignals.increase.connect() must create a temporary instance of ChangeSignals that provides the signalling end and once __init__ exits this goes out of scope, gets destroyed, and the connection broken. Shouldn't this be "sig.increase.connect()" instead?

illuzioner1
2nd May 2020, 17:36
No, you didn't miss anything. I went back to the version where I instantiated the class and used that signal, but I didn't update the code in my question. THEN I removed the pyqtSignal decorator.

Sorry to throw you off!