Qt学习笔记
基本完结,之后做项目的过程会不断进行补充~~~
1. 基本介绍
1.1 创建项目
打开 Qt Creator 后,创建新项目:
选择 Application ->Qt Widgets Application
指定项目地址和名称
Build System: qmake
Details
类信息,在这里面有三个基类供我们选择:
- QWidget:最简单的空白窗口。
- QMainWindow:主窗口(包含了标题栏,菜单栏等)。
- QDialog:对话框。
下图展示了三个基类的继承关系
Kits:我这里选择 MinGW。
Summary:这里可以选择版本控制系统,可以不选择。
1.2 文件结构介绍
项目创建完毕后,会生成一个 .pro
的项目文件和一些 .h, .cpp
的文件。
main.cpp
1 |
|
widget.h
1 |
|
widget.cpp
1 |
|
1.3 命名规范
- 类名:首字母大写,单词和单词之间首字母大写。
- 函数名 变量名称:首字母小写,单词和单词直接首字母大写。
1.4 常用快捷键
- 注释 ctrl + /
- 运行 ctrl + r
- 编译 ctrl + b
- 字体缩放 ctrl + 鼠标滚轮
- 整行移动 ctrl + shift + ↑ / ↓
- 帮助文档 F1
- 自动对齐 ctrl + i
- 同名之间的 .h 和 .cpp 切换 F4
2. 小试牛刀
2.1 窗口操作
这一节介绍一些窗口操作的 API。
在对窗口的属性进行设置时,往往在构造函数中完成。
1 | //重置窗口大小 |
这些方法都是继承父类得来的,直接调用即可,如果想进一步增强可读性,可添加 this 指针,但在类方法调用的过程中,编译器已经为我们默认添加了 this 指针。
2.2 QPushButton
该类表示按钮,是十分常用的控件之一,仍然实在构造函数中创建
1 |
|
上述代码是错误的,show()
方法以顶层方式弹出窗口空间,因此会在另一个窗口中弹出按钮控件。现在要让 btn
依赖于 QWidget
窗口,需要把 btn
的父类对象设置为 QWidget
。
1 |
|
上面通过两种方法创建按钮控件,都是可行的。
设置相关属性
1 | //设置父亲 |
2.3 对象树
在每次创建一个对象时,都要指定一个父亲,而 QObject
是最顶端的对象,所有的子类对象都是由 QObject
派生而来。
- 构造函数:由上往下执行,先执行
QObject
的构造函数,再执行其子类的,不断执行下去。 - 析构函数:父类在执行析构函数时需要释放自身,这是先会检查其属下的子类对象是否被释放,因此由上往下检查子类对象,而由下往上不断释放类对象。
2.4 坐标系
Qt 中的坐标系以左上角为原点,向右为 x 正方向;向下为 y 正方向。
2.5 信号和槽
2.5.1 基本概念
1 | connect(btn, &QPushButton::clicked, this, &QWidget::close); |
该函数传入四个参数:信号的发送者(类实例对象),发送的信号(函数地址),信号的接收者(类实例对象),处理的槽函数(函数地址)。
上面的代码则实现了点击按钮控件关闭窗口的连接函数。
松散耦合
参数的前两者和后两者没有什么关联,是松散的,但通过 connect 函数耦合起来。
click & clicked
初学者可能对这两个函数有困惑。就我自己的理解,click 表示的是按钮的点击动作,而 clicked 是执行了按钮点击函数触发的函数。因此点击按钮,出发的是 clicked 函数,如果上面的 clicked 函数写成
1 | connect(btn, &QPushButton::click, this, &QWidget::close); |
则没有反应。
假设现在有两个按键,我们对其建立 connect 连接来更好地理解两个函数的区别。
1 | connect(btn1, &QPushButton::clicked, btn2, &QPushButton::click); |
上面代码的逻辑是,btn1 出发点击事件,会导致 btn2 点击动作的执行,而 btn2 点击动作的执行又导致 btn2 点击事件的发生,进而触发窗口关闭函数的执行。
最终的效果是,无论点击哪一个按键,窗口都会关闭。
2.5.2 自定义信号和槽
- 自定义信号:返回 void;需要声明,不需要实现;可以有参数,可以重载。
- 自定义槽函数:返回 void,需要声明,也需要实现;可以有参数,可以重载。
出发信号关键字:emit
1 | emit signal(); |
小案例实战
项目放在 tc_st 中
现在实现一个小案例,分别创建 Teacher
和 Student
对象,实现以下功能:
Teacher
发出hungry
信号,Student
响应treat
信号。- 实现信号和槽的重载。
信号和槽的重载后,这样写便会出现二义性,编译无法通过:
1 | connect(tc, &Teacher::hungry, st, &Student::treat); |
需要根据函数重载的特性定义两个指针变量,指向我们需要的函数:
1 | void (Teacher::*tcSignal)(QString) = &Teacher::hungry; |
槽函数执行时,其接收的参数通过信号传入,具体内容见拓展知识。
2.5.3 拓展知识
信号可以连接信号。
一个信号可以连接多个槽函数。
多个信号可以连接同一个槽函数。
信号与槽的参数必须一一对应。
信号的参数个数可以多余槽的参数个数。
对后面两条内容做点说明,槽函数接收的参数,对于信号函数来说在前面必须有相同的对应的参数,而信号函数后面参数多了也没事。对于一一对应的参数,传入信号函数时会被自动传入到槽中。
Qt4 版本的信号与槽
1 | connect(sz, SIGNAL(hungry()), tc, SLOT(treat())); |
2.5.4 Lambda表达式
这玩意长的很阴间,刚开始怎么都看不懂,但是现在看看其实很简单的一个小东西。
最简单的 Lambda 表达式:
1 | [](){}(); |
当然,上述表达式什么也做不了,现在看看它的语法形式:
1 | [capture](params) opt ->ret {body;}(); |
[capture]
对 lambda 表达式所在作用域变量的捕获。[]:不捕获任何变量,[=]:表示按值捕获变量,[&]:按引用捕获变量
[=, &x]:按值捕获变量,但对于 x 是按引用捕获。
(params)
表示 lambda 的参数。opt
表示 lambda 的选项,比如 mutable,表示变量可以修改,如果不声明,则变量无法在 lambda 表达式中被修改。->ret
表示 lambda 的返回值。{body;}
表示函数体。()
最后的小括号则是函数的调用。
使用 Lambda 表达式建立信号与槽:
1 | connect(btn, &QPushButton::clicked, this, [=](){ |
事实上,使用 Lambda 表达式建立信号与槽还有许多好处,之后会慢慢体现。
3. QMainWindow
QMainWindow 继承于 QWidget,为用户提供主窗口程序的类,是许多应用程序的基础,正如我正在写的 Typora 主体,就可以被称为一个 QMainWindow。其包含:
- 菜单栏(menu bar)
- 工具栏(tool bar)
- 铆接部件(dock widget)
- 状态栏(status bar)
- 中心部件(central widget)
现在分别介绍各个部件
3.1 菜单栏
现在我们想在 qMainWindow 中实现如图效果,代码如下:
1 |
|
基本的思路就是 菜单栏 -> 菜单 -> QAction,注意,菜单栏只能有一个。
3.2 工具栏
工具栏可以有很多个,下面一段代码介绍了工具栏的创建和一些基本的 API:
1 | QToolBar *toolBar = new QToolBar; |
在学习菜单栏的时候,容易发现菜单栏先是包含菜单(QMenu),然后是在菜单中放入 QAction。工具栏则是直接放入 QAction。
1 | toolBar->addAction(buildAction); |
可以看到,传入的参数正是菜单栏中已经有的 QAction,正可谓条条大道通罗马。
菜单栏还可以添加其他的控件,比如按钮控件:
1 | QPushButton *btn = new QPushButton; |
需要注意的是,下面的代码是存在问题的
1 | //创建控件时直接设置父亲为工具栏 |
这样会导致控件默认在左上角显示,会覆盖掉原有的内容。
3.3 状态栏
状态栏往往在窗口的最下方,显示一些状态信息。最多只有一个状态栏。
1 | //创建状态栏 |
效果如下图
3.4 铆接部件
铆接部件可以有很多个。
1 | QDockWidget *dockWidget = new QDockWidget("float"); |
3.5 中心部件
中心部件只能有一个,这里设置中心部件为 QTextEdit,中心部件也可以是其他东西。
1 | QTextEdit *textEdit = new QTextEdit(this); |
QMainWindow 的内容就结束了,观察五个部分,可以知道唯一的部分用 set ,而可以有多个的部分用 add。
4. 添加资源文件
对于 QAtion 或其他东西,左边有个图片小图标,这种就是资源文件。现在我们要学习一下如何添加图片(好看的皮囊才是最重要的!)
4.1 代码中添加
在这里省略了直接通过 ui 界面设置控件,很容易上手,但不容易记录下来,不再赘述。现在假设我们建立了一个 QAction,现在对其添加图标。
将图片拷贝到项目位置下
img
里面要存的就是我们的图片
文件 -> 新建文件或项目 -> Qt -> Qt Resource File
这个是专门管理资源文件的东西。我们命名为
res
进入界面
添加 -> 前缀,前缀添加完毕后,直接在里面添加文件即可。
最后是加载文件:
1 | ui->actionNew->setIcon(QIcon(":/img/aaa.jpg")); |
4.2 在 UI 界面中添加
对于某个控件,找到 icon,点击倒三角,选择资源,
5. 对话框
5.1 模态与非模态
- 模态对话框:不可以对其他窗口进行操作。
- 非模态对话框:可以对其他窗口进行操作。
1 | connect(ui->actionNew, &QAction::triggered, [=](){ |
代码中我们建立了一个信号与槽的连接,当新建按下时,lambda 表达式中创建了一个对话框,并以阻塞的方式显示在那里。
现在创建一个非模态的对话框:
1 | connect(ui->actionNew, &QAction::triggered, [=](){ |
但是这段代码是有问题的,lambda 函数执行完后,dlg 对象被销毁了,所以对话框只是一闪而过的形式。如果将 dlg 对象放到堆上,就可以解决:
1 | connect(ui->actionNew, &QAction::triggered, [=](){ |
然而这段代码还是有问题,我每次点击一下新建,就在堆创建了一个 dlg 对象,但是退出窗口后又不会删除这个对象,就会造成内存泄漏的问题。
添加如下代码即可解决:
1 | dlg->setAttribute(Qt::WA_DeleteOnClose); |
表示设置了窗口关闭后自动删除自身的属性。
5.2 消息对话框
1 | connect(ui->actionNew, &QAction::triggered, [=](){ |
前三个都是基本的参数:父亲,标题,提示信息。
对于提问对话框,还需要额外传入一个按钮控件的信息,效果如图所示。
如果用户点击了 Save
,程序怎么知道呢?
1 | if(QMessageBox::Save == QMessageBox::question(this, "ques", "提问", QMessageBox::Save | QMessageBox::cancel)){ |
5.3 颜色对话框
1 | QColor color = QColorDialog::getColor(QColor(255, 0, 0)); |
选择某个颜色后,返回 QColor
对象类型的数据,包含了四通道数等信息。
5.4 文件对话框
这个对话框可能比较常用,基本就是选择了某个文件或文件夹后,返回被选择文件的路径,是一个 QString
类型。
1 | QString str = QFileDialog::getOpenFileName(this, "打开文件", path); |
常用的几个参数:
参数1 - 父亲 参数2 - 标题 参数3 - 默认打开路径 参数4 - 过滤文件格式。
6. 界面布局
这一块主要设计 ui 操作,不太适合以文本的方式记录下来,这里放个实操视频。
四大布局方式:
水平布局:将控件水平排列。
垂直布局:将控件垂直排列。
栅格布局:以某个特定大小的矩阵进行布局。
嵌套布局:是以上几种布局的综合。
例如在
QMainWindow
中放入几个QWidget
并垂直布局,然后对于每个QWdiget
内部又可以放许多控件,使用不同的布局方式。
小案例实战
设计一个登录界面,用合理的布局,并加入弹簧控件。
项目文件已经放在 example/login
当中
7. 常用控件介绍
Qt 为我们提供的控件很多,不可能一下子学完,暂时介绍常用的,之后基本是用到了再来补充。
7.1 按钮类
7.1.1 Push Button
最常用的控件,不再多说。
7.1.2 Tool Button
该控件可以看成是 Push Button 的升级版,比较适合添加图标,还可以设置透明等。
7.1.3 Radio Button
该控件长这样:
还可以用 Containers
中的 Group Box
将多个 RadioButton
放到一个组,这样每个组只有一个 RadioButton
会被选中。下图是一个小案例。
7.1.4 Check Box
CheckBox
和 RadioButton
差不多,圆圈变成了方框,但是放入 Containers
默认可以复选。
7.2 Item Widgets
观察左边的控件栏,发现 Item Widgets 有两种,一个是 Model-Based,这种是基于数据库,我们一般用不上;另一种是 Item-Based,主要介绍这里面的控件。
7.2.1 List Widgets
这个是显示文本的控件,但是它的每一行是 QListWidgetItem
对象。
1 | QListWidgetitem *item = new QListWidgetItem("xxxxx"); |
它也可以一次性添加多行控件。
1 | QStringList list; |
重载了 <<
运算符还是很方便的。
7.2.2 Tree Widget
7.2.3 Tabel Widget
还有许多控件就不说了,之后再来补档 QwQ
8. 自定义控件
Qt 的一大特色就是可以随意地进行控件的自定义封装,自己写一个可爱小巧又多功能的控件谁不爱呢?
项目实战
实现滑动条和值进行互联,如图所示,滑动条从左滑到右,左边数值从 0 到 100;修改左边的数值,滑动条也会移动。把这个东西封装成一个控件。
新建文件
在新建文件或项目中选择 Qt -> Qt 设计师界面类,命名时输入自己定义的类名,这里我取名 MyWidget
,创建完毕后,在创建了 C++ 文件的同时,还有一个 .ui
的文件。
控件设计与引入
我们在生成的 .ui
文件中设计我们想要的控件即可。
接下来我们来到 mainwindow.ui
中,添加 Widget
控件,右键,选择 提升为 -> 在 提升的类的名称 中输入我们自定义的控件类名即可,可以选择 全局包含。
逻辑代码的编写
之后,我们在 mywidget.cpp
的构造函数中进行逻辑代码的编写:
1 | //设置两者的范围 |
9. 事件与定时器
接下来这一章介绍 Qt 中的事件和定时器的使用,这两个东西非常非常重要。
在 Qt 中,所有的事件都是继承自 QEvent
类,常见的事件有:鼠标事件、键盘事件、定时事件等。
对于事件的处理,就是重写一系列的 Event
函数,使得某个事件发生时,会跳转到我们重写的函数中。进行相关的处理。
9.1 鼠标事件
如果需要对事件做出一系列响应,就需要重写一个自定义控件。我们以 QLabel
为例,我们编写一个继承于它的自定义控件 MyLabel
,当鼠标移入 MyLabel
或者移出,都显示响应的信息。
新建 C++ 类,可以默认继承于
QWidget
(反正之后都要改)。在新建的类中重写两个事件:
1
2
3
4
5
6
7
8
9
10//.h文件中
void enterEvent(QEvent *event);
void leaveEvent(QEvent *event);
//.c文件中
void MyLabel::enterEvent(QEvent *event){
qDebug()<<"鼠标进入了";
}
void MyLabel::leaveEvent(QEvent *event){
qDebug()<<"鼠标离开了";
}让
MyLabel
重新继承于QLabel
1
2
3
4
5
6
7
8
9
10
11//.h文件
class MyLabel : public QLabel
{
Q_OBJECT
public:
explicit MyLabel(QWidget *parent = nullptr);
};
//.c文件构造函数
MyLabel::MyLabel(QWidget *parent) : QLabel(parent)
{}现在看这段函数,发现有不少东西。
MyLabel
继承于QLabel
。在构造函数中,用QWidget
指向子类对象,并用初始化列表QLabel(parent)
初始化QLabel
,这一块的理解参考 《C++ Primer》 P557
将 label 实例控件提升为我们自定义的类,并设置基类为
QLabel
现在我们再来介绍几个鼠标事件:
1 | void MyLabel::mousePressEvent(QMouseEvent *event) |
设置鼠标追踪状态
1 | setMouseTracking(true); |
说实话,事件这一块不是很懂,之后再补吧 QwQ
9.2 定时器
9.2.1 事件实现
使用定时器的基本流程:启动一个定时器 -> 重写定时器事件 -> 每次定时器进入事件,进行相关处理。
1 | //MainWindow构造函数中,启动定时器 |
如果想设置多个定时器,并对不同定时器的触发事件做出处理,应该怎么办?
1 | timerId1 = startTimer(1000); |
9.2.2 信号与槽实现
这是定时器使用的另一种方法。创建一个定时器对象,并建立信号与槽监听定时器溢出状态,如果溢出就触发一次槽函数。而槽函数用 lambda 表达式实现较为方便。
1 | QTimer *timer = new QTimer(this); |
9.3 事件分发器
APP 在下发事件时,都要先经过一个事件分发器:
1 | bool event(QEvent *); |
我们可以在这里对事件进行拦截,代表用户要处理这个事件。
回到 MyLabel
,重写这个事件分发器。
1 | bool MyLabel::event(QEvent *e) |