Qt signal 和 slot

参考:Qt for Python Signals and SlotsSignals & SlotsSupport for Signals and Slots

在 GUI 编程中,当我们更改一个小部件时,我们通常希望通知另一个小部件。更一般而言,我们希望任何类型的对象都能够相互通信。例如,如果用户单击“关闭”按钮,我们可能希望调用窗口的close()函数。

其他工具包使用回调来实现这种通信。回调是指向函数的指针,因此,如果您希望处理函数将某些事件通知您,则可以将指向另一个函数的指针(回调)传递给处理函数。然后,处理函数将在适当时调用回调。尽管确实存在使用此方法的成功框架,但回调可能不直观,并且可能在确保回调参数的类型正确性方面遇到问题。

1 Signals and Slots

在 Qt 中,我们有一种替代回调技术的方法:我们使用 Signals and Slots。发生特定事件时会发出信号。Qt 的小部件具有许多预定义的信号,但是我们始终可以对小部件进行子类化,以向其添加自己的信号。Slot 是响应特定信号而调用的函数。Qt 的窗口小部件具有许多预定义的插槽,但是通常的做法是对窗口小部件进行子类化并添加自己的插槽,以便您可以处理感兴趣的信号。

信号和插槽机制是类型安全的:信号的签名(signature)必须与接收插槽的签名匹配。(实际上,插槽可以比接收到的信号具有更短的签名,因为它可以忽略其他参数)由于签名是兼容的,因此编译器可以帮助我们在使用基于函数指针的语法时检测类型不匹配的情况。基于字符串的 SIGNALSLOT 语法将在运行时检测类型不匹配。信号和插槽之间是松散耦合的:发出信号的类既不知道也不关心哪个插槽接收信号。Qt 的信号和插槽机制可确保如果您将信号连接到插槽,则会在正确的时间使用信号的参数调用该插槽。信号和插槽可以采用任意数量的任何类型的参数。它们是完全类型安全的。

QObject或其子类之一(例如QWidget)继承的所有类都可以包含信号和插槽。当对象以其他对象可能感兴趣的方式更改其状态时,会发出信号。这就是对象要传达的所有信息。它不知道也不关心是否有任何东西正在接收它发出的信号。这是真正的信息封装,可确保将对象用作软件组件。

插槽可用于接收信号,但它们也是正常的成员函数。就像对象不知道是否有任何信号接收到一样,插槽也不知道是否有任何信号连接到它上。这确保了可以使用 Qt 创建真正独立的组件。

您可以将任意数量的信号连接到单个插槽,并且信号可以根据需要连接到任意数量的插槽。甚至可以将一个信号直接连接到另一个信号。 (这将在发出第一个信号时立即发出第二个信号。)

信号和插槽一起构成了强大的组件编程机制。

2 Signals

当对象的内部状态以某种可能使对象的客户或所有者感兴趣的方式改变时,它会发出信号。信号是公共访问函数,可以从任何地方发出,但是我们建议仅从定义信号及其子类的类中发出信号。

发出信号后,与其连接的插槽通常会立即执行,就像正常的函数调用一样。发生这种情况时,信号和插槽机制完全独立于任何 GUI 事件循环。一旦所有插槽都返回,将执行 emit 语句之后的代码。使用 queued connections 时情况略有不同;在这种情况下,emit 关键字之后的代码将立即继续,并且稍后将执行插槽。

如果将多个插槽连接到一个信号,则在发出信号时,将按照连接的顺序依次执行插槽。

信号是由 Moc 自动生成的,不得在 .cpp 文件中实现。它们永远不能具有返回类型(即使用void)。

关于参数的注释:我们的经验表明,如果信号和插槽不使用特殊类型,则它们将更可重用。如果valueChanged() 使用特殊的类型,例如假设的 QScrollBar::Range,则只能将其连接到专门为 QScrollBar 设计的插槽。将不同的输入小部件连接在一起是不可能的。

3 Slots

