PySide和PyQt中Signal的工作原理

PySide和PyQt中Signal的工作原理

背景

PySide和PyQt能够让Qt在Python中运行。Qt的信号槽机制在PySide和PyQt中是这样使用的:

PySide:

from PySide6.QtCore import Signal, Slot, QObject


class Foo(QObject):
    signal = Signal(str)

    def emit_signal(self, message):
        self.signal.emit(message)


class Bar(QObject):
    @Slot(str)
    def slot(self, message):
        print(message)


f = Foo()
b = Bar()
f.signal.connect(b.slot)
f.emit_signal("Hello World!")

# 输出:
# Hello World!

PyQt:

from PyQt6.QtCore import pyqtSignal, pyqtSlot, QObject


class Foo(QObject):
    signal = pyqtSignal(str)

    def emit_signal(self, message):
        self.signal.emit(message)


class Bar(QObject):
    @pyqtSlot(str)
    def slot(self, message):
        print(message)


f = Foo()
b = Bar()
f.signal.connect(b.slot)
f.emit_signal("Hello World!")

# 输出:
# Hello World!

可以发现,信号定义为类的一个静态成员。但是,信号和槽的连接是在不同的对象之间进行的。所以,这到底是怎么实现的?

原理

查阅PyQt的文档,我们可以发现:

A signal (specifically an unbound signal) is a class attribute. When a signal is referenced as an attribute of an instance of the class then PyQt6 automatically binds the instance to the signal in order to create a bound signal. This is the same mechanism that Python itself uses to create bound methods from class functions.

A bound signal has connect(), disconnect() and emit() methods that implement the associated functionality. It also has a signal attribute that is the signature of the signal that would be returned by Qt’s SIGNAL() macro.

其中提到,信号定义为类的属性(类的静态成员),它是一个未绑定信号(unbound signal)。当我们用类的一个对象去访问信号时,该对象会自动绑定到信号,并创建一个绑定信号(bound signal)。我们可以做一个实验:

from PySide6.QtCore import Signal, QObject


class Foo(QObject):
    signal = Signal()


signal = Signal()
print(type(signal))

f = Foo()
print(type(f.signal))

# 输出:
# <class 'PySide6.QtCore.Signal'>
# <class 'PySide6.QtCore.SignalInstance'>

可以发现,单独创建信号时,它的类型是PySide6.QtCore.Signal,而在类中创建并且通过类的实例访问时,类型却是PySide6.QtCore.SignalInstance。说明后者通过了某种方式创建了一个新的对象。

查阅资料发现,这其实是利用了python语言的一种机制:Descriptors

简单来说,就是可以通过重写__get__, __set__, __delete__函数,来实现getter/setter。举例说明:

class MySignalInstance:
    pass


class MySignal:
    def __get__(self, instance, owner=None):
        if (instance == None):
            print('instance is None')
        else:
            print(f'instance is not None. type(instance): {type(instance)}')
        print(owner)
        return MySignalInstance()


class Foo:
    mySignal = MySignal()


mySignal = MySignal()
print(type(mySignal))
f = Foo()
print(type(f.mySignal))
print(type(Foo.mySignal))

# 输出:
# <class '__main__.MySignal'>
# instance is not None. type(instance): <class '__main__.Foo'>
# <class '__main__.Foo'>
# <class '__main__.MySignalInstance'>
# instance is None
# <class '__main__.Foo'>
# <class '__main__.MySignalInstance'>

分析:

  • MySignal中重写了__get__函数,返回MySignalInstance对象
  • 如果直接创建mySignal = MySignal(),返回的就是MySignal的对象
  • 如果在Foo中定义一个MySignal的类成员变量,然后通过两种不同的方式访问:
    • 通过类的对象访问f.mySignal,此时,MySignal__get__函数被调用,instance被设为fowner被设为Foo这意味着,在被调用方得到了调用方的对象。
    • 通过类直接访问Foo.mySignal,此时,MySignal__get__函数同样被调用,因为是通过类直接访问,所以instance被设为Noneowner被设为Foo

正是通过这种机制,使用对象.信号访问信号时,对象被传入信号中,接着将对象信号进行绑定,返回一个绑定信号。然后就可以调用connect和其他对象的槽进行连接。

参考

https://doc.qt.io/qtforpython/tutorials/basictutorial/signals_and_slots.html
https://www.riverbankcomputing.com/static/Docs/PyQt6/signals_slots.html
https://stackoverflow.com/questions/3798835/understanding-get-and-set-and-python-descriptors
https://docs.python.org/3/reference/datamodel.html#implementing-descriptors
https://docs.python.org/3/reference/datamodel.html#invoking-descriptors

posted @ 2023-01-02 15:33  TruthHell  阅读(1326)  评论(0编辑  收藏  举报