Qt实战教程
-
本文档创作于2021/1/17。
-
作者:南京航空航天大学 Eric
1 创建第一个Qt程序
1.1 创建过程
-
点击 文件-新建文件或项目可开始创建。
-
Location为路径选择;Build System为创建的系统,建议使用默认Qmake。
- Details即创建一个类,父类可以选择QMainWindow、QDialog、QWidget之中的一个,其中QMainWindow、QDialog都是QWidget的子类。下方的Generate Form勾选上会额外生成.ui文件,且可使用设计窗口。
- 构建套件Kits建议全部勾选上。
1.2 各文件的代码含义
- 创建完工程后会自动生成如上文件,其中.pro为工程文件,main是主程序入口,另外两个.h和.cpp是创建时的类。
- 工程文件FirstProject.pro
# 增加core核心和gui图像模块
QT += core gui
# 如果版本号>4 增加widget模块
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp \
mywidget.cpp
HEADERS += \
mywidget.h
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
- 主程序入口main.cpp
#include "mywidget.h"
#include <QApplication> //包含一个应用程序类的头文件
//main程序入口
int main(int argc, char *argv[]) //argc为命令行变量的数量,argv为命令行变量的数组
{
//创建应用程序对象a,有且只有一个
QApplication a(argc, argv);
//创建窗口对象w,父类为QWidget
myWidget w;
//窗口对象默认不会显示,必须调用show方法显示窗口
w.show();
//让应用程序对象进入消息循环机制(死循环),防止窗口一闪而过
//让代码阻塞到这行,后面代码均不执行
return a.exec();
}
- 类的头文件mywidget.h
//避免mywidget.h头文件被重复包含,相当于设立了标识
#ifndef MYWIDGET_H
#define MYWIDGET_H
#include <QWidget>
class myWidget : public QWidget
{
Q_OBJECT //Q_OBJECT宏 允许类中使用信号和槽的机制
public:
//有参构造(用父类初始化列表 )
myWidget(QWidget *parent = nullptr);
//析构
~myWidget();
};
#endif // MYWIDGET_H
- 类的源文件mywidget.cpp
#include "mywidget.h"
//有参构造(调用父类构造函数)
myWidget::myWidget(QWidget *parent)
: QWidget(parent)
{
}
myWidget::~myWidget()
{
}
1.3 命名规范及快捷键
-
命名规范
类名:首字母大写,单词和单词之间首字母大写。
函数名、变量名:首字母小写,单词和单词之间首字母大写。
-
快捷键
注释:Ctrl + /
运行:Ctrl + r
编译:Ctrl + b
字体缩放:Ctrl + 鼠标滚轮
查找:Ctrl + f
自动对齐:Ctrl + i
帮助文档:F1(光标要查询的代码上) ,再按一次全屏帮助,退出按Esc / 打开Assistant快捷方式
同名间的 .h / .cpp切换:F4
1.4 新建一个按钮
#include "mywidget.h"
//包含按钮头文件
#include<QPushButton>
myWidget::myWidget(QWidget *parent)
: QWidget(parent)
{
//创建第一个按钮,默认构造
QPushButton * btn1 = new QPushButton;
//创建第二个按钮,有参构造,参数为(按钮文本,父亲)
QPushButton * btn2 = new QPushButton("按钮2",this);
//设置按钮文本,此处由char*强转为QString
btn1->setText("按钮");
//设置按钮的"父亲",即为myWidget窗口
btn1->setParent(this);
//重置窗口大小
this->resize(300,300);
//设置固定窗口大小(用户不能拖拽)
this->setFixedSize(300,500);
//移动btn2按钮
btn2->move(100,200);
//重置btn2按钮大小
btn2->resize(80,40);
//设置窗口标题
this->setWindowTitle("第一个程序");
}
myWidget::~myWidget()
{
}
1.5 对象树
-
QObject是以对象树的形式组织起来的。
-
当你创建一个QObject对象时,会看到QObject的构造函数接收一个QObject指针作为参数,这个参数就是 parent,也就是父对象指针。
-
这相当于,在创建QObject对象时,可以提供一个其父对象,我们创建的这个QObject对象会自动添加到其父对象的children()列表。
-
当父对象析构的时候,这个列表中的所有对象也会被析构。(注意,这里的父对象并不是继承意义上的父类!)
-
这种机制在 GUI 程序设计中相当有用。例如,一个按钮有一个QShortcut(快捷键)对象作为其子对象。当我们删除按钮的时候,这个快捷键理应被删除。这是合理的。
-
QWidget是能够在屏幕上显示的一切组件的父类。
-
QWidget继承自QObject,因此也继承了这种对象树关系。一个孩子自动地成为父组件的一个子组件。因此,它会显示在父组件的坐标系统中,被父组件的边界剪裁。例如,当用户关闭一个对话框的时候,应用程序将其删除,那么,我们希望属于这个对话框的按钮、图标等应该一起被删除。事实就是如此,因为这些都是对话框的子组件。
-
当然,**我们也可以自己删除子对象,它们会自动从其父对象列表中删除。**比如,当我们删除了一个工具栏时,其所在的主窗口会自动将该工具栏从其子对象列表中删除,并且自动调整屏幕显示。
-
-
Qt 引入对象树的概念,在一定程度上解决了内存问题。
-
当一个QObject对象在堆上创建的时候,Qt 会同时为其创建一个对象树。不过,对象树中对象的顺序是没有定义的。这意味着,销毁这些对象的顺序也是未定义的。
-
任何对象树中的 QObject对象 delete 的时候,如果这个对象有 parent,则自动将其从 parent 的children()列表中删除;如果有孩子,则自动 delete 每一个孩子。Qt 保证没有QObject会被 delete 两次,这是由析构顺序决定的。
-
对象树析构顺序判断方法:析构大的,快析构完后,找"儿子",等所有儿子都析构完后,析构自己。
创建顺序:从上到下;析构顺序:从下到上。
1.6 信号和槽
- 连接函数connect()
语法:
connect(信号的发送者,发送的信号(函数地址),信号的接受者,处理的槽函数(函数地址));
//如按下按钮使myWidget窗口关闭
connect( myBtn, &QPushButton::clicked, this, &QWidget::close);
-
信号和槽的特点:松散耦合。
-
常用QPushButton类的四个信号:
void clicked(bool checked = false) 点击
void pressed() 按下
void released() 松开
void toggled(bool checked) 切换
1.7 自定义信号和槽
-
实现效果:定义老师和学生类,在myWidget主窗口类默认构造下定义这两个类对象,并创建下课函数,当调用下课函数时,触发“老师饿了”信号,并通过connect调用槽函数,并打印相应文字。
-
1.在老师类中建立信号函数:
- 写在signals:下。
- 返回值是void,只需要声明,不需要实现。
- 可以有参数,可以重载。
-
老师类头文件
#ifndef TEACHER_H
#define TEACHER_H
#include <QObject>
class Teacher : public QObject
{
Q_OBJECT
public:
explicit Teacher(QObject *parent = nullptr);
signals:
//自定义信号写在signals下
//返回值是void,只需要声明,不需要实现
//可以有参数,可以重载
void hungry();
};
#endif // TEACHER_H
- 老师类的实现(空)
#include "teacher.h"
Teacher::Teacher(QObject *parent) : QObject(parent)
{
}
- 2.在学生类中建立槽函数
- 5.6版本以上槽函数写在public下就可以,5.6版本之前需要写到public slots下。
- 返回值是void,需要声明也需要实现。
- 可以有参数,可以发生重载。
- 学生类头函数
#ifndef STUDENT_H
#define STUDENT_H
#include <QObject>
class Student : public QObject
{
Q_OBJECT
public:
explicit Student(QObject *parent = nullptr);
//5.6版本以上槽函数写在public下就可以,5.6版本之前需要写到public slots下
//返回值是void,需要声明也需要实现
//可以有参数,可以发生重载
void treat();
signals:
};
#endif // STUDENT_H
- 学生类的实现
#include "student.h"
#include <QDebug>
Student::Student(QObject *parent) : QObject(parent)
{
}
void Student::treat()
{
qDebug()<<"请吃饭";
}
-
3.主窗口新建老师和学生对象,并连接信号和槽函数
-
主窗口头函数
#ifndef MYWINDOW_H
#define MYWINDOW_H
#include <QMainWindow>
#include"teacher.h"
#include"student.h"
class myWindow : public QMainWindow
{
Q_OBJECT
public:
myWindow(QWidget *parent = nullptr);
~myWindow();
private:
Teacher *lh;
Student *st;
void classIsOver();
};
#endif // MYWINDOW_H
- 主窗口的实现
#include "mywindow.h"
#include <QPushButton>
myWindow::myWindow(QWidget *parent)
: QMainWindow(parent)
{
//新建老师和学生对象
this->lh=new Teacher(this);
this->st=new Student(this);
//新建连接
connect(lh,&Teacher::hungry,st,&Student::treat);
classIsOver();
}
myWindow::~myWindow()
{
}
void myWindow::classIsOver()
{
//emit表示触发信号,不写也没事
emit lh->hungry();
}
注意点:必须要先创建连接再触发下课函数。
1.8 信号和槽发生重载
- 案例:当Teacher类对象的“饿了”信号有重载版本
hungry(QString)
抑或是Student对象“请客”有重载版本treat(QString)
时,再用connect
函数连接时编译器不知道要连接哪一个信号和槽,则需要定义函数指针指向函数地址。
//语法: 返回值类型(类::函数指针名)(参数列表)=&类::函数名;
void (Teacher::*teacherSignal)(QString)=&Teacher::hungry;
void (Student::*studentSignal)(QString)=&Student::treat;
connect(lh,teacherSignal,st,studentSignal);
注意点:信号函数和槽函数的参数必须一致,且编译器自动将信号函数中的参数传到槽函数中。
- 如果直接用
QString
输出会输出一个双引号,用char*
输出就不会出现问题。QString
转成char*
需要先转为QByteArray
再转成char*
。
qDebug()<<"请吃"<<foodName.toUtf8().data();
1.9 信号连接信号
- 在上面的案例中,都是通过主窗口的默认构造中调用了
classIsOver()
函数,从而自动发送”下课“信号的,本例旨在通过按钮触发。 - 思路1:按钮信号触发下课槽函数,在下课槽函数中发送"饿了"信号,触发“请客”。
//连接带参数的函数和槽
//需要定义函数指针指向函数地址
void (Teacher::*teacherSignal1)(QString)=&Teacher::hungry;
void (Student::*studentSignal1)(QString)=&Student::treat;
connect(lh,teacherSignal1,st,studentSignal1);
QPushButton *classButton1=new QPushButton;
classButton1->setText("下课1");
classButton1->setParent(this);
classButton1->move(100,100);
classButton1->resize(80,40);
//按下课1按钮触发下课槽函数,下课槽函数会发送hungry有参信号
connect(classButton1,&QPushButton::clicked,this,&myWindow::classIsOver);
- 思路2:按钮信号直接触发“饿了”信号,再触发“请客”。
//直接通过按钮触发下课信号,即信号连接信号
void (Teacher::*teacherSignal2)()=&Teacher::hungry;
void (Student::*studentSignal2)()=&Student::treat;
connect(lh,teacherSignal2,st,studentSignal2);
QPushButton *classButton2=new QPushButton;
classButton2->setText("下课2");
classButton2->setParent(this);
classButton2->move(200,100);
classButton2->resize(80,40);
//按下课2按钮直接触发饿了信号
connect(classButton2,&QPushButton::clicked,lh,teacherSignal2);
- 断开信号disconnect
disconnect(classButton2,&QPushButton::clicked,lh,teacherSignal2);
1.10 信号和槽拓展知识
- 信号可以连接信号。
- 一个信号可以连接多个槽函数。
- 多个信号可以连接一个槽函数。
- 信号和槽的参数类型必须一一对应,且参数自动传递。
- 信号参数个数可以多于槽的参数个数,即信号可以浪费参数,槽函数的所有参数必须要有信号来传,且类型必须一一对应。
书上常见的QT4版本以前的信号和槽连接方式
connect(lh,SIGNAL(hungry()),st,SLOT(treat()));
- 优点:参数直观,且对于有参的信号和槽无需写函数指针。
- 缺点:如果信号和槽参数类型没有对应,编译器不作检测,在运行阶段才能看到这种错误,故不推荐这种写法,且此时的槽函数必须写到
public slots
下。
1.11 Lambda表达式
C++11中的Lambda表达式用于定义并创建匿名的函数对象,以简化编程工作。首先看一下Lambda表达式的基本构成:
//[函数对象参数](操作符重载函数参数)mutable ->返回值{函数体}
[capture](parameters) mutable ->return-type
{
statement
}
-
1.函数对象参数
[],标识一个Lambda的开始,这部分必须存在,不能省略。函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义Lambda为止时Lambda所在作用范围内可见的局部变量(包括Lambda所在类的this)。函数对象参数有以下形式:
-
n 空。没有使用任何函数对象参数。
-
n =。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
-
n &。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)。
-
n this。函数体内可以使用Lambda所在类中的成员变量。
-
n a。将a按值进行传递。按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的。要修改传递进来的a的拷贝,可以添加mutable修饰符。
-
n &a。将a按引用进行传递。
-
n a, &b。将a按值进行传递,b按引用进行传递。
-
n =,&a, &b。除a和b按引用进行传递外,其他参数都按值进行传递。
-
n &, a, b。除a和b按值进行传递外,其他参数都按引用进行传递。
-
-
2.操作符重载函数参数
标识重载的()操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如:(a,b))和按引用(如:(&a,&b))两种方式进行传递。
-
3.可修改标示符
mutable声明,这部分可以省略。按值传递函数对象参数时,加上mutable修饰符后,可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)。
QPushButton * myBtn = new QPushButton (this);
QPushButton * myBtn2 = new QPushButton (this);
myBtn2->move(100,100);
int m = 10;
connect(myBtn,&QPushButton::clicked,this,[m] ()mutable { m = 100 + 10; qDebug() << m; });
connect(myBtn2,&QPushButton::clicked,this,[=] () { qDebug() << m; });
-
4.函数返回值
->返回值类型,标识函数返回值的类型,当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。
int ret = []()->int{return 1000;}(); //时刻记住lambda表达式是一个匿名函数,不加()只是函数声明,即函数地址,加了()才是函数实现
-
5.是函数体
{},标识函数的实现,这部分不能省略,但函数体可以为空。
-
lambda表达式的优势
作为一个匿名函数,常用于简写槽函数,且可以写“不对应”的信号和槽。
在上面的例子中,"饿了"无参信号只能连接“请客”无参槽函数,否则会报错,使用lambda表达式可以解决这一问题。
connect(lh,&Teacher::hungry,st,[=](){emit st->treat("宫保鸡丁");});
//connect(btn,&QPushButton::clicked,this,&QWidget::close);重写 connect(btn,&QPushButton::clicked,this,[=](){close();});
-
最常用的lambda表达式格式是
[=](){函数体}
,函数体内写槽函数。
第一章作业
- 案例:创建一个窗口,并设置一个按钮“打开”。按下按钮时,打开第二个窗口,并将按钮字符改为“关闭”;再次按下按钮时。关闭第二个窗口,并将按钮字符改为“打开”。
- 案例使用了lambda表达式作为槽函数,在槽函数中设置选择语句
if
可以很方便地切换字符并实现开闭窗口。
setWindowTitle("第一个窗口");
setFixedSize(400,200);
QPushButton *btn1 = new QPushButton("打开",this);
btn1->move(100,100);
btn1->setVisible(true);
secondWindow *w2=new secondWindow;
connect(btn1,&QPushButton::clicked,w2,[=](){
if(btn1->text()=="打开")
{
btn1->setText("关闭");
w2->show();
}
else
{
btn1->setText("打开");
w2->close();
}
});