当发出与其连接的信号时,将调用插槽。插槽是正常的 C++ 函数,可以正常调用。它们唯一的特色是可以将信号连接到它们。

由于插槽是普通的成员函数,因此在直接调用时,它们遵循普通的 C++ 规则。但是,作为插槽,它们可以由任何组件通过信号插槽连接调用,无论其访问级别如何。这意味着从任意类的实例发出的信号可以导致在不相关类的实例中调用专用插槽。

您还可以将插槽定义为虚拟插槽,在实践中我们发现该插槽非常有用。

与回调相比,信号和插槽由于提供了更大的灵活性而稍慢一些,尽管实际应用中的差异并不明显。通常,发出连接到某些插槽的信号的速度比使用非虚拟函数调用直接调用接收器的速度大约慢十倍。这是定位连接对象,安全地迭代所有连接(即检查后续接收方在发射期间是否未被销毁)以及以通用方式编组任何参数所需的开销。尽管十个非虚拟函数调用听起来很像,但其开销要比例如任何新操作或删除操作少。一旦执行了在后台需要newdelete的字符串,向量或列表操作,信号和插槽开销仅占整个函数调用成本的很小一部分。每当您在插槽中进行系统调用时,情况都是如此。或间接调用十多个函数。信号和时隙机制的简单性和灵活性非常值得开销,您的用户甚至不会注意到。

请注意,与基于 Qt 的应用程序一起编译时,其他定义变量(称为信号或插槽)的库可能会导致编译器警告和错误。要解决此问题,请 #undef 有问题的预处理器符号。

4 Python 版的例子

4.1 传统语法:SIGNAL () & SLOT()

传统的语法使用 QtCore.SIGNAL()QtCore.SLOT() 定义 Qt 的信号与槽机制。

import sys
from xinet import QtWidgets, QtCore


def func():
    print("func has been called!")

app = QtWidgets.QApplication(sys.argv)
button = QtWidgets.QPushButton("Call func")
QtCore.QObject.connect(button, QtCore.SIGNAL ('clicked()'), func)
button.show()    
sys.exit(app.exec_())

这种写法很复杂,通过静态函数和两个宏来完成 signal 和 slot 的对接所以一点也不 pythonic,官方不推荐的写法。

4.2 新的语法:Signal() & Slot()

上面的代码可以重写为:

from xinet import QtWidgets, QtCore
import sys


def func():
    print("func has been called!")

app = QtWidgets.QApplication(sys.argv)
button = QtWidgets.QPushButton("Call func")
button.clicked.connect(func)
button.show()                                                                                             

sys.exit(app.exec_())

这个语法是官方推荐的。

4.3 自定义 Signal & Slot

4.3.1 QtCore.Signal()

可以使用 QtCore.Signal() 类定义信号。可以将 Python 类型和 C 类型作为参数传递给它。如果需要重载,只需将类型作为元组或列表传递。除此之外,它还可以接收定义信号名称的命名参数名称。如果没有传递任何名称作为名称,则新信号将具有与为其分配的变量相同的名称。

注意:仅应在从 QObject 继承的类中定义信号。这样,信号信息将添加到类 QMetaObject 结构中。

4.3.2 QtCore.Slot()

使用装饰器 QtCore.Slot 分配和重载。同样,要定义签名,只需传递类似QtCore.Signal 类的类型。与 Signal() 类不同,要重载函数,您不必将每个变量都作为元组或列表传递。相反,您必须为每个不同的签名定义一个新的装饰器。

另一个区别是keywordsSlot() 接受 nameresultresult 关键字定义了将返回的类型,可以是 C 或 Python 类型。name 的行为与 Signal() 中的行为相同。如果未传递任何对象作为 name,则新插槽将与正在装饰的函数具有相同的名称。

4.3.3 简单例子

以下示例显示了如何将信号连接到没有任何参数的插槽。

from xinet import QtWidgets, Slot
import sys


# @Slot()是一个装饰器,标志着这个函数是一个 slot(槽)
@Slot()
def output():
    """在控制台输出内容"""
    print("Button clicked")

app = QtWidgets.QApplication(sys.argv)
button = QtWidgets.QPushButton("Call func")
button.clicked.connect(output)
button.show()
sys.exit(app.exec_())

在这个例子中,我们定义了一个槽 output() 和一个按钮的点击信号,并通过 connect 将二者进行连接。这样,当点击按钮时槽就会接收到这个点击信号,相当于调用了 output()

4.3.4 带参数示例

当使用带有参数的信号和槽时,在定义的时候,需要注意声明所带的参数类型。

from xinet import QtWidgets, QtCore, Signal, Slot


# 定义一个带有字符串参数的槽
app = QtWidgets.QApplication([])                                                

# define a new slot that receives a string and has                          
# 'saySomeWords' as its name                                                
@Slot(str)                                                                  
def say_some_words(words):                                                  
    print(words)                                                               

class Communicate(QtCore.QObject):                                                 
    # create a new signal on the fly and name it 'speak'                       
    speak = Signal(str)                                                        

someone = Communicate()                                                     
# connect signal and slot                                                   
someone.speak.connect(say_some_words)                                         
# emit 'speak' signal                                                         
someone.speak.emit("Hello everybody!")
app.exec_()

4.3.5 多个信号连接一个槽

当多个信号传入不同类型参数时,需要增加槽所能接收的参数类型:

from xinet import QtWidgets, QtCore, Signal, Slot

# 增加槽的可接收参数类型
@Slot(str)
@Slot(int)
def say_something(stuff):                                                   
    print(stuff)


class Communicate(QtCore.QObject):                                                 
    # create two new signals on the fly: one will handle                    
    # int type, the other will handle strings                               
    speak_number = Signal(int)                                              
    speak_word = Signal(str)

someone = Communicate()                                                     
# connect signal and slot properly                                          
someone.speak_number.connect(say_something)                                 
someone.speak_word.connect(say_something)                                   
# emit each 'speak' signal                                                  
someone.speak_number.emit(10)                                                
someone.speak_word.emit("Hello everybody!")

当每次只传入其中一种参数时,可以改写成如下形式:

from xinet import QtWidgets, QtCore, Signal, Slot


# define a new slot that receives a C 'int' or a 'str'
# and has 'saySomething' as its name

@Slot(int)
@Slot(str)
def say_something(stuff):
    print(stuff)

class Communicate(QtCore.QObject):
    # create two new signals on the fly: one will handle
    # int type, the other will handle strings
    # 自定义不同参数类型的信号
    # 声明可接收的参数类型,但每次只能够接收其中一种
    speak = Signal((int,), (str,))

someone = Communicate()
# connect signal and slot. As 'int' is the default
# we have to specify the str when connecting the
# second signal
someone.speak.connect(say_something)

someone.speak[str].connect(say_something)

# emit 'speak' signal with different arguments.
# we have to specify the str as int is the default
someone.speak.emit(10)
someone.speak[str].emit("Hello everybody!")

4.3.6 实例方法发送信号

from xinet import QtWidgets, QtCore, Signal, Slot

# Must inherit QObject for signals                                          
class Communicate(QtCore.QObject):                                                 
    speak = Signal()                                                       
    def __init__(self):                                                     
        super().__init__()    
        self.speak.connect(self.say_hello)                             

    def speaking_method(self):                                              
        self.speak.emit()   

    def say_hello(self):
        print("Hello")                                                

someone = Communicate()                                                 
someone.speaking_method()

4.3.7 QThread 发送信号

from xinet import QtWidgets, QtCore, Signal, Slot

# Create the Slots that will receive signals
@Slot(str)
def update_a_str_field(message):
    print(message)

@Slot(int)
def update_a_int_field(self, value):
    print(value)


# Signals must inherit QObject                              
class Communicate(QtCore.QObject):                                                 
    signal_str = Signal(str)
    signal_int = Signal(int)


