为啥这个信号/槽代码不起作用

Posted

技术标签:

【中文标题】为啥这个信号/槽代码不起作用【英文标题】:Why isn't this signal/slot code working为什么这个信号/槽代码不起作用 【发布时间】:2018-06-19 19:11:08 【问题描述】:

我有一个基本的信号/槽代码,我想在一个小部件中发出信号,然后在另一个小部件中连接该信号。

#!/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_())

如果我取消选项 1 的注释,则代码有效,并且接收到信号。但是,这两个小部件之间存在耦合。在这种情况下,这很好,因为一个小部件是另一个小部件的父级,并且自然会跟踪其子级。但在更复杂的场景中,这意味着跟踪许多小部件只是为了配置信号,这不是很好。

如果我取消注释选项 2,代码将不起作用,并且控制台仅显示“Emitting...”。这有点烦人,因为在我看来,这是在一个地方配置信号并从另一个地方发出信号的最干净的方式,而不会引入耦合。

我在这里缺少一些基本的东西吗?

编辑:

如果你像这样修改代码,通过添加returnASignal函数

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

运行时控制台显示如下:

Returning a signal
Emitting...

“Returning a signal”只打印一次而不是两次,这表明虽然有多个SignalFactory实例,但它们都共享同一个selectedTextureToLoad对象。这绝对是正确的static class member,而不是实例变量。所以,信号对象到处都是一样的,我还是不明白为什么选项2不起作用。

【问题讨论】:

我想你一直在说哪个有效,选项 1 无效而另一个有效。 您说得对,感谢您的关注;让我编辑代码来解决这个问题 【参考方案1】:

这里没有真正的奥秘。信号对象的行为方式与类中定义的方法完全相同。

如果你像这样输入一些调试打印:

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)

并运行您的示例,它将产生如下输出:

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...

如您所见,从实例访问时信号和方法都是bound objects,从类访问时是未绑定对象。方法必须绑定到实例,以便self 可以作为第一个参数传递。同样,必须将信号对象绑定到实例,以确保连接的插槽仅接收来自发送它的特定实例的信号

因此,在您的示例中有 两个 信号名为 selectedTextureToLoad - 每个创建的 SignalFactory 实例一个。您的示例不起作用,因为 doSomeStuff 插槽未连接到发出 selectedTextureToLoad 信号的特定绑定信号对象

信号槽机制是为对象之间的通信而设计的。在没有已知发送者的情况下,无法广播消息,因此必须始终在发送者和接收者之间建立明确的连接。如果您想发送更通用的信号,请创建SignalFactory 的全局实例。

【讨论】:

非常感谢您的回答。看来我还没有完全理解班员真正的工作原理,现在清楚多了。 我不明白的最后一件事是为什么Returning a signal 只显示一次。它不应该显示的次数与SignalFactory 的实例一样多吗? @Overdrivr。否 - returnASignal 只会在创建类时(即加载模块时)被调用一次。这将创建 unbound 信号对象,它是SignalFactory 类的一个属性。 PyQt 使用一些元类hackery 来为每个后续创建的SignalFactory 实例创建一个唯一的bound 信号对象。所以有一个未绑定的信号对象,和多个绑定的信号对象。 明白了。从中学到了很多东西,非常感谢分享

以上是关于为啥这个信号/槽代码不起作用的主要内容,如果未能解决你的问题,请参考以下文章

qt自定义槽函数不起作用

谁能解释为啥这段代码不起作用?

为啥这个方向代码不起作用?

为啥这个 Python“循环代码”不起作用?

为啥这个运动检测代码不起作用?

为啥这个 SwiftUI Picker 代码不起作用?