纯Python实现Qt的信号与槽机制

纯Python实现Qt的信号与槽机制

Qt中的信号与槽详解


在Qt框架中,信号与槽是一种非常有特色的机制,用于对象之间的通信。这一机制是Qt中实现事件驱动编程的核心部分。下面我将详细解释信号与槽的概念和它们是如何工作的。

信号(Signals)

信号是一个由对象发出的消息,表明发生了一个特定的事件。当对象内部的状态发生变化时,信号就被发出。例如,当一个按钮被点击时,它就会发出一个clicked信号。
信号可以携带任意数量的参数,但它们不能有返回值。发出信号的对象不关心是否有其他的对象监听这个信号,这是所谓的“fire and forget”机制。

槽(Slots)

槽是响应特定信号的函数。槽可以用来处理信号,执行一些操作,比如更新用户界面、处理数据等。槽是普通的C++成员函数,它们可以接受任意数量的参数,并且可以有返回值。

emit关键字

在Qt中,emit关键字用于从对象中发射一个信号。当你在类的实现中使用emit时,你是在告诉Qt框架你想要发出一个信号,这样连接到这个信号的所有槽都会被调用。

这里是一个简单的例子,展示了如何在自定义的Qt对象中发出信号:

// MyObject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

#include <QObject>

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_SIGNALQ_SLOT宏,或者在Qt 5及以后的版本中使用signalsslots关键字,可以很容易地定义它们。

信号与槽的优势

  • 松耦合:发出信号的对象不需要知道哪个槽会接收这个信号。
  • 类型安全:信号和槽的参数类型必须匹配,这减少了类型错误的可能性。
  • 跨线程通信:Qt支持在不同线程中的对象之间进行信号与槽的连接,这使得多线程编程更加简单。
    信号与槽机制是Qt框架中实现对象间通信的关键特性,理解这一机制对于开发高效和可维护的Qt应用程序至关重要。

Python实现


Qt的信号与槽机制有点像Go语言中channel(通道)模型,Go语言的通道(channel)是用于在goroutine之间进行通信的内置类型。通道可以传输特定类型的数据,并且提供了一种在goroutine之间同步数据交换的方式,而无需使用显式的锁或条件变量。

定义channel

我们用一个列表模仿来模仿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]

connect和disconnect

有必要去了解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__方法,这是所有可调用对象都必须实现的魔法方法。

connect

如果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)
disconnect

从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)

发射(emit)信号

通过遍历通道中的槽函数并执行。

# 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

channel相关

# 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)

进阶,实现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中,类可以拥有属性,这些属性可以是类属性(也称为静态属性)或实例属性。下面是它们的简要说明:

  1. 类属性(Class Attributes):
    • 类属性是属于类的,它们被所有类的实例共享。
    • 类属性在类的定义体中定义,通常在方法之外。
    • 类属性的值在所有实例之间是共享的,因此对一个实例修改类属性会影响到所有其他实例。
  2. 实例属性(Instance Attributes):
    • 实例属性是属于类的特定实例的。
    • 实例属性通常在类的构造函数__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实例。makemodel是实例属性,它们属于my_car这个特定的Car实例。
类属性和实例属性都可以通过点号(.)语法来访问和修改。不过,通常建议避免在实例方法中修改类属性,因为这可能会导致意外的副作用。如果需要,可以使用类方法来修改类属性。

类属性(静态属性)

Python 类属性是类定义的一部分,它们属于类本身而不是类的某个特定实例。类属性的特点包括:

  1. 共享性:类属性是由类的所有实例共享的。也就是说,类的任何一个实例都可以访问和修改这个属性,而且一个实例对类属性的修改会影响到其他所有实例。
  2. 定义方式:类属性通常在类的定义中,但在方法之外定义。它们可以在类定义的任何位置,但习惯上通常在顶部或底部。
  3. 访问方式:可以通过类名直接访问类属性(ClassName.attribute),也可以通过类的实例访问(instance.attribute)。
  4. 修改方式:如果通过一个实例修改了类属性,实际上是创建或修改了这个实例的一个实例属性,而不会影响到类属性本身。如果想要修改类属性,应该通过类名来修改。
  5. 继承性:如果子类没有覆盖父类的类属性,那么子类会继承父类的类属性。
  6. 描述符协议:类属性可以实现描述符协议,即实现 __get__, __set__, __delete__ 方法,从而控制属性的访问和修改。
  7. 动态性:Python 是动态语言,所以可以在运行时动态地添加或删除类属性。
  8. 命名空间:类属性存储在类的命名空间中,而不是实例的命名空间。
  9. 用途:类属性常用于存储类的相关信息,比如常量、默认值、类级别的状态等。
    使用类属性时,应当注意不要与实例属性混淆,避免因误操作导致的数据不一致问题。同时,在设计类时,应合理利用类属性来提供清晰、合理的类结构。
区分以及遍历方法

在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__属性来获取类和实例的属性字典。然后,我们检查每个属性是否存在于这些字典中,从而判断它是类属性还是实例属性。这种方法更为准确,因为它直接检查了属性的实际存储位置。

修改Signal类

添加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

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)
  • 10
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

childish_tree

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值