《Qt MOOC系列教程》第二章第二节:对象通信:信号和槽

2023-11-04

几乎所有的UI工具包都有一种机制来检测用户操作,并对该操作做出响应。其中一些使用回调,另一些使用监听器,但基本上,所有这些都是受观察者模式的启发。

观察者模式用于观察对象想要通知其他观察者对象状态变化的情况。下面是一些具体的例子:

  • 用户单击了一个按钮,应该会显示一个菜单。
  • 一个Web页面刚刚加载完毕,一个进程应该从这个加载的页面中提取一些信息。
  • 一个用户正在滚动一个项目列表(例如在一个app store),并到达了终点,所以其他项目应该被加载。

观察者模式在GUI应用程序中随处可见,并且常常导致一些样板代码。创建Qt的初衷是删除这些样板代码,并提供一种漂亮而干净的语法,这就是信号和槽机制。

信号和槽是Qt和内部对象通信的关键。它们在某种意义上可以与回调相比较,但区别在于它们是类型安全的,而回调通常不是。我们在开始之前提到的一个好处是,信号和槽允许您构建多对多连接,通常情况下,如果使用多个虚函数,则虚方法是一对一或一对多的。

我们在上一章讨论了Q_OBJECT宏,它也会在本主题中再次出现。

1. 事件简介

在讲解信号和槽之前,让我们简要介绍一下事件。事件在事件循环中执行。这不是Qt特有的,可以证明,您使用的大多数应用程序都在等待输入事件,这花费了大部分时间,不管输入是来自用户、网络还是其他地方。可以有多个事件循环,例如每个线程都有一个。Qt支持使用事件处理器,但是通常您会希望使用信号和槽系统。我们在这里引入事件的原因是,您应该了解事件循环的概念,因为它与信号和槽相关。本课程我们只介绍单线程的应用程序,但是当您跨线程发送信号时应该记住,该槽可能不会立即执行,而是可能被放置在接收线程的事件循环中,以等待控制权交给该线程。

2. 性能

与回调相比,信号和槽稍微慢一些,这是因为它们提供了更高的灵活性,但对于实际应用程序来说,这种差异是微不足道的。一般来说,发送一个信号连接到某些槽,比直接调用非虚函数要慢大约10倍。这是寻找连接对象、安全地遍历所有连接(即检查后续接收方在发射过程中没有被销毁)以及以通用方式排列参数所需的开销。

虽然相对于10个非虚函数的调用听起来很耗时,但是它比任何新增操作或删除操作的开销要小得多。一旦您在后台执行一个需要新建或删除的字符串、向量或列表操作,信号和槽开销只占整个函数调用开销的很小一部分。在槽中执行系统调用时也是如此,或间接调用超过十个函数。信号和槽机制的简单性和灵活性是值得的,用户甚至不会注意到这些开销。

3. 信号

当对象的内部状态以某种可能引起对象客户端或所有者兴趣的方式发生变化时,对象就会发出信号。信号是公有函数,可以从任何地方发出,但是我们建议只从定义信号的类及其子类发出信号。

要定义信号,请将其放在signals:类定义的块中:

...
signals:
    void valueChanged(int newValue);
...

要发出信号,请使用emit关键字。这个关键字是纯语法的,但有助于将其与正常函数调用区分开来。

void Counter::setValue(int value)
{
    if (value != m_value) {
        m_value = value;
        emit valueChanged(value);
    }
}

当一个信号发出时,连接到它的槽通常会立即执行,就像一个普通的函数调用一样。当发生这种情况时,信号和槽机制完全独立于任何GUI事件循环。emit语句之后的代码将在所有槽都返回之后执行。不过如果您使用排队连接,情况会略有不同。在这种情况下,emit关键字后面的代码将立即继续,槽将稍后执行。

下面是一些QPushButton类的信号示例:

  • clicked
  • pressed
  • released

如您所见,它们的名称非常明确。当用户点击(按下然后释放)、按下或释放按钮时,就会发送这些信号。

这些信号是由moc(元对象编译器)自动生成的,不能在.cpp文件中实现。它们永远不能有返回类型(只能使用void)。

