基本完结,之后做项目的过程会不断进行补充~~~

1. 基本介绍

1.1 创建项目

打开 Qt Creator 后,创建新项目:

  1. 选择 Application ->Qt Widgets Application

  2. 指定项目地址和名称

  3. Build System: qmake

  4. Details

    类信息,在这里面有三个基类供我们选择:

    • QWidget:最简单的空白窗口。
    • QMainWindow:主窗口(包含了标题栏,菜单栏等)。
    • QDialog:对话框。

    下图展示了三个基类的继承关系

    image-20230505203809254

  5. Kits:我这里选择 MinGW。

  6. Summary:这里可以选择版本控制系统,可以不选择。

1.2 文件结构介绍

项目创建完毕后,会生成一个 .pro 的项目文件和一些 .h, .cpp 的文件。

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "widget.h"
#include <QApplication> //包含一个应用程序类的头文件
//main程序入口 argc命令行变量的数量 argv命令行变量的数组
int main(int argc, char *argv[])
{
//应用程序对象,有且仅有一个
QApplication a(argc, argv);
//实例化对象widget
Widget w;
//显示窗口
w.show();
//应用程序进入消息循环
return a.exec();
}

widget.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

class Widget : public QWidget
{
Q_OBJECT //宏,允许类中使用信号和槽的机制

public:
//构造函数
Widget(QWidget *parent = nullptr);
//析构函数
~Widget();
};
#endif // WIDGET_H

widget.cpp

1
2
3
4
5
6
7
8
9
10
#include "widget.h"

Widget::Widget(QWidget *parent)
: QWidget(parent) //初始化列表
{
}

Widget::~Widget()
{
}

1.3 命名规范

  • 类名:首字母大写,单词和单词之间首字母大写。
  • 函数名 变量名称:首字母小写,单词和单词直接首字母大写。

1.4 常用快捷键

  • 注释 ctrl + /
  • 运行 ctrl + r
  • 编译 ctrl + b
  • 字体缩放 ctrl + 鼠标滚轮
  • 整行移动 ctrl + shift + ↑ / ↓
  • 帮助文档 F1
  • 自动对齐 ctrl + i
  • 同名之间的 .h 和 .cpp 切换 F4

2. 小试牛刀

2.1 窗口操作

这一节介绍一些窗口操作的 API。

在对窗口的属性进行设置时,往往在构造函数中完成。

1
2
3
4
5
6
//重置窗口大小
resize(600, 400);
//固定窗口大小
setFixedSize(600, 400);
//设置窗口标题
setWindowTitle("hello_Qt");

这些方法都是继承父类得来的,直接调用即可,如果想进一步增强可读性,可添加 this 指针,但在类方法调用的过程中,编译器已经为我们默认添加了 this 指针。

2.2 QPushButton

该类表示按钮,是十分常用的控件之一,仍然实在构造函数中创建

1
2
3
4
5
6
7
#include <QPushButton>
Widget::Widget(QWidget *parent) //构造函数
: QWidget(parent)
{
QPushButton *btn = new QPushButton;
btn->show();
}

上述代码是错误的,show() 方法以顶层方式弹出窗口空间,因此会在另一个窗口中弹出按钮控件。现在要让 btn 依赖于 QWidget 窗口,需要把 btn 的父类对象设置为 QWidget

1
2
3
4
5
6
7
8
9
10
#include <QPushButton>
Widget::Widget(QWidget *parent) //构造函数
: QWidget(parent)
{
QPushButton *btn1 = new QPushButton;
btn1->setParent(this);
btn1->setText("btn1");

QPushButton *btn2 = new QPushButton("btn2", this);
}

上面通过两种方法创建按钮控件,都是可行的。

设置相关属性

1
2
3
4
5
6
//设置父亲
btn->setParent(this);
//设置按钮名称
btn->setText("btn");
//移动按钮位置
btn->move(100, 100);

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
2
connect(btn1, &QPushButton::clicked, btn2, &QPushButton::click);
connect(btn2, &QPushButton::clicked, this, &QWidget::close);

上面代码的逻辑是,btn1 出发点击事件,会导致 btn2 点击动作的执行,而 btn2 点击动作的执行又导致 btn2 点击事件的发生,进而触发窗口关闭函数的执行。

最终的效果是,无论点击哪一个按键,窗口都会关闭。

2.5.2 自定义信号和槽
  • 自定义信号:返回 void;需要声明,不需要实现;可以有参数,可以重载。
  • 自定义槽函数:返回 void,需要声明,也需要实现;可以有参数,可以重载。

出发信号关键字:emit

1
emit signal();
小案例实战

项目放在 tc_st 中

现在实现一个小案例,分别创建 TeacherStudent 对象,实现以下功能:

  • Teacher 发出 hungry 信号,Student 响应 treat 信号。
  • 实现信号和槽的重载。

信号和槽的重载后,这样写便会出现二义性,编译无法通过:

1
connect(tc, &Teacher::hungry, st, &Student::treat);

需要根据函数重载的特性定义两个指针变量,指向我们需要的函数:

1
2
3
void (Teacher::*tcSignal)(QString) = &Teacher::hungry;
void (Student::*stSignal)(Qstring) = &Student::treat;
connect(tc, tcSignal, st, stSignal);

槽函数执行时,其接收的参数通过信号传入,具体内容见拓展知识。

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
2
3
connect(btn, &QPushButton::clicked, this, [=](){
this->close();
});

事实上,使用 Lambda 表达式建立信号与槽还有许多好处,之后会慢慢体现。

3. QMainWindow

QMainWindow 继承于 QWidget,为用户提供主窗口程序的类,是许多应用程序的基础,正如我正在写的 Typora 主体,就可以被称为一个 QMainWindow。其包含:

  • 菜单栏(menu bar)
  • 工具栏(tool bar)
  • 铆接部件(dock widget)
  • 状态栏(status bar)
  • 中心部件(central widget)

image-20230506113228675

现在分别介绍各个部件

3.1 菜单栏

image-20230506114234425

现在我们想在 qMainWindow 中实现如图效果,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<QMenuBar>

//创建菜单栏对象
QMenuBar *bar = menuBar();
//将菜单栏放入主窗口中
setMenuBar(bar);
//创建菜单
QMenu *fileMenu = bar->addMenu("file");
QMenu *editMenu = bar->addMenu("edit");

//创建菜单项
QAction *buildAction = fileMenu->addAction("build");
//添加分割线
fileMenu->addSeparator();
QAction *openAction = fileMenu->addAction("open");

基本的思路就是 菜单栏 -> 菜单 -> QAction,注意,菜单栏只能有一个。

3.2 工具栏

工具栏可以有很多个,下面一段代码介绍了工具栏的创建和一些基本的 API:

1
2
3
4
5
6
7
8
9
QToolBar *toolBar = new QToolBar;
//添加工具栏并设置默认左侧停靠
addToolBar(Qt::LeftToolBarArea, toolBar);
//只允许左右停靠
toolBar->setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea);
//不允许浮动
toolBar->setFloatable(false);
//不允许移动
toolBar->setMovable(false);

在学习菜单栏的时候,容易发现菜单栏先是包含菜单(QMenu),然后是在菜单中放入 QAction。工具栏则是直接放入 QAction。

1
2
toolBar->addAction(buildAction);
toolBar->addAction(openAction);

可以看到,传入的参数正是菜单栏中已经有的 QAction,正可谓条条大道通罗马。

菜单栏还可以添加其他的控件,比如按钮控件:

1
2
QPushButton *btn = new QPushButton;
toolBar->addWidget(btn);

需要注意的是,下面的代码是存在问题的

1
2
//创建控件时直接设置父亲为工具栏
QPushButton *btn = new QPushButton(toolBar);

这样会导致控件默认在左上角显示,会覆盖掉原有的内容。

3.3 状态栏

状态栏往往在窗口的最下方,显示一些状态信息。最多只有一个状态栏。

1
2
3
4
5
6
7
8
9
10
//创建状态栏
QStatusBar *stBar = new QStatusBar;
setStatusBar(stBar);

QLabel *label1 = new QLabel("提示信息",this);
QLabel *label2 = new QLabel("右侧提示信息",this);

//将两个label放入状态栏中
stBar->addWidget(label1);
stBar->addPermanentWidget(label2);

效果如下图

image-20230507194637051

3.4 铆接部件

铆接部件可以有很多个。

1
2
3
4
QDockWidget *dockWidget = new QDockWidget("float");
addDockWidget(Qt::TopDockWidgetArea, dockWidget);
//设置允许停靠范围
dockWidget->setAllowedAreas(Qt::TopDockWidgetArea | Qt::BottomDockWidgetArea);

3.5 中心部件

中心部件只能有一个,这里设置中心部件为 QTextEdit,中心部件也可以是其他东西。

1
2
QTextEdit *textEdit = new QTextEdit(this);
setCentralWidget(textEdit);

QMainWindow 的内容就结束了,观察五个部分,可以知道唯一的部分用 set ,而可以有多个的部分用 add。

4. 添加资源文件

image-20230507195834191

对于 QAtion 或其他东西,左边有个图片小图标,这种就是资源文件。现在我们要学习一下如何添加图片(好看的皮囊才是最重要的!)

4.1 代码中添加

