转: https://blog.csdn.net/perfectguyipeng/article/details/78082360

本文使用 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两个宏,宏定义如下:

  •  # define SLOT(a)     "1"#a
    # define SIGNAL(a) "2"#a

程序运行时,connect借助两个字符串,即可将信号与槽的关联建立起来,那么,它是如果做到的呢?C++的经验可以告诉我们:

  • 类中应该保存有信号和槽的字符串信息
  • 字符串和信号槽函数要关联

而这,就是通过神奇的元对象系统所实现的(Qt的元对象系统预处理器叫做moc,对文件预处理之后生成一个moc_xxx.cpp文件,然后和其他文件一块编译即可)。

接下来,我们不妨尝试用纯C++来实现自己的元对象系统(我们需要有一个自己的预处理器,本文中用双手来代替了,预处理生成的文件是db_xxx.cpp)。

继续之前,我们可以先看一下我们最终的类定义

  1. class Object
  2. {
  3. DB_OBJECT
  4. public:
  5. Object();
  6. virtual ~Object();
  7. static void db_connect(Object *, const char *, Object *, const char *);
  8. void testSignal();
  9. db_signals:
  10. void sig1();
  11. public db_slots:
  12. void slot1();
  13. friend class MetaObject;
  14. private:
  15. ConnectionMap connections;
  16. };

引入元对象系统

首先定义自己的信号和槽

  • 为了和普通成员进行区别(以使得预处理器可以知道如何提取信息),我们需要创造一些"关键字"
  • db_signals
  • db_slots
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++ 编译器不认识啊,怎么办?

呵呵,好办,通过定义一下宏,问题是不是解决了:

  • # define db_slots
    # define db_signals protected

建立信号槽链接

我们的最终目的就是:当信号被触发的时候,能找到并触发相应的槽。所以有了信号和槽的信息,我们就可以建立信号和槽的连接了。我们通过 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;
};
}

至此,我们已经实现的一个简化的自己的信号与槽的程序。下面我们总体上看看程序的所有代码:

全家福

  • 类定义文件 object.h
  1. #ifndef DB_OBJECT
  2. #define DB_OBJECT
  3. #include <map>
  4. # define db_slots
  5. # define db_signals protected
  6. # define db_emit
  7. class Object;
  8. struct MetaObject
  9. {
  10. const char * sig_names;
  11. const char * slts_names;
  12. static void active(Object * sender, int idx);
  13. };
  14. struct Connection
  15. {
  16. Object * receiver;
  17. int method;
  18. };
  19. typedef std::multimap<int, Connection> ConnectionMap;
  20. typedef std::multimap<int, Connection>::iterator ConnectionMapIt;
  21. class Object
  22. {
  23. static MetaObject meta;
  24. void metacall(int idx);
  25. public:
  26. Object();
  27. virtual ~Object();
  28. static void db_connect(Object*, const char*, Object*, const char*);
  29. void testSignal();
  30. db_signals:
  31. void sig1();
  32. public db_slots:
  33. void slot1();
  34. friend class MetaObject;
  35. private:
  36. ConnectionMap connections;
  37. };
  38. #endif
  • 类实现文件 object.cpp
  1. #include <stdio.h>
  2. #include <string.h>
  3. #include "object.h"
  4. Object::Object()
  5. {
  6. }
  7. Object::~Object()
  8. {
  9. }
  10. static int find_string(const char * str, const char * substr)
  11. {
  12. if (strlen(str) < strlen(substr))
  13. return -1;
  14. int idx = 0;
  15. int len = strlen(substr);
  16. bool start = true;
  17. const char * pos = str;
  18. while (*pos) {
  19. if (start && !strncmp(pos, substr, len) && pos[len]=='/n')
  20. return idx;
  21. start = false;
  22. if (*pos == '/n') {
  23. idx++;
  24. start = true;
  25. }
  26. pos++;
  27. }
  28. return -1;
  29. }
  30. void Object::db_connect(Object* sender, const char* sig, Object* receiver, const char* slt)
  31. {
  32. int sig_idx = find_string(sender->meta.sig_names, sig);
  33. int slt_idx = find_string(receiver->meta.slts_names, slt);
  34. if (sig_idx == -1 || slt_idx == -1) {
  35. perror("signal or slot not found!");
  36. } else {
  37. Connection c = {receiver, slt_idx};
  38. sender->connections.insert(std::pair<int, Connection>(sig_idx, c));
  39. }
  40. }
  41. void Object::slot1()
  42. {
  43. printf("hello dbzhang800!");
  44. }
  45. void MetaObject::active(Object* sender, int idx)
  46. {
  47. ConnectionMapIt it;
  48. std::pair<ConnectionMapIt, ConnectionMapIt> ret;
  49. ret = sender->connections.equal_range(idx);
  50. for (it=ret.first; it!=ret.second; ++it) {
  51. Connection c = (*it).second;
  52. c.receiver->metacall(c.method);
  53. }
  54. }
  55. void Object::testSignal()
  56. {
  57. db_emit sig1();
  58. }
  • 我们自己的预处理需要生成这样一个文件 db_object.cpp
  • 注意看这个文件:其实内容非常简单
    • 将信号和槽的信息存放到字符串中 ==>按顺序排放,所以有了索引值
    • 信号发射 其实就是 信号函数==> 信号的索引
    • metacall 其实就是 槽的索引==> 槽函数
  1. #include "object.h"
  2. static const char sig_names[] = "sig1/n";
  3. static const char slts_names[] = "slot1/n";
  4. MetaObject Object::meta = {sig_names, slts_names};
  5. void Object::sig1()
  6. {
  7. MetaObject::active(this, 0);
  8. }
  9. void Object::metacall(int idx)
  10. {
  11. switch (idx) {
  12. case 0:
  13. slot1();
  14. break;
  15. default:
  16. break;
  17. };
  18. }
  • 最后,我们可以写一个小小的例子main.cpp :
  1. #include "object.h"
  2. int main()
  3. {
  4. Object obj1, obj2;
  5. Object::db_connect(&obj1, "sig1", &obj2, "slot1");
  6. obj1.testSignal();
  7. return 0;;
  8. }
  • 程序的编译就不用多数了,用你熟悉的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究竟是神马东西)。

转: Qt信号槽实现原理 清晰明了的更多相关文章

  1. Qt信号槽-原理分析

    目录 一.问题 二.Moc 1.变量 2.Q_OBJECT展开后的函数声明 3.自定义信号 三.connect 四.信号触发 1.直连 2.队列连接 五.总结 六.推荐阅读 一.问题 学习Qt有一段时 ...

  2. Qt信号槽的一些事 Qt::带返回值的信号发射方式

    一般来说,我们发出信号使用emit这个关键字来操作,但是会发现,emit并不算一个调用,所以它没有返回值.那么如果我们发出这个信号想获取一个返回值怎么办呢? 两个办法:1.通过出参形式返回,引用或者指 ...

  3. QT源码之Qt信号槽机制与事件机制的联系

    QT源码之Qt信号槽机制与事件机制的联系是本文要介绍的内容,通过解决一个问题,从中分析出的理论,先来看内容. 本文就是来解决一个问题,就是当signal和slot的连接为Qt::QueuedConne ...

  4. Qt信号槽源码剖析(一)

    大家好,我是IT文艺男,来自一线大厂的一线程序员 大家在使用Qt开发程序时,都知道怎么使用Qt的信号槽,但是Qt信号槽是怎么工作的? 大部分人仍然不知道:也就是说大家只知道怎么使用,却不知道基于什么原 ...

  5. Qt信号槽源码剖析(二)

    大家好,我是IT文艺男,来自一线大厂的一线程序员 上节视频给大家讲解了Qt信号槽的基本概念.元对象编译器.示例代码以及Qt宏:今天接着深入分析,进入Qt信号槽源码剖析系列的第二节视频. Qt信号槽的宏 ...

  6. (文字版)Qt信号槽源码剖析(三)

    大家好,我是IT文艺男,来自一线大厂的一线程序员 上节视频给大家讲解了Qt信号槽的Qt宏展开推导:今天接着深入分析,进入Qt信号槽源码剖析系列的第三节视频. Qt信号槽宏推导归纳 #define si ...

  7. Qt信号槽的一些事(第一次知道信号还有返回值,以及Qt::UniqueConnection)

    注:此文是站在Qt5的角度说的,对于Qt4部分是不适用的. 1.先说Qt信号槽的几种连接方式和执行方式. 1)Qt信号槽给出了五种连接方式: Qt::AutoConnection 0 自动连接:默认的 ...

  8. QT 信号槽connect中解决自定义数据类型或数组作为函数参数的问题——QT qRegisterMetaType 注册MetaType——关键:注册自定义数据类型或QMap等容器类

    一般情况下信号槽直接连接方式不会出现问题,但是如果信号与槽在不同线程或Qt::QueuedConnection方式连接,可能会在连接期间报以下类似问题,如: QObject::connect: Can ...

  9. QT信号槽详解

    1         QT信号槽详解 1.1  信号和槽的定义 信号是触发信号,例如按钮的点击触发一个clicked信号,槽是用来接收信号,并处理信号,相当于信号响应函数.一个信号可以关联多个槽函数,信 ...

