有网友抱怨:
哪个大牛能帮帮我,讲解一下信号槽机制的底层实现?
不要那种源码的解析,只要清楚的讲讲是怎么发送信号,怎么去选择相应的槽,再做出反应。也就是类似于一个信号槽的相应流程。。。求解啊!!!
看了源码,真的是一头雾水。。。撞墙的心都有了~~~~
本文使用 ISO C++ 一步一步实现了一个极度简化的信号与槽的系统
(整个程序4个文件共121行代码)
。希望能有助于刚进入Qt世界的C++用户理解Qt最核心的信号槽与元对象系统是如何工作的。
另:你可能会对 从 C++ 到 Qt
一文感兴趣
dbzhang800 2011.04.30
注:Qt5 staging仓库已经引入一种全新的信号与槽的语法:信号可以和普通的函数、类的普通成员函数、lambda函数连接(而不再局限于信号函数和槽函数),详见 信号与槽的新语法(Qt5)
dbzhang800 2011.06.15
Qt信号与槽
GUI程序中,当我们我们点击一个按钮时,我们会期待我们自定义的某个函数被调用。对此,较老的工具集(toolkits)都是通过回调函数(callback)来实现的,Qt的神奇之处就在于,它使用信号(signal)与槽(slot)的技术来取代了回调。
在继续之前,我们先看一眼最最常用的 connnect 函数:
connect(btn, "2clicked()", this, "1onBtnClicked()")
可能你会觉得稍有点眼生,因为为了清楚起见,我没有直接使用大家很熟悉的SIGNAL和SLOT两个宏,宏定义如下:
程序运行时,connect借助两个字符串,即可将信号与槽的关联建立起来,那么,它是如果做到的呢?C++的经验可以告诉我们:
- 类中应该保存有信号和槽的字符串信息
- 字符串和信号槽函数要关联
而这,就是通过神奇的元对象系统所实现的(Qt的元对象系统预处理器叫做moc,对文件预处理之后生成一个moc_xxx.cpp文件,然后和其他文件一块编译即可)。
接下来,我们不妨尝试用纯C++来实现自己的元对象系统(我们需要有一个自己的预处理器,本文中用双手来代替了,预处理生成的文件是db_xxx.cpp)。
继续之前,我们可以先看一下我们最终的类定义
引入元对象系统
首先定义自己的信号和槽
- 为了和普通成员进行区别(以使得预处理器可以知道如何提取信息),我们需要创造一些"关键字"
class Object
{
public:
Object();
virtual ~Object();
db_signals:
void sig1();
public db_slots:
void slot1();
};
- 通过自己的预处理器,将信息提取取来,放置到一个单独的文件中(比如db_object.cpp):
- 规则很简单,将信号和槽的名字提取出来,放到字符串中。可以有多个信号或槽,按顺序"sig1/nsig2/n"
static const char sig_names[] = "sig1/n";
static const char slts_names[] = "slot1/n";
- 这些信号和槽的信息,如何才能与类建立关联,如何被访问呢?
我们可以定义一个类,来存放信息:
struct MetaObject
{
const char * sig_names;
const char * slts_names;
};
然后将其作为一个Object的静态成员(注意哦,这就是我们的元对象啦
):
class Object
{
static MetaObject meta;
...
这样一来,我们的预处理器可以生成这样的 db_object.cpp 文件:
#include "object.h"
static const char sig_names[] = "sig1/n";
static const char slts_names[] = "slot1/n";
MetaObject Object::meta = {sig_names, slts_names};
信息提取的问题解决了:可是,还有一个严重问题,我们定义的关键字 C++ 编译器不认识啊,怎么办?
呵呵,好办,通过定义一下宏,问题是不是解决了:
建立信号槽链接
我们的最终目的就是:当信号被触发的时候,能找到并触发相应的槽。所以有了信号和槽的信息,我们就可以建立信号和槽的连接了。我们通过 db_connect 将信号和槽的对应关系保存到一个 mutlimap 中:
struct Connection
{
Object * receiver;
int method;
};
class Object
{
public:
...
static void db_connect(Object*, const char*, Object*, const char*);
...
private:
std::multimap<int, Connection> connections;
上面应该不需要什么解释了,我们直接看看db_connect该怎么写:
void Object::db_connect(Object* sender, const char* sig, Object* receiver, const char* slt)
{
int sig_idx = find_string(sender->meta.sig_names, sig);
int slt_idx = find_string(receiver->meta.slts_names, slt);
if (sig_idx == -1 || slt_idx == -1) {
perror("signal or slot not found!");
} else {
Connection c = {receiver, slt_idx};
sender->connections.insert(std::pair<int, Connection>(sig_idx, c));
}
}
首先从元对象信息中查找信号和槽的名字是否存在,如果存在,则将信号的索引和接收者的信息存入信号发送者的的一个map中。如果信号或槽无效,就什么都不用做了。
我们这儿定义了一个find_string函数,就是个简单的字符串查找(此处就不列出了)。
信号的激活
连接信息有了,我们看看信号到底是怎么发出的。
在 Qt 中,我们都知道用 emit 来发射信号:
class Object
{
public:
void testSignal()
...
};
void Object::testSignal()
{
db_emit sig1();
}
这儿 db_emit 是神马东西?C++编译器不认识啊,没关系,看仔细喽,加一行就行了
#define db_emit
从前面我的Object定义中可以看到,所谓的信号或槽,都只是普普通通的C++类的成员函数。既然是成员函数,就需要函数定义:
- 槽函数:由于它包含我们需要的功能代码,我们都会想到在 object.cpp 文件中去定义它,不存在问题。
- 信号函数:它的函数体不需要自己编写。那么它在哪儿呢?这就是本节的内容了
信号函数由我们的"预处理器"来生成,也就是它要定义在我们的 db_object.cpp 文件中:
void Object::sig1()
{
MetaObject::active(this, 0);
}
我们预处理源文件时,就知道它是第几个信号。所以根据它的索引去调用和它关联的槽即可。具体工作交给了MetaObject类:
class Object;
struct MetaObject
{
const char * sig_names;
const char * slts_names;
static void active(Object * sender, int idx);
};
这个函数该怎么写呢:思路很简单
- 从前面的保存连接的map中,找出与该信号关联的对象和槽
- 调用该对象这个槽
typedef std::multimap<int, Connection> ConnectionMap;
typedef std::multimap<int, Connection>::iterator ConnectionMapIt;
void MetaObject::active(Object* sender, int idx)
{
ConnectionMapIt it;
std::pair<ConnectionMapIt, ConnectionMapIt> ret;
ret = sender->connections.equal_range(idx);
for (it=ret.first; it!=ret.second; ++it) {
Connection c = (*it).second;
//c.receiver->metacall(c.method);
}
}
补遗:
槽的调用
这个最后一个关键问题了,槽函数如何根据一个索引值进行调用。
- 直接调用槽函数我们都知道了,就一个普通函数
- 可现在通过索引调用了,那么我们必须定义一个接口函数
class Object
{
void metacall(int idx);
...
该函数如何实现呢?这个又回到我们的元对象预处理过程中了,因为在预处理的过程,我们能将槽的索引和槽的调用关联起来。
所以,在预处理生成的文件(db_object.cpp)中,我们很容易生成其定义:
void Object::metacall(int idx)
{
switch (idx) {
case 0:
slot1();
break;
default:
break;
};
}
至此,我们已经实现的一个简化的自己的信号与槽的程序。下面我们总体上看看程序的所有代码:
全家福
- 我们自己的预处理需要生成这样一个文件 db_object.cpp
- 注意看这个文件:其实内容非常简单
- 将信号和槽的信息存放到字符串中
==>按顺序排放,所以有了索引值
- 信号发射 其实就是 信号函数==> 信号的索引
- metacall 其实就是 槽的索引==> 槽函数
- 最后,我们可以写一个小小的例子main.cpp :
- 程序的编译就不用多数了,用你熟悉的msvc或者g++
cl main.cpp object.cpp db_object.cpp -o dbzhang800
g++ main.cpp object.cpp db_object.cpp -o dbzhang800
零零散散,写在后面
我不确定是不是已经元对象系统和信号槽最基本的概念表达清楚了。反正我想,如果你对Qt感兴趣,相对Qt的信号和槽进一步的了解,但是目前尚对阅读Qt的源码觉得无比恐怖,本文可能会对你有帮助。
文中将东西精简到我个人能做到的极限了,所以有很多很多没提到的东西:
Q_OBJECT
用Qt,我们都知道这个宏,可是我们前面压根没提。因为我怕打乱思路,这儿补上吧。我的前面的代码可以替换为:
# define DB_OBJECT static MetaObject meta; void metacall(int idx);
class Object
{
DB_OBJECT
DB_OBJECT 还可以作为一个标记:如果我们写好了自己的类似于Qt中的moc的预处理器,如何判断一个文件是否需要预处理来生成 db_object.cpp 文件呢?此时就可以根据类定义中是否有宏来判断。
题外:
为什么添加宏后会容易遇到链接错误?你能看到原因么?因为它展开后就是类的成员,可是其定义要通过预处理进行生成。如果你没有运行预处理器,也就没有 db_object.cpp 这种文件,肯定要出错了。
Connection
我们前面在Connection只保存了接收者的指针和槽的索引,我们可以保存更多一点的信息的:可以看看Qt保存了哪些东西
QObjectPrivate::Connection *c = new QObjectPrivate::Connection;
c->sender = s;
c->receiver = r;
c->method = method_index;
c->connectionType = type;
c->argumentTypes = types;
c->nextConnectionList = 0;
应该很容易看懂,不做解释了。
Qt中信号和槽主要有直接连接和队列连接两种方式,我们这儿只提到了前者,后者和Qt的事件系统搅和在一起。只要搞清楚了Qt事件系统,就会发现和直接连接没有什么区别了。
其他
信号和槽的参数
这个,例子中举的都是信号和槽都是无参数的例子。加上参数,尽管概念上没变化,但复杂度就大大提高了。所以本文对此不想涉及,也没必要吧,直接去看Qt的源码吧。
信号和信号连接
信号和槽一样,都可以被调用,本例进行扩展也很容易,需要metacall那个函数,以及信号和槽要加个区别的标记(回到最前面不妨看看Qt的SLOT和SIGNAL究竟是神马东西)。
派生
本文中只涉及到一个类,如何在该类的基础上进行派生呢? 个人能力有限,例子中没考虑这个问题。
...
好了,忙到下午终于把昨天冒出来的这个想法付诸实施了,希望五一之后,生活会精彩一点。dbzhang800 2011.04.30
相关推荐
"使用纯C++信号槽实现代码"的主题旨在探讨如何在不直接依赖Qt库的情况下,模仿Qt的信号槽机制来实现类似的事件驱动编程模式。 信号和槽是Qt的核心特性之一,它们提供了一种安全、灵活的方式来连接对象间的通信。...
基于Qt使用C++实现图书管理系统源码 基于Qt使用C++实现图书管理系统源码 基于Qt使用C++实现图书管理系统源码 基于Qt使用C++实现图书管理系统源码 基于Qt使用C++实现图书管理系统源码 基于Qt使用C++实现...
在本项目“c++实现的信号-槽机制程序(非boost库)”中,开发者使用纯C++语言实现了这一功能,而且它支持跨平台运行,意味着可以在多种操作系统上使用。 信号-槽机制的核心思想是解耦合。在传统的回调函数模式中,...
期末大作业C++课程设计基于Qt实现图书管理系统源码,VS开发。期末大作业C++课程设计基于Qt实现图书管理系统源码,VS开发。期末大作业C++课程设计基于Qt实现图书管理系统源码,VS开发。期末大作业C++课程设计基于Qt...
本篇文章将深入探讨如何在不使用Qt库的情况下,用纯C++实现一个类似Qt信号槽的运行机制。 首先,理解信号与槽的概念。信号(Signal)是对象在特定事件发生时发出的通知,而槽(Slot)是响应这些通知的函数。当信号...
- 相比于标准 C++,Qt Core 提供了更丰富的特性,如信号与槽、动态属性等,使得开发者可以更加专注于业务逻辑,而不是底层的实现细节。 - 虽然其他库如 Boost 也有类似信号和槽的机制,但 Qt 的信号与槽在集成性、...
银行管理系统QT(c++)银行管理系统QT(c++)银行管理系统QT(c++)银行管理系统QT(c++)银行管理系统QT(c++)银行管理系统QT(c++)银行管理系统QT(c++)银行管理系统QT(c++)银行管理系统QT(c++)银行管理系统QT(c++)银行管理...
c++11引入了std::bind及std::function,实现了函数的存储和绑定,即先将可调用的对象保存起来...定义了SignalObject信号类和SlotObject槽类,其中信号类中的 std::function(int)> _call就是要绑定的槽函数,即回调函数
在本文中,我们将深入探讨如何使用MFC(Microsoft Foundation Classes)框架来创建窗口类,并结合Qt的信号槽机制实现消息处理的优化。MFC是微软为C++开发Windows应用程序提供的一种库,它提供了丰富的类库,简化了...
基于C++实现一个简单的QT文件传输系统包含客户端和服务端源码.zip 基于C++实现一个简单的QT文件传输系统包含客户端和服务端源码.zip 基于C++实现一个简单的QT文件传输系统包含客户端和服务端源码.zip 基于C++实现一...
基于Qt和C++实现的高亮发光按钮控件+源码 基于Qt和C++实现的高亮发光按钮控件+源码 基于Qt和C++实现的高亮发光按钮控件+源码 基于Qt和C++实现的高亮发光按钮控件+源码 基于Qt和C++实现的高亮发光按钮控件+源码 ...
Qt 还提供了强大的信号槽机制,允许开发者轻松地实现 GUI 组件之间的交互。 《C++ GUI Programming with Qt 4, Second Edition》是 Qt 官方认证的编程指南,提供了从基础到高级的 Qt 编程技术,涵盖 Qt 4.3 的最新...
基于C++&Qt实现的在线点餐系统(客户端+服务端——.zip 基于C++&Qt实现的在线点餐系统(客户端+服务端——.zip 基于C++&Qt实现的在线点餐系统(客户端+服务端——.zip 基于C++&Qt实现的在线点餐系统(客户端+服务端...
**Qt 5.9 C++ 开发指南:深入理解Qt核心特点与源码解析** Qt是一个流行的开源跨平台应用程序开发框架,广泛应用于桌面、移动和嵌入式系统。Qt 5.9是其一个重要的版本,它在稳定性和性能上都有显著提升,同时也引入...
基于QT - SOCKET 的 C++ 实现矩阵压力数据采集及压力云图现实。 基于QT - SOCKET 的 C++ 实现矩阵压力数据采集及压力云图现实。 基于QT - SOCKET 的 C++ 实现矩阵压力数据采集及压力云图现实。 基于QT - SOCKET 的 ...
QT C++学习代码案例是为初学者准备的一系列实践教程,旨在帮助他们快速掌握QT库在C++编程中的应用。这个资源包含32个精心设计的代码实例,涵盖了QT库的基础到进阶功能,使学习者能够从实践中理解并运用QT。 首先,...
下面将详细介绍如何使用Qt实现UDP的发送与接收。 首先,理解UDP通信的基本概念。UDP协议不保证数据包的顺序、完整性和可靠性,而是以尽可能快的速度发送数据,因此适合于对实时性要求高的场景。在Qt中,我们通常...
C++利用Poco库实现QT的信号与槽效果,采用CmakeLists构建工程。 QT的信号与槽机制是QT的一大特点,利用该机制进行对象之间的通信特别方便,并且可以降低对象之间的耦合度。 在使用C++编程的过程中,通过网络库Poco的...
操作系统-课程设计-超级马里奥游戏设计实现(Qt&C++实现)包含以下两部分文件: 1.【报告】分为六章进行展示,包括摘要、参考文献,正文部分为包括前言、主要任务、总体设计、详细设计、遇到的问题和解决方法,以及...
2. **C++与Qt的结合**:讲解如何利用C++语言特性与Qt库进行有效结合,包括对象模型、信号与槽机制、事件处理等,这些都是Qt编程的基础。 3. **环境配置与项目设置**:指导读者如何安装Qt开发环境,如Qt Creator,并...