QT 自定义信号和槽

Qt信号与槽机制详解
转载地址:http://www.cnblogs.com/tangkaixuan/p/6505010.html
文章来自:https://lug.ustc.edu.cn/sites/qtguide/
 
4.3 自定义信号和槽


本节首先介绍一下 C++ 编程中常用的传递数据机制,包括类对象的公有成员变量、友元类/函数 、公有函数、回调函数等,这些机制在 Qt 程序中也是可以使用的。然后重点介绍如何在 Qt 类里面自定义信号和槽,通过手动触发信号来调用槽函数,完成两个对象之间的信息传递,本节最后示范一个信号接力触发的例子。本节内容较多,可以分两三次来学。

4.3.1 C++ 的沟通方式


C++ 编程中常遇到各个对象之间进行沟通的情景,需要将数据从一个对象传递给另一个对象来处理。大致的方法有如下几种:

  • 接收端定义公有成员变量以供源端修改,然后接收端处理数据; (不建议用!)
  • 接收端将私有成员变量通过友元方式共享给源端,源端可以修改接收端变量; (除了特殊情况,一般不建议用!)
  • 接收端定义公开的 get 和 set 函数,提供给源端调用 (推荐使用,可以与信号和槽机制协同工作);
  • 源端给出回调函数约定,接收端定义相同参数和返回值类型的静态成员函数,将静态成员函数作为回调函数交给源端,源端再调用该函数 (可以使用)。

这些方式都是基于标准 C++ 的,在 Qt 中都可以用,但由于 Qt 有更好的信号和槽机制,因此一般更推荐使用信号和槽机制实现通信。下面对 C++ 常见的传递数据方式依次举例示范,主要是让读者大致了解一下传递数据的过程。

本小节的示例是多个独立的 cpp 文件,可以通过 2.1 节命令行方式编译运行,这几个 cpp 文件都放到 D:\QtProjects\ch04\cppcom 文件夹里的。
首先是公有成员变量的例子,publicmember.cpp 代码:

//publicmember.cpp
#include <iostream>
using namespace std;
//接收端类
class Dst
{
public:
    double m_dblValue;
    int m_nCount;
    //处理数据函数
    double DoSomething()
    {
        double dblResult = m_dblValue / m_nCount;
        return dblResult;
    }
};
//源端类
class Src
{
public:
    void SendDataTo( Dst &theDst)
    {
        //设置接收端公有变量
        theDst.m_dblValue = 2.0;
        theDst.m_nCount = 3;
    }
};
int main()
{
    //定义两个对象
    Dst theDst;
    Src theSrc;
    //传递数据
    theSrc.SendDataTo(theDst);
    //接收端处理
    cout<<theDst.DoSomething();
    //
    return 0;
}

接收端的类对象 theDst 有两个公有成员 m_nCount 和 m_dblValue,源端对象 theSrc 在 SendDataTo 函数里面修改了接收端的成员变量,然后接收端再对数据进行处理。
这种传递数据方式最大的问题是谁都可以修改接收端的公有成员变量,如果有其他代码也修改了 theDst,那么处理结果是很难预知的,尤其是在多线程程序里,公有变量在另一个线程被修改了,本线程很可能都不知道。如果通过全局变量传递数据的情景,这种负面效果是 类 似的,而且更严重。
公有成员变量和全局变量方式都会破坏类数据的封装特性,谁都能修改,结果不可控。另外,这种方式不方便做数值有效性鉴定,比如把 m_nCount 设置为 0, 除 0 会直接导致程序出错。
所以不建议使用这种方式。

接下来是友元类的例子,通过友元声明,源端可以直接设置接收端的私有成员变量,如 friend.cpp 代码:

//friend.cpp
#include <iostream>
using namespace std;
//接收端类
class Dst
{
private:    //私有变量
    double m_dblValue;
    int m_nCount;
public:
    //处理数据函数
    double DoSomething()
    {
        double dblResult = m_dblValue / m_nCount;
        return dblResult;
    }
    //友元类
    friend class Src;
};
//源端类
class Src
{
public:
    void SendDataTo( Dst &theDst)
    {
        //因为是友元类,所以能设置接收端私有变量
        theDst.m_dblValue = 2.0;
        theDst.m_nCount = 3;
    }
};
int main()
{
    //定义两个对象
    Dst theDst;
    Src theSrc;
    //传递数据
    theSrc.SendDataTo(theDst);
    //接收端处理
    cout<<theDst.DoSomething();
    //
    return 0;
}