开发经验表明,如果信号和槽不使用特殊类型,它们将具有更高的可重用性。如果QScrollBar::valueChanged()要使用一个特殊类型,比如假设的QScrollBar::Range,那么它只能连接到专门为QScrollBar设计的槽上。将不同的输入widgets连接在一起是不可能的。

4. 槽

当一个连接到槽的信号被发射时,该槽被调用。槽是普通的C++函数,可以正常调用。它们唯一的特点是信号可以与它们相连。

以下是来自不同类的一些槽:

  • QApplication::quit
  • QWidget::setEnabled
  • QPushButton::setText

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

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

5. 定义信号和槽

如前一章所述,所有使用信号和槽机制的类都需要在类定义的私有部分中定义Q_OBJECT宏。下面是实现信号和槽的类的头文件示例。

#include <QObject>

class Counter : public QObject
{
    Q_OBJECT

public:
    Counter() { m_value = 0; }

    int value() const { return m_value; }

public slots:
    void setValue(int value);

signals:
    void valueChanged(int newValue);

private:
    int m_value;
};

6. 连接信号和槽

要将信号连接到槽函数,请使用QObject::connect()。有几种连接信号和槽的方法。第一种是使用函数指针:

connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);

QObject::connect()与函数指针一起使用有几个优点。首先,它允许编译器检查信号的参数是否与槽的参数兼容。如果需要,还可以由编译器隐式转换参数。

您还可以连接到普通函数或C++11的lambda表达式:

connect(sender, &QObject::destroyed, [=](){ this->m_objects.remove(sender); });

将信号连接到槽的传统方法是使用QObject::connect()SIGNAL()/SLOT()宏。之所以在这里介绍它是因为它仍被广泛使用,但是通常,您应该使用前面介绍的一种较新的连接类型。如果参数具有默认值,则关于是否在SIGNAL()SLOT()宏中包含参数的规则是,传递给SIGNAL()宏的签名不得少于传递给SLOT()宏的签名。

所以下面这些都会起作用:

connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*)));
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed()));
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));

但这是行不通的:

connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));

…因为该槽函数期望的一个没有QObject的信号不会发送。该连接将报告运行时错误。请注意,使用此QObject::connect()重载时,编译器不会检查signalslot参数。

在本练习中,您将创建两个类来练习定义信号和槽。您可以在main.cpp中找到相关说明。

7. 第三方库

您可能熟悉其他的信号槽机制,比如Boost.Signals2库。可以将Qt与第三方信号/槽机制一起使用,甚至可以在同一个项目中同时使用这两种机制。将以下定义添加到您的工程(.pro)文件中:

CONFIG += no_keywords

它告诉Qt不要定义moc关键字signalsslotsemit,因为这些名称将被第三方库使用,例如Boost。然后,如果要继续在no_keywords环境中使用Qt信号和槽,只需将代码中的Qtmoc关键字的替换为对应的Qt宏Q_SIGNALS(或Q_SIGNAL)、Q_SLOTS(或Q_SLOT)和Q_EMIT

8. 对象通信中的事件

虽然在对象通信中通常首选使用信号和槽,但在某些情况下,所需的功能更容易用事件处理。例如,如果我们想发出多个信号,我们可以子类化相应的QObject并重新实现事件处理器。事件处理器可以在没有将信号连接到槽的情况下发出信号,槽随后会发出几个信号。

Qt是一个基于事件的系统。当调用QCoreApplication::exec()时,GUI线程进入事件循环。QCoreApplication可以处理GUI线程中的每个事件并将事件转发给QObject。接收者QObject可以处理或忽略相应的事件。

事件可以是自发的,也可以是合成的。自发事件在应用程序进程之外创建,例如由窗口管理器,并发送到应用程序。对于GUI事件,平台抽象插件(QPA)接收事件并将其转换为Qt事件类型。Qt事件是值类型,派生自QEvent,它为每个事件提供了枚举类型。如果我们想修改计时器事件处理,可以通过重新实现QObject::event()函数来完成,如下所示。

首先,先检查事件类型。如果事件类型是QEvent::Timer,我们就把QEvent转化为具体数据类型QTimerEvent。对于计时器,它有一个timerId。如果事件进一步传播到下一个接收对象(如果存在),则该函数返回一个布尔值以通知事件系统。所有未在此函数中处理的事件均由其基类处理。

