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
被设为f
,owner
被设为Foo
。这意味着,在被调用方得到了调用方的对象。 - 通过类直接访问
Foo.mySignal
,此时,MySignal
的__get__
函数同样被调用,因为是通过类直接访问,所以instance
被设为None
,owner
被设为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
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步