在Qt框架中,信号与槽是一种非常有特色的机制,用于对象之间的通信。这一机制是Qt中实现事件驱动编程的核心部分。下面我将详细解释信号与槽的概念和它们是如何工作的。
信号是一个由对象发出的消息,表明发生了一个特定的事件。当对象内部的状态发生变化时,信号就被发出。例如,当一个按钮被点击时,它就会发出一个clicked
信号。
信号可以携带任意数量的参数,但它们不能有返回值。发出信号的对象不关心是否有其他的对象监听这个信号,这是所谓的“fire and forget”机制。
槽是响应特定信号的函数。槽可以用来处理信号,执行一些操作,比如更新用户界面、处理数据等。槽是普通的C++成员函数,它们可以接受任意数量的参数,并且可以有返回值。
在Qt中,emit关键字用于从对象中发射
一个信号。当你在类的实现中使用emit时,你是在告诉Qt框架你想要发出一个信号,这样连接到这个信号的所有槽都会被调用。
这里是一个简单的例子,展示了如何在自定义的Qt对象中发出信号:
// MyObject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H
#include
class MyObject : public QObject
{
Q_OBJECT
public:
explicit MyObject(QObject *parent = nullptr);
signals:
void mySignal(); // 定义一个信号
public slots:
void onMySignal(); // 定义一个槽
};
#endif // MYOBJECT_H
// MyObject.cpp
#include "MyObject.h"
MyObject::MyObject(QObject *parent) : QObject(parent)
{
}
void MyObject::onMySignal()
{
// 槽函数的实现
qDebug() << "Signal received!";
}
void MyObject::someFunction()
{
// 在某个条件下发出信号
if (someCondition()) {
emit mySignal(); // 使用emit发出信号
}
}
在这个例子中,MyObject类有一个名为mySignal的信号和一个名为onMySignal的槽。当someFunction函数中的条件满足时,mySignal信号会被发出。任何连接到这个信号的槽都会被调用,例如在这个例子中,onMySignal槽会输出一条消息。
在Qt中,一个对象的信号可以与另一个对象的槽函数相连接。当信号发出时,连接的槽函数将被自动调用。这种机制实现了对象之间的解耦,因为发出信号的对象不需要知道哪个槽会响应它。
连接信号与槽可以使用connect
函数,这个函数是QObject
类的一个成员函数,几乎所有的Qt类都继承自QObject
。连接可以是同步的,也可以是异步的,并且可以指定连接的类型,比如一个信号可以连接到多个槽,或者多个信号可以连接到一个槽。
QPushButton *button = new QPushButton("Click me!");
QObject::connect(button, &QPushButton::clicked, this, &MyClass::handleClick);
在上面的例子中,我们创建了一个按钮,并将其clicked
信号连接到了MyClass
类的handleClick
槽函数上。当按钮被点击时,handleClick
函数将被调用。
除了使用Qt提供的预定义信号和槽,你还可以定义自己的信号和槽。通过使用Q_SIGNAL
和Q_SLOT
宏,或者在Qt 5及以后的版本中使用signals
和slots
关键字,可以很容易地定义它们。
Qt的信号与槽机制有点像Go语言中channel(通道)模型,Go语言的通道(channel)是用于在goroutine之间进行通信的内置类型。通道可以传输特定类型的数据,并且提供了一种在goroutine之间同步数据交换的方式,而无需使用显式的锁或条件变量。
我们用一个列表模仿来模仿channel(通道)的效果。列表的第一个元素是信号的名称,除第一个元素以外的地方,用来存放槽函数。
定义如下:
# Global signal chaneels
# Define the different signal channels as lists with an identifier at index 0
# You can customize the channel like below
CONFIG_MODIFIED = ["CONFIG_MODIFIED"]
APP_STATUS = ["APP_STATUS"]
# Define the ALL_CHANNELS list to hold all signal channels
ALL_CHANNELS = [CONFIG_MODIFIED, APP_STATUS]
有必要去了解python内置的callable()
函数。在Python中,callable()是一个内置函数,它可以用来检查一个对象是否是可调用的。如果一个对象是可调用的,那么它可以被直接调用,比如函数、类实例或类。
callable()函数的返回值是一个布尔值:如果对象是可调用的,它返回True;否则,返回False。
以下是一些使用callable()函数的例子:
# 检查一个函数是否可调用
print(callable(print)) # 输出: True
# 检查一个类是否可调用(通常类是可调用的,除非它是抽象基类)
class MyClass:
pass
print(callable(MyClass)) # 输出: True
# 检查一个类实例是否可调用
obj = MyClass()
print(callable(obj)) # 输出: True
# 检查一个普通的对象是否可调用
obj = object()
print(callable(obj)) # 输出: False
# 检查一个字典是否可调用
obj = {}
print(callable(obj)) # 输出: False
# 检查一个列表是否可调用
obj = []
print(callable(obj)) # 输出: False
在上述例子中,我们可以看到print函数、类MyClass、类实例obj和类本身都是可调用的,因为它们可以被直接调用。而普通对象、字典和列表是不可调用的,因为它们没有__call__方法,这是所有可调用对象都必须实现的魔法方法。
如果receiver是函数就添加到channel中。
# Function to connect a receiver (callback) to a signal channel
def connect(channel, receiver):
"""
Connects a signal receive method (receiver) to the provided channel.
The receiver will be called when the signal is emitted on this channel.
"""
# Check if the receiver is callable (e.g., a function or method)
if callable(receiver):
try:
# Append the receiver to the channel's list of receivers
channel.append(receiver)
except Exception:
# Log an exception if the receiver cannot be connected
msg = "Cannot connect to channel <%s> receiver: %s"
LOG.exception(msg, channel[0], receiver)
从channel中移除槽函数。
# Function to disconnect a receiver from a signal channel
def disconnect(channel, receiver):
"""
Disconnects a signal receive method (receiver) from the provided channel.
The receiver will no longer be called when the signal is emitted on this channel.
"""
# Check if the receiver is callable
if callable(receiver):
try:
# Remove the receiver from the channel's list of receivers
channel.remove(receiver)
except Exception:
# Log an exception if the receiver cannot be disconnected
msg = "Cannot disconnect from channel <%s> receiver: <%s>"
LOG.exception(msg, channel[0], receiver)
通过遍历通道中的槽函数并执行。
# Function to emit a signal to all receivers in a channel
def emit(channel, *args):
"""
Sends a signal to all receivers connected to the provided channel.
Passes any additional arguments to the receivers.
"""
# Iterate over all receivers in the channel (starting from index 1, as index 0 is the channel name)
for receiver in channel[1:]:
try:
# Check if the receiver is callable and call it with the provided arguments
if callable(receiver):
receiver(*args)
except Exception:
# Log an exception if there's an error calling the receiver
msg = "Error calling <%s> receiver %s with %s"
LOG.exception(msg, channel[0], receiver, args)
continue
# Function to clean a single channel, removing all receivers
def clean_channel(channel):
"""
Cleans a channel by removing all receivers, leaving only the channel name.
"""
# Store the channel name
name = channel[0]
# Clear the channel list
channel[:] = []
# Append the channel name back to the list
channel.append(name)
# Function to clean all channels, removing all receivers from each channel
def clean_all_channels(channels=ALL_CHANNELS):
"""
Cleans all channels by removing all receivers from each channel.
"""
# Iterate over all channels in the channels list
for item in channels:
# Clean each channel
clean_channel(item)
# Example usage:
if __name__ == "__main__":
# Define the signal channels as list with an identifier at index 0
signal = ["signal"]
all_signal = [
signal,
]
# Define a slot function that will be called when a signal is emitted
def my_slot(*args):
print("Signal received with arguments:", args)
def my_slot1(str1, str2):
print("This is a slot,arg1:{},arg2:{}.".format(str1, str2))
# Connect the slot to a signal channel
connect(signal, my_slot)
connect(signal, my_slot1)
print("connect slot:", signal) # print channel that signal `connect` slot
# Emit a signal on the channel
emit(signal, "attr", "value")
print("emit signal:", signal) # print channel that signal `emit` slot
# Later, if you want to stop receiving signals, disconnect the slot
disconnect(signal, my_slot)
print("disconnect my_slot:", signal) # print channel that signal `disconnect` slot
# Emitting the signal now will not call my_slot anymore
emit(signal, "attr", "new_value")
# clean channel
clean_channel(signal)
print("clean channel:", signal)
print(signal)
# clean all channels
clean_all_channels(all_signal)
print(all_signal)
class Signal:
"""
The Signal class provides a Qt-like signal-slot functionality.
It allows connecting receivers (slots) to signals and emitting signals
to call all connected receivers with the provided arguments.
"""
# Initialize the list of receivers with a placeholder for the signal name
def __init__(self, signal_name: str):
"""
Initializes the Signal instance with the given signal name.
The signal name is used for identification and logging purposes.
"""
# Ensure the signal_name is a string
if not isinstance(signal_name, str):
raise TypeError("Signal name must be a string")
# Initialize the list of receivers with the signal name as the first element
self._receivers = [signal_name]
def connect(self, receiver):
"""
Connects a receiver (slot) to the signal.
The receiver will be called when the signal is emitted.
"""
connect(self._receivers, receiver)
def disconnect(self, receiver):
"""
Disconnects a receiver (slot) from the signal.
The receiver will no longer be called when the signal is emitted.
"""
disconnect(self._receivers, receiver)
def emit(self, *args):
"""
Emits the signal, calling all connected receivers with the provided arguments.
"""
emit(self._receivers, *args)
def clean(self):
"""
Cleans the signal, removing all connected receivers.
"""
clean_channel(self._receivers)
# Example usage:
if __name__ == "__main__":
# Create a signal instance
my_signal = Signal("clicked")
# Define a slot function
def my_slot(*args):
print("Clicked Signal received with arguments:", args)
# Connect the slot to the signal
my_signal.connect(my_slot)
# Emit the signal with some arguments
my_signal.emit("Hello", "World")
# Disconnect the slot
my_signal.disconnect(my_slot)
# Emit the signal again (my_slot should not be called)
my_signal.emit("Hello", "Again")
# ================ Example two =========================
那么通过上面的学习,我们要干票大的,能否在类中使用信号,编写一个信号的管理类,一个类中单独的信号管理器,用于管理类中的所有信号;一个全局信号管理器,用于管理整个代码中的信号。(也就是模仿Qt中的元对象系统)
在Python中,类可以拥有属性,这些属性可以是类属性(也称为静态属性)或实例属性。下面是它们的简要说明:
__init__
中定义,但也可以在类的其他方法中动态添加。class Car:
# 类属性
num_wheels = 4
def __init__(self, make, model):
# 实例属性
self.make = make
self.model = model
# 创建Car类的实例
my_car = Car("Toyota", "Corolla")
# 访问类属性
print(Car.num_wheels) # 输出: 4
# 访问实例属性
print(my_car.make) # 输出: Toyota
print(my_car.model) # 输出: Corolla
在上面的例子中,num_wheels
是一个类属性,它属于Car
类本身,而不是任何一个特定的Car
实例。make
和model
是实例属性,它们属于my_car
这个特定的Car
实例。
类属性和实例属性都可以通过点号(.
)语法来访问和修改。不过,通常建议避免在实例方法中修改类属性,因为这可能会导致意外的副作用。如果需要,可以使用类方法来修改类属性。
Python 类属性是类定义的一部分,它们属于类本身
,而不是类的某个特定实例
。类属性的特点包括:
ClassName.attribute
),也可以通过类的实例访问(instance.attribute
)。__get__
, __set__
, __delete__
方法,从而控制属性的访问和修改。在Python中,你可以使用dir()
函数来列出对象的所有属性,包括它的类属性和实例属性。然而,dir()
会返回一个包含所有属性的列表,其中包括内置的属性和方法。为了过滤出真正的属性,你可以使用getattr()
和hasattr()
函数,并结合一些额外的检查。
以下是一个示例,展示了如何遍历一个类的属性,并判断它们是类属性还是实例属性:
class MyClass:
class_attr = "This is a class attribute"
def __init__(self):
self.instance_attr = "This is an instance attribute"
# 创建MyClass的实例
my_instance = MyClass()
# 遍历my_instance的所有属性
for attr in dir(my_instance):
# 跳过内置属性和方法
if attr.startswith("__"):
continue
# 判断属性是类属性还是实例属性
if hasattr(my_instance, attr) and not hasattr(MyClass, attr):
print(f"{attr} is an instance attribute.")
elif hasattr(MyClass, attr):
print(f"{attr} is a class attribute.")
在这个示例中,我们首先创建了一个MyClass
的实例my_instance
。然后,我们使用dir()
函数来获取my_instance
的所有属性。我们遍历这些属性,并使用hasattr()
函数来判断属性是属于实例还是类。
请注意,这种方法并不能完美地区分实例属性和类属性,因为如果实例属性和类属性具有相同的名称,它将错误地识别为实例属性。为了更准确地判断,你可以直接检查类的__dict__
和实例的__dict__
:
class MyClass:
class_attr = "This is a class attribute"
def __init__(self):
self.instance_attr = "This is an instance attribute"
# 创建MyClass的实例
my_instance = MyClass()
# 获取类和实例的属性字典
class_dict = MyClass.__dict__
instance_dict = my_instance.__dict__
# 遍历实例的所有属性
for attr in dir(my_instance):
# 跳过内置属性和方法
if attr.startswith("__"):
continue
# 判断属性是类属性还是实例属性
if attr in instance_dict:
print(f"{attr} is an instance attribute.")
elif attr in class_dict:
print(f"{attr} is a class attribute.")
在这个改进的示例中,我们使用了__dict__
属性来获取类和实例的属性字典。然后,我们检查每个属性是否存在于这些字典中,从而判断它是类属性还是实例属性。这种方法更为准确,因为它直接检查了属性的实际存储位置。
添加name属性。
class Signal:
"""
The Signal class provides a Qt-like signal-slot functionality.
It allows connecting receivers (slots) to signals and emitting signals
to call all connected receivers with the provided arguments.
"""
# Initialize the list of receivers with a placeholder for the signal name
def __init__(self, signal_name: str = ""):
"""
Initializes the Signal instance with the given signal name.
The signal name is used for identification and logging purposes.
"""
# Ensure the signal_name is a string
if not isinstance(signal_name, str):
raise TypeError("Signal name must be a string")
# Initialize the list of receivers with the signal name as the first element
self._receivers = [signal_name]
self._signal_name = signal_name
def connect(self, receiver):
"""
Connects a receiver (slot) to the signal.
The receiver will be called when the signal is emitted.
"""
connect(self._receivers, receiver)
def disconnect(self, receiver):
"""
Disconnects a receiver (slot) from the signal.
The receiver will no longer be called when the signal is emitted.
"""
disconnect(self._receivers, receiver)
def emit(self, *args):
"""
Emits the signal, calling all connected receivers with the provided arguments.
"""
emit(self._receivers, *args)
def clean(self):
"""
Cleans the signal, removing all connected receivers.
"""
clean_channel(self._receivers)
@property
def name(self):
"""
This is a property getter method used to get the signal name.
This method is called when the .name property of a instance is accessed.
"""
return self._signal_name
@name.setter
def name(self, value):
"""
This is a property setter method used to set the signal name.
This method is called when trying to set the .name property of the instance.
"""
self._signal_name = value
self._receivers[0] = value
SignalManager 类管理自定义类中的所有 Signal 实例。它确保每个信号只有一个实例并提供访问它们的便捷方式。
class SignalManager:
"""
The SignalManager class manages all Signal instances in custom classes.
It ensures that there is only one instance of each signal and provides
a convenient way to access them.
"""
_signal_instances = {}
# It ensures that there is only one instance of each signal by using a class-level dictionary _instances
# to store instances of Signal objects keyed by their signal_name.
def __init__(self) -> None:
pass
def add_signal(self, signal_name: str):
# Ensure the signal_name is a string
if not isinstance(signal_name, str):
raise TypeError("Signal name must be a string")
if signal_name not in self._signal_instances:
self._signal_instances[signal_name] = Signal(signal_name)
else:
raise ValueError("The signal has been defined")
def remove_signal(self, signal_name: str):
# Ensure the signal_name is a string
if not isinstance(signal_name, str):
raise TypeError("Signal name must be a string")
if signal_name in self._signal_instances:
del self._signal_instances[signal_name]
else:
raise ValueError("The signal does not exist")
def get_signal(self, signal_name: str):
# Ensure the signal_name is a string
if not isinstance(signal_name, str):
raise TypeError("Signal name must be a string")
if signal_name in self._signal_instances:
return self._signal_instances[signal_name]
else:
raise ValueError("The signal does not exist")
def is_signal(self, signal_name: str) -> bool:
# Ensure the signal_name is a string
if not isinstance(signal_name, str):
raise TypeError("Signal name must be a string")
return signal_name in self._signal_instances
if __name__ == "__main__":
smr = SignalManager()
smr.add_signal("clicked")
clicked_signal = smr.get_signal("clicked")
smr.is_signal("clicked")
smr.remove_signal(clicked)