目录
1.Qt简介和案例
2.第一个Qt程序
3.学会查看帮助文档
4.创建一个按钮
5.对象树简介
6.Qt的坐标系
7. 信号和槽
7.1自定义信号和槽
7.2信号连接信号
7.3拓展
7.4Qt4版本以前的connect
1.Qt简介和案例
Qt是一个跨平台的C++图形用户界面应用程序框架(就是一个库吧),它是完全面向对象的,允许真正意义上的组件式编程。
Qt是Linux桌面环境KDE的基础,它有商业版和开源版。
Qt的成功案例有很多,例如Linux桌面环境KDE、WPS等等。
它的优点如下:
- 跨平台,几乎支持所有平台
- 接口简单
- 一定程度上简化了内存回收机制(对象树)
- 可以进行嵌入式开发
2.第一个Qt程序
在创建项目时会提示选择三个基类当中的某一个:
这三个基类之间的关系如下:
QMainWindow和QDialog继承自QWidget:
- QWidget是一个空白窗口
- QMainWindow是一个带有菜单栏、工具栏、状态栏等等的一个窗口
- QDialog是一个提示对话框
项目创建完成后会有一个".pro"文件,这个文件时当前项目的工程文件,里面有一些很重要的内容,不要不要随意修改".pro"文件的内容甚至不要加注释。
可以看到默认情况下,Qt包含了core模块和gui模块,如果是4版本以上的话还会添加一个widgets模块。原因在于4版本以前widgets模块属于gui模块,4版本以后将widgets独立出来了。
当前项目自动构建出了一些源文件和头文件,可以很轻松的找到主函数:
其中,应用程序对象有且仅有一个, Widget类继承自QWidget,调用该类的show方法可以单独显示一个窗口:
注意Widget类定义当中的"Q_OBJECT",这是一个宏,它的作用是让该类支持信号和槽机制。还要注意其构造函数,有一个名为parent指向QWidget类的指针,这与对象树有关,稍后做解释。
3.学会查看帮助文档
学习任何技术都需要有帮助文档,Qt提供了这样的帮助文档。选中关键字并且按下"F1"可以弹出帮助文档:
还有一种方法是直接打开帮助文档的独立引用程序,它的位置在"Qt安装路径/版本号/mingw_64/bin"下:
4.创建一个按钮
Qt当中的按钮类为"QPushButton",通过帮助文档可以查看的到:
接下来在代码当中创建一个按钮并让其显示:
#include "widget.h"
#include <QApplication>
#include <QPushButton> // 包含头文件
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
QPushButton *btn = new QPushButton;
btn->show();// 显示
w.show();
return a.exec();
}
可以看到如果使用QPushButton类的show方法,是单独打开一个窗口并显示。很显然这并不是想要的效果。如果想要依附于已存在的窗口显示,只需要为按钮指定一个父亲即可:
#include "widget.h"
#include <QApplication>
#include <QPushButton> // 包含头文件
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
QPushButton *btn = new QPushButton;
btn->setParent(&w);// 依附于已经存在的窗口
//btn->show();// 显示
w.show();
return a.exec();
}
此时按钮便依附于已存在的窗口了。如果想要设置一下按钮的大小、显示文字、改变位置,可以使用QPushButton类的某些成员函数:
#include "widget.h"
#include <QApplication>
#include <QPushButton> // 包含头文件
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
QPushButton *btn = new QPushButton;
btn->setParent(&w);// 依附于已经存在的窗口
btn->setText("First Button");// 显示文字
btn->resize(200,200);// 设置一下按钮的大小
btn->move(200,100);// 移动到指定为止
//btn->show();// 显示
w.resize(600,400);// 固定一下窗口的大小
w.show();
return a.exec();
}
5.对象树简介
在没有接触Qt之前,上面的代码对于C++程序员来说是难受的,因为new了一个QPushButton对象却并没有做delete操作。
事实上在Qt当中有些情况下并不需要做delete操作,这就是对象树的作用。
对象树是Qt当中的一种自动回收内存的机制,它可以简化程序员的工作。就如上面的代码,程序员可以不用担心内存泄漏,即不用delete。
想要不delete堆上的内存空间,必须符合两个条件:
- new出来的对象必须是QObject的亲缘类对象
- 该对象必须指定一个QObject的亲缘类对象
QObject是最顶层的基类,基本上Qt当中的内置类都继承自于QObect。因此比如QWidget、QMainWindow、QDialog、QPushButton等等,它们的最顶层的基类就是QObject。
当new出来的对象指定了父亲之后,就相当于加入了对象树。这颗对象树的任意节点(对象)析构时,会自动释放其所有子孙节点(对象)。
此时可以做一个实验,即自定义一个类,取名为"MyTest"并继承自QPushButton类,指定它的父亲为Widget类:
#ifndef MYTEST_H
#define MYTEST_H
#include <QWidget>
class MyTest : public QWidget
{
Q_OBJECT
public:
explicit MyTest(QWidget *parent = nullptr);
~MyTest();
signals:
};
#endif // MYTEST_H
#include "mytest.h"
#include <QDebug>
MyTest::MyTest(QWidget *parent)
: QWidget{parent}
{
}
MyTest::~MyTest()
{
qDebug() << "~MyPushButton()";
}
修改一下Wdiget类的析构函数:
#include "widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
}
Widget::~Widget()
{
qDebug() << "~Widget()";
}
然后在主函数当中new出来MyPushButton类,指定其父亲为Wdiget,运行后关闭窗口观察打印结果:
#include "widget.h"
#include <QApplication>
#include <QPushButton> // 包含头文件
#include "mytest.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
MyTest *mbtn = new MyTest(&w);
w.resize(600,400);
w.show();
return a.exec();
}
关闭窗口后的输出结果,可以看到mbtn所指向的堆空间并没有手动delete,但是由于对象树的存在,不会造成内存泄漏。
这里可以用一幅图来解释:
此时可能会有个问题,即刚才说的是对象树当中的某个对象释放时会先析构其所有子孙节点。那么为什么先打印父对象的析构,后打印子孙对象的析构呢?这个原因在于析构函数分成两部分执行,一是执行析构函数函数体内的代码,二是执行函数体之后的清理工作。函数体内的代码是释放当前对象的大部分资源,例如对象内部new出来的空间;函数体之后是清理对象本身。所以对象树可以理解成清理对象本身之前先析构其所有子孙对象。
6.Qt的坐标系
Qt的窗口是有坐标系的,上面在控制按钮的位置时涉及到坐标系。Qt窗口的坐标系如下图所示:
7. 信号和槽
其实信号和槽可以理解为"信号和信号处理"。熟悉Linux系统编程的话,理解信号和槽不会太难。
信号和槽涉及到两个角色,即信号的发送方和信号的接收方;涉及到两个动作,即信号的发送和信号的处理。
总体而言可以分成两个部分:
connect是一个函数,作用类似于Linux当中的signal()。它的作用我个人理解成"注册一个信号产生时需要做的动作"。
通过帮助文档可以查看该函数:
原型可以是这样的:
connect(信号的发送发,要发送的信号,信号的接收方,接收到信号后要做的动作)
做一个小任务,即创建一个按钮,点击该按钮后关闭窗口:
#include "widget.h"
#include <QApplication>
#include <QPushButton> // 包含头文件
#include "mytest.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
QPushButton *btn = new QPushButton(&w);
btn->setText("PushButton");
btn->move(200,100);
btn->resize(100,100);
QObject::connect(btn,&QPushButton::clicked,&w,&Widget::close);// 注册信号
w.show();
return a.exec();
}
该按钮点击后,窗口就关闭了。
综上,信号和槽的优点非常明显:
- 使得编程多样化,可以实现更多丰富的功能,非常自由
- 低耦合,信号的发送方和接收方是低耦合的,这样设计有利于代码维护,出错的概率降低
7.1自定义信号和槽
如果自定义了一些类,并且想让这些类也能够使用信号和槽机制,Qt是支持的。
创建一个Student类和Teacher类,signals关键字下写自定义信号函数,该函数只声明不实现;public slots关键字(较高版本的Qt可以写到public下和全局下)下写自定义槽函数,该函数要声明和定义。
当前的任务是Teacher发出一个"下课"信号,学生做出"走出教室"动作。
#ifndef TEACHER_H
#define TEACHER_H
#include <QObject>
class Teacher : public QObject
{
Q_OBJECT
public:
explicit Teacher(QObject *parent = nullptr);
signals:
void ClassOver();// 下课,只声明不实现
};
#endif // TEACHER_H
#ifndef STUDENT_H
#define STUDENT_H
#include <QObject>
class Student : public QObject
{
Q_OBJECT
public:
explicit Student(QObject *parent = nullptr);
void GoOutOfClass();// 要声明和实现
signals:
};
#endif // STUDENT_H
#include "student.h"
#include <QDebug>
Student::Student(QObject *parent)
: QObject{parent}
{
}
void Student::GoOutOfClass()
{
qDebug() << "学生走出教室!";
}
此时在主函数当中完成任务:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Student *stu = new Student;
Teacher *tec = new Teacher;
QObject::connect(tec,&Teacher::ClassOver,stu,&Student::GoOutOfClass);
emit tec->ClassOver();// emit关键字,发送信号
delete stu;delete tec;
return a.exec();
}
此时要注意一个关键字,即"emit",它的作用在于触发信号。
其他方面,无论是信号函数还是槽函数,都是可以带参数的。
这里再做一个实验,即让信号函数之间发生重载,槽函数之间发生重载:
#ifndef TEACHER_H
#define TEACHER_H
#include <QObject>
#include <QString>
class Teacher : public QObject
{
Q_OBJECT
public:
explicit Teacher(QObject *parent = nullptr);
signals:
void ClassOver();// 下课,只声明不实现
void ClassOver(QString str);
};
#endif // TEACHER_H
#ifndef STUDENT_H
#define STUDENT_H
#include <QObject>
#include <QString>
class Student : public QObject
{
Q_OBJECT
public:
explicit Student(QObject *parent = nullptr);
void GoOutOfClass();// 要声明和实现
void GoOutOfClass(QString str);
signals:
};
#endif // STUDENT_H
#include "student.h"
#include <QDebug>
Student::Student(QObject *parent)
: QObject{parent}
{
}
void Student::GoOutOfClass()
{
qDebug() << "学生走出教室!";
}
void Student::GoOutOfClass(QString str)
{
qDebug() << "老师说:" << str;
}
那么在主函数当中修改一下代码:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Student *stu = new Student;
Teacher *tec = new Teacher;
QObject::connect(tec,&Teacher::ClassOver,stu,&Student::GoOutOfClass);
emit tec->ClassOver("下课啦!");// emit关键字,发送信号
delete stu;delete tec;
return a.exec();
}
此时出现了编译报错!原因在于connect函数当中的参数,信号函数和槽函数根本没有明确唯一指向,导致编译器报错。解决方法是利用函数指针来明确唯一指向:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Student *stu = new Student;
Teacher *tec = new Teacher;
void (Teacher::*tecSignal)(QString) = &Teacher::ClassOver;
void (Student::*stuSlot)(QString) = &Student::GoOutOfClass;
QObject::connect(tec,tecSignal,stu,stuSlot);
emit tec->ClassOver("下课啦!");// emit关键字,发送信号
delete stu;delete tec;
return a.exec();
}
需要注意,在声明和定义槽函数时,它们的返回值都为void。
如果需要取消注册的话,可以使用disconnect函数,这里就不做示例了。
7.2信号连接信号
信号不一定非要与槽函数connect在一起,信号和信号之间也可以连接。
现在实现一个需求,即创建一个按钮,按钮点击后老师下课,学生走出教室。此时代码可以修改为:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
Student *stu = new Student(&w);
Teacher *tec = new Teacher(&w);
void (Teacher::*tecSignal)() = &Teacher::ClassOver;
void (Student::*stuSlot)() = &Student::GoOutOfClass;
QObject::connect(tec,tecSignal,stu,stuSlot);
QPushButton *btn = new QPushButton(&w);
btn->setText("PushButton");
btn->resize(200,100);
QObject::connect(btn,&QPushButton::clicked,tec,tecSignal);
w.resize(600,400);
w.show();
return a.exec();
}
此时点击按钮会让老师产生下课信号,随后学生走出教室。
但是注意这里有个非常局限的东西,那就是信号连接信号时,作为槽函数的信号函数无法传递参数!这个时候就要配合全局函数或者lambda表达式,即在它们内部触发信号!
7.3拓展
下面的规则是总结和拓展:
- 一个信号可以连接多个槽函数
- 多个信号可以连接同一个槽函数
- 信号和槽函数的参数类型必须一一对应
- 信号函数的参数个数可以多于槽函数的参数个数
对于规则3和4的解释为,槽函数和可以选择接收和不接受信号传递过来的参数,如果选择接收了信号的参数,那么类型必须一致。以一幅图来理解:
7.4Qt4版本以前的connect
上面介绍的都是Qt4版本之后的connect函数用法,在Qt4版本之前,写法是下面这样的:
QObject::connect(tec,SIGNAL(&Teacher::ClassOver(QString)),stu,SLOT(&Student::GoOutOfClass(QString)));
显然这样的方式是非常直观的,因为可以免除函数指针的繁琐写法。但是这种写法必然存在恶劣的缺点否则较新的Qt版本也不会出现较新的写法。
根本原因在于SIGNAL和SLOT关键字会将括号内容字符串化,并且这些字符串之间还不会做任何的比较,所以导致编译器无法报错,导致后续维护项目困难。