4.3.1 C++ 的沟通方式
C++编程中,经常遇到各个对象之间进行沟通的情况,需要将数据从一个对象传递给另一个对象来处理。大致的方法有如下几种:
这些方式都是基于标准 C++ 的,在 Qt 中都可以用,但由于 Qt 有更好的信号和槽机制,因此一般更推荐使用信号和槽机制实现通信。下面对 C++ 常见的传递数据方式依次举例示范,主要是让读者大致了解一下传递数据的过程。
首先是公有成员变量的例子,代码如下:
#include
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<return 0;
}
接收端的类对象 theDst 有两个公有成员 m_nCount 和 m_dblValue,源端对象 theSrc 在 SendDataTo 函数里面修改了接收端的成员变量,然后接收端再对数据进行处理。这种传递数据方式最大的问题是谁都可以修改接收端的公有成员变量,如果有其他代码也修改了 theDst,那么处理结果是很难预知的,尤其是在多线程程序里,公有变量在另一个线程被修改了,本线程很可能都不知道。如果通过全局变量传递数据的情景,这种负面效果是 类 似的,而且更严重。公有成员变量和全局变量方式都会破坏类数据的封装特性,谁都能修改,结果不可控。另外,这种方式不方便做数值有效性鉴定,比如把 m_nCount 设置为 0, 除 0 会直接导致程序出错。
所以不建议使用这种方式。
友元类的例子,代码如下:
#include
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<//
return 0;
}
接收端的成员变量是私有的,因此不用担心成员变量被其他代码段胡乱修改,只有友元授权的 Src 类对象才能修改接收端对象私有变量。这种方式的缺陷是 Src 类和 Dst 类是紧耦合的,它们息息相关,修改了一个类的代码很可能影响另一个。除非程序员确定需要这种紧耦合设计,否则一般也不建议使用友元。另外,对于传递的数值有效性鉴定,需要 友元类对象确保不把 m_nCount 设置成 0。
再来看看第三种,使用 get 和 set 函数对的方式,在 Qt 类库里面,对于设置数值的函数以 set 字样打头,而获取数值的函数默认省略 get 字样,比如 QLabel 对象,设置文本函数为 setText(),获取文本函数为 text() 。
代码如下:
#include
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<//
return 0;
}
代码里通过 value()和 setValue() 函数封装私有变量 m_dblValue,通过 count()和 setCount() 函数封装 m_nCount 私有变量,在 set 函数里面可以对输入的数据做判断,确认是否合法,数据的可控性大大增强。
下面是个回调函数的例子,多线程编程和 Windows 编程会经常遇到类似的回调函数,通常源端会给出通用的回调函数类型声明,接收端按照该格式定义自己的回调函数。因为回调函数一般是通用的,所以参数里常用的是 void * 指针,而不会针对某一个固定的类对象传参。因为类的普通成员函数需要隐藏的 this 指针,会导致不符合回调函数类型声明,所以回调函数只能用静态成员函数或全局函数定义。代码如下:
#include
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<//
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 项目,选择基类为QWidget
建好项目之后,打开窗体 widget.ui 文件,进入设计模式,向其中拖入一个按钮控件,按钮 objectName 默认为 pushButton,将其显示文本的 text 属性设置为“发送自定义信号”,并调整按钮宽度将文本都显示出来,如下图所示:
完成后保存,打开widget.h文件添加处理按钮 clicked 信号的槽函数,和新的自定义的信号 SendMsg:
#ifndef WIDGET_H
#define WIDGET_H
#include
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 信号的过程如下图所示:
接下来我们造一个接收 SendMsg 信号的类对象和槽函数,并将收到的字符串参数弹窗显示。我们先为该项目添加一个新的类,并编写接收 SendMsg 信号的槽函数。
打开 QtCreator 菜单“文件”–>“新建文件或项目”,在“新建”对话框里,左边部分选择“C++”,中间部分选 “C++ Class”
然后点击右下角 Choose,进入新建 C++ 类的向导界面,将 Class name 修改为 ShowMsg,基类选择 QObject,其他的就用自动填充的,选择基类 QObject 之后,会自动包含相应头文件。要使用信号和槽机制,必须直接或间接从 QObject 类派生,我们这里是直接从 QObject 派生了子类 ShowMsg:
继续下一步就好了。接下来,我们编辑 showmsg.h ,声明接收 SendMsg 信号的槽函数 RecvMsg:
#ifndef SHOWMSG_H
#define SHOWMSG_H
#include
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
ShowMsg::ShowMsg(QObject *parent) : QObject(parent)
{
}
ShowMsg::~ShowMsg()
{
}
//str 就是从信号里发过来的字符串
void ShowMsg::RecvMsg(QString str)
{
QMessageBox::information(NULL, tr("Show"), str);
}
添加头文件 包含之后,我们添加槽函数 RecvMsg 的实体代码,里面就是一句弹窗的代码,显示收到的字符串。QMessageBox::information 函数第一个参数是父窗口指针,设置为 NULL,代表没有父窗口,就是在系统桌面直接弹窗的意思。
信号和槽机制有三步,一是有源头对象发信号,我们完成了;第二步是要有接收对象和槽函数,注意,上面只是类的声明,并没有定义对象。我们必须定义一个接收端的对 象,然后才能进行第三步 connect。
编辑项目里 main.cpp,向其中添加代码,定义接收端对象,然后进行 connect:
#include "widget.h"
#include
#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 槽函数,然后会出现弹窗显示传递的字符串。
4.3.3 信号关联到信号
信号除了可以关联到槽函数,还可以关联到类型匹配的信号,实现信号的接力触发。也是通过connect函数进行关联。
函数模型为connect(对象指针, SIGNAL(信号), 对象指针, SIGNAL(信号));这里就不做演示了。
最后教大家一个小技巧,如果向项目新添加了类文件,如 showvoid.h 和 showvoid.cpp,如果这时候 QtCreator 左下角的按钮变成了长时间持续灰色,无法使用,那么可以通过来回切换 Debug 和 Release 构建模式,让 QtCreator 重新解析一下项目文件就可以了: