8

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.

Marcus Ottosson
  • 3,241
  • 4
  • 28
  • 34
  • 1
    Why do you need dynamic signal creation rather than a static with custom parameter, really? – László Papp Jan 13 '14 at 21:22
  • I'm also curious about the use-case for this. On the face of it, your question has all the hallmarks of a classic [XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). – ekhumoro Jan 13 '14 at 23:46
  • 1
    I don't see how any of your code is actually interfacing with Qt signal/slots. I mean `ObjectB.pressed.connect()` is, but everything else after that doesn't interact with Qt at all, it is just your `Signal` implementation calling Python functions (which incidentally I think will completely break if you have QThreads). It is thus no surprise to me that the sender object is the one that actually uses the qt signal. However I haven't the faintest idea how to make your code work properly. – three_pineapples Jan 13 '14 at 23:51
  • Added use-case, please have a look. Thanks. – Marcus Ottosson Jan 14 '14 at 06:03
  • @ekhumoro: Interesting problem. Have not heard of that definition before. – Marcus Ottosson Jan 14 '14 at 06:25
  • 1
    @three_pineapples: Good point, QThread does not seem to work using this method, shouting out "QObject::setParent: Cannot set parent, new parent is in a different thread". I would love to hear your ideas on how to make it work? – Marcus Ottosson Jan 14 '14 at 06:25
  • 2
    @MarcusOttosson I'm sorry, I don't know how to do it. I'm not even sure it is possible. One place to start might be trying to mimic the behaviour of http://qt-project.org/doc/qt-4.8/threads-qobject.html#signals-and-slots-across-threads but even then I'm not sure you'll ever make something that is truly equivalent without modifying the PyQt source to do what you want. – three_pineapples Jan 14 '14 at 07:47

1 Answers1

1

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.

Neil G
  • 32,138
  • 39
  • 156
  • 257