接收端的成员变量是私有的,因此不用担心成员变量被其他代码段胡乱修改,只有友元授权的 Src 类对象才能修改接收端对象私有变量。这种方式的缺陷是 Src 类和 Dst 类是紧耦合的,它们息息相关,修改了一个类的代码很可能影响另一个。除非程序员确定需要这种紧耦合设计,否则一般也不建议使用友元。另外,对于传递的数值有效性鉴定,需要 友元类对象确保不把 m_nCount 设置成 0。

再来看看第三种,使用 get 和 set 函数对的方式,在 Qt 类库里面,对于设置数值的函数以 set 字样打头,而获取数值的函数默认省略 get 字样,比如 QLabel 对象,设置文本函数为 setText(),获取文本函数为 text() 。下面的示例也按照 Qt 命名风格, getset.cpp 代码:

//getset.cpp
#include <iostream>
using namespace std;
//接收端类
class Dst
{
private:    //私有变量
    double m_dblValue;
    int m_nCount;
public:
    //get函数
    double value()
    {
        return m_dblValue;
    }
    int count()
    {
        return m_nCount;
    }
    //set函数
    void setValue(double v)
    {
        m_dblValue = v;
    }
    void setCount(int n)
    {
        if( n < 1 ) //防止除 0 ,并且计数限定为正整数
        {
            m_nCount = 1;
        }
        else
        {
            m_nCount = n;
        }
    }
    //处理数据函数
    double DoSomething()
    {
        double dblResult = m_dblValue / m_nCount;
        return dblResult;
    }
};
//源端类
class Src
{
public:
    void SendDataTo( Dst &theDst)
    {
        //通过set函数传递数据
        theDst.setValue(2.0);
        theDst.setCount(3);
    }
};
int main()
{
    //定义两个对象
    Dst theDst;
    Src theSrc;
    //传递数据
    theSrc.SendDataTo(theDst);
    //接收端处理
    cout<<theDst.DoSomething();
    //
    return 0;
}

代码里通过 value()和 setValue() 函数封装私有变量 m_dblValue,通过 count()和 setCount() 函数封装 m_nCount 私有变量,在 set 函数里面可以对输入的数据做判断,确认是否合法,数据的可控性大大增强。get/set 方式是推荐的做法,下一节有规范的 Qt 属性封装介绍,与这种方式是类似的。

接下来示范一个回调函数的例子,多线程编程和 Windows 编程会经常遇到类似的回调函数,通常源端会给出通用的回调函数类型声明,接收端按照该格式定义自己的回调函数。因为回调函数一般是通用的,所以参数里常用的是 void * 指针,而不会针对某一个固定的类对象传参。因为类的普通成员函数需要隐藏的 this 指针,会导致不符合回调函数类型声明,所以回调函数只能用静态成员函数或全局函数定义。回调函数示范 callback.cpp:

//callback.cpp
#include <iostream>
using namespace std;
//源端约定回调函数类型
typedef void (*PFUNC)(double v, int n, void *pObject);
//接收端类
class Dst
{
private:
    double m_dblValue;
    int m_nCount;
public:
    //处理数据函数
    double DoSomething()
    {
        double dblResult = m_dblValue / m_nCount;
        return dblResult;
    }
    //回调函数
    static void FuncCallBack(double v, int n, void *pObject)
    {
        //转换成 Dst 指针
        Dst *pDst = (Dst *)pObject;
        //静态成员函数也是可以设置私有变量的,但需要手动传对象指针
        //设置 value
        pDst->m_dblValue = v;
        //设置count
        if( n < 1)
        {
            pDst->m_nCount = 1;
        }
        else
        {
            pDst->m_nCount = n;
        }
    }
};
//源端类
class Src
{
public:
    void SendDataTo( Dst *pDst, PFUNC pFunc)
    {
        //通过回调函数传数据
        pFunc(2.0, 3, pDst);
    }
};
int main()
{
    //定义两个对象
    Dst theDst;
    Src theSrc;
    //传递数据
    theSrc.SendDataTo(&theDst, Dst::FuncCallBack);
    //接收端处理
    cout<<theDst.DoSomething();
    //
    return 0;
}