class WorkerThread(QtCore.QThread):
    def __init__(self, parent=None):
        QThread.__init__(self, parent)
        self.signals = Communicate()
        # Connect the signals to the main thread slots
        self.signals.signal_str.connect(parent.update_a_str_field)
        self.signals.signal_int.connect(parent.update_a_int_field)

    def run(self):
        self.signals.update_a_int_field.emit(1)
        self.signals.update_a_str_field.emit("Hello World.")

注意: 类的实例才能发射信号 ,类是不可以发射信号的:

# Erroneous: refers to class Communicate, not an instance of the class
Communicate.speak.connect(say_something)
# raises exception: AttributeError: 'PySide2.QtCore.Signal' object has no attribute 'connect'

4.4 Signals and Slots 机制

当潜在感兴趣的事情发生时,会发出一个信号。插槽是 Python 可调用的。如果将信号连接到插槽,则在发出信号时将调用该插槽。如果没有连接信号,则什么也不会发生。发出信号的代码(或组件)不知道或不在乎是否正在使用该信号。

信号/插槽机制具有以下功能:

  • 一个 signal 可以连接多个 slot。
  • 一个 signal 可以连接另一个 signal。
  • signal 参数可以是任何 Python 类型。
  • 一个 slot 可以监听多个 signal。
  • signal 与 slot 的连接方式可以是同步连接,也可以是异步连接。
  • signal 与 slot 的连接可能会跨线程。
  • signal 可能会断开。

4.5 Signals 的绑定与解绑

信号(特别是未绑定的信号)是类属性。当信号被引用为该类实例的属性时,PyQt5/PySide2 会自动将该实例绑定到该信号,以创建绑定信号。这与 Python 本身用于从类函数创建绑定方法的机制相同。

被绑定的信号具有实现关联功能的connect()disconnect()emit() 方法。它还具有一个 signal 属性,它是 Qt 的 SIGNAL() 宏返回的信号的签名(signature)。

信号可能重载,即具有特定名称的信号可能支持多个签名。可以用签名对信号进行索引,以选择所需的信号。签名是一系列类型。类型可以是Python类型对象,也可以是C ++类型名称的字符串。C ++类型的名称会自动进行规范化,例如,可以使用QVariant代替未规范化的 const QVariant&

如果信号重载,那么它将有一个默认值,如果没有给出索引,它将被使用。

发出信号时,如有可能,所有参数都将转换为 C++ 类型。如果参数没有对应的C ++类型,则将其包装在特殊的 C++ 类型中,以使其在 Qt 的元类型系统中传递,同时确保其引用计数得到正确维护。

5 Signal & Slot 类编程

当事件或者状态发生改变时,就会发出 signal。同时,signal 会触发所有与这个事件(signal)相关的函数(slot)。signal 与 slot 可以是多对多的关系。

关于 PyQt5 API 中信号与槽的更详细解释,可以参考官方网站:PyQt5.QtCore.pyqtSignal

以下示例显示了许多新信号的定义:

from xinet import QtWidgets, QtCore, Signal, Slot

class Foo(QtCore.QObject):
    # This defines a signal called 'closed' that takes no arguments.
    closed = Signal()

    # This defines a signal called 'rangeChanged' that takes two
    # integer arguments.
    range_changed = Signal(int, int, name='rangeChanged')

    # This defines a signal called 'valueChanged' that has two overloads,
    # one that takes an integer argument and one that takes a QString
    # argument.  Note that because we use a string to specify the type of
    # the QString argument then this code will run under Python v2 and v3.
    valueChanged = Signal([int], ['QString'])

新信号只能在QObject的子类中定义。它们必须是类定义的一部分,并且在定义类之后不能动态添加为类属性。

以这种方式定义的新信号将自动添加到类的 QMetaObject。这意味着它们将出现在 Qt Designer 中,并且可以使用 QMetaObject API 进行内省。

当参数的 Python 类型没有对应的 C++ 类型时,应谨慎使用重载信号。PyQt5/PySide2 使用相同的内部 C++ 类来表示此类对象,因此,可能会产生带有不同 Python 签名的重载信号,这些信号通过相同的 C++ 签名实现,从而产生意外结果。以下是一个示例:

class Foo(QObject):

    # This will cause problems because each has the same C++ signature.
    valueChanged = Signal([dict], [list])

signal 与 slot 有三种使用方法:

  1. 内置的 signal 与 slot 的使用
  2. 自定义 signal 与 slot 的使用
  3. 装饰器的 signal 与 slot 的使用

5.1 操作信号

使用 connect() 函数可以把信号绑定到 slot 函数上。使用 disconnect() 函数可以解除信号与 slot 函数的绑定。使用 emit() 函数可以发射信号。

以下代码演示了不带参数的信号的定义,连接和发射:

from xinet import QtWidgets, QtCore, Signal, Slot

class Foo(QObject):
    # Define a new signal called 'trigger' that has no arguments.
    trigger = Signal()

    def connect_and_emit_trigger(self):
        # Connect the trigger signal to a slot.
        self.trigger.connect(self.handle_trigger)
        # Emit the signal.
        self.trigger.emit()

    def handle_trigger(self):
        # Show that the slot has been called.
        print("trigger signal received")

以下代码演示了 重载 信号的连接:

from xinet import QtWidgets, QtCore, Signal, Slot

class Bar(QtWidgets.QComboBox):
    def connect_activated(self):
        # The PyQt5 documentation will define what the default overload is.
        # In this case it is the overload with the single integer argument.
        self.activated.connect(self.handle_int)

        # For non-default overloads we have to specify which we want to
        # connect.  In this case the one with the single string argument.
        # (Note that we could also explicitly specify the default if we
        # wanted to.)
        self.activated[str].connect(self.handle_string)

    def handle_int(self, index):
        print("activated signal passed integer", index)

    def handle_string(self, text):
        print("activated signal passed QString", text)

创建对象时,也可以通过传递一个插槽作为与信号名称相对应的关键字参数来传递信号,或者使用 pyqtConfigure() 方法来连接信号。例如,以下三个片段是等效的:

act = QAction("Action", self)
act.triggered.connect(self.on_triggered)

act = QAction("Action", self, triggered=self.on_triggered)

act = QAction("Action", self)
act.pyqtConfigure(triggered=self.on_triggered)

通过将 PyQt_PyObject 指定为签名中参数的类型,可以将任何 Python 对象作为信号参数传递。例如:

finished = Signal('PyQt_PyObject')

通常,这将用于传递不知道实际 Python 类型的对象。例如,它也可以用于传递整数,这样就不需要从 Python 对象正常转换为 C++ 整数再返回。

被传递对象的引用计数将自动维护。即使在连接已排队的情况下,在调用 finish.emit() 之后,信号的发射器也无需保留对对象的引用。

5.2 操作 Slots

尽管 PyQt5/PySide2 允许在连接信号时将任何可调用的 Python 用作插槽,但有时有必要将 Python 方法显式标记为 Qt 插槽并为其提供 C++ 签名。PyQt5/PySide2 提供了 Slot() 函数装饰器来执行此操作。

from xinet import QtCore, Signal, Slot

class Foo(QtCore.QObject):

    @Slot()
    def foo(self):
        """ C++: void foo() """

    @Slot(int, str)
    def foo(self, arg1, arg2):
        """ C++: void foo(int, QString) """

    @Slot(int, name='bar')
    def foo(self, arg1):
        """ C++: void bar(int) """

    @Slot(int, result=int)
    def foo(self, arg1):
        """ C++: int foo(int) """

    @Slot(int, QObject)
    def foo(self, arg1):
        """ C++: int foo(int, QObject *) """

也可以链接装饰器,以使用不同的签名多次定义 Python 方法。例如:

class Foo(QtCore.QObject):

    @Slot(int)
    @Slot('QString')
    def valueChanged(self, value):
        """ Two slots will be defined in the QMetaObject. """