随机推荐

  1. linux用户键拷贝文件

    如果A用户没有权限写B用户下面的文件,可以用这个命令拷贝 scp AAA.tar.gz B用户名@IP:全路径

  2. 原博客地址http://blog.chinaunix.net/uid/20656672.html不再维护(10年前数百篇oracle/teradata性能优化、故障处理案例)

    原博客地址http://blog.chinaunix.net/uid/20656672.html不再维护(数百篇oracle/teradata性能优化.故障处理原创文章) 858871 top 500 ...

  3. day16

    列表生成式语法[表达式 for in 遍历 if 条件]会从li一次去除所有值,进行判断 如果满足条件 就装到新的列表里 生成数据的函数函数体中又yield关键字yield 暂停函数的执行 还能返回一 ...

  4. JS(JavaScript)的进一步了解1(更新中···)

    1.作用域链 作用域:浏览器给JS的一个生存环境(栈内存). 作用域链:JS中的关键字var 和function 都可以提前声明和定义.提前声明和定义的内容放在我们的 内存地址(堆内存)中.然后JS从 ...

  5. 为git关联编辑器(比如notepad++) Associating text editors with Git

    Associating text editors with git Using Notepad++ as your editors you can set your default editor in ...

  6. 【做题】SRM704 Div1 Median - ModEquation——数论

    原文链接 https://www.cnblogs.com/cly-none/p/SRM704Div1B.html 给出\(n\)和模数\(P\).\(q\)次询问,每次给出一个\([0,P-1]\)范 ...

  7. Jenkins学习

    1.jenkins启动卡在密码初始化处不动的情况,参照: https://blog.csdn.net/lylload/article/details/82754101 https://blog.csd ...

  8. 目标指定法——S.M.A.R.T.

    一个有效的目标一定要是 具体的(Specific), 可测量的(Measureable), 可实现的(Attainable), 有现实意义的(Realistic), 以及有明确期限的(Time-bas ...

  9. OpenVPN部署,实现访问云服务器的内网

    本教程不描述如何FQ 一.OpenVPN服务端部署 $ yum -y install net-tools lzo lzo-devel openssl-devel pam-devel gcc gcc-c ...

  10. MySQL查询缓存总结

    可以通过下面的SQL查看当前查询缓存相关参数状态: show variables like '%query_cache%'; 1)  query_cache_type 查询缓存类型: 0 表示始终不适 ...