在这里省略了直接通过 ui 界面设置控件,很容易上手,但不容易记录下来,不再赘述。现在假设我们建立了一个 QAction,现在对其添加图标。

  1. 将图片拷贝到项目位置下

    img 里面要存的就是我们的图片

image-20230507231129992

  1. 文件 -> 新建文件或项目 -> Qt -> Qt Resource File

    这个是专门管理资源文件的东西。我们命名为 res

  2. 进入界面

    image-20230507231526231

    添加 -> 前缀,前缀添加完毕后,直接在里面添加文件即可。

最后是加载文件:

1
ui->actionNew->setIcon(QIcon(":/img/aaa.jpg"));

4.2 在 UI 界面中添加

对于某个控件,找到 icon,点击倒三角,选择资源,

image-20230508212144784

5. 对话框

5.1 模态与非模态

  • 模态对话框:不可以对其他窗口进行操作。
  • 非模态对话框:可以对其他窗口进行操作。
1
2
3
4
connect(ui->actionNew, &QAction::triggered, [=](){
QDialog dlg(this);
dlg.exec();
});

代码中我们建立了一个信号与槽的连接,当新建按下时,lambda 表达式中创建了一个对话框,并以阻塞的方式显示在那里。

现在创建一个非模态的对话框:

1
2
3
4
connect(ui->actionNew, &QAction::triggered, [=](){
QDialog dlg(this);
dlg.show();
});

但是这段代码是有问题的,lambda 函数执行完后,dlg 对象被销毁了,所以对话框只是一闪而过的形式。如果将 dlg 对象放到堆上,就可以解决:

1
2
3
4
connect(ui->actionNew, &QAction::triggered, [=](){
QDialog *dlg = new QDialog(this);
dlg.show();
});

然而这段代码还是有问题,我每次点击一下新建,就在堆创建了一个 dlg 对象,但是退出窗口后又不会删除这个对象,就会造成内存泄漏的问题。

添加如下代码即可解决:

1
dlg->setAttribute(Qt::WA_DeleteOnClose);

表示设置了窗口关闭后自动删除自身的属性。

5.2 消息对话框

1
2
3
4
5
6
7
8
9
10
connect(ui->actionNew, &QAction::triggered, [=](){
//错误对话框
QMessageBox::critical(this, "critical", "错误");
//信息对话框
QMessageBox::information(this, "info", "信息");
//提问对话框
QMessageBox::question(this, "ques", "提问", QMessageBox::Save | QMessageBox::cancel);
//警告对话框
QmessageBox::warning(this, "warning", "警告");
});

前三个都是基本的参数:父亲,标题,提示信息。

对于提问对话框,还需要额外传入一个按钮控件的信息,效果如图所示。

image-20230508000734688

如果用户点击了 Save ,程序怎么知道呢?

1
2
3
if(QMessageBox::Save == QMessageBox::question(this, "ques", "提问", QMessageBox::Save | QMessageBox::cancel)){ 
//点击了Save后的一系列处理
}

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

该控件长这样:

image-20230508213245964

还可以用 Containers 中的 Group Box 将多个 RadioButton 放到一个组,这样每个组只有一个 RadioButton 会被选中。下图是一个小案例。

image-20230508213628671

7.1.4 Check Box

CheckBoxRadioButton 差不多,圆圈变成了方框,但是放入 Containers 默认可以复选。

image-20230508214204995

7.2 Item Widgets

观察左边的控件栏,发现 Item Widgets 有两种,一个是 Model-Based,这种是基于数据库,我们一般用不上;另一种是 Item-Based,主要介绍这里面的控件。

7.2.1 List Widgets

这个是显示文本的控件,但是它的每一行是 QListWidgetItem 对象。

1
2
3
4
5
QListWidgetitem *item = new QListWidgetItem("xxxxx");
//将控件添加到listWidget中
ui->listWidget->addItem(item);
//设置文本在该行居中
item->setTextAlignment(Qt::AlignHCenter);

它也可以一次性添加多行控件。

1
2
3
QStringList list;
list<<"aaaaa"<<"bbbbb"<<"ccccc";
ui->listWidget->addItems(list);

重载了 << 运算符还是很方便的。

7.2.2 Tree Widget

image-20230508221319464

7.2.3 Tabel Widget

image-20230508221642857

还有许多控件就不说了,之后再来补档 QwQ

其他常用控件介绍

image-20230508221837397

8. 自定义控件

Qt 的一大特色就是可以随意地进行控件的自定义封装,自己写一个可爱小巧又多功能的控件谁不爱呢?

项目实战

实现滑动条和值进行互联,如图所示,滑动条从左滑到右,左边数值从 0 到 100;修改左边的数值,滑动条也会移动。把这个东西封装成一个控件。

image-20230509102732080

新建文件

