找回密码
 立即注册
收起左侧

Qt学习之路第5篇 自定义信号槽

26
回复
62136
查看
[复制链接]
累计签到:3 天
连续签到:1 天
来源: 2013-9-8 19:24:46 显示全部楼层 |阅读模式

马上注册,查看详细内容!注册请先查看:注册须知

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
版权声明
该文章原创于Qter开源社区(www.qter.org),作者devbean,博客www.devbean.net转载请注明出处!



上一节我们详细分析了 connect() 函数。使用 connect() 可以让我们连接系统提供的信号和槽。但是,Qt 的信号槽机制并不仅仅是使用系统提供的那部分,还会允许我们自己设计自己的信号和槽。这也是 Qt 框架的设计思路之一,用于我们设计解耦的程序。本节将讲解如何在自己的程序中自定义信号槽。


信号槽不是 GUI 模块提供的,而是 Qt 核心特性之一。因此,我们可以在普通的控制台程序使用信号槽。


经典的观察者模式在讲解举例的时候通常会举报纸和订阅者的例子。有一个报纸类 Newspaper,有一个订阅者类 Subscriber。Subscriber 可以订阅 Newspaper。这样,当 Newspaper 有了新的内容的时候,Subscriber 可以立即得到通知。在这个例子中,观察者是 Subscriber,被观察者是 Newspaper。在经典的实现代码中,观察者会将自身注册到被观察者的一个容器中(比如 subscriber.registerTo(newspaper))。被观察者发生了任何变化的时候,会主动遍历这个容器,依次通知各个观察者(newspaper.notifyAllSubscribers())。


下面我们看看使用 Qt 的信号槽,如何实现上述观察者模式。注意,这里我们仅仅是使用这个案例,我们的代码并不是去实现一个经典的观察者模式。也就是说,我们使用 Qt 的信号槽机制来获得同样的效果。

  1. //!!! Qt5
  2. #include <QObject>

  3. ////////// newspaper.h
  4. class Newspaper : public QObject
  5. {
  6.     Q_OBJECT
  7. public:
  8.     Newspaper(const QString & name) :
  9.         m_name(name)
  10.     {
  11.     }

  12.     void send()
  13.     {
  14.         emit newPaper(m_name);
  15.     }

  16. signals:
  17.     void newPaper(const QString &name);

  18. private:
  19.     QString m_name;
  20. };

  21. ////////// reader.h
  22. #include <QObject>
  23. #include <QDebug>

  24. class Reader : public QObject
  25. {
  26.     Q_OBJECT
  27. public:
  28.     Reader() {}

  29.     void receiveNewspaper(const QString & name)
  30.     {
  31.         qDebug() << "Receives Newspaper: " << name;
  32.     }
  33. };

  34. ////////// main.cpp
  35. #include <QCoreApplication>

  36. #include "newspaper.h"
  37. #include "reader.h"

  38. int main(int argc, char *argv[])
  39. {
  40.     QCoreApplication app(argc, argv);

  41.     Newspaper newspaper("Newspaper A");
  42.     Reader reader;
  43.     QObject::connect(&newspaper, &Newspaper::newPaper,
  44.                      &reader,    &Reader::receiveNewspaper);
  45.     newspaper.send();

  46.     return app.exec();
  47. }
复制代码

当我们运行上面的程序时,会看到终端输出 Receives Newspaper: Newspaper A 这样的字样。


下面我们来分析下自定义信号槽的代码。


这段代码放在了三个文件,分别是 newspaper.h,reader.h 和 main.cpp。为了减少文件数量,可以把 newspaper.h 和 reader.h 都放在 main.cpp 的 main() 函数之前吗?答案是,可以,但是需要有额外的操作。具体问题,我们在下面会详细说明。


