Overdrivr
Overdrivr

Reputation: 6596

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_())

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.

Upvotes: 2

Views: 415

Answers (1)

ekhumoro
ekhumoro

Reputation: 120738

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.

Upvotes: 3

Related Questions