在上面示例代码中,源端先给出了回调函数类型 PFUNC 的声明,参数为 double 和 int,返回值为空。
接收端的类就按照这个声明定义参数、返回值都一样的静态成员函数 FuncCallBack,作为实际执行的回调函数。回调函数只能是全局函数或静态成员函数,因为类的普通成员函数需要隐藏的 this 指针参数。
在 main 函数里,程序执行流程为:
①theSrc 调用 SendDataTo 函数,参数有目标对象 theDst 指针和目标类里定义好的 回调函数 Dst::FuncCallBack。
②在 SendDataTo 函数内部,回调函数 Dst::FuncCallBack 作为 pFunc ,被调用执行,三个参数值为 2.0、3 和目标对象 指针。
③pFunc 就是 Dst::FuncCallBack 回调函数,这个回调函数会根据三个参数里数值,首先将 void * 指针转为目标对象指针 pDst,然后设置目标对象里的两个私有成员变量,并且可以直接做数据有效性检查。
④theDst 对象里的成员变量被设置好之后,就可以调用 DoSomething 函数做处理了。

回调函数机制是很常见的,Windows 消息机制本身也是回调函数的应用,多线程编程也使用回调函数作为新线程里的任务函数。我们上一节示范的三个例子,信号与槽函数可以一对一关联,一对多关联,多对一关联,如 果用回调函数实现这些复杂的映射,那会是非常头疼的事。比如希望 theSrc 同时把数据传递给 A、B、C 三个目标对象,那 SendDataTo 函数必须手动执行三次。回调函数难以实现同时一发多收、多发一收,而信号和槽机制是完全可以的,并且代码非常简洁明了。另外信号与槽函数可以在运行时解除关联关系,这也是回调函数不好实现的特性。
介绍完常规的传递数据方法之后,下面来看看 Qt 自定义信号和槽的通信过程。

4.3.2 通过自定义信号和槽沟通


通过信号和槽机制通信,通信的源头和接收端之间是松耦合的:

  • 源头只需要顾自己发信号就行,不用管谁会接收信号;
  • 接收端只需要关联自己感兴趣的信号,其他的信号都不管;
  • 只要源头发了信号,关联该信号的接收端全都会收到该信号,并执行相应的槽函数。

源头和接收端是非常自由的,connect 函数决定源头和接收端的关联关系,并会自动根据信号里的参数传递给接收端的槽函数。
因为源头是不关心谁接收信号的,所以 connect 函数一般放在接收端类的代码中,或者放在能同时访问源端和接收端对象的代码位置。
下面开始自定义信号和槽的例子,打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 qobjcom,创建路径 D:\QtProjects\ch04,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
建好项目之后,打开窗体 widget.ui 文件,进入设计模式,向其中拖入一个按钮控件,按钮 objectName 默认为 pushButton,将其显示文本的 text 属性设置为“发送自定义信号”,并调整按钮宽度将文本都显示出来,如下图所示:

ui

编辑好之后保存界面,回到代码编辑模式,打开 widget.h,添加处理按钮 clicked 信号的槽函数,和新的自定义的信号 SendMsg:

#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
    Q_OBJECT
public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();
signals:    //添加自定义的信号
    void SendMsg(QString str);  //信号只需要声明,不要给信号写实体代码
public slots:   //接收按钮信号的槽函数
    void ButtonClicked();
private:
    Ui::Widget *ui;
};
#endif // WIDGET_H

signals: 就是信号代码段的标志,这个标志不带 public 、protected、private 等前缀,那是因为信号默认强制规定为公有类型,这样才能保证其他对象能接收到信号。
我们定义了 SendMsg 信号,带一个 QString 参数,这个声明与普通函数声明类似。注意信号只是一个空壳,只需要声明它,而不要给它写实体代码。自定义信号的全部代码就是头文件这里的两行(包括 signals: 行),不需要其他的。signals: 标识的代码段只能放置信号声明,不能放其他任何东西,普通的函数或变量、槽函数都不要放在这里。