bool QObjectSubclass::event(QEvent *event)
{
   
   if (event->type() == QEvent::Timer) {
       QTimerEvent *timerEvent = static_cast<QTimerEvent *>(event);
       if (timerEvent->timerId()) {
           
       }
   }
   
   return QObject::event(event);
}

通常,我们不需要重新实现event()函数,而是需要使用一些特定于事件的处理器函数(请参阅:事件处理器)。例如,可以在void CustomObject::timerEvent(QTimerEvent *event)中处理计时器事件。注意,这些函数不返回布尔值,但是它们通过QEvent::accept()QEvent::ignore()来接受或忽略事件,以告知事件系统是否应该进一步传播该事件。。

void QObjectSubclass::timerEvent(QTimerEvent *event)
{
   
   if (event->timerId() == m_timerId) {
         
   }
   return QObject::timerEvent(event);
}

8.1 事件过滤器

如果需要在几个不同的类中以相同的方式处理相同的事件,那么使用事件过滤器比子类化许多类型更容易。事件过滤器QObject::eventFilter(QObject *watched, QEvent *event)QObject的成员函数,会在实际的事件处理函数之前调用。与QObject::event()函数类似,布尔返回值告诉我们事件是被过滤掉了(true)还是应该进一步传播(false)。实际上只有安装了void QObject::installEventFilter(QObject *filterObject)事件过滤器才会被调用。

事件过滤器有两种:应用程序范围的和对象本地的事件过滤器。唯一的区别是事件过滤器被安装到哪个对象。如果将它安装到QCoreApplication对象,则主线程中的所有事件都将通知事件过滤器。如果将其安装到其他QObject子类,则仅发送到该对象的事件过滤器。

应用程序范围的事件过滤器对于调试检查非常有用,例如,窗口管理器将预期的事件提供给Qt应用程序。通常,要避免应用程序范围的事件过滤器,因为为应用程序中的每个事件调用额外的函数可能会影响事件的处理性能。

大多数情况下,在事件处理程序中处理事件就足够了,但是正如上面所看到的,我们可以在传播的早期捕获事件。例如,对于触摸事件,没有特定于触摸的事件处理程序,因此它们必须在QObject::event()函数中处理。如果您希望对几种不同类型有类似的事件处理,事件过滤器很有用。
在这里插入图片描述

8.2 自定义事件

有时我们没有合适的Qt事件类型,比如通知特定操作。在这些情况下,可以创建自定义事件。这可以很容易地从QEvent派生。每个自定义的事件都包含特定于事件的数据,因此需要添加成员数据并实现访问器函数来获取和设置数据。最后,事件必须被Qt事件系统识别,因此事件需要一个唯一的事件类型。您可以扩展现有的事件枚举,如下面的示例所示。

const QEvent::Type customEventType = QEvent::Type(QEvent::User + 1);

class CustomEvent : public QEvent
{
public:
   CustomEvent();
   int value() const;
   void setValue(int value);

private:
   int m_m_value;
};

8.3 同步和异步事件

在Qt中,事件可以同步或异步发送。异步事件使用QCoreApplication::postEvent()在事件队列中排队,该队列由QAbstractEventDispatcher的一个特定于平台的子类管理。同步事件QCoreApplication::sendEvent()无需排队。还要注意,异步事件是由事件系统管理的,这意味着它们必须在堆中分配,并且不能被开发人员使用代码删除。

异步事件是线程安全的。实际上,跨线程信号和槽是基于异步事件的。当一个线程中的一个对象向另一个线程中的一个对象发出信号时,假设连接类型是自动的或排队的,那么实际上在线程之间发送了一个事件。当事件在另一个线程中处理时,处理程序代码自动调用槽。

因为Qt中的任何线程都可以有自己的事件循环,所以不要直接从另一个线程调用slot或任何函数来中断事件处理,这一点很重要。使用排队连接或异步事件是安全的。只要接收线程返回到事件循环并开始处理新事件,事件就会进入队列。此外,如果您从工作线程通知GUI线程有新数据可用时,您应该始终异步地执行此操作。

