2
votes

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.

1
I think you have been confused in saying which works, Option 1 does not work and the other does.eyllanesc
You're right, thanks for noticing; let me edit the code to fix thisOverdrivr

1 Answers

3
votes

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.