Qt界面编程(三)—— 父子关系、对象树、信号和槽(自定义信号和槽、Qt5与Qt4的写法)

一、Qt按钮小程序

1.  按钮的创建和父子关系

        在Qt程序中,最常用的控件之一就是按钮了,首先我们来看下如何创建一个按钮:

#include 

    QPushButton * btn = new QPushButton; 
    //设置父亲
    btn->setParent(this);
    //设置文字
    btn->setText("德玛西亚");
    //移动位置
    btn->move(100,100);

    //第二种创建
    QPushButton * btn2 = new QPushButton("孙悟空",this);
    //重新指定窗口大小
    this->resize(600,400);

    //设置窗口标题
    this->setWindowTitle("第一个项目");

    //限制窗口大小
    this->setFixedSize(600,400);

       上面代码中,一个按钮其实就是一个QPushButton类的对象,如果只是创建出对象,是无法显示到窗口中的,所以我们需要依赖一个父窗口,也就是指定一个父亲,利用setParent函数或者按钮创建的时候通过构造函数传参,此时我们称两个窗口建立了父子关系。在有父窗口的情况下,窗口调用show会显示在父窗口中如果没有父窗口,那么窗口调用show显示的会是一个顶层的窗口(顶层窗口是能够在任务栏中找到的,不依赖于任何一个窗口而独立存在)(按钮也是继承于QWidget,也属于窗口)。

        如果想设置按钮上显示的文字可以用setText,移动按钮位置用move。

        对于窗口而言,我们可以修改左上角窗口的标题setWindowTitle,重新指定窗口大小:resize,或者设置固定的窗口大小setFixedSize。 

2. Qt窗口坐标体系   

        通过以上代码可以看出Qt的坐标体系。以左上角为原点(0,0),以向右的方向为x轴的正方向,以向下方向为y轴的正方向。如下图所示:

Qt界面编程(三)—— 父子关系、对象树、信号和槽(自定义信号和槽、Qt5与Qt4的写法)_第1张图片

         对于嵌套窗口,其坐标是相对于父窗口来说的。顶层窗口的父窗口就是屏幕。

3. 对象树模型 

        QObject是Qt里边绝大部分类的根类(基类)。

  1. QObject对象之间是以对象树的形式组织起来的。
    1. 当两个QObject(或子类)的对象建立了父子关系的时候。子对象就会加入到父对象的一个成员变量叫children(孩子)的list(列表)中。
    2. 父对象析构的时候这个列表中的所有对象也会被析构(注意,这里是说父对象和子对象,不要理解成父类和子类)
  2. QWidget是能够在屏幕上显示的一切组件的父类
    1. QWidget继承自QObject,因此也继承了这种对象树关系。一个孩子自动地成为父组件的一个子组件。我们向某个窗口中添加了一个按钮或者其他控件(建立父子关系),当用户关闭这个窗口的时候,该窗口就会被析构,之前添加到他上边的按钮和其他控件也会被一同析构。这个结果也是我们开发人员所期望的。
    2. 当然,我们也可以手动删除子对象。当子对象析构的时候会发出一个信号destroyed,父对象收到这个信号之后就会从children列表中将它剔除。比如,当我们删除了一个按钮时,其所在的主窗口会自动将该按钮从其子对象列表(children)中删除,并且自动调整屏幕显示,按钮在屏幕上消失。当这个窗口析构的时候,children列表里边已经没有这个按钮子对象,所以我们手动删除也不会引起程序错误。

Qt 引入对象树的概念,在一定程度上解决了内存问题。

  1. 对象树中对象的顺序是没有定义的。这意味着,销毁这些对象的顺序也是未定义的。
  2. 任何对象树中的 QObject对象 delete 的时候,如果这个对象有 parent,则自动将其从 parent 的children()列表中删除;如果有孩子,则自动 delete 每一个孩子。Qt 保证没有QObject会被 delete 两次,这是由析构顺序决定的。

如果QObject在栈上创建,Qt 保持同样的行为。正常情况下,这也不会发生什么问题。来看下下面的代码片段:

{
    QWidget window;
    QPushButton quit("Quit", &window);
}

作为父组件的 window 和作为子组件的 quit 都是QObject的子类(事实上,它们都是QWidget的子类,而QWidget是QObject的子类)。这段代码是正确的,quit 的析构函数不会被调用两次,因为标准 C++要求,局部对象的析构顺序应该按照其创建顺序的相反过程。因此,这段代码在超出作用域时,会先调用 quit 的析构函数,将其从父对象 window 的子对象列表中删除,然后才会再调用 window 的析构函数。

但是,如果我们使用下面的代码:

{
    QPushButton quit("Quit");
    QWidget window;
    quit.setParent(&window);
}

情况又有所不同,析构顺序就有了问题。我们看到,在上面的代码中,作为父对象的 window 会首先被析构,因为它是最后一个创建的对象。在析构过程中,它会调用子对象列表中每一个对象的析构函数,也就是说, quit 此时就被析构了。然后,代码继续执行,在 window 析构之后,quit 也会被析构,因为 quit 也是一个局部变量,在超出作用域的时候当然也需要析构。但是,这时候已经是第二次调用 quit 的析构函数了,C++ 不允许调用两次析构函数,因此,程序崩溃了。

由此我们看到,Qt 的对象树机制虽然帮助我们在一定程度上解决了内存问题,但是也引入了一些值得注意的事情。这些细节在今后的开发过程中很可能时不时跳出来烦扰一下,所以,我们最好从开始就养成良好习惯,在 Qt 中,尽量在构造的时候就指定 parent 对象,并且大胆在堆上创建。

二、信号和槽机制

信号:各种事件

槽: 响应信号的动作

当某个事件发生后,如某个按钮被点击了一下,它就会发出一个被点击的信号(signal

某个对象接收到这个信号之后,就会做一些相关的处理动作(称为槽slot)。

但是Qt对象不会无故收到某个信号,要想让一个对象收到另一个对象发出的信号,这时候需要建立连接(connect

1. 系统自带的信号和槽

下面我们完成一个小功能,上面我们已经学习了按钮的创建,但是还没有体现出按钮的功能,按钮最大的功能也就是点击后触发一些事情,比如我们点击按钮,就把当前的窗口给关闭掉,那么在Qt中,这样的功能如何实现呢?

其实两行代码就可以搞定了,我们看下面的代码:

QPushButton * quitBtn = new QPushButton("关闭窗口",this);
connect(quitBtn,&QPushButton::clicked,this,&MyWidget::close);

第一行是创建一个关闭按钮,这个之前已经学过,第二行就是核心了,也就是信号槽的使用方式。

connect函数是建立信号发送者、信号、信号接收者、槽四者关系的函数:

connect(sender, signal, receiver, slot);

参数解释:

  1. sender:信号发送者
  2. signal:信号
  3. receiver:信号接收者
  4. slot:接收对象在接收到信号之后所需要调用的函数(槽函数)

这里要注意的是connect的四个参数都是指针,信号和槽是函数指针

系统自带的信号和槽如何查找呢,这个就需要利用帮助文档了,在帮助文档中比如我们上面的按钮的点击信号,在帮助文档中输入QPushButton,首先我们可以在Contents中寻找关键字 signals,信号的意思,但是我们发现并没有找到,这时候我们应该想到也许这个信号的被父类继承下来的,因此我们去他的父类QAbstractButton中就可以找到该关键字,点击signals索引到系统自带的信号有如下几个 

Qt界面编程(三)—— 父子关系、对象树、信号和槽(自定义信号和槽、Qt5与Qt4的写法)_第2张图片

 这里的clicked就是我们要找到,槽函数的寻找方式和信号一样,只不过他的关键字是slot。

2. 自定义信号和槽

        Qt框架默认提供的标准信号和槽不足以完成我们日常应用开发的需求,比如说点击某个按钮让另一个按钮的文字改变,这时候标准信号和槽就没有提供这样的函数。但是Qt信号和槽机制提供了允许我们自己设计自己的信号和槽。 

2.1 自定义信号使用条件

  1. 声明在类的signals域下
  2. 没有返回值,void类型的函数
  3. 只有函数声明,没有定义
  4. 可以有参数,可以重载
  5. 通过emit关键字来触发信号,形式:emit object->sig(参数);

2.2 自定义槽函数使用条件

  1. qt4 必须声明在 private/public/protected slots域下面,qt5之后可以声明public下,同时还可以是静态的成员函数,全局函数,lambda表达式
  2. 没有返回值,void类型的函数
  3. 不仅有声明,还得要有实现
  4. 可以有参数,可以重载

2.3 使用自定义信号和槽

        定义场景:下课了,老师跟同学说肚子饿了(信号),学生请老师吃饭(槽)

        首先定义一个学生类和老师类:

        老师类中声明信号 饿了 hungry

signals:
       void hungry();

        学生类中声明槽 请客treat

public slots:
       void treat();

        在窗口中声明一个公共方法下课,这个方法的调用会触发老师饿了这个信号,而响应槽函数学生请客

void MyWidget::ClassIsOver()
{
    //发送信号
    emit teacher->hungry();
}
学生响应了槽函数,并且打印信息
//自定义槽函数 实现
void Student::treat()
{
       qDebug() << "Student treat teacher";
}

在窗口中连接信号槽

teacher = new Teacher(this);
student = new Student(this);
connect(teacher,&Teacher::hungury,student,&Student::treat);

并且调用下课函数,测试打印出相应log

自定义的信号 hungry带参数,需要提供重载的自定义信号和 自定义槽

void hungry(QString name);  // 自定义信号
void treat(QString name );   // 自定义槽
// 但是由于有两个重名的自定义信号和自定义的槽,直接连接会报错,
// 所以需要利用函数指针来指向函数地址, 然后在做连接
void (Teacher:: * teacherSingal)(QString) = &Teacher:: hangry;
void (Student:: * studentSlot)(QString) = &Student::treat;
connect(teacher,teacherSingal,student,studentSlot);
// 也可以使用static_cast静态转换挑选我们要的函数
connect(
teacher,
static_cast(&Teacher:: hangry),
student,
static_cast(& Student::treat));

3. 信号和槽的拓展

  1. 一个信号可以和多个槽相连

如果是这种情况,这些槽会一个接一个的被调用,但是槽函数调用顺序是不确定的。像上面的例子,可以将一个按钮点击信号连接到关闭窗口的槽函数,同时也连接到学生请吃饭的槽函数,点击按钮的时候可以看到关闭窗口的同时也学生请吃饭的log也打印出来。

  1. 多个信号可以连接到一个槽

只要任意一个信号发出,这个槽就会被调用。如:一个窗口多个按钮都可以关闭这个窗口。

  1. 一个信号可以连接到另外的一个信号

当第一个信号发出时,第二个信号被发出。除此之外,这种信号-信号的形式和信号-槽的形式没有什么区别。注意这里还是使用connect函数,只是信号的接收者和槽函数换成另一个信号的发送者和信号函数。如上面老师饿了的例子,可以新建一个按钮btn。

connect(btn,&QPushButton::clicked,teacher,&Teacher::hungry);
  1. 信号和槽可以断开连接

可以使用disconnect函数,当初建立连接时connect参数怎么填的,disconnect里边4个参数也就怎么填。这种情况并不经常出现,因为当一个对象delete之后,Qt自动取消所有连接到这个对象上面的槽

  1. 信号和槽函数参数类型和个数必须同时满足两个条件
  1. 信号函数的参数个数必须大于等于槽函数的参数个数
  2. 信号函数的参数类型和槽函数的参数类型必须一一对应

4. Qt4版本的信号槽写法

connect(
    teacher,
    SIGNAL(hungry(QString)),
    student,
    SLOT(treat(QString))
);

这里使用了SIGNALSLOT这两个宏,宏的参数是信号函数和槽函数的函数原型。

因为直接填入了函数原型,所有这里边编译不会出现因为重载导致的函数指针二义性的问题。但问题是如果函数原型填错了,或者不符合信号槽传参个数类型约定,编译期间也不会报错,只有运行期间才会看到错误log输出。

原因就是这两个宏将后边参数(函数原型)转化成了字符串。目前编译器还没有那么智能去判断字符串里边的内容符不符合运行条件。

5. Lambda表达式 

C++11中的Lambda表达式用于定义匿名的函数对象,以简化编程工作。首先看一下Lambda表达式的基本构成:

分为四个部分:[局部变量捕获列表]、(函数参数)、函数额外属性设置opt、函数返回值->retype、{函数主体}

[capture](parameters) opt ->retType
{
    ……;
}

5.1 局部变量引入方式

[ ],标识一个Lambda的开始。由于lambda表达式可以定义在某一个函数体A里边,所以lambda表达式有可能会去访问A函数中的局部变量。中括号里边内容是描述了在lambda表达式里边可以使用的外部局部变量的列表:

    1. []

    表示lambda表达式不能访问外部函数体的任何局部变量

    1. [a]

    在函数体内部使用值传递的方式访问a变量

    1. [&b]

    在函数体内部使用引用传递的方式访问b变量

    1. [=]

    函数外的所有局部变量都通过值传递的方式使用, 函数体内使用的是副本

    1. [&]

    引用的方式使用lambda表达式外部的所有变量

    1. [=, &foo]

    foo使用引用方式, 其余是值传递的方式

    1. [&,foo]

    foo使用值传递方式, 其余是引用传递的方式

    1.  [this]

在函数内部可以使用类的成员函数和成员变量,=和&形式也都会默认引入

由于引用方式捕获对象会有局部变量释放了而lambda函数还没有被调用的情况。如果执行lambda函数那么引用传递方式捕获进来的局部变量的值不可预知。

所以在无特殊情况下建议使用[=](){}的形式

5.2 函数参数

        (params)表示lambda函数对象接收的参数,类似于函数定义中的小括号表示函数接收的参数类型和个数。参数可以通过按值(如:(int a,int b))和按引用(如:(int &a,int &b))两种方式进行传递。函数参数部分可以省略,省略后相当于无参的函数。

5.3 选项Opt

        Opt 部分是可选项,最常用的是mutable声明,这部分可以省略。外部函数局部变量通过值传递引进来时,其默认是const,所以不能修改这个局部变量的拷贝,加上mutable就可以        

int a = 10 ;
    [=]()
    {
        a=20;//编译报错,a引进来是const
}
[=]()mutable
    {
        a=20;//编译成功
};

5.4 函数返回值 ->retType

        ->retType,标识lambda函数返回值的类型。这部分可以省略,但是省略了并不代表函数没有返回值编译器会自动根据函数体内的return语句判断返回值类型,但是如果有多条return语句,而且返回的类型都不一样,编译会报错,如:

[=]()mutable
{
        int b = 20;
        float c = 30.0;
        if(a>0)
            return b;
        else
            return c;//编译报错,两条return语句返回类型不一致
};

5.5 是函数主体{}

        {},标识函数的实现,这部分不能省略,但函数体可以为空。

5.6 槽函数使用Lambda表达式

        以QPushButton点击事件为例:

connect(btn,&QPushButton::clicked,[=](){
        qDebug()<<"Clicked";
});

这里可以看出使用Lambda表达式作为槽的时候不需要填入信号的接收者。当点击按钮的时候,clicked信号被触发,lambda表达式也会直接运行。当然lambda表达式还可以指定函数参数,这样也就能够接收到信号函数传递过来的参数了。

由于lambda表达式比我们自己自定义槽函数要方便而且灵活得多,所以在实现槽函数的时候优先考虑使用Lambda表达式。一般我们的使用习惯也是lambda表达式外部函数的局部变量全部通过值传递捕获进来,也就是:

        [=](){  }的形式

你可能感兴趣的:(【C++成长之路】,qt,开发语言)