【1】Qt Start
一、Qt 概述
1.1 什么是 Qt
Qt 是一个跨平台的 C++ 图形用户界面应用程序框架。它为应用程序开发者提供建立艺术级图形界面所需的所有功能。它是完全面向对象的,很容易扩展,并且允许真正的组件编程。
1.2 支持的平台
-
Windows – XP、Vista、Win7、Win8、Win2008、Win10
-
Uinux/X11 – Linux、Sun Solaris、HP-UX、Compaq Tru64 UNIX、IBM AIX、SGI IRIX、FreeBSD、BSD/OS、和其他很多 X11 平台
-
Macintosh – Mac OS X
-
Embedded – 有帧缓冲支持的嵌入式 Linux 平台,Windows CE
Windows 建议下载 MinGW 版本,安装时建议组件全部选中。
1.3 Qt 的优点
-
跨平台,几乎支持所有的平台
-
接口简单,容易上手,学习 QT 框架对学习其他框架有参考意义。
-
一定程度上简化了内存回收机制。
-
开发效率高,能够快速的构建应用程序。
-
有很好的社区氛围,市场份额在缓慢上升。
-
可以进行嵌入式开发。
1.4 成功案例
-
Linux 桌面环境 KDE
-
WPS Office 办公软件
-
Skype 网络电话
-
Google Earth 谷歌地图
-
VLC 多媒体播放器
-
VirtualBox 虚拟机软件
二、创建 Qt 项目
2.1 使用向导创建
打开 Qt Creator 界面选择 New Project 或者选择菜单栏 【文件】-【新建文件或项目】菜单项
弹出 New Project 对话框,选择 Qt Widgets Application。选择【Choose】按钮,弹出如下对话框
设置项目名称和路径,按照向导进行下一步
选择编译套件
向导会默认添加一个继承自 CMainWindow 的类,可以在此修改类的名字和基类。默认的基类有 QMainWindow、QWidget 以及 QDialog 三个,可以选择 QWidget(类似于空窗口),这里先创建一个不带 UI 的界面,继续下一步
系统会默认添加 main.cpp、mywidget.cpp、 mywidget.h 和一个 .pro 项目文件,点击完成,即可创建出一个 Qt 桌面程序。
创建后,结果如图
2.2 .pro 文件
在使用 Qt 向导生成的应用程序 .pro 文件格式如下:
QT += core gui # 包含的模块
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets # 大于 Qt4 版本 才包含 widget 模块
TARGET = QtFirst # 应用程序名 生成的.exe程序名称
TEMPLATE = app # 模板类型 应用程序模板 Application
SOURCES += main.cpp\ # 源文件
mywidget.cpp
HEADERS += mywidget.h # 头文件
.pro
就是工程文件(project),它是 qmake 自动生成的用于生产 makefile 的配置文件。.pro
文件的写法如下:
- 注释:从“#”开始,到这一行结束。
- 模板变量告诉 qmake 为这个应用程序生成哪种 makefile。下面是可供使用的选择:
TEMPLATE = app
app
- 建立一个应用程序的 makefile。这是默认值,所以如果模板没有被指定,这个将被使用。lib
- 建立一个库的 makefile。vcapp
- 建立一个应用程序的 Visual Studio 项目文件。vclib
- 建立一个库的 Visual Studio 项目文件。subdirs
-这是一个特殊的模板,它可以创建一个能够进入特定目录并且为一个项目文件生成 makefile 并且为它调用 make 的 makefile。
- 指定生成的应用程序名:
TARGET = QtFirst
- 工程中包含的头文件:
HEADERS += mywidget.h
或HEADERS += include/painter.h
(可以有路径) - 工程中包含的
.ui
设计文件:FORMS += forms/painter.ui
- 工程中包含的源文件:
SOURCES += main.cpp
- 工程中包含的资源文件:
RESOURCES += qrc/painter.qrc
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
- 如果 QT_MAJOR_VERSION 大于 4(也就是当前使用的 Qt5 及更高版本)需要增加 widgets 模块。如果项目仅需支持 Qt5,也可以直接添加 “QT += widgets” 一句。不过为了保持代码兼容,最好还是按照 QtCreator 生成的语句编写。
- 配置信息
- CONFIG 用来告诉 qmake 关于应用程序的配置信息。
CONFIG += c++11
//使用 c++11 的特性
- 在这里使用 “+=” ,是因为添加[配置选项]到任何一个已经存在中。这样做比使用 “=” 那样替换已经指定的所有选项更安全。
2.3 分析其他文件
【mywidget.h】
#ifndef MYWIDGET_H
#define MYWIDGET_H
#include <QWidget> //包含头文件 QWidget 窗口类
class myWidget : public QWidget
{
Q_OBJECT // Q_OBJECT宏,允许类中使用信号和槽的机制
public:
myWidget(QWidget *parent = 0); //构造函数
~myWidget(); //析构函数
};
#endif // MYWIDGET_H
【mywidget.cpp】
#include "mywidget.h"
MyWidget::MyWidget(QWidget *parent)
: QWidget(parent)
{
}
MyWidget::~MyWidget()
{
}
【main.cpp】
#include "mywidget.h"
#include <QApplication>// 包含一个应用程序类的头文件
//main程序入口 argc命令行变量的数量 argv命令行变量的数组
int main(int argc, char *argv[])
{
//a应用程序对象,在Qt中,应用程序对象 有且仅有一个
QApplication a(argc, argv);
//窗口对象 myWidget父类 -> QWidget
myWidget w;
//窗口对象 默认不会显示,必须要调用show方法显示窗口
w.show();
//让应用程序对象进入消息循环
//代码阻塞到这行,只有当点击窗口右上角的叉,才会关闭窗口,中止运行
return a.exec();
}
【注意】
- Qt 系统提供的标准类名声明头文件没有 .h 后缀
- Qt 一个类对应一个头文件,类名就是头文件名
- QApplication 应用程序类
- 管理图形用户界面应用程序的控制流和主要设置。
- 是 Qt 的整个后台管理的命脉。它包含主事件循环,在其中来自窗口系统和其它资源的所有事件处理和调度。它也处理应用程序的初始化和结束,并且提供对话管理。
- 对于任何一个使用 Qt 的图形用户界面应用程序,都正好存在一个 QApplication 对象,而不论这个应用程序在同一时间内是不是有 0、1、2 或更多个窗口。
- a.exec()
- 程序进入消息循环,等待对用户输入进行响应。这里 main() 把控制权转交给 Qt,Qt 完成事件处理工作,当应用程序退出的时候 exec() 的值就会返回。在 exec() 中,Qt 接受并处理用户和系统的事件并且把它们传递给适当的窗口部件。
2.4 命名规范 & 快捷键
【命名规范】
- 类名 首字母大写,单词和单词之间首字母大写
- 函数名 变量名称 首字母小写,单词和单词之间首字母大写
【快捷键】
- 注释
ctrl + /
- 运行
ctrl + r
- 编译
ctrl + b
- 字体缩放
ctrl + 鼠标滚轮
- 查找
ctrl + f
- 整行移动
ctrl + shift + ↑ 或者↓
- 帮助文档
F1
按两次 F1
,大屏显示帮助文档。按Esc
退出帮助文档 - 自动对齐
ctrl + i
; - 同名之间的
.h
和.cpp
切换F4
- 帮助文档 第一种方式
F1
第二种 左侧按钮 第三种C:\Qt\Qt5.6.0\5.6\mingw49_32\bin
三、第一个 Qt 小程序
3.1 在窗口中新建按钮
-
引入按钮头文件
#include <QPushButton>
-
创建按钮方式一:
QPushButton *btn = new QPushButton;
- 显示按钮:
btn->show();
。show 以顶层方式(即新建一个窗口)弹出窗口控件
- 为了在主窗口中显示按钮,将其父窗口设置为主窗口:
btn->setParent(this);
-
为按钮设置文本:
btn->setText("第一个按钮");
-
为按钮设置尺寸:
btn->resize(150,50);
- 默认是窗口大小以控件大小显示,可以设置窗口大小:
resize(600, 400);
- 显示按钮:
-
创建按钮方式二:
QPushButton *btn2 = new QPushButton("第二个按钮",this);
-
如果一个程序中同时创建上述两个按钮,则只会显示【第二个按钮】,因为第二个按钮将第一个按钮覆盖了
- 解决:移动按钮
btn2->move(100,100);
- 解决:移动按钮
-
将窗口设置为固定大小,用户不可修改窗口尺寸:
setFixedSize(600, 400);
-
修改窗口左上角的窗口名字:
setWindowTitle("第一个窗口");
#include "mywidget.h"
#include <QPushButton>
MyWidget::MyWidget(QWidget *parent)
: QWidget(parent)
{
//创建一个按钮
QPushButton *btn = new QPushButton;
//btn->show(); //新建一个窗口,显示按钮
btn->setParent(this);
btn->setText("第一个按钮");
btn->resize(150,50);
QPushButton *btn2 = new QPushButton("第二个按钮",this);
btn2->move(100,100);
resize(600, 400);
setFixedSize(600, 400);
setWindowTitle("第一个窗口");
}
MyWidget::~MyWidget()
{
}
3.2 对象模型/对象树
QObject 是以对象树的形式组织起来的。
-
当你创建一个 QObject 对象时,会看到 QObject 的构造函数接收一个 QObject 指针作为参数,这个参数就是 parent,也就是父对象指针。这相当于,在创建 QObject 对象时,可以提供一个其父对象,创建的这个 QObject 对象会自动添加到其父对象的 children() 列表。
-
当父对象析构的时候,这个列表children()中的所有对象也会被析构。(注意,这里的父对象并不是继承意义上的父类!)这种机制在 GUI 程序设计中相当有用。例如,一个按钮有一个 QShortcut(快捷键)对象作为其子对象。当删除按钮的时候,这个快捷键理应被删除。这是合理的。
QWidget 是能够在屏幕上显示的一切组件的父类。
-
QWidget 继承自 QObject,因此也继承了这种对象树关系。一个孩子自动地成为父组件的一个子组件。因此,它会显示在父组件的坐标系统中,被父组件的边界剪裁。例如,当用户关闭一个对话框的时候,应用程序将其删除,那么,希望属于这个对话框的按钮、图标等应该一起被删除。事实就是如此,因为这些都是对话框的子组件。
-
当然,也可以自己删除子对象,它们会自动从其父对象列表中删除。比如,当删除了一个工具栏时,其所在的主窗口会自动将该工具栏从其子对象列表中删除,并且自动调整屏幕显示。
Qt 引入对象树的概念,在一定程度上解决了内存问题。
-
当一个 QObject 对象在堆上创建的时候,Qt 会同时为其创建一个对象树。不过,对象树中对象的顺序是没有定义的。这意味着,销毁这些对象的顺序也是未定义的。
- 当创建的对象在堆区时候,如果指定的父亲是 QObject 或其派生下来的类,可以不用管理释放的操作,Qt 自动将对象会放入到对象树中。
-
任何对象树中的 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 对象,并且大胆在堆上创建。
3.3 Qt 窗口坐标系
坐标体系:以左上角为原点(0,0),X向右增加,Y向下增加。
对于嵌套窗口,其坐标是相对于父窗口来说的。
四、信号和槽机制
信号槽是 Qt 框架引以为豪的机制之一。所谓信号槽,实际就是观察者模式。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,将想要处理的信号和自己的一个函数(称为槽(slot))绑定来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。这就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。
4.1 系统自带的信号和槽
下面完成一个小功能,上面已经学习了按钮的创建,但是还没有体现出按钮的功能,按钮最大的功能也就是点击后触发一些事情,比如点击按钮,就把当前的窗口给关闭掉,那么在 Qt 中,这样的功能如何实现呢?
QPushButton * quitBtn = new QPushButton("关闭窗口",this);
connect(quitBtn, &QPushButton::clicked, this, &MyWidget::close); // 信号槽的使用方式
// MyWidget也可以使用父对象:&QWidget::close
// 是 clicked,不是click
connect() 函数最常用的一般形式:connect(sender, signal, receiver, slot);
-
sender:发出信号的对象
-
signal:发送对象发出的信号(函数地址)
-
receiver:接收信号的对象
-
slot:接收对象在接收到信号之后所需要调用的函数(槽函数)(函数的地址)
信号槽的优点:松散耦合。信号发送端与接收端本身没有关联,通过 connect 函数连接,将两端连接起来。
系统自带的信号和槽通常查找方法:通过帮助文档,如上面的按钮的点击信号,在帮助文档中输入 QPushButton,首先可以在 Contents 中寻找关键字 signals(信号),但是发现并没有找到,这时候应该想到也许这个信号的被父类继承下来的,因此去他的父类 QAbstractButton 中就可以找到该关键字,点击 signals 索引到系统自带的信号有如下几个:
这里的 clicked 就是要找到的信号,槽函数的寻找方式和信号一样,只不过他的关键字是slot。
4.2 自定义信号和槽
使用 connect() 可以连接系统提供的信号和槽。但是,Qt 的信号槽机制并不仅仅是使用系统提供的那部分,还会允许自己设计自己的信号和槽。
🔶 Qt 的信号槽代码:
首先定义一个学生类和老师类:
* 老师类中声明信号 饿了 hungry
signals:
void hungury();
* 学生类中声明槽 请客 treat
public slots:
void treat();
在窗口中声明一个公共方法下课,这个方法的调用会触发老师饿了这个信号,而响应槽函数学生请客
void MyWidget::ClassIsOver()
{
//发送信号
emit teacher->hungury();
}
学生响应了槽函数,并且打印信息
//自定义槽函数 实现
void Student::treat()
{
qDebug() << "该吃饭了!";
}
在窗口中连接信号槽
teacher = new Teacher(this);
student = new Student(this);
connect(teacher,&Teacher::hungury,student,&Student::treat);
在窗口中调用下课函数,测试打印出 “该吃饭了”
ClassIsOver();
🔶 自定义信号槽需要注意的事项:
-
发送者和接收者都需要是 QObject 的子类(当然,槽函数是全局函数、Lambda 表达式等无需接收者的时候除外);
-
信号和槽函数返回值是 void,可以有参数 ,可以重载。
-
信号只需要声明,不需要实现,写到关键字 signals 下。
-
槽函数需要声明也需要实现,早期 Qt 版本必须要写到 public slots,高级版本可以写到 public 或者 全局下。
-
槽函数是普通的成员函数,作为成员函数,会受到 public、private、protected 的影响;
-
使用 emit 在恰当的位置触发信号(发送信号);
-
使用 connect() 函数连接信号和槽。
-
任何成员函数、static 函数、全局函数和 Lambda 表达式都可以作为槽函数。
-
信号槽要求信号和槽的参数一致,所谓一致,是参数类型一致。
-
如果信号和槽的参数不一致,允许的情况是,槽函数的参数可以比信号的少,即便如此,槽函数存在的那些参数的顺序也必须和信号的前面几个一致起来。这是因为,你可以在槽函数中选择忽略信号传来的数据(也就是槽函数的参数比信号的少)。
🔶 程序中同时定义带参数与不带参数的 hungry 函数,需要利用函数指针,明确指向函数的地址:
void hungury();
void hungury(QString name); 自定义信号
void treat();
void treat(QString name ); 自定义槽
// 🍓 但是由于有两个重名的自定义信号和自定义的槽,直接连接会报错,所以需要利用【函数指针】来指向函数地址,然后再做连接
void (Teacher:: * teacherSingal)(QString) = &Teacher::hungury;
void (Student:: * studentSlot)(QString) = &Student::treat;
connect(teacher,teacherSingal,student,studentSlot);
// 无参信号和槽连接
void(Teacher:: *teacherSignal2)(void) = &Teacher::hungry;
void(Student:: *studentSlot2)(void) = &Student::treat;
connect(teacher,teacherSignal2,student,studentSlot2);
🔶 程序中直接打印 QString 类型的数据时,输出数据会带有引号:
qDebug() << "请老师吃饭,老师要吃:"<<footName;
// 输出(宫保鸡丁有引号)
// 请老师吃饭,老师要吃: "宫保鸡丁"
解决:将 QString
转成 char*
-
.ToUtf8()
转为QByteArray
-
.Data()
转为Char*
qDebug() << "请老师吃饭,老师要吃:"<<footName.toUtf8().data();
// 输出
// 请老师吃饭,老师要吃: 宫保鸡丁
🔶 信号连接信号:
信号除了连接槽函数,还可以连接信号!直接去触发[老师饿了]信号,而不通过调用函数 ClassIsOver()
connect(btn,&QPushButton::clicked, teacher, teacherSignal2);
🔶 断开信号:
参数与想要断开的 connect 函数完全一样!
disconnect(btn,&QPushButton::clicked, teacher, teacherSignal2)
4.3 信号槽的拓展
一个信号可以和多个槽相连
- 如果是这种情况,这些槽会一个接一个的被调用,但是它们的调用顺序是不确定的。
多个信号可以连接到一个槽
- 只要任意一个信号发出,这个槽就会被调用。
一个信号可以连接到另外的一个信号
- 当第一个信号发出时,第二个信号被发出。除此之外,这种信号-信号的形式和信号-槽的形式没有什么区别。
**信号和槽的参数 **
- 必须类型一一对应
- 信号的参数个数可以多余槽函数的参数个数,但是信号前面几个参数与多余槽函数必须一一相对应(信号多出的参数要放到后面)
槽可以被取消链接
- 这种情况并不经常出现,因为当一个对象 delete 之后,Qt 自动取消所有连接到这个对象上面的槽。
信号槽可以断开
- 利用 disconnect 关键字是可以断开信号槽的
使用 Lambda 表达式
- 在使用 Qt 5 的时候,能够支持 Qt 5 的编译器都是支持 Lambda 表达式的。在连接信号和槽的时候,槽函数可以使用Lambda表达式的方式进行处理。
4.4 Qt4 版本的信号槽写法
connect(teacher,SIGNAL(hungry(QString)),student,SLOT(treat(QString)));
这里使用了 SIGNAL 和 SLOT 这两个宏,将两个函数名转换成了字符串。注意到 connect() 函数的 signal 和 slot 都是接受字符串,一旦出现连接不成功的情况,Qt4 是没有编译错误的(因为一切都是字符串,编译期是不检查字符串是否匹配),而是在运行时给出错误。这无疑会增加程序的不稳定性。Qt5 在语法上完全兼容 Qt4,而反之是不可以的。
- 优点 参数直观
- 缺点 编译器不会检测参数类型
4.5 Lambda 表达式
C++11 中的 Lambda 表达式用于定义并创建匿名的函数对象,以简化编程工作。
Lambda 表达式的基本构成:
[capture](parameters) mutable ->return-type
{
statement
}
即:[函数对象参数](操作符重载函数参数)mutable ->返回值{函数体}
① 函数对象参数;
[]
,标识一个 Lambda 的开始,这部分必须存在,不能省略。函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义 Lambda 为止时 Lambda 所在作用范围内可见的局部变量(包括 Lambda 所在类的 this)。函数对象参数有以下形式:
空
。没有使用任何函数对象参数。=
。函数体内可以使用 Lambda 所在作用范围内所有可见的局部变量(包括 Lambda 所在类的 this),并且是值传递方式(相当于编译器自动按值传递了所有局部变量)。&
。函数体内可以使用 Lambda 所在作用范围内所有可见的局部变量(包括 Lambda 所在类的 this),并且是引用传递方式(相当于编译器自动按引用传递了所有局部变量)。this
。函数体内可以使用 Lambda 所在类中的成员变量。a
。将 a 按值进行传递。按值进行传递时,函数体内不能修改传递进来的 a 的拷贝,因为默认情况下函数是 const 的。要修改传递进来的 a 的拷贝,可以添加 mutable 修饰符。&a
。将 a 按引用进行传递。a, &b
。将 a 按值进行传递,b 按引用进行传递。=,&a, &b
。除 a 和 b 按引用进行传递外,其他参数都按值进行传递。&, a, b
。除 a 和 b 按值进行传递外,其他参数都按引用进行传递。
② 操作符重载函数参数;
标识重载的()操作符的参数
,没有参数时,这部分可以省略。参数可以通过按值(如:(a,b))和按引用(如:(&a,&b))两种方式进行传递。
③ 可修改标示符;
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; }); // 第二步:按该按钮,输出 110
connect(myBtn2,&QPushButton::clicked,this,[=] () { qDebug() << m; }); // 第三步:按该按钮,输出 10 (由此可见,外部变量的 m 没有被改变)
qDebug() << m; //第一个输出:运行时先执行此语句,输出 10
④ 函数返回值;
->返回值类型
,标识函数返回值的类型,当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。
⑤ 是函数体;
{}
,标识函数的实现,这部分不能省略,但函数体可以为空。
Lambda表达式
[]标识符 匿名函数
- = 值传递
- & 引用传递