public slots: 是公有槽函数代码段的标志,定义了 ButtonClicked 槽函数,接收按钮被点击的信号,这个槽函数以后会触发我们自定义的信号。槽函数代码段也只能放槽函数声明的代码,不要把其他的东西放在这个代码段里。

下面来编写 widget.cpp 里面的代码,实现发送我们自定义信号的槽函数,并和按钮的信号关联起来:

#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //关联
    connect(ui->pushButton, SIGNAL(clicked()), this, SLOT(ButtonClicked()));
}
Widget::~Widget()
{
    delete ui;
}
//槽函数
void Widget::ButtonClicked()
{
    //用 emit 发信号
    emit SendMsg( tr("This is the message!") );
}

在 Widget 构造函数里,我们将按钮的 clicked 信号关联到槽函数 ButtonClicked,当按钮被点击时,ButtonClicked 会自动被调用。
ButtonClicked 里面只有一句代码,就是

emit SendMsg( tr("This is the message!") );

emit 是发信号的关键字,然后接下来就与调用函数是一样的格式,SendMsg 里面放置我们想传递的字符串参数。除了 emit 字样,触发信号就与函数调用一样。这样简单一句就实现了触发信号的过程,同之前所说的,源端就顾自己发信号,至于谁接收 SendMsg 信号,源端是不管的。
Widget 窗体代码就是上面那么多,发送我们自定义的 SendMsg 信号的过程如下图所示:

emit

对于上图左半段,按钮的点击信号和相应的槽函数我们之前两节已经用过多次了,这个不是关注的重点。重点就是在主窗体槽函数里面,我们发出了自定义的信号 SendMsg ,谁来接收它呢?

接下来我们造一个接收 SendMsg 信号的类对象和槽函数,并将收到的字符串参数弹窗显示。我们先为该项目添加一个新的类,并编写接收 SendMsg 信号的槽函数。
打开 QtCreator 菜单“文件”-->“新建文件或项目”,在“新建”对话框里,左边部分选择“C++”,中间部分选 “C++ Class”,如下图所示:

new

然后点击右下角 Choose,进入新建 C++ 类的向导界面,将 Class name 修改为 ShowMsg,基类选择 QObject,其他的就用自动填充的,选择基类 QObject 之后,会自动包含相应头文件。要使用信号和槽机制,必须直接或间接从 QObject 类派生,我们这里是直接从 QObject 派生了子类 ShowMsg:

newclass

然后点击“下一步”,进入项目管理界面:

manage

这里就按照默认的值,不用修改,自动添加到项目 qobjcom.pro 里面,版本控制默认是没有。点击“完成”,稍等一会,QtCreator 就会生成好 ShowMsg 类的两个文件 showmsg.h 和 showmsg.cpp,并添加到项目里。

接下来,我们编辑 showmsg.h ,声明接收 SendMsg 信号的槽函数 RecvMsg:

#ifndef SHOWMSG_H
#define SHOWMSG_H
#include <QObject>
class ShowMsg : public QObject
{
    Q_OBJECT
public:
    explicit ShowMsg(QObject *parent = 0);
    ~ShowMsg();
signals:
public slots:
    //接收 SendMsg 信号的槽函数
    void RecvMsg(QString str);
};
#endif // SHOWMSG_H

RecvMsg 槽函数声明的参数类型和返回类型要与 SendMsg 信号保持一致,所以参数是 QString,返回 void。

然后我们编辑 showmsg.cpp,实现 RecvMsg 槽函数:

#include "showmsg.h"
#include <QMessageBox>
ShowMsg::ShowMsg(QObject *parent) : QObject(parent)
{
}
ShowMsg::~ShowMsg()
{
}
//str 就是从信号里发过来的字符串
void ShowMsg::RecvMsg(QString str)
{
    QMessageBox::information(NULL, tr("Show"), str);
}

