Qt学习(17)

Qt学习(17)——自定义信号和槽

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

1、C++的沟通方式

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

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

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

在本小节的示例中是多个独立的cpp文件,可以通过“Hello Word”一节中使用命令行方式进行编译运行,这几个cpp文件都放到E:\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 自定义信号和槽的通信过程。

2、通过自定义信号和槽沟通

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

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

源头和接收端是非常自由的,connect函数决定源头和接收端的关联关系,并会自动根据信号里的参数传递给接收端的槽函数。

因为源头是不关心谁接收信号的,所以 connect 函数一般放在接收端类的代码中,或者放在能同时访问源端和接收端对象的代码位置。

下面开始自定义信号和槽的例子,打开 Qt Creator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
       ①项目名称 qobjcom,创建路径 E:\QtProjects\ch04,点击下一步;
       ②套件选择里面选择全部套件,点击下一步;
       ③基类选择 QWidget,点击下一步;
       ④项目管理不修改,点击完成。
        建好项目之后,打开窗体 widget.ui 文件,进入设计模式,向其中拖入一个按钮控件,按钮 objectName 默认为pushButton,将其显示文本的 text 属性设置为“发送自定义信号”,并调整按钮宽度将文本都显示出来,如下图所示:

编辑好之后保存界面,回到代码编辑模式,打开 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!") );
}
时间: 2024-07-31 18:07:11

Qt学习(17)的相关文章

【qt学习006】Dialogs and MainWindows 小结

学习<c++GUI Programming with Qt 4>已有一段时间,非常享受这本书的阅读过程,内容简洁清晰,让人一目了然. 马上要学习更难的内容,所以先做个总结,然后再继续前进. 总结的形式尽量简洁,以代码为主,再将一些我认为重要的笔记作为注释添加在代码中.内容大多是摘抄自书本,但也有一些地方属于个人理解. 闲话少谈,下面列出代码: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // example1

Qt 学习(2)

Qt 学习(2) Qt 的 QXmlStreamReader 在 Qt 应用程序中访问 XML 格式的文件数据,可以使用 [QXmlStreamReader][sreamreader] 对文件进行读取.关于 QXmlStreamReder 的使用,官方文档中有 QXmlStream Bookmarks Example 的示例可供参考. 常用的方法有: TokenType readNext() 读取下一个标记并返回它的类型 bool readNextStartElement() 在当前元素内,读取

ndk学习17: jni之Java调用C&C++

一.Hello World 1. 定义函数原型 native关键字定义的函数即为jni函数 2.生成头文件 切换到src目录执行: (这个过程可以写脚本自动完成,比如自动拷贝到jni目录) javah -jni 包名.类名 在根目录下生成: org_bing_testjni_MainActivity.h 3. 工程中添加jni代码 工程右键->添加native code->输入名字->finish 多了如下文 新建一个Application.mk,配置相关选项(详细查看ndk有关Appl

Qt学习之路

  Qt学习之路_14(简易音乐播放器) Qt学习之路_13(简易俄罗斯方块) Qt学习之路_12(简易数据管理系统) Qt学习之路_11(简易多文档编辑器) Qt学习之路_10(Qt中statusBar,MessageBox和Timer的简单处理) Qt学习之路_9(Qt中Item Widget初步探索) Qt学习之路_8(Qt中与文件目录相关操作) Qt学习之路_7(线性布局和网格布局初步探索) Qt学习之路_6(Qt局域网聊天软件) Qt学习之路_5(Qt TCP的初步使用) Qt学习之路

QT学习之路(1):彩票绝对不中模拟器

//============================================//绝对不中,彩票开奖模拟器#include "mainwindow.h"#include "ui_mainwindow.h"#include <QHash>#include <QDebug>MainWindow::MainWindow(QWidget *parent) :    QMainWindow(parent),    ui(new Ui::M

qt学习(三):鼠标图标改变

qt学习 (三):鼠标图标改变 当你进入一个美好的qt软件场景,比如游戏,电脑的黑白图标会让程序逊色不少, 1改图标要加光标的头文件, 2 载入光标图, 3 再设置改光标就可以了 1在头文件中加 #include <QtGui>  //光标类的父类 //再在public成员中声明换的函数void keyPressEvent(QKeyEvent *k); //声明按键换图的函数         .h文件    --注意头文件和声明 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Qt学习总结-ui篇(二)

qccs定义圆角 border-radius:10px; 如果想给特定位置定义圆角,如: 左上角:border-left-top-radius:10px; 右下角色:border-right-bottom-rasius:10px; 半透明效果 只需要在css中使用rgba(100,100,100,40)这种形式来表示颜色即可. 为可执行文件添加图标 1.新建文件:finename.rc 文件名无所谓,只要后缀为rc就可以. 2.编辑新建的文件,输入以下内容: IDI_ICON1 ICON DIS

【qt学习005】搞不明白的布局

记录一下自己在布局这一章遇见的各种狗屎问题. 问题主要出现在布局最后一节:综合布局实例,类似于一个qq管理器的界面(见下图1).看见这个时,打算动手实现一下,于是开始写代码,写着写着发现不知道怎么写了,遇见一些无法解决的问题(问题中描述的布局类之间的关系见下图2): 1. 最底层应该使用哪一类? 2. 怎么将QListWidget加入到最底层? 3. 怎么往QStackWidget加入三个页面,每个页面代表不同的信息? 4. 怎么将QHBoxLayout中的CLOSE按钮连接到退出函数,要完整地

【Qt学习笔记】13.拖放技术:Drag & Drop

1.接受拖放 Drag & Drop 是一个界面操作,用于在两个窗口间传递数据. Drag Source: 拖放源窗口 Drag Target: 拖放目标窗口 拖放操作: 1.在源窗口:选中目标,按下鼠标,移动,拖至目标窗口(Drag) 2.在目标窗口:取消鼠标,到指定位置,松开鼠标(Drop) (按下ESC取消操作) MIME: MIME(Multipurpose Internet Mail Extensions)被传递的数据以MIME格式传送,它是多组type-data数据:(type0,