PyQt5/PySide2 支持 connectSlotsByName() 函数,pyuic5 生成的 Python 代码最常使用该函数将信号自动连接到符合简单命名约定的插槽。但是,在类重载 Qt 信号的地方(即,具有相同名称但具有不同参数的 Qt 信号),PyQt5 需要附加信息才能自动连接正确的信号。

例如,QSpinBox 类具有以下信号:

void valueChanged(int i);
void valueChanged(const QString &text);

当旋转框的值更改时,将同时发出这两个信号。如果您实现了一个名为 on_spinbox_valueChanged 的插槽(假定您已给 QSpinBox 实例命名为 spinbox),则该插槽将连接到两种信号变体。因此,当用户更改值时,您的广告位将被调用两次-一次使用整数参数,一次使用字符串参数。

Slot() 装饰器可用于指定应将哪些信号连接到插槽。

例如,如果您只对信号的整数变体感兴趣,则您的插槽定义将如下所示:

@Slot(int)
def on_spinbox_valueChanged(self, i):
    # i will be an integer.
    pass

如果您想使用两种不同的 Python 方法来处理信号的两个变体,则插槽定义可能如下所示:

@Slot(int, name='on_spinbox_valueChanged')
def spinbox_int_value(self, i):
    # i will be an integer.
    pass

@Slot(str, name='on_spinbox_valueChanged')
def spinbox_qstring_value(self, s):
    # s will be a Python string object (or a QString if they are enabled).
    pass

5.3 内置的 signal 与 slot 的使用

所谓内置 signal 与 slot 的使用,是指在发射信号时,使用窗口控件的函数,而不是自定义的函数。在 signal 与 slot 中,可以通过 QObject.signal.connect 将一个 QObject 的信号连接到另一个 QObject 的 slot 函数。

from xinet import QtWidgets, QtCore, Signal, Slot
from xinet.run_qt import run


class Window(QtWidgets.QWidget):
    def __init__(self, *args, **kw):
        super().__init__(*args, **kw)
        btn = QtWidgets.QPushButton("测试点击按钮", self)
        btn.clicked.connect(self.showMsg)

    def showMsg(self):
        QtWidgets.QMessageBox.information(self, "信息提示框", "ok,弹出测试信息")


if __name__ == "__main__":
    run(Window)
图1 内置的 signal 与 slot 的使用

5.4 自定义 signal 与 slot 的使用

所谓自定义 signal 与 slot 的使用,是指在发射信号时,不使用窗口控件的函数,而是使用自定义的函数(简单地说,就是使用 Signal 类实例发射信号)。之所以要使用自定义,是因为通过内置函数发射信号有自身的缺陷。首先,内置函数只包含一些常用的信号,有些信号的发射找不到对应的内置函数;其次,只有在特定情况下(如按钮的点击事件)才能发射这种信号;最后,内置函数传递的参数是特定的,不可以自定义。使用自定义的信号函数则没有这些缺陷。

在 PyQt5/PySide2 编程中,自定义 signal 与 slot 的适用范围很灵活,比如因为业务需求,在程序中的某个地方需要发射一个信号,传递多种数据类型(实际上就是传递参数),然后在 slot 函数中接收传递过来的数据,这样就可以非常灵活地实现一些业务逻辑。

from xinet import QtWidgets, QtCore, Signal, Slot
from xinet.run_qt import run


class Window(QtWidgets.QMainWindow):
    def __init__(self, *args, **kw):
        super().__init__(*args, **kw)
        self.setWindowTitle('控件中的信号槽通信')
        self.button1 = QtWidgets.QPushButton('Button 1')
        # 连接
        self.button1.clicked.connect(self.onButtonClick)

        layout = QtWidgets.QHBoxLayout()
        layout.addWidget(self.button1)

        main_frame = QtWidgets.QWidget()
        main_frame.setLayout(layout)
        self.setCentralWidget(main_frame)

    def onButtonClick(self):
        # sender 是发送信号的对象
        sender = self.sender()
        print(sender.text() + ' 被按下了')


if __name__ == "__main__":
    run(Window)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容