添加头文件 <QMessageBox> 包含之后,我们添加槽函数 RecvMsg 的实体代码,里面就是一句弹窗的代码,显示收到的字符串。QMessageBox::information 函数第一个参数是父窗口指针,设置为 NULL,代表没有父窗口,就是在系统桌面直接弹窗的意思。
信号和槽机制有三步,一是有源头对象发信号,我们完成了;第二步是要有接收对象和槽函数,注意,上面只是类的声明,并没有定义对象。我们必须定义一个接收端的对 象,然后才能进行第三步 connect。

编辑项目里 main.cpp,向其中添加代码,定义接收端对象,然后进行 connect:

#include "widget.h"
#include <QApplication>
#include "showmsg.h"
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;   //①主窗体对象,内部会发送 SendMsg 信号
    ShowMsg s;  //②接收端对象,有槽函数 RecvMsg
    //③关联,信号里的字符串参数会自动传递给槽函数
    QObject::connect(&w, SIGNAL(SendMsg(QString)), &s, SLOT(RecvMsg(QString)));
    //显示主界面
    w.show();
    return a.exec();
}

首先添加 "showmsg.h" 头文件包含,然后在主窗体对象 w 定义之后,定义了接收端对象 s。主窗体对象 w 会发 SendMsg 信号,接收端 s 有对应的槽函数 RecvMsg,这样完成了信号和槽机制的头两步。接下来第三步就是调用关联函数 QObject::connect,将源头对象、信号、接收端对象、槽函数关联。connect 函数是通用基类 QObject 里面定义的,之前用 connect 函数都没有加类前缀,是因为在 QObject 派生类里面自动继承了 connect 函数,不需要额外的前缀。在 main 函数里,需要手动加 QObject:: 前缀来调用 connect 函数。
关联完成之后,一旦用户点击主窗体里的按钮,我们自定义的 SendMsg 信号就会发出去,然后 接收端对象 s 里的槽函数就会执行,并且信号里的字符串也会自动传递给 RecvMsg 槽函数,然后会出现弹窗显示传递的字符串。
这个示例的运行效果如下图所示:

run

这个例子完整的执行流程如下图所示:

allprocess

本小节需要大家学习的就是右半段的部分,我们在主窗体 ButtonClicked 函数里触发自定义的信号 SendMsg,然后通过 connect 函数关联,自动调用了接收端对象 s 的槽函数 RecvMsg,并弹窗显示了传递的字符串。

也许有读者会问,费这么大劲,为什么不直接在 ButtonClicked 里面弹窗?那不简单多了?
因为本小节的目的不是弹窗,而是为了展现自定义信号和槽函数的代码写法,理解信号和槽机制的运行流程。以后遇到复杂多窗口的界面程序,在多个窗体对象之间就可以用 上图示范的流程,来进行通信、传递数据。

4.3.3 信号关联到信号示例


信号除了可以关联到槽函数,还可以关联到类型匹配的信号,实现信号的接力触发。上个示例中因为 clicked 信号没有参数,而 SendMsg 信号有参数,所以不方便直接关联。本小节示范一个信号到信号的关联,将按钮的 clicked 信号关联到一个参数匹配的 SendVoid 信号。
重新打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 signalsconn,创建路径 D:\QtProjects\ch04,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
建好项目之后,打开窗体 widget.ui 文件,进入设计模式,向窗体拖一个按钮,按钮默认对象名称为 pushButton,将显示的文本属性 text 修改为“信号接力触发”,如下图所示:

ui

编辑好界面之后保存,回到代码编辑模式,编辑 widget.h ,添加我们自定义的信号:

#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
    Q_OBJECT
public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();
signals:    //只添加一个信号,不需要按钮对应的槽函数
    void SendVoid();
private:
    Ui::Widget *ui;
};
#endif // WIDGET_H

新添加的 SendVoid 信号声明,没有参数,所以能和按钮的 clicked 信号匹配,实现信号到信号的关联。
然后我们编辑 widget.cpp ,添加关联函数调用:

#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //关联按钮信号到自定义的信号
    connect(ui->pushButton, SIGNAL(clicked()), this, SIGNAL(SendVoid()));
}
Widget::~Widget()
{
    delete ui;
}

