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

KS05_02 信号-槽:三种编程方式

0
回复
3886
查看
[复制链接]
累计签到:41 天
连续签到:1 天
来源: 原创 2019-9-1 21:01:49 显示全部楼层 |阅读模式

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

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

x
本帖最后由 baizy77 于 2019-9-2 16:47 编辑

该文章原创于Qter开源社区(www.qter.org
作者: 女儿叫老白
转载请注明出处!
----------------------------------------------------------------

引言
----------------------------------------------------------------
  从Qt3开始,Qt提供了一种信号槽机制,用来将控件的信号与触发的操作建立联系,以便当用户操作界面时,可以触发软件执行我们编写的代码。本节我们将为朋友们介绍Qt的信号槽机制以及其使用方法。

正文
----------------------------------------------------------------
   本节从Qt3的信号槽方案开始,引入当前Qt最新版本提供的3种实现信号槽绑定的方案。分别是:
1.  使用connect宏
2.  使用connect函数
3.  使用Lambda表达式【扩展阅读】

connect
    Qt3提供了一种通过宏定义实现的信号槽绑定方式,语法如下:
代码清单05-02-01
  1. connect(sender, SIGNAL(信号名称(参数列表)), receiver, SLOT(槽函数名(参数列表)), Qt::ConnectionType );
复制代码
    代码清单05-02-01中:
    connect是一个宏,这个宏看上去像一个函数,它带有4个参数,介绍如下:
    sender:代表发出信号的对象地址,比如一个按钮或者一个下拉列表。
    SIGNAL(信号名称(参数列表)):代表sender发出的信号,其中参数列表只需给出参数类型,不提供变量名称。对于QT提供的类,如果它提供信号则可以在它的头文件的Q_SIGNALS部分找到,也可以从assistant中找到该类后,查看它的signals描述。除了使用本类的信号之外,还可以使用父类或者父类族谱中任何一个层级父类的信号。信号的定义形式同函数类似,信号也可以待参数。比如QFontComboBox类的信号定义:
代码清单05-02-02

qfontcombobox.h
  1. class QFontComboBox
  2. {
  3.     //......
  4. Q_SIGNALS:
  5. void currentFontChanged(const QFont &f);
  6.     //......
  7. }
复制代码
    回到代码清单05-02-01,
    receiver:代表信号的接收方,也就是上面的信号要发给谁,然后调用谁的槽函数。
    SLOT(槽函数名(参数列表)):代表接收方的槽函数名,也就是收到前面的信号后,receiver需要调用哪个槽函数来处理。参数列表必须同SIGNAL中的参数列表一一对应。而且也是仅提供参数类型,不提供具体参数对象。同信号类似,槽函数也可以使用父类的或者父类族谱中任意层级父类的槽函数。
    我们以本节的示例代码举例来说明:
代码清单05-02-03

dialog.cpp
  1. CDialog::CDialog(QWidget* pParent) : QDialog(pParent) {
  2.         ui.setupUi(this);        
  3. connect(ui.fontComboBox, SIGNAL(currentFontChanged(const QFont &)), this, SLOT(slot_fontFamilyChanged(const QFont &)));
  4.         connect(ui.cbFontSize, SIGNAL(currentIndexChanged(int)), this, SLOT(slot_fontSizeChanged(int)));
  5.     //......
  6. }
复制代码
    代码清单05-02-03中,
    第3~6行编写了两处connect()调用。以第一个connect()调用为例进行介绍:ui.fontComboBox是sender,也就是发信号者。
    SIGNAL(currentFontChanged(const QFont &))表示发送的信号为currentFontChanged(const QFont&)。这个信号提供一个const的QFont&对象作为参数。
    第三个参数this代表receiver,也就是信号接收者。
    SLOT(slot_fontFamilyChanged(const QFont&))表示信号接收者收到信号后需要调用的槽函数,该槽函数也接收一个const的QFont&对象,也就是说槽函数必须与信号的参数列表保持完全一致。
    第3行处,connect()调用目的在于当字体下拉列表fontComboBox发出currentFontChanged信号时,CDilaog对象的slot_fontFamilyChanged()槽函数将被调用。
    CDialog对象的槽函数定义如下:
代码清单05-02-04

dialog.h
  1. class CDialog : public QDialog
  2. {
  3.     Q_OBJECT
  4.         // ……
  5. private slots:
  6.         void slot_fontFamilyChanged(const QFont &font);
  7.         void slot_fontSizeChanged(int);
  8. };
复制代码
    代码清单05-02-04中,
    可以看出,这些槽函数定义在"slots:"之后。slots是Qt的一个宏,用来标志槽函数定义区。它的前面可以是public、protected、private这3个关键字中的任何一个。这些关键字表明该槽函数的可见性。一般情况下,这些槽函数只给槽函数所在的类或者其派生类使用,所以一般定义为protected slots或者private slots。当然了,这个槽函数不一定是开发者自己实现的,开发者可以直接使用该类的父类的或者父类系中任何一个类的已有槽函数。
    如果一个类需要使用信号槽,那么该类必须从QObject派生,而且在类定义时,必须使用Q_OBJECT宏。见代码清单05-02-04中第3行。原因是Qt的moc命令需要将该宏展开生成一系列保障信号槽可以正常运行的接口。
    而且如果在类定义中增加Q_OBJECT宏之后,必须重新运行qmake(或qmake  -tp  vc)[注1]命令从而重新生成项目文件。

    另外,如果类是多重继承而来,那么务必保证父类中QObject(或者从QObject派生的那个父类)写在第一个父类的位置,否则编译会出错。比如,应写成:
  1. class CDialog : public QDialog, public  CMsgHandler
  2. {
  3.         Q_OBJECT
  4.     // ……
  5. };
复制代码
    QDialog从QObject派生而来,因此把它写在前面。如果CMsgHandler也是继承自QObject,那么它也可以写在考前的位置。比如:
  1. class CDialog : public  CMsgHandler, public   QDialog
复制代码
    在使用connect()宏的方案中,因为使用了宏定义,所以如果槽函数或者信号名称拼写有误,那么编译器是无法发现的。这种错误只能到运行时才能发现。排查的方法是观察connect调用之后,IDE开发环境的调试窗口中是否输出了错误信息,如果提示没有找到槽函数就说明槽函数可能未定义或者拼写错误。
    代码清单05-02-01中,最后一个参数是"信号槽的连接类型"Qt::ConnectionType,其取值见表05-02-01:

表05-02-01
  
Constant
  
  
Value
  
  
Description
  
  
Qt::AutoConnection
  
  0  
  
  当信号发送者和接收者处于同一线程内时,这个类型等同于DirectConnection,反之等同于QueuedConnection,这个类型也是connect函数的默认连接类型
  
  
Qt:: DirectConnection
  
  1
  
  信号一旦发射,与之关联的槽函数立即执行
  
  
Qt:: QueuedConnection
  
  2
  
  当信号产生,信号会暂时被缓冲到一个消息队列中,等待接收者的事件循环处理去队列中获取消息,然后执行和信号关联的槽函数,这种方式既可以在同一线程内传递消息也可以跨线程操作
  
  
Qt::BlockingQueuedConnection
  
  4
  
  这种类型类似于QueuedConnection,但是它只能应用于跨线程操作即发送者和接收者处于不同的线程中的情况,并且信号发送者线程会阻塞等待接收者的槽函数执行结束
  
  
Qt::AutoCompatConnection
  
  3
  
  当兼容Qt3程序是的默认连接类型
  
注意:
1.  一个信号可以和多个槽相连(槽会一个接一个地被调用,但是调用的顺序是不确定的);
2.  多个信号可以连接到同一个槽(只要任意一个信号产生,这个槽就会被调用);
3.  一个信号可以连接到另一个信号(将在后续章节讲解);

        最后看一下槽函数的实现:
代码清单05-02-05
dialog.cpp
  1. void CDialog::slot_fontFamilyChanged(const QFont &font){
  2.         int fontSize = ui.cbFontSize->currentText().toInt();
  3.         QFont ft = font;
  4.         ft.setPointSize(fontSize);
  5.     ui.plainTextEdit->setFont(ft);
  6. }
  7. void CDialog::slot_fontSizeChanged(int /*idx*/){
  8.         int fontSize = ui.cbFontSize->currentText().toInt();
  9.         QFont ft = ui.fontComboBox->currentFont();
  10.         ft.setPointSize(fontSize);
  11.     ui.plainTextEdit->setFont(ft);
  12. }
复制代码
    代码清单05-02-05中:
    第1~6行,提供了槽函数CDialog::slot_fontFamilyChanged(const QFont &font)的实现;
    第7~12行,提供了槽函数CDialog::slot_fontSizeChanged(int idx)的实现。
    在这两个槽函数中,根据各自的功能对控件进行操作。前者操作了字体家族,后者更新字体尺寸。

connect()函数

除了使用connect()宏这种方法之外,Qt5还提供了另外一种方法,通过信号地址或者槽函数地址来绑定信号链接。语法如下:
  1. connect(sender, 信号地址, receiver, 槽函数地址, Qt::ConnectionType);
复制代码
    sender、receiver的含义不变。信号地址与槽函数地址的语法如下:
        &sender对象的类名::信号名
        &receiver对象的类名::槽函数名
举例如下:
代码清单05-02-06

dialog.cpp
  1. CDialog::CDialog(QWidget* pParent) : QDialog(pParent) {
  2.         ui.setupUi(this);
  3. connect(ui.fontComboBox, &QFontComboBox::currentFontChanged, this,
  4. &CDialog::slot_fontFamilyChanged);
  5. //......;
  6. }
复制代码
代码清单05-02-05中第3行处,为connect()函数提供的各变量含义如下:
sender:ui.fontComboBox;
信号地址:&QFontComboBox::currentFontChanged;
receiver:this;
槽函数地址:&CDialog::slot_fontFamilyChanged
请注意,务必不要在信号地址或者槽函数地址的位置写函数参数,仅提供“&类名::函数名”即可;也不用写括号"()",即写成上面示例代码中的&QFontComboBox::currentFontChanged的形式即可。

Lambda表达式【扩展阅读】

    除了上述两种方法之外,针对C++11,Qt还提供了使用Lambda表达式的方案。由于用了C++11的特性,需要在.pro文件中添加:
  1. CONFIG += C++11
复制代码
    如果改用Lambda表达式,代码清单05-02-05可以改成:
代码清单05-02-07

dialog.cpp
  1. CDialog::CDialog(QWidget* pParent) : QDialog(pParent) {
  2.         ui.setupUi(this);
  3. connect(ui.fontComboBox, &QFontComboBox::currentFontChanged,
  4. [=]( const QFont &font) {
  5.         int fontSize = ui.cbFontSize->currentText().toInt();
  6.         QFont ft = font;
  7.         ft.setPointSize(fontSize);
  8.     ui.plainTextEdit->setFont(ft);
  9. });
  10. //......;
  11. }
复制代码
    代码清单05-02-07中:
    第3~9行使用了lambda表达式实现信号槽绑定。其中第5~8行是槽函数体。
    第3行的connect()中,前两个参数sender、信号地址跟前一个方案中的含义一样;
    第4行开始是Lambda表达式的语法,我们把Lambda表达式的参数列表写在了第4行;
    第9行是Lambda表达式结束处的右花括号"}",以及connect()的右括号")"。

    Lambda表达式语法如下:
  1. [捕获列表] (参数列表) mutable -> 返回值类型{
  2.     函数体
  3. }
复制代码
    其中mutable和返回值类型可以省略。
    根据C++11的语法,对于Lambda表达式的写法,举例如下:

表05-02-02
  
Lambda表达式
  
  
含义
  
  
[=] () {}
  
  Lambda函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是值传递方式,此时Lambda表达式中,实体默认为只读的,在大括号内不能进行修改,如果想修改,可以用mutable修饰,如 [=]() mutable{}。代码清单05-02-07中使用了[=]的写法来按值传递局部变量以及this。
  
  
[&] () {}
  
  Lambda函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是引用传递方式。
  
  
[this] () {}
  
  函数体内可以使用Lambda所在类中的所有成员变量
  
    个人推荐:尽量使用 = 而不使用 & ,以免造成内存问题
    这第三种方法其实针对那些比较简洁的槽函数体,如果槽函数体比较复杂,那么使用Lambda表达式的写法将导致主代码比较混乱、不宜阅读和维护。

结语
----------------------------------------------------------------
    在本节中,我们介绍了三种绑定信号槽的方式:
    第一种,使用SIGNAL、SLOT宏;第二种使用信号和槽函数的地址;第三种使用Lambda表达式。
    对于这几种方式来说,我们都需要为编写connect的类提供Q_OBJECT宏,而且如果该类存在多个父类,一定要把QObject派生来的父类写在前面。
    那么前两种方法有什么不同吗?实际上,从编写形式就可以看出,使用SIGNAL、SLOT宏时可以明确定义参数列表,而使用信号槽地址时只能提供函数地址而不提供参数列表,所以也就不能存在重载的信号或槽函数。否则重载的信号名相同、槽函数名也相同,只有参数列表不同,那么编译器就不知道我们到底想绑定哪一个。
    本节的内容是信号槽的基本内容,下一节开始,我们将介绍使用自定义信号槽以及信号转发。

注解
----------------------------------------------------------------
[注1]: 如果使用命令行编译,则运行qmake从而生成Makefile文件。如果使用VS2017的IDE环境编译,则运行qmake  -tp  vc生成VS2017可以识别的.vcxproj项目文件。

----------------------------------------------------------------

回复

使用道具 举报

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

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