在新建文件或项目中选择 Qt -> Qt 设计师界面类,命名时输入自己定义的类名,这里我取名 MyWidget,创建完毕后,在创建了 C++ 文件的同时,还有一个 .ui 的文件。

控件设计与引入

我们在生成的 .ui 文件中设计我们想要的控件即可。

接下来我们来到 mainwindow.ui 中,添加 Widget 控件,右键,选择 提升为 -> 在 提升的类的名称 中输入我们自定义的控件类名即可,可以选择 全局包含。

逻辑代码的编写

之后,我们在 mywidget.cpp 的构造函数中进行逻辑代码的编写:

1
2
3
4
5
6
7
8
9
10
11
//设置两者的范围
ui->horizontalSlider->setRange(1,100);
ui->spinBox->setRange(1,100);

//QSpinBox改变,QSlider跟着移动
//由于 valueChanged 有重载版本,故设置指针选择我们想要的版本
void(QSpinBox::* spSignal)(int) = &QSpinBox::valueChanged;
connect(ui->spinBox, spSignal, ui->horizontalSlider, &QSlider::setValue);

//QSlider移动,QSpinBox跟着改变
connect(ui->horizontalSlider, &QSlider::valueChanged, ui->spinBox, &QSpinBox::setValue);

9. 事件与定时器

接下来这一章介绍 Qt 中的事件和定时器的使用,这两个东西非常非常重要。

在 Qt 中,所有的事件都是继承自 QEvent 类,常见的事件有:鼠标事件、键盘事件、定时事件等。

对于事件的处理,就是重写一系列的 Event 函数,使得某个事件发生时,会跳转到我们重写的函数中。进行相关的处理。

9.1 鼠标事件

如果需要对事件做出一系列响应,就需要重写一个自定义控件。我们以 QLabel 为例,我们编写一个继承于它的自定义控件 MyLabel,当鼠标移入 MyLabel 或者移出,都显示响应的信息。

  1. 新建 C++ 类,可以默认继承于 QWidget(反正之后都要改)。

  2. 在新建的类中重写两个事件:

    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()<<"鼠标离开了";
    }
  3. MyLabel 重新继承于 QLabel

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //.h文件
    #include <QLabel>
    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
  4. 将 label 实例控件提升为我们自定义的类,并设置基类为 QLabel

现在我们再来介绍几个鼠标事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void MyLabel::mousePressEvent(QMouseEvent *event)
{
QString str = QString("鼠标按下了x=%1, y=%2").arg(event->x()).arg(event->y());
qDebug()<<str;
}
void MyLabel::mouseReleaseEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
qDebug()<<"鼠标释放了";
}
void MyLabel::mouseMoveEvent(QMouseEvent *event)
{
qDebug()<<"鼠标移动了";
}

设置鼠标追踪状态

1
setMouseTracking(true);

说实话,事件这一块不是很懂,之后再补吧 QwQ

9.2 定时器

9.2.1 事件实现

使用定时器的基本流程:启动一个定时器 -> 重写定时器事件 -> 每次定时器进入事件,进行相关处理。

1
2
3
4
5
6
7
8
9
//MainWindow构造函数中,启动定时器
startTimer(1000);

//重写定时器事件
void MainWindow::timerEvent(QTimerEvent *event)
{
static int num = 1;
ui->label->setText(QString::number(num++));
}

如果想设置多个定时器,并对不同定时器的触发事件做出处理,应该怎么办?

1
2
3
4
5
6
7
8
9
10
11
12
timerId1 = startTimer(1000);
timerId2 = startTimer(2000);

void MainWindow::timerEvent(QTimerEvent *event)
{
if(event->timerId() == timerId1){
...
}
if(event->timerId() == timerId2){
...
}
}
9.2.2 信号与槽实现

这是定时器使用的另一种方法。创建一个定时器对象,并建立信号与槽监听定时器溢出状态,如果溢出就触发一次槽函数。而槽函数用 lambda 表达式实现较为方便。

1
2
3
4
5
QTimer *timer = new QTimer(this);
timer->start(500);
connect(timer, &QTimer::timerout, [=](){
...
});

9.3 事件分发器

APP 在下发事件时,都要先经过一个事件分发器:

1
bool event(QEvent *);

我们可以在这里对事件进行拦截,代表用户要处理这个事件。

回到 MyLabel ,重写这个事件分发器。

1
2
3
4
5
6
7
8
9
10
11
12
bool MyLabel::event(QEvent *e)
{
if(e->type() == QEvent::MouseButtonPress){
//在使用该类型前先进行静态类型转换
QMouseEvent *ev = static_cast<QMouseEvent *>(e);
...
//返回true,代表用户自己处理这个事件
return ture;
}
//其他事件默认处理
return QLabel::event(e);
}