首先看 Newspaper 这个类。这个类继承了 QObject 类。只有继承了 QObject 类的类,才具有信号槽的能力。所以,为了使用信号槽,必须继承 QObject。凡是 QObject 类(不管是直接子类还是间接子类),都应该在第一行代码写上 Q_OBJECT。不管是不是使用信号槽,都应该添加这个宏。这个宏的展开将为我们的类提供信号槽机制、国际化机制以及 Qt 提供的不基于 C++ RTTI 的反射能力。因此,如果你觉得你的类不需要使用信号槽,就不添加这个宏,就是错误的。其它很多操作都会依赖于这个宏。注意,这个宏将由 moc(我们会在后面章节中介绍 moc。这里你可以将其理解为一种预处理器,是比 C++ 预处理器更早执行的预处理器。) 做特殊处理,不仅仅是宏展开这么简单。moc 会读取标记了 Q_OBJECT 的头文件,生成以 moc_ 为前缀的文件,比如 newspaper.h 将生成 moc_newspaper.cpp。你可以到构建目录查看这个文件,看看到底增加了什么内容。注意,由于 moc 只处理头文件中的标记了 Q_OBJECT 的类声明,不会处理 cpp 文件中的类似声明。因此,如果我们的 Newspaper 和 Reader 类位于 main.cpp 中,是无法得到 moc 的处理的。解决方法是,我们手动调用 moc 工具处理 main.cpp,并且将 main.cpp 中的 include “newspaper.h” 改为 include “moc_newspaper.h” 就可以了。不过,这是相当繁琐的步骤,为了避免这样修改,我们还是将其放在头文件中。许多初学者会遇到莫名其妙的错误,一加上 Q_OBJECT 就出错,很大一部分是因为没有注意到这个宏应该放在头文件中。


Newspaper 类的 public 和 private 代码块都比较简单,只不过它新加了一个 signals。signals 块所列出的,就是该类的信号。信号就是一个个的函数名,返回值是 void(因为无法获得信号的返回值,所以也就无需返回任何值),参数是该类需要让外界知道的数据。信号作为函数名,不需要在 cpp 函数中添加任何实现(我们曾经说过,Qt 程序能够使用普通的 make 进行编译。没有实现的函数名怎么会通过编译?原因还是在 moc,moc 会帮我们实现信号函数所需要的函数体,所以说,moc 并不是单纯的将 Q_OBJECT 展开,而是做了很多额外的操作)


Newspaper 类的 send() 函数比较简单,只有一个语句 emit newPaper(m_name);。emit 是 Qt 对 C++ 的扩展,是一个关键字(其实也是一个宏)。emit 的含义是发出,也就是发出 newPaper() 信号。感兴趣的接收者会关注这个信号,可能还需要知道是哪份报纸发出的信号?所以,我们将实际的报纸名字 m_name 当做参数传给这个信号。当接收者连接这个信号时,就可以通过槽函数获得实际值。这样就完成了数据从发出者到接收者的一个转移。


Reader 类更简单。因为这个类需要接受信号,所以我们将其继承了 QObject,并且添加了 Q_OBJECT 宏。后面则是默认构造函数和一个普通的成员函数。Qt 5 中,任何成员函数、static 函数、全局函数和 Lambda 表达式都可以作为槽函数。与信号函数不同,槽函数必须自己完成实现代码。槽函数就是普通的成员函数,因此作为成员函数,也会受到 public、private 等访问控制符的影响。(我们没有说信号也会受此影响,事实上,如果信号是 private 的,这个信号就不能在类的外面连接,也就没有任何意义。)


main() 函数中,我们首先创建了 Newspaper 和 Reader 两个对象,然后使用 QObject::connect() 函数。这个函数我们上一节已经详细介绍过,这里应该能够看出这个连接的含义。然后我们调用 Newspaper 的 send() 函数。这个函数只有一个语句:发出信号。由于我们的连接,当这个信号发出时,自动调用 reader 的槽函数,打印出语句。


这样我们的示例程序讲解完毕。我们基于 Qt 的信号槽机制,不需要观察者的容器,不需要注册对象,就实现了观察者模式。


下面总结一下自定义信号槽需要注意的事项:

  • 发送者和接收者都需要是 QObject 的子类(当然,槽函数是全局函数、Lambda 表达式等无需接收者的时候除外);
  • 使用 signals 标记信号函数,信号是一个函数声明,返回 void,不需要实现函数代码;
  • 槽函数是普通的成员函数,作为成员函数,会受到 public、private、protected 的影响;
  • 使用 emit 在恰当的位置发送信号;
  • 使用 QObject::connect() 函数连接信号和槽。

Qt 4