练习

通过子类化QObject来实现CustomObject,并且:

  • 在构造函数中启动一个计时器(例如3秒)。
  • 重新实现event()函数。检查是否存在CustomEvent并打印到调试控制台:“Custom event handled: Event data”
  • 重新实现timerEvent()函数。计时器到期后,退出应用程序。

实现具有字符串成员的CustomEvent类。

使用main.cpp中提供的代码,程序应该向调试控制台打印两条消息,然后在计时器结束后退出应用程序。

9. 延伸阅读

有关信号和槽的更多详细信息,请参见官方信号和槽文档。

Source

获取更多信息,请关注作者公众号:程序员练兵场
在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

《Qt MOOC系列教程》第二章第二节:对象通信:信号和槽 的相关文章

  • 删除文件的最后 10 个字符

    我想删除文件的最后 10 个字符 说一个字符串 hello i am a c learner 是文件内的数据 我只是希望该文件是 hello i am a 文件的最后 10 个字符 即字符串 c learner 应在文件内消除 解决方案 将
  • WPF DataGrid 多选

    我读过几篇关于这个主题的文章 但很多都是来自 VS 或框架的早期版本 我想做的是从 dataGrid 中选择多行并将这些行返回到绑定的可观察集合中 我尝试创建一个属性 类型 并将其添加到可观察集合中 它适用于单个记录 但代码永远不会触发多个
  • 如何在 Cassandra 中存储无符号整数?

    我通过 Datastax 驱动程序在 Cassandra 中存储一些数据 并且需要存储无符号 16 位和 32 位整数 对于无符号 16 位整数 我可以轻松地将它们存储为有符号 32 位整数 并根据需要进行转换 然而 对于无符号 64 位整
  • std::vector 与 std::stack

    有什么区别std vector and std stack 显然 向量可以删除集合中的项目 尽管比列表慢得多 而堆栈被构建为仅后进先出的集合 然而 堆栈对于最终物品操作是否更快 它是链表还是动态重新分配的数组 我找不到关于堆栈的太多信息 但
  • 如何从本机 C(++) DLL 调用 .NET (C#) 代码?

    我有一个 C app exe 和一个 C my dll my dll NET 项目链接到本机 C DLL mynat dll 外部 C DLL 接口 并且从 C 调用 C DLL 可以正常工作 通过使用 DllImport mynat dl
  • -webkit-box-shadow 与 QtWebKit 模糊?

    当时有什么方法可以实现 webkit box shadow 的工作模糊吗 看完这篇评论错误报告 https bugs webkit org show bug cgi id 23291 我认识到这仍然是一个问题 尽管错误报告被标记为RESOL
  • 如何连接重叠的圆圈?

    我想在视觉上连接两个重叠的圆圈 以便 becomes 我已经有部分圆的方法 但现在我需要知道每个圆的重叠角度有多大 但我不知道该怎么做 有人有主意吗 Phi ArcTan Sqrt 4 R 2 d 2 d HTH Edit 对于两个不同的半
  • 方程“a + bx = c + dy”的积分解

    在等式中a bx c dy 所有变量都是整数 a b c and d是已知的 我如何找到整体解决方案x and y 如果我的想法是正确的 将会有无限多个解 由最小公倍数分隔b and d 但我只需要一个解决方案 我可以计算其余的 这是一个例
  • 两个静态变量同名(两个不同的文件),并在任何其他文件中 extern 其中一个

    在一个文件中将变量声明为 static 并在另一个文件中进行 extern 声明 我认为这会在链接时出现错误 因为 extern 变量不会在任何对象中看到 因为在其他文件中声明的变量带有限定符 static 但不知何故 链接器 瑞萨 没有显
  • C# - 当代表执行异步任务时,我仍然需要 System.Threading 吗?

    由于我可以使用委托执行异步操作 我怀疑在我的应用程序中使用 System Threading 的机会很小 是否存在我无法避免 System Threading 的基本情况 只是我正处于学习阶段 例子 class Program public
  • 如何定义一个可结构化绑定的对象的概念?

    我想定义一个concept可以检测类型是否T can be 结构化绑定 or not template
  • x:将 ViewModel 方法绑定到 DataTemplate 内的事件

    我基本上问同样的问题这个人 https stackoverflow com questions 10752448 binding to viewmodels property from a template 但在较新的背景下x Bind V
  • 为什么使用小于 32 位的整数?

    我总是喜欢使用最小尺寸的变量 这样效果就很好 但是如果我使用短字节整数而不是整数 并且内存是 32 位字可寻址 这真的会给我带来好处吗 编译器是否会做一些事情来增强内存使用 对于局部变量 它可能没有多大意义 但是在具有数千甚至数百万项的结构
  • 有没有办法让 doxygen 自动处理未记录的 C 代码?

    通常它会忽略未记录的 C 文件 但我想测试 Callgraph 功能 例如 您知道在不更改 C 文件的情况下解决此问题的方法吗 设置变量EXTRACT ALL YES在你的 Doxyfile 中
  • C# 中的 IPC 机制 - 用法和最佳实践

    不久前我在 Win32 代码中使用了 IPC 临界区 事件和信号量 NET环境下场景如何 是否有任何教程解释所有可用选项以及何时使用以及为什么 微软最近在IPC方面的东西是Windows 通信基础 http en wikipedia org
  • C++ 继承的内存布局

    如果我有两个类 一个类继承另一个类 并且子类仅包含函数 那么这两个类的内存布局是否相同 e g class Base int a b c class Derived public Base only functions 我读过编译器无法对数
  • 当文件流没有新数据时如何防止fgets阻塞

    我有一个popen 执行的函数tail f sometextfile 只要文件流中有数据显然我就可以通过fgets 现在 如果没有新数据来自尾部 fgets 挂起 我试过ferror and feof 无济于事 我怎样才能确定fgets 当
  • C++ 中的参考文献

    我偶尔会在 StackOverflow 上看到代码 询问一些涉及函数的重载歧义 例如 void foo int param 我的问题是 为什么会出现这种情况 或者更确切地说 你什么时候会有 对参考的参考 这与普通的旧参考有何不同 我从未在现
  • MySQL Connector C/C API - 使用特殊字符进行查询

    我是一个 C 程序 我有一个接受域名参数的函数 void db domains query char name 使用 mysql query 我测试数据库中是否存在域名 如果不是这种情况 我插入新域名 char query 400 spri
  • 如何确定 CultureInfo 实例是否支持拉丁字符

    是否可以确定是否CultureInfo http msdn microsoft com en us library system globalization cultureinfo aspx我正在使用的实例是否基于拉丁字符集 我相信你可以使

随机推荐

  • nginx配置部署一个域名,多个端口

    最近用基于windows下的nginx部署了服务器 1 安装好windows下的nginx以后 会有以下文件 找到conf下的nginx 此文件为nginx的配置文件 2 初始只有一个默认80端口 这是nginx的默认端口号 server
  • 高并发的epoll+线程池,业务在线程池内

    epoll是linux下高并发服务器的完美方案 因为是基于事件触发的 所以比select快的不只是一个数量级 单线程epoll 触发量可达到15000 但是加上业务后 因为大多数业务都与数据库打交道 所以就会存在阻塞的情况 这个时候就必须用
  • Web服务器

    文章目录 1 HTTP 协议 1 1 概述 1 2 URI和URL的区别 1 3 请求消息 Request 1 4 响应消息 Response 1 5 状态码 1 6 HTTP 1 0 和 HTTP 1 1 1 7 Cookie 1 8 S
  • Spring是如何支持多数据源的

    大家好 我是课代表 欢迎关注我公众号 Java课代表 上篇介绍了数据源基础 并实现了基于两套DataSource 两套mybatis配置的多数据源 从基础知识层面阐述了多数据源的实现思路 不了解的同学请戳 同学 你的多数据源事务失效了 正如
  • 9.5位操作(二)——给定一个介于0和1之间的实数,类型为double,打印它的二进制表示

    功能 给定一个介于0和1之间的实数 类型为double 打印它的二进制表示 如果该数字无法精准地用32位以内的二进制表示 则打印 ERROR 两种方法 方法一 将数字乘以2以后 与1比较 public static String print
  • QT字节数组类QByteArray

    QT字节数组类QByteArray 初始化 访问某个元素 截取字符串 获取字节数组的大小 数据转换与处理 Hex转换 数值转换与输出 字母大小写转换 字符串数值转化为各类数值 QBQyteArray和char 互转 QByteArray 和
  • CentOS 安装 Jenkins

    本文目录 1 安装 JDK 2 获取 Jenkins 安装包 3 将安装包上传到服务器 4 修改 Jenkins 配置 5 启动 Jenkins 6 打开浏览器访问 7 获取并输入 admin 账户密码 8 跳过插件安装 9 添加管理员账户
  • Linux防火墙的配置

    Linux防火墙的配置 防火墙是一种网络安全设备 可用于保护网络中的计算机和其他设备免遭来自互联网或其他网络连接的未授权访问 其主要作用是监控网络流量 根据预先设定的规则对网络流量进行过滤 以防止未经授权的访问 攻击和病毒等威胁 在Linu
  • pip安装pyspark报错

    报错 Traceback most recent call last File
  • UG/NX二次开发Siemens官方NXOPEN实例解析—1.3 BlockStyler/ExtrudewithPreview

    列文章目录 UG NX二次开发Siemens官方NXOPEN实例解析 1 1 BlockStyler ColoredBlock UG NX二次开发Siemens官方NXOPEN实例解析 1 2 BlockStyler EditExpress
  • 一维数组 ——Java

    目录 前言 一 一维数组的声明及初始化 1 一维数组的声明 2 一维数组的初始化 2 1静态初始化 2 2动态初始化 二 访问数组元素 1 添加元素 2 遍历数组 2 1 for循环 2 2 foreach 循环 增强for循环 2 3 f
  • 从深度学习的角度考虑sift关键点匹配

    试一试 看看行不行 得到内点后 再进行下一步
  • 组词典

    组词典将多个图形对象编组存储 存储后的效果 可以通过组来找到所需要的实体 在界面上选中一个就可以找到其他实体 即选中的为一个组 但是与块对比 不同的是将实体分组并不形成新的实体对象 在CAD中的的命令为group 创建组词典 AcDbGro
  • Pytorch创建与安装(无GPU) 无敌教程

    第一步 Pytorch创建与安装 无GPU 独爱相关算法的博客 CSDN博客 pytorch没有gpu 第一步中要看这一步 anaconda下载及安装 保姆级教程 知乎
  • 【编译原理】- 递归下降的语法分析器的实现

    目录 一 实验题目 二 分析与设计 三 源代码 一 实验题目 编写识别由下列文法G E 所定义的表达式的递归下降语法分析器 E E T E T T T T F T F F F E i 输入 含有十进制数或十六进制数的表达式 如 75 1ah
  • 关于vector的emplace_back和push_back的区别

    实验代码 class A public A int x x x cout lt lt construct A lt lt endl A const A a x a x cout lt lt copy construct A lt lt en
  • 正则重难点和个人见解

    正则表达式 RegExp 常用不常写 1 查询 正则大全 2 作用 描述了一种字符串的匹配模式 用在表单验证 搜索替换 模糊查询 一 声明 1 构造函数 new RegExp 规则 模式修正符 2 字面量 let reg 规则 模式修正符
  • https://www.cs.usfca.edu/~galles/visualization/Algorithms.html

    https www cs usfca edu galles visualization Algorithms html
  • 编码(NRZ、NRZI、曼彻斯特、4B/5B)

    将节点和链路变成可用构件的第一步 是清楚它们如何连接 以使比特从一个节点传输到另一个节点 正如在前一节中提到的 信号是在物理链路上传播的 因此 我们的任务是将源节点准备发送的二进制数据编码为链路能够传送的信号 然后在接收节点将信号解码成相应
  • 《Qt MOOC系列教程》第二章第二节:对象通信:信号和槽

    几乎所有的UI工具包都有一种机制来检测用户操作 并对该操作做出响应 其中一些使用回调 另一些使用监听器 但基本上 所有这些都是受观察者模式的启发 观察者模式用于观察对象想要通知其他观察者对象状态变化的情况 下面是一些具体的例子 用户单击了一