仅在构造函数里加了一句 connect 调用,注意 connect 函数第四个参数是 SIGNAL(SendVoid()),这就是关联到信号的用法。以前都是关联到槽函数,这里直接关联到自定义的信号,而不需要槽函数中转。
关联之后,一旦按钮的 clicked 信号触发,主窗体的信号 SendVoid() 紧跟着自动触发,实现信号触发的接力过程。

自定义信号的触发过程编完之后,下面为项目添加新的 ShowVoid 类,也是从 QObject 派生,和上面小节添加 C++ 类是类似的:

class

然后我们声明自定义的槽函数,用于接收 SendVoid() 信号,打开 showvoid.h,编辑如下:

#ifndef SHOWVOID_H
#define SHOWVOID_H
#include <QObject>
class ShowVoid : public QObject
{
    Q_OBJECT
public:
    explicit ShowVoid(QObject *parent = 0);
    ~ShowVoid();
signals:
public slots:
    //接收 SendVoid() 信号的槽函数
    void RecvVoid();
};
#endif // SHOWVOID_H

头文件增加了与 SendVoid() 信号匹配的槽函数 RecvVoid() 声明。然后我们编辑 showvoid.cpp,添加槽函数实体代码:

#include "showvoid.h"
#include <QMessageBox>
ShowVoid::ShowVoid(QObject *parent) : QObject(parent)
{
}
ShowVoid::~ShowVoid()
{
}
//槽函数,弹窗
void ShowVoid::RecvVoid()
{
    QMessageBox::information(NULL, tr("Show"), tr("Just void."));
}

有了 ShowVoid 类声明是不够的,接收信号需要一个对象实体,然后才能关联,所以同样地,编辑 main.cpp 文件,添加代码如下:

#include "widget.h"
#include <QApplication>
#include "showvoid.h"
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;   //①源头对象,内部发送信号 SendVoid()
    ShowVoid s; //②接收对象,有对应槽函数 RecvVoid()
    //③关联源头的信号和接收端的槽函数
    QObject::connect(&w, SIGNAL(SendVoid()), &s, SLOT(RecvVoid()));
    //显示主界面
    w.show();
    return a.exec();
}

这一小节大部分代码都和上一小节类似,所以不做过多重复描述了。程序运行效果如下图:

run

本小节示例的执行流程如下图所示:

process

主窗体里将信号关联到信号,是需要大家学会用的。也许有读者会问,为什么不直接将 ui->pushButton 的信号关联到最终的目的端 s 呢?因为 ui 是主窗体对象 w 的私有成员变量,在类外不可访问,无论是 main 函数还是 ShowVoid 类的代码里,都是看不到 ui->pushButton 这个按钮的,源头都找不到,是没法关联的。如果把私有变量 ui 改成公有的,那会破坏类的封装性,不建议这么弄。在面对私有成员无法访问的情况下,使用信号接力是比较科学的方法。

关于自定义信号和槽的通信就讲这些,最后教大家一个小技巧,如果向项目新添加了类文件,如 showvoid.h 和 showvoid.cpp,如果这时候 QtCreator 左下角的按钮变成了长时间持续灰色,无法使用,那么可以通过来回切换 Debug 和 Release 构建模式,让 QtCreator 重新解析一下项目文件就可以了:

mode
Qt框架中的信号(Signal and Slot)机制是其核心功能之一,它提供了一种高效且类型安全的对象间通信方式。这一机制的底层实现涉及多个层次的设计优化,包括宏定义、元对象系统(Meta-Object System)、事件驱动以及线程安全等。 ### 信号的语法基础 在Qt中,`signals`、`slots`、`Q_OBJECT`、`emit`、`SIGNAL` `SLOT` 等关键字并不是标准C++的关键字,而是Qt对C++的扩展[^1]。这些关键字实际上是宏定义,在编译阶段会被预处理器替换为相应的代码结构。例如: - `Q_OBJECT` 宏用于启用类的元对象特性,使该类能够使用信号机制。 - `signals` `slots` 用于声明信号槽函数。 - `emit` 用于触发信号。 - `SIGNAL()` `SLOT()` 是用于连接信号的字符串宏。 这些宏最终会在MOC(Meta-Object Compiler)处理阶段被转换为标准C++代码。 --- ### 元对象系统(Meta-Object System) Qt信号机制依赖于其元对象系统(Meta-Object System),该系统由 MOC 工具生成的代码支持。MOC 是一个预处理器工具,它读取 C++ 源文件并生成额外的 C++ 代码,以支持 Qt 的扩展功能,如信号、属性系统、动态对象模型等。 当一个类继承自 `QObject` 并使用了 `Q_OBJECT` 宏时,MOC 会为其生成以下内容: - 一个静态元对象(`staticMetaObject`),其中包含类的所有信号、属性等信息。 - 用于调用信号的函数指针数组。 - `qt_metacall()` 方法,用于处理元调用(meta-call)。 信号本质上是特殊的成员函数,它们没有返回值(必须为 `void` 类型),但可以有参数。每个信号在 MOC 生成的代码中都有对应的索引,并可以通过 `QMetaObject::activate()` 被调用。 --- ### 信号的连接机制 信号的连接是通过 `connect()` 函数完成的。Qt 提供了多种重载版本的 `connect()`,最常见的是使用 `SIGNAL()` `SLOT()` 宏的形式: ```cpp connect(sender, SIGNAL(signalName()), receiver, SLOT(slotName())); ``` 内部实现上,`connect()` 函数会解析信号的签名,查找它们在元对象中的索引,并将这些信息存储在一个连接列表中。这个连接列表保存在 `QObjectPrivate` 中的 `connections` 成员变量中。 当信号被发射时,Qt 会遍历所有连接到该信号,并根据连接类型(如直接调用、排队调用等)决定如何执行这些。例如: - **Qt::DirectConnection**:直接调用接收者的槽函数,发生在发送者的线程上下文中。 - **Qt::QueuedConnection**:将调用封装为事件放入接收者所在线程的事件队列中,等待事件循环处理。 --- ### 事件驱动与元调用 当信号被 `emit` 触发时,实际上会调用 `QMetaObject::activate()` 函数。这个函数负责创建一个 `QMetaCallEvent` 事件,并将其投递到接收者的事件队列中(如果是跨线程连接)或直接调用槽函数(如果是同一线程)。 在 `QObject::event()` 函数中,会处理这种类型的事件: ```cpp case QEvent::MetaCall: { QAbstractMetaCallEvent *mce = static_cast<QAbstractMetaCallEvent*>(e); if (!d_func()->connections.loadRelaxed()) { QBasicMutexLocker locker(signalSlotLock(this)); d_func()->ensureConnectionData(); } QObjectPrivate::Sender sender(this, const_cast<QObject*>(mce->sender()), mce->signalId()); mce->placeMetaCall(this); break; } ``` 这段代码展示了如何在事件循环中处理元调用事件。`QAbstractMetaCallEvent` 封装了调用槽函数所需的信息,包括参数、索引等。`placeMetaCall()` 方法最终会调用目标对象的槽函数。 --- ### 性能与线程安全 Qt信号机制在设计上考虑了性能线程安全。例如: - 同步连接(`Qt::DirectConnection`)避免了跨线程开销,适合在同一线程内的对象通信。 - 异步连接(`Qt::QueuedConnection`)确保了线程安全,适用于跨线程通信。 - 内部使用原子操作锁机制来保护连接数据结构,防止多线程环境下的竞争条件。 此外,Qt 还支持 `Qt::BlockingQueuedConnection`,用于强制阻塞直到槽函数执行完毕,这在某些特定场景下非常有用。 --- ### 示例代码:手动模拟信号行为 下面是一个简化的示例,演示如何手动模拟信号的行为(不使用 Qt 的 MOC): ```cpp #include <iostream> #include <vector> #include <functional> class Signal { public: using Slot = std::function<void()>; void connect(Slot slot) { slots.push_back(std::move(slot)); } void emit_signal() { for (const auto& slot : slots) { slot(); } } private: std::vector<Slot> slots; }; int main() { Signal s; s.connect([]{ std::cout << "Slot 1 called!" << std::endl; }); s.connect([]{ std::cout << "Slot 2 called!" << std::endl; }); s.emit_signal(); return 0; } ``` 这个示例展示了信号的基本原理:维护一组回调函数(),并在适当的时候调用它们。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值