下面给出 Qt 4 中相应的代码:

  1. //!!! Qt4
  2. #include <QObject>

  3. ////////// newspaper.h
  4. class Newspaper : public QObject
  5. {
  6.     Q_OBJECT
  7. public:
  8.     Newspaper(const QString & name) :
  9.         m_name(name)
  10.     {
  11.     }

  12.     void send() const
  13.     {
  14.         emit newPaper(m_name);
  15.     }

  16. signals:
  17.     void newPaper(const QString &name) const;

  18. private:
  19.     QString m_name;
  20. };

  21. ////////// reader.h
  22. #include <QObject>
  23. #include <QDebug>

  24. class Reader : public QObject
  25. {
  26.     Q_OBJECT
  27. public:
  28.     Reader() {}

  29. public slots:
  30.     void receiveNewspaper(const QString & name) const
  31.     {
  32.         qDebug() << "Receives Newspaper: " << name;
  33.     }
  34. };

  35. ////////// main.cpp
  36. #include <QCoreApplication>

  37. #include "newspaper.h"
  38. #include "reader.h"

  39. int main(int argc, char *argv[])
  40. {
  41.     QCoreApplication app(argc, argv);

  42.     Newspaper newspaper("Newspaper A");
  43.     Reader reader;
  44.     QObject::connect(&newspaper, SIGNAL(newPaper(QString)),
  45.                      &reader,    SLOT(receiveNewspaper(QString)));
  46.     newspaper.send();

  47.     return app.exec();
  48. }
复制代码

注意下 Qt 4 与 Qt 5 的区别。


Newspaper 类没有什么区别。


Reader 类,receiveNewspaper() 函数放在了 public slots 块中。在 Qt 4 中,槽函数必须放在由 slots 修饰的代码块中,并且要使用访问控制符进行访问控制。其原则同其它函数一样:默认是 private 的,如果要在外部访问,就应该是 public slots;如果只需要在子类访问,就应该是 protected slots。


main() 函数中,QObject::connect() 函数,第二、第四个参数需要使用 SIGNAL 和 SLOT 这两个宏转换成字符串(具体事宜我们在上一节介绍过)。注意 SIGNAL 和 SLOT 的宏参数并不是取函数指针,而是除去返回值的函数声明,并且 const 这种参数修饰符是忽略不计的。


下面说明另外一点,我们提到了“槽函数是普通的成员函数,作为成员函数,会受到 public、private、protected 的影响”,public、private 这些修饰符是供编译器在编译期检查的,因此其影响在于编译期。对于 Qt4 的信号槽连接语法,其连接是在运行时完成的,因此即便是 private 的槽函数也是可以作为槽进行连接的。但是,如果你使用了 Qt5 的新语法,新语法提供了编译期检查(取函数指针),因此取 private 函数的指针是不能通过编译的。








参与人数 4人气 +6 收起 理由
asdfg1023 + 1
txjg + 1 很实用!
listener868 + 2 很实用!
恒星shephered + 2 必须支持!

查看全部评分总评分 : 人气 +6

回复

使用道具 举报

累计签到:5 天
连续签到:1 天
2013-12-8 15:26:21 显示全部楼层
本帖最后由 花洛之多少 于 2013-12-8 15:33 编辑

为什么我将代码敲上去之后是这种情况呢
Cannot find file: C:\Users\Jelon\Desktop\自定义信号槽\自定义信号槽.pro.
15:22:33: 进程"H:\Qt\Qt5.1.1\5.1.1\msvc2010\bin\qmake.exe"退出,退出代码 2 。
Error while building/deploying project 自定义信号槽 (kit: Desktop Qt 5.1.1 MSVC2010 32bit)
当执行步骤 'qmake'时
15:22:33: Elapsed time: 00:00.






已经解决了~谢谢!!
回复 支持 反对

使用道具 举报

累计签到:17 天
连续签到:1 天
2014-8-22 10:25:28 显示全部楼层
"因此,如果你觉得你的类不需要使用信号槽,就不添加这个宏,就是错误的"

这说的是另一篇教程里的内容 ……^^
回复 支持 反对

使用道具 举报

累计签到:1 天
连续签到:1 天
2015-1-22 23:21:56 显示全部楼层
为什么我执行代码的时候出现
E:\APP-Qt\untitled3\main.cpp:45: error: C1083: 无法打开包括文件:“newspaper.h”: No such file or directory
不理解,希望楼主能给我讲解一下   我是菜鸟   

我百度了好多 发现都解决不了
回复 支持 反对

使用道具 举报

累计签到:6 天
连续签到:1 天
2015-7-19 22:01:58 显示全部楼层
今天看了一天的教程,感觉帮助很大
回复 支持 反对

使用道具 举报

累计签到:1 天
连续签到:1 天
2015-10-21 20:04:12 显示全部楼层
高手帮忙, 本人QT 新手
Newspaper(const QString & name) :  m_name(name)
这个是什么意思呢?
如果是类 Newspapaer的构造函数, 他继承 m_name函数吗?
回复 支持 反对

