Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom pyqtSignal implementation

In PyQt, you can use QtCore.pyqtSignal() to create custom signals.

I tried making my own implementation of the Observer pattern in place of pyqtSignal to circumvent some of its limitations (e.g. no dynamic creation).

It works for the most part, with at least one difference.

Here is my implementation so far

class Signal:   
    def __init__(self):
        self.__subscribers = []

    def emit(self, *args, **kwargs):
        for subs in self.__subscribers:
            subs(*args, **kwargs)

    def connect(self, func):
        self.__subscribers.append(func)  

    def disconnect(self, func):  
        try:  
            self.__subscribers.remove(func)  
        except ValueError:  
            print('Warning: function %s not removed from signal %s'%(func,self))

The one thing noticed was a difference in how QObject.sender() works.

I generally stay clear of sender(), but if it works differently then so may other things.

With regular pyqtSignal signals, the sender is always the widget closest in a chain of signals.

In the example at the bottom, you'll see two objects, ObjectA and ObjectB. ObjectA forwards signals from ObjectB and is finally received by Window.

With pyqtSignal, the object received by sender() is ObjectA, which is the one forwarding the signal from ObjectB.

With the Signal class above, the object received is instead ObjectB, the first object in the chain.

Why is this?

Full example

# Using PyQt5 here although the same occurs with PyQt4

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

class Window(QWidget):
    def __init__(self, parent=None):
        super(Window, self).__init__(parent)

        object_a = ObjectA(self)
        object_a.signal.connect(self.listen)

        layout = QBoxLayout(QBoxLayout.TopToBottom, self)
        layout.addWidget(object_a)

    def listen(self):
        print(self.sender().__class__.__name__)


class ObjectA(QWidget):
    signal = Signal()
    # signal = pyqtSignal()
    def __init__(self, parent=None):
        super(ObjectA, self).__init__(parent)

        object_b = ObjectB()
        object_b.signal.connect(self.signal.emit)

        layout = QBoxLayout(QBoxLayout.TopToBottom, self)
        layout.addWidget(object_b)


class ObjectB(QPushButton):
    signal = Signal()
    # signal = pyqtSignal()
    def __init__(self, parent=None):
        super(ObjectB, self).__init__('Push me', parent)
        self.pressed.connect(self.signal.emit)

if __name__ == '__main__':
    import sys

    app = QApplication([])

    win = Window()
    win.show()

    sys.exit(app.exec_())

More reference

Edit:

Apologies, I should have provided a use-case.

Here are some of the limitations of using pyqtSignals:

pyqtSignal:

  1. ..only works with class attributes
  2. ..cannot be used in an already instantiated class
  3. ..must be pre-specified with the data-types you wish to emit
  4. ..produces signals that does not support keyword arguments and
  5. ..produces signals that cannot be modified after instantiation

Thus my main concern is using it together with baseclasses.

Consider the following.

6 different widgets of a list-type container widget share the same interface, but look and behave slightly different. A baseclass provides the basic variables and methods, along with signals.

Using pyqtSignal, you would have to first inherit your baseclass from at least QObject or QWidget.

The problem is neither of these can be use in as mix-ins or in multiple inheritance, if for instance one of the widgets also inherits from QPushButton.

class PinkListItem(QPushButton, Baseclass)

Using the Signal class above, you could instead make baseclasses without any previously inherited classes (or just object) and then use them as mix-ins to any derived subclasses.

Careful not to make the question about whether or not multiple inheritance or mix-ins are good, or of other ways to achieve the same thing. I'd love your feedback on that as well, but perhaps this isn't the place.

I would be much more interested in adding bits to the Signal class to make it work similar to what pyqtSignal produces.

Edit 2:

Just noticed a down-vote, so here comes some more use cases.

Key-word arguments when emitting.

signal.emit(5)

Could instead be written as

signal.emit(velocity=5)

Use with a Builder or with any sort of dependency injection

def create(type):
  w = MyWidget()
  w.my_signal = Signal()
  return w

Looser coupling

I'm using both PyQt4 and PyQt5. With the Signal class above, I could produce baseclasses for both without having it depend on either.

like image 426
Marcus Ottosson Avatar asked Jan 13 '14 21:01

Marcus Ottosson


1 Answers

You can do this with a metaclass that inherits from pyqtWrapperType. Inside __new__, call pyqtSignal() as needed and set the attributes on the result class.

like image 122
Neil G Avatar answered Oct 13 '22 01:10

Neil G