使用道具 举报

累计签到:20 天
连续签到:1 天
2016-5-15 12:42:37 显示全部楼层
持续不断的学习当中,谢谢楼主
回复 支持 反对

使用道具 举报

累计签到:3 天
连续签到:1 天
2016-5-25 13:48:19 显示全部楼层
Honlly 发表于 2015-10-21 20:04
高手帮忙, 本人QT 新手
Newspaper(const QString & name) :  m_name(name)
这个是什么意思呢?

c++构造语法,与qt无关的,m_name 代表类成员函数,name代表方法参数,用于快速书写构造方法.
回复 支持 反对

使用道具 举报

累计签到:3 天
连续签到:1 天
2016-8-24 22:35:15 显示全部楼层
学习学习。感谢楼主这么用心整理自己的理解
回复 支持 反对

使用道具 举报

尚未签到

2016-9-9 20:17:05 显示全部楼层
F:\QT_,Programs\slot_ management\slot\newspaper.h:15: error: C3861: “newPaPer”: 找不到标识符
回复 支持 反对

使用道具 举报

累计签到:1 天
连续签到:1 天
2016-10-31 17:50:50 显示全部楼层
main.obj:-1: error: LNK2001: 无法解析的外部符号 "public: virtual struct QMetaObject const * __cdecl Newspaper::metaObject(void)const " (?metaObject@Newspaper@@UEBAPEBUQMetaObject@@XZ)
回复 支持 反对

使用道具 举报

累计签到:1 天
连续签到:1 天
2016-10-31 17:51:46 显示全部楼层
小苏鱿鱼 发表于 2016-10-31 17:50
main.obj:-1: error: LNK2001: 无法解析的外部符号 "public: virtual struct QMetaObject const * __cdecl  ...

麻烦给位告诉一下这个怎么解决,谢谢了。
回复 支持 反对

使用道具 举报

尚未签到

2017-1-8 22:20:44 显示全部楼层
在添加文件指定C++ class时候,会自动生成cpp文件,编译报错,删!

另外,能不能开个脑洞:
reader收到newspaper 的信号后,再发一条信号make a  [phone call]信号  to his [friend]接收者?
回复 支持 反对

使用道具 举报

累计签到:71 天
连续签到:1 天
2017-2-23 14:03:22 显示全部楼层
提示  call of overloaded "Reader()" is ambiguous 错误如何解决呢。
回复 支持 反对

使用道具 举报

累计签到:153 天
连续签到:5 天
2017-6-28 16:17:03 显示全部楼层
感谢楼主,,来学习。。
回复 支持 反对

使用道具 举报

累计签到:120 天
连续签到:1 天
2017-8-21 22:21:50 显示全部楼层
请问这里的 连接函数 connect 里面 传递 了 非静态成员函数的地址(信号函数和槽函数),您使用了 类名::函数名,请问 对象.函数名 是否可以?而且 非静态成员函数不是只能由对象调用吗?

槽函数和信号函数 对象是否可以直接调用?
回复 支持 反对

使用道具 举报

累计签到:120 天
连续签到:1 天
2017-8-22 08:04:24 显示全部楼层
饭量大大 发表于 2016-5-25 13:48
c++构造语法,与qt无关的,m_name 代表类成员函数,name代表方法参数,用于快速书写构造方法. ...

C++的初始化列表语法,只能用于构造函数,非静态const成员必须用这种方式初始化,基类也要这样初始化
回复 支持 反对

使用道具 举报

累计签到:4 天
连续签到:1 天
2017-10-20 11:17:25 显示全部楼层
菜鸟一个,想问一下在哪查看moc_前缀的文件啊?工程目录下没有找到相关的 文件
回复 支持 反对

使用道具 举报

尚未签到

2017-11-9 18:13:36 显示全部楼层
chuckie6323 发表于 2017-10-20 11:17
菜鸟一个,想问一下在哪查看moc_前缀的文件啊?工程目录下没有找到相关的 文件 ...

工程目录下的debug文件夹呢
回复 支持 反对

使用道具 举报

累计签到:1 天
连续签到:1 天
2018-1-4 17:21:18 显示全部楼层
非常感谢楼主这么用心整理,难得的基础教学
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

公告
可以关注我们的微信公众号yafeilinux_friends获取最新动态,或者加入QQ会员群进行交流:190741849、186601429(已满) 我知道了