一、C++梳理
1. 面向对象的三大特性
1.1 封装
把类的一些属性和细节隐藏(private、protected),根据读写需要重新开放外部调用接口(public、protected)。
1.2 继承
在已有的类的基础上创建一个新的类,新的类拥有之前类的特性,通常新的类还会做出一些必要的修改或增加。
1.3 多态
一种接口,多种状态。
通常用于参数传递,只需要写一个参数接口,就可以支持不同的继承类型,触发需要三个条件:
1. 公有继承
2. 函数覆盖
3. 基类引用/指针指向派生类对象
1.4 综合案例
#include
using namespace std;
class Car
{
private:
string brand;
string model;
string displacement; // 排量
public:
Car(string brand,
string model,
string displacement):
brand(brand),model(model),displacement(displacement)
{
}
// 读函数:getter
// 写函数:setter
string get_brand() const
{
return brand;
}
void set_brand(string brand)
{
this->brand = brand;
}
string get_model() const
{
return model;
}
void set_model(string model)
{
this->model = model;
}
string get_displacement() const
{
return displacement;
}
void set_displacement(string displacement)
{
this->displacement = displacement;
}
// 纯虚函数
// Car类是抽象类:无法创建Car类对象,可以支持多态
virtual void advertise() = 0;
// 虚析构函数
virtual ~Car(){}
};
class BMW:public Car
{
public:
BMW(string model,string displacement):Car("BMW",model,displacement){}
// 做出一些与基类的差异,函数覆盖
void advertise()
{
cout << "不给中国人吃冰激凌!" << endl;
}
};
class Benz:public Car
{
public:
Benz(string model,string displacement):Car("Benz",model,displacement){}
// 做出一些与基类的差异,函数覆盖
void advertise()
{
cout << "奔驰女车主坐在引擎盖上哭!" << endl;
}
};
class BYD:public Car
{
public:
BYD(string model,string displacement):Car("比亚迪",model,displacement){}
// 做出一些与基类的差异,函数覆盖
void advertise()
{
cout << "Build Your Dreams" << endl;
}
};
// 使用基类指针作为函数参数测试多态
void test_advertise(Car* c)
{
c->advertise();
delete c; // 销毁一个多态情况的对象
}
int main()
{
BMW w("330i","2.0T");
cout << w.get_brand() << endl; // BMW
cout << w.get_displacement() << endl; // 2.0T
cout << w.get_model() << endl; // 330i
Benz b1("S600","3.0T");
BYD b2("汉","无排量");
// 基类引用派生类对象
Car& c1 = w;
Car& c2 = b1;
Car& c3 = b2;
c1.advertise(); // 不给中国人吃冰激凌!
c2.advertise(); // 奔驰女车主坐在引擎盖上哭!
c3.advertise(); // Build Your Dreams
BMW* w2 = new BMW("Z4","2.0T");
Benz* b12 = new Benz("C300L","1.5T");
BYD* b22 = new BYD("秦","无排量");
test_advertise(w2); // 不给中国人吃冰激凌!
test_advertise(b12); // 奔驰女车主坐在引擎盖上哭!
test_advertise(b22); // Build Your Dreams
return 0;
}
二、Qt简介
1. 什么是Qt?(了解)
Qt经常被认为是一个图形用户界面(GUI)框架,但是这不是Qt的全部。Qt除了可以绘制界面外,还包含了很多非图形化的内容,比如线程、数据库、图形图形处理、音视频处理、网络通信、文件IO等......
Qt的使用场景:
Qt的最大优势在于其跨平台特性:一次编程,到处编译。
还有一些其他特点:
- 面向对象开发
- 丰富的API和开发文档
- 开源
- 快速界面开发
2. 开发环境
Qt主要使用的开发环境是Qt Creator,这也是官方推荐的开发环境。
本次授课使用的Qt Creator版本为5.2.1,这也是之前C++授课使用的版本。
新建一个Qt项目的操作步骤如下所示:
1. 打开Qt Creator后,确认文件编码为UTF-8,如果不是(学完C++的都不是)UTF-8,需要改为UTF-8,如下图所示。
2. 重新设置编码后,重启Qt Creator后,点击
,创建新项目。
3. 在弹出的窗口中,按照下图所示进行操作。
4. 在弹出的窗口中,设置项目名称和工作目录的路径(都不得包含任何中文字符)后,点击“下一步”。
5. 在弹出的窗口中,直接点击“下一步”。
6. 在弹出的窗口中,按照下图所示进行操作。
7. 在项目管理界面,直接点击完成。可以看到项目中的代码文件比C++项目要多。
3. 工作目录和构建目录(熟悉)
工作目录就是新创建项目时选定的目录,内部存放项目的源代码文件与项目配置文件等。
构建目录指的是项目在编译后创建的目录,内部存放项目编译生成的文件,例如exe可执行文件。
构建目录会在编译后创建,从红色显示为黑色。
少数同学在排查了各种问题之后,仍然无法正常运行项目,此时可以尝试取消影子构建模式
,取消后构建目录不单独生成,直接使用工作目录进行构建,优点是提升了项目编译运行的稳定性,缺点是所有文件堆积,不方便分类整理。
4. 项目结构
4.1 .pro项目配置文件(熟悉)
#-------------------------------------------------
#
# Project created by QtCreator 2023-06-05T11:31:22
#
#-------------------------------------------------
# 给当前的Qt项目添加core模块和gui模块,后期会手动增加其他模块
QT += core gui
# 当Qt的主版本大于4时,添加widgets模块
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = HelloWorldQt # 生成的可执行文件的名称
TEMPLATE = app # 项目模板为应用程序
# 项目中包含的源文件
SOURCES += main.cpp\
dialog.cpp
# 项目中包含的头文件
HEADERS += dialog.h
4.2 main.cpp 主文件(掌握)
程序的入口(主函数)所在的文件。
#include "dialog.h"
#include <QApplicationn>
int main(int argc, char *argv[])
{
// Qt应用程序的“大管家”
QApplication a(argc, argv);
// 项目默认创建的类型,表示自定义对话框窗口栈内存对象
Dialog w;
// 显示窗口
w.show();
return a.exec(); // 进入主事件循环
}
4.3 dialog.h(掌握)
项目自动创建的类的头文件,内部包含了类中所有部分的声明。
#ifndef DIALOG_H
#define DIALOG_H
// QDialog是Qt中所有对话框窗口的基类
#include <QDialog>
// Dialog类是自定义的对话框窗口类
class Dialog : public QDialog
{
Q_OBJECT // 别删!
public:
Dialog(QWidget *parent = 0); // 构造函数
~Dialog(); // 析构函数
};
#endif // DIALOG_H
4.4 dialog.cpp(掌握)
项目自动创建的类的源文件,内部包含了头文件中所有声明部分的定义。
#include "dialog.h"
// 构造函数
Dialog::Dialog(QWidget *parent)
: QDialog(parent) //透传构造
{
}
// 析构函数
Dialog::~Dialog()
{
}
4.5 .user文件(熟悉)
进入项目的工作目录,可以看到一个.user格式的用户文件。
此文件在Qt Creator中不显示,此文件是Qt本地环境根据项目生成的配置文件,建议在提交代码之前删除,删除后直接在Windows中双击.pro文件即可导入项目。
5. 帮助文档(掌握)
Qt的内容十分多,必须借助帮助文档,必要时随用随查。
几种查询文档的方法:
(1) 直接打开Assistant程序进行查询。
(2)在Qt Creator中,点击,可以打开一个内置的Assistant程序。
(3)在代码编辑过程中,光标定位到要查询的内容上,双击键盘F1,直接跳转到内容对应的文档位置。不同品牌的电脑点击F1的方法不尽相同,有些笔记本可能需要同时点击Fn。
6. 调试函数(掌握)
Qt是一个GUI框架,界面是给用户看的。
因此输出的内容应该分给用户看的和给开发者看的,可以使用QDebug类的调试函数,将调试信息输出在Qt Creator的后台,用户在界面上不可见。
dialog.h
#ifndef DIALOG_H
#define DIALOG_H
#include <QDialog>
// 调试信息类头文件
#include <QDebug>
class Dialog : public QDialog
{
Q_OBJECT
public:
Dialog(QWidget *parent = 0);
~Dialog();
};
#endif // DIALOG_H
dialog.cpp
#include "dialog.h"
Dialog::Dialog(QWidget *parent)
: QDialog(parent)
{
// Qt函数命名使用驼峰命名法(小驼峰命名法)
// 从第二个单词开始,每个单词的首字母大写
qDebug() << "构造" << "函数"; // 自动换行,连续输出自动加空格
}
Dialog::~Dialog()
{
qDebug() << "析构函数";
}
三、UI入门
1. QWidget类(重点)
QWidget类是Qt中几乎所有的组件和窗口等可视化类型的基类。
QWidget类内部规定了组件和窗口的基础规则,通过继承给各个直接或间接派生类。其中最基础的就是位置和大小相关的属性和函数。
QWidget部分属性如下:
- x : const int
横坐标,原点在左上角,正方向向右。
- y : const int
纵坐标,原点在左上角,正方向向下。
虽然x和y属性不能直接修改,但是可以通过下面的函数修改。需要注意的是,无论是窗口还是组件,都是靠左上角定位的。
void move(int x, int y)
- width : const int
宽度
- height : const int
高度
虽然width和height属性不能直接修改,但是可以通过下面的函数修改。
void resize(int w, int h)
可以使用下面的函数,同时修改坐标和大小。
void setGeometry(int x, int y, int w, int h)
- styleSheet : QString
样式表,可以使用setter函数设置组件和窗口的样式,详见第3节。
通常Qt中属性的getter读函数名称与属性名称同名,写函数setter通常以set后跟属性名称。
dialog.cpp
#include "dialog.h"
Dialog::Dialog(QWidget *parent)
: QDialog(parent)
{
// 移动的是w对象,即窗口对象
this->move(300,300); // 移动到100,100的位置
// 改变大小到300,300
resize(300,1000);
// 同时更改位置和宽高
setGeometry(400,100,300,300);
}
Dialog::~Dialog()
{
}
QWidget的属性和函数可以继承,因此后续讲解其派生类时不再赘述。
2. 子组件(掌握)
之前的窗口里面什么都没有,窗口中可以包含组件(按钮、图片、输入框等)对象。
本节先试用QPushButton类作为子组件进行讲解,QPushButton是最常用的点击按钮。
QPushButton的构造函数如下所示。
// 参数1:显示的文字
// 参数2:父窗口,子组件在哪个窗口里
QPushButton::QPushButton(const QString & text,
QWidget * parent = 0)
dialog.h
#ifndef DIALOG_H
#define DIALOG_H
#include <QDialog>
// 按压式按钮的头文件
#include <QPushButton>
class Dialog : public QDialog
{
Q_OBJECT
public:
Dialog(QWidget *parent = 0);
~Dialog();
private:
QPushButton* btn; // 成员变量
};
#endif // DIALOG_H
dialog.cpp
#include "dialog.h"
Dialog::Dialog(QWidget *parent)
: QDialog(parent)
{
resize(500,500);
// 创建一个堆内存按钮对象
// 参数1:显示文字
// 参数2:父窗口对象,this→w,w是Dialog类型
// Dialog的爷爷是QWidget,此处要一个QWidget*
// 多态!!!
btn = new QPushButton("哈哈",this);
// 移动位置,设置大小
btn->setGeometry(100,100,100,100);
}
Dialog::~Dialog()
{
// 释放内存(C++方式)
delete btn;
}
任何项目运行后出现
表示已经有一个项目正在运行,关闭正在运行的项目后,重新运行即可。
3. 样式表(熟悉)
默认的组件比较朴素,可以通过样式表设置来达到各种效果,样式表主要通过QSS(CSS)语法实现。
计算机中最常使用的记色法是RGB记色法,分别是用8位的数值表示红绿蓝三种光线的亮暗程度,例如(255,0,0)表示纯红色,(255,255,255)表示白色,(0,0,0)表示黑色。也可以把上述十进制转换为十六进制,例如#FF0000表示纯红色,#FFFFFF表示白色,#000000表示黑色......
配色方案可以参考下面的网站:
在线颜色选择器 | RGB颜色查询对照表
Color Palette Generator - Create Beautiful Color Schemes
dialog.h
#ifndef DIALOG_H
#define DIALOG_H
#include <QDialog>
#include <QPushButton>
#define QPushButton_STYTLE (QString("\
/*按钮普通态*/\
QPushButton\
{\
font-family:Microsoft Yahei;\
/*字体大小为20点*/\
font-size:20pt;\
/*字体颜色为白色*/\
color:#e3f7ff;\
/*背景颜色*/\
background-color:#3c5d6b;\
/*边框圆角半径为8像素*/\
border-radius:8px;\
}\
/*按钮悬停态*/\
QPushButton:hover\
{\
/*背景颜色*/\
background-color:#567899;\
}\
/*按钮按下态*/\
QPushButton:pressed\
{\
/*背景颜色*/\
background-color:#c7eeff;\
/*左内边距为3像素,让按下时字向右移动3像素*/\
padding-left:3px;\
/*上内边距为3像素,让按下时字向下移动3像素*/\
padding-top:3px;\
}"))
class Dialog : public QDialog
{
Q_OBJECT
public:
Dialog(QWidget *parent = 0);
~Dialog();
private:
QPushButton* btn;
};
#endif // DIALOG_H
dialog.cpp
#include "dialog.h"
Dialog::Dialog(QWidget *parent)
: QDialog(parent)
{
resize(400,400);
btn = new QPushButton("随便",this);
btn->setGeometry(100,100,200,200);
// 设置样式表
btn->setStyleSheet(QPushButton_STYTLE);
}
Dialog::~Dialog()
{
delete btn;
}
四、信号槽(重点)
信号槽是Qt新增的特性,主要的功能是实现Qt对象的通信机制,最常用的方式是人机交互,例如点击按钮以后触发某个动作等。
类似于其他技术中的回调的概念。
4.1 信号槽基本使用
信号槽的使用需要具备以下两个前提条件:
- 通信的对象必须直接或间接继承自QObject类,QObject类是Qt自带类型的基类。
- 类的头文件中要有 Q_OBJECT宏。
信号槽的建立主要通过下面的函数:
// 参数1:发射者,发射信号函数的对象,通常作为因的发起对象(n.)
// 参数2:信号函数,由参数1发射,通常是因的动作(v.),需要使用SIGNAL()包裹
// 参数3:接收者,执行槽函数的对象,通常作为果的执行对象(n.)
// 参数4:槽函数,由参数3执行,通常是果的动作(v.),需要使用SLOT()包裹
QObject::connect(const QObject * sender,
const char * signal,
const QObject * receiver,
const char * method) [static]
本次学习循序渐进,使用三种情况讲解信号槽连接的类型:
- 自带信号 → 自带槽
- 自带信号 → 自定义槽
- 自定义信号
4.2 自带信号 → 自带槽
这是最简单的连接方式,因为信号函数和槽函数都在Qt源代码中,只需要找到对应关系后连接即可。
【例子】点击按钮,关闭窗口。
分析:
参数1:按钮对象
参数2:点击(信号)
参数3:窗口对象
参数4:关闭(槽函数)
dialog.h
#ifndef DIALOG_H
#define DIALOG_H
#include <QDialog>
#include <QPushButton>
class Dialog : public QDialog
{
Q_OBJECT
public:
Dialog(QWidget *parent = 0);
~Dialog();
private:
QPushButton* btn;
};
#endif // DIALOG_H
dialog.cpp
#include "dialog.h"
Dialog::Dialog(QWidget *parent)
: QDialog(parent)
{
resize(400,400);
btn = new QPushButton("随便",this);
btn->setGeometry(100,100,200,200);
// 连接信号槽
// 参数1:按钮对象
// 参数2:点击(信号)
// 参数3:窗口对象
// 参数4:关闭(槽函数)
connect(btn,SIGNAL(clicked()),this,SLOT(close()));
}
Dialog::~Dialog()
{
delete btn;
}
练习:设计如图所示用户展示界面。
按照下图所示设计,按钮整体居中,位置自己计算。
在线颜色选择器 | RGB颜色查询对照表
Color Palette Generator - Create Beautiful Color Schemes
界面效果使用样式表自行设计,尽量美观。
helloQt.pro代码
#-------------------------------------------------
#
# Project created by QtCreator 2023-06-05T14:01:36
#
#-------------------------------------------------
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = helloQt
TEMPLATE = app
SOURCES += main.cpp\
dialog.cpp
HEADERS += dialog.h
dialog.h代码
#ifndef DIALOG_H
#define DIALOG_H
#include <QDialog>
#include <QDebug>
#include <QPushButton>
class Dialog : public QDialog
{
Q_OBJECT
private:
QPushButton* mid;
QPushButton* nor;
QPushButton* sor;
QPushButton* wes;
QPushButton* eas;
public:
Dialog(QWidget *parent = 0);
~Dialog();
};
#endif // DIALOG_H
dialog.cpp代码:
#include "dialog.h"
Dialog::Dialog(QWidget *parent)
: QDialog(parent)
{
resize(500,500);
mid=new QPushButton("中",this);
nor=new QPushButton("北",this);
sor=new QPushButton("南",this);
wes=new QPushButton("西",this);
eas=new QPushButton("东",this);
mid->setGeometry(175,175,150,150);
nor->setGeometry(175,75,150,100);
sor->setGeometry(175,325,150,100);
wes->setGeometry(25,200,150,100);
eas->setGeometry(325,200,150,100);
}
Dialog::~Dialog()
{
delete mid;
delete nor;
delete sor;
delete wes;
delete eas;
}
main.cpp代码:
#include "dialog.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Dialog w;
w.show();
return a.exec();
}
4.3 自带信号 → 自定义槽
这是使用的最多的一种连接方式,槽函数是具体要执行的动作,这个动作Qt源代码不能包罗万象,所以需要开发者自定义一个槽函数。
槽函数是一种特殊的成员函数。
【例子】点击按钮,把窗口向右下方移动,并且输出窗口当前的坐标值。
分析:
参数1:按钮对象
参数2:点击(信号)
参数3:this窗口对象
参数4:自定义槽函数mySlot
dialog.h
#ifndef DIALOG_H
#define DIALOG_H
#include
// 头文件
#include
#include
class Dialog : public QDialog
{
Q_OBJECT
public:
Dialog(QWidget *parent = 0);
~Dialog();
private:
QPushButton* btn;
// 1. 先声明自定义槽函数
private slots:
void mySlot();
};
#endif // DIALOG_H
dialog.cpp
#include "dialog.h"
Dialog::Dialog(QWidget *parent)
: QDialog(parent)
{
resize(300,300);
btn = new QPushButton("自定义槽函数",this);
btn->move(100,100);
// 参数1:按钮对象
// 参数2:点击(信号)
// 参数3:this窗口对象
// 参数4:自定义槽函数mySlot
connect(btn,SIGNAL(clicked()),this,SLOT(mySlot()));
}
// 2. 定义槽函数
void Dialog::mySlot()
{
//3. 编写槽函数执行的内容
// 获得当前坐标
int xPos = x();
int yPos = y();
// 向右下方移动
move(xPos+10,yPos+10);
// 输出新坐标
qDebug() << xPos+10 << yPos+10;
}
Dialog::~Dialog()
{
delete btn;
}
4.4 自定义信号
自定义信号通常用于一些复杂条件下的信号槽通信,当前还不能遇到这么复杂的情况,因此把简单问题复杂化强行使用,仅用于教学。
信号函数没有权限,没有定义,不能调用,只有声明,只能发射。
【例子】点击按钮,关闭窗口。
dialog.h
#ifndef DIALOG_H
#define DIALOG_H
#include
#include
class Dialog : public QDialog
{
Q_OBJECT
public:
Dialog(QWidget *parent = 0);
~Dialog();
private:
QPushButton* btn;
private slots:
void mySlot(); // 自定义槽函数
// 声明自定义信号函数
signals:
void mySignal();
};
#endif // DIALOG_H
dialog.cpp
#include "dialog.h"
Dialog::Dialog(QWidget *parent)
: QDialog(parent)
{
resize(400,400);
btn = new QPushButton("杀鸡用牛刀",this);
btn->move(200,200);
// 第一个连接
connect(btn,SIGNAL(clicked()),this,SLOT(mySlot()));
// 第二个连接
connect(this,SIGNAL(mySignal()),this,SLOT(close()));
}
void Dialog::mySlot()
{
// 发射自定义信号
emit mySignal();
}
Dialog::~Dialog()
{
delete btn;
}
4.5 传递参数
【例子】点击按钮,按钮上显示点击的次数。
分析:
自定义槽函数执行setText函数,设置按钮显示的内容,使用成员变量存储点击次数。
dialog.h
#ifndef DIALOG_H
#define DIALOG_H
#include
#include
class Dialog : public QDialog
{
Q_OBJECT
public:
Dialog(QWidget *parent = 0);
~Dialog();
private:
QPushButton* btn;
int count; // 计数
// 自定义槽函数
private slots:
void btnClickedSlot();
};
#endif // DIALOG_H
dialog.cpp
#include "dialog.h"
Dialog::Dialog(QWidget *parent)
: QDialog(parent),count(0) //使用构造初始化列表
{
resize(300,400);
btn = new QPushButton("0",this);
btn->move(100,200);
connect(btn,SIGNAL(clicked()),this,SLOT(btnClickedSlot()));
}
void Dialog::btnClickedSlot()
{
// 计数+1
count++;
// int → QString
// 函数名称:number静态的
// 参数:要转换的数字
// 返回值:转换后的字符串
QString text = QString::number(count);
// 设置显示
btn->setText(text);
}
Dialog::~Dialog()
{
delete btn;
}
上面的例子可以强加信号槽进行参数传递,前后对比如下所示。
dialog.h
#ifndef DIALOG_H
#define DIALOG_H
#include
#include
class Dialog : public QDialog
{
Q_OBJECT
public:
Dialog(QWidget *parent = 0);
~Dialog();
private:
QPushButton *btn;
int count = 0; // 计数
private slots:
void btnClickedSlot(); // 点击按钮触发的槽函数
void mySlot(int); // 与void mySignal(int)连接
signals:
// 声明一个带参数的信号函数
void mySignal(int);
};
#endif // DIALOG_H
dialog.cpp
#include "dialog.h"
Dialog::Dialog(QWidget *parent)
: QDialog(parent)
{
resize(300,300);
btn = new QPushButton("0",this);
btn->move(100,150);
// 第一个信号槽连接
connect(btn,SIGNAL(clicked()),
this,SLOT(btnClickedSlot()));
// 第二个信号槽连接
connect(this,SIGNAL(mySignal(int)),
this,SLOT(mySlot(int)));
}
void Dialog::btnClickedSlot()
{
// 先加再发
emit mySignal(++count);
}
/**
* @brief Dialog::mySlot
* @param count 信号函数发射过来的参数
*/
void Dialog::mySlot(int count)
{
// int → QString
QString text = QString::number(count);
// 设置显示
btn->setText(text);
}
Dialog::~Dialog()
{
delete btn;
}
需要注意的是,信号槽传参有以下几点:
- 理论上可以传递任意多个参数
- 信号函数的参数个数必须大于等于槽函数的参数个数
- 参数类型需要匹配
4.6 信号槽的对应关系
信号槽在建立好连接的对应关系后,也可以取消连接。
多个信号可以连接同一个槽函数。
dialog.h
#ifndef DIALOG_H
#define DIALOG_H
#include
#include
#include
class Dialog : public QDialog
{
Q_OBJECT
public:
Dialog(QWidget *parent = 0);
~Dialog();
private:
QPushButton* btn1;
QPushButton* btn2;
private slots:
void mySlot();
};
#endif // DIALOG_H
dialog.cpp
#include "dialog.h"
Dialog::Dialog(QWidget *parent)
: QDialog(parent)
{
resize(400,400);
btn1 = new QPushButton("第一个",this);
btn1->move(200,200);
btn2 = new QPushButton("第二个",this);
btn2->move(200,250);
// 分别连接两个按钮的信号槽
connect(btn1,SIGNAL(clicked()),this,SLOT(mySlot()));
connect(btn2,SIGNAL(clicked()),this,SLOT(mySlot()));
}
void Dialog::mySlot()
{
// 接收者对象在槽函数中直接拿到发射者对象
if(btn1 == this->sender())
qDebug() << "点击了按钮1!";
else if(btn2 == sender())
qDebug() << "点击了按钮2!";
}
Dialog::~Dialog()
{
delete btn1;
delete btn2;
}
一个信号函数也可以连接多个槽函数。
dialog.h
#ifndef DIALOG_H
#define DIALOG_H
#include
#include
#include
class Dialog : public QDialog
{
Q_OBJECT
public:
Dialog(QWidget *parent = 0);
~Dialog();
private:
QPushButton* btn1;
QPushButton* btn2;
// 两个槽函数
private slots:
void mySlot1();
void mySlot2();
void mySlot3(); // 槽函数1与槽函数2,二合一
};
#endif // DIALOG_H
dialog.cpp
#include "dialog.h"
Dialog::Dialog(QWidget *parent)
: QDialog(parent)
{
resize(300,300);
btn1 = new QPushButton("第一个",this);
btn1->move(100,150);
btn2 = new QPushButton("第二个",this);
btn2->move(150,200);
// 按钮1同时连接两个槽函数
connect(btn1,SIGNAL(clicked()),this,SLOT(mySlot1()));
connect(btn1,SIGNAL(clicked()),this,SLOT(mySlot2()));
// 按钮2连接一个槽函数,效果等同按钮1
connect(btn2,SIGNAL(clicked()),this,SLOT(mySlot3()));
}
void Dialog::mySlot1()
{
qDebug() << "第一个槽函数";
}
void Dialog::mySlot2()
{
qDebug() << "第二个槽函数";
}
void Dialog::mySlot3()
{
// 槽函数也是成员函数
this->mySlot1();
mySlot2();
}
Dialog::~Dialog()
{
delete btn1;
delete btn2;
}
五、UI设计与组件
1. Designer 设计师(掌握)
Designer是Qt自带的界面设计软件,可以使用可视化拖拽的方式快速设计出软件界面。
使用Designer软件保存的格式是.ui,这种文件被称为界面文件。
在Qt Creator中新建项目时,如果选中界面文件选项,新建出的项目自带一个dialog.ui文件,这个文件就是Designer软件的格式。
双击界面文件,可以直接使用Qt Creator内置的Designer软件打开。
2. Layout 布局 (掌握)
可以把布局看做是一个透明的盒子,盒子内部放置了若干组件对象,这些组件对象会按照布局制定的规则自动排布。
Qt中有四种布局:
垂直布局与水平布局常用的属性有:
布局之间可以嵌套,内层布局相当于外层布局的一个组件。
如果只想破坏布局对象,可以点击工具栏中的
。
当选中窗口后,点击工具栏的任意布局按钮(通常为垂直或水平),可以使内部的布局贴合窗口。
伸展器组件可以用于填充空白。
布局不光可以使用Designer实现,也可以使用C++代码实现,相对比较复杂。新建一个不带界面的项目进行演示。
dialog.h
#ifndef DIALOG_H
#define DIALOG_H
#include
#include
#include // 垂直布局
class Dialog : public QDialog
{
Q_OBJECT
public:
Dialog(QWidget *parent = 0);
~Dialog();
private:
QPushButton *btn1;
QPushButton *btn2;
QVBoxLayout *layout; // 布局对象
};
#endif // DIALOG_H
dialog.cpp
#include "dialog.h"
Dialog::Dialog(QWidget *parent)
: QDialog(parent)
{
resize(300,300);
btn1 = new QPushButton("23031",this);
btn2 = new QPushButton("23032",this);
// 创建布局对象
layout = new QVBoxLayout(this);
// 添加组件对象到布局中
layout->addWidget(btn1);
layout->addWidget(btn2);
}
Dialog::~Dialog()
{
delete layout;
delete btn1;
delete btn2;
}
3. QWidget 属性(掌握)
在Designer中随便选择一个窗口对象或一个组件对象,在属性配置面板都能看到很多淡黄色的属性,这些属性就是QWidget的属性,属于通用的一些属性。
这些属性的getter 和setter请自行查询文档。
练习:设计计算机界面程序。
设计如下所示的程序,要求:
1. 外观可参考配色网站
2. 能随窗口大小改变自适应缩放效果
4. ui指针(掌握)
当项目中使用了界面文件后,可以在Dialog类中看到一个新增加的成员变量。ui指针可以在C++代码中调用和管理ui文件中设计的组件对象。
ui指针的工作原理:
可以在Designer的组件对象概览面板更改各个组件对象的名称:
5. 基础组件
在此章节会选择一些常用的基础性组件,分别介绍其常用属性和函数。本节红色标记的属性和信号函数起码掌握到英译汉的水平。
5.1 QLabel 标签(掌握)
QLabel类用于显示文字或图片。
QLabel类常用属性如下所示。
Qt对图片的支持比较完善,建议使用jpg、png等格式,图片的大小(不要超过1M)和分辨率(不超过1920x1080)不宜过高。
建议使用Bing搜索取代Baidu图片搜索,Baidu图片直接另存为的格式是webp,Qt不支持。
必应
软件设置素材(图标、插画等图片)可以使用阿里巴巴矢量图标库:
iconfont-阿里巴巴矢量图标库
图片最好添加到项目中,成为项目资源文件的内容,以下是添加的步骤:
1. 给图片命名为英文+数字+下划线的组合(数字和下划线不要开头),其他字符不可用。
2. 把图片文件移动到项目的工作目录中。
3. 在Qt Creator中选中项目名称,鼠标右键,点击“添加新文件”。
4. 在弹出的窗口中,按照下图所示进行操作。
5. 给资源文件命名后,点击“下一步”。
6. 在项目管理界面,直接点击完成。可以看到项目中多了一个.qrc格式的资源文件。
7. 选中qrc资源文件后,点击 添加---添加前缀。
8. 执行了第七步以后,再次点击 添加,可以看到之前不能点击的 添加文件,现在就可以点击了,点击 添加文件,选择要导入的图片文件。
添加成功后,可以在.qrc文件中看到已经导入图片文件。
9. 一个qrc文件中可以导入若干个图片文件,导入其它图片只需要执行128步骤。
10. 如果要在Designer中使用图片,还需要在使用前点击构建按钮
。
在Designer中设置图片显示:
在C++代码中设置图片显示要先复制资源路径到剪切板。
尽量不要使用Qt代码进行图像处理,因为图像处理非常消耗性能,有能力的同学可以使用PS。在公司中通常由专业的美工进行处理。
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
5.2 QAbstractButton 按钮类(掌握)
QAbstractButton是所有按钮类的抽象基类,内部规定了按钮类的通用框架。
QAbstractButton的常用属性如下所示。
QRadioButton因具有互斥性,所以在使用时需要给不同的对象进行分组,可以使用布局或QGroupBox。
QAbstractButton有四种按钮信号:
当需要给若干个同类型的按钮对象设置信号槽时,可以使用QButtonGroup类对其进行分组管理,减少信号槽的连接数量。
QButtonGroup类直接继承QObject类,因此不会在界面上有显示,仅仅对按钮对象起到一个逻辑分组和管理的功能。因此QButtonGroup对象不属于ui指针管理,需要手动控制其生命周期。
QButtonGroup的信号函数:
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
5.3 QLineEdit 单行文本输入框(掌握)
用于输入一行文本到程序中。
常用属性如下。
信号函数如下:
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
5.4 QComboBox 组合框(掌握)
QComboBox是一个下拉菜单,可以菜单中选择对应的选项,从某种层面上讲,可以代替QRadioButton,相比于QRadioButton,QComboBox占用空间更小。
在一些组件中有Item部分,Item在Qt Creator中翻译为“项目”,与单词Project冲突,本次课程称Item为“条目”。
QComboBox点击后的下拉菜单中,包含了每个选项,每个选项就是一个条目(Item)。
常用属性如下。
信号函数包括:
练习:设计一个电子相册。
设计一个电子相册,点击上一张,切换到上一张图片,点击下一张,切换到下一张图片。图片的展示可以循环。
选做功能:输入照片库中的关键字,显示关联的照片,例如输入“熊猫”,显示熊猫照片。
5.5 一些与数字有关的组件(熟悉)
这些组件都是与数字打交道的,具有一些共同的属性和函数。
共有属性如下:
共有信号函数如下:
// value属性发生变化时发射的信号
void valueChanged(int value)
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
六、常用类
1. QString 字符串类(熟悉)
QString是Qt中的字符串类, 与C++的std::string不同,QString采用Unicode编码。QString每个字符是一个16位的QChar,而不是8位的char,所以QString处理中文没有任何问题,而且一个汉字算作一个字符。
QString几乎兼容所有std::string的API,所以本次授课不再一一演示,建议掌握一些常用的函数单词,随用随查,官方文档自带示例代码。
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
2. 容器类(掌握)
Qt的容器来比C++ STL中的容器类更轻巧、安全和易于使用,Qt对容器类进行了速度和存储优化,可以减少可执行文件的大小;Qt的容器类是线程安全的,可以被多个线程同时访问;几乎在支持C++容器类的API又进行了拓展。
2.1 顺序容器 QList
本例使用QList存储一个自定义的学生类Student,因此需要先在项目中添加一个自定义的Student类,操作步骤如下:
1. 在Qt Creator中选中项目名称,鼠标右键,点击“添加新文件”。
2. 在弹出的窗口中,按照下图所示进行操作。
3. 在弹出的窗口中,给类名命名(大驼峰命名法、帕斯卡命名法)后,点击“下一步”。
4. 在项目管理界面,直接点击完成。可以看到项目中多了新创建的C++类的头文件和源文件,头文件用于声明,源文件用于定义。
QList不仅支持C++ STL风格的迭代器,也支持Java风格的迭代器,对应关系如下。
C++ | Java | |
只读迭代器 | QList::const_iterator | QListIterator |
读写迭代器 | QList::iterator | QMutableListIterator |
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
QStringList = QList
2.2 关联容器 QMap
QMap不仅支持C++ STL风格的迭代器,也支持Java风格的迭代器,对应关系如下。
C++ | Java | |
只读迭代器 | QMap::const_iterator | QMapIterator |
读写迭代器 | QMap::iterator | QMutableMapIterator |
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
3. Qt数据类型(熟悉)
Qt是具有跨平台的特性,即一份代码可以在多个平台下无差别运行。需要保证在各个平台上数据类型都有统一的长度,因此Qt为各种常见的数据类型定义了类型符号。
QVariant类支持若干Qt常见的数据类型,当函数参数出现此类型时,可以传递这些常见的数据类型。
甚至QVariant类可以用于类型转换。
// int → QString
int a = 123;
QVariant v(a);
QString text = v.toString();
4. 日期与时间处理(熟悉-掌握)
在Qt中,使用QDate类处理日期,使用QTime类处理时间,两个类合并的QDateTime类既能处理日期,又能处理时间。
Qt中获得的时间和日期数据均来自于系统数据。
本次以QDateTime为例进行日期与时间处理的讲解。
常用函数如下所示:
// 返回一个从1970年1月1日00:00:00(格林威治时间)到现在的毫秒数
qint64 QDateTime::currentMSecsSinceEpoch() [static]
// 返回一个包含当前时区的日期和时间信息的QDateTime对象
QDateTime QDateTime::currentDateTime() [static]
// 返回一个QDateTime对象中包含的日期和时间字符串
// 参数是制定的格式
QString QDateTime::toString(const QString & format) const
常用格式的翻译如下。
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
5. QTimer 定时器类(重点)
定时器类可以执行一个一次性的延迟任务或执行一个周期性的重复任务。
QTimer的常用属性如下所示:
- singleShot : bool
是否是一次性,如果值为true,则表示一次性;如果值为false,则表示周期性。
- interval : int
如果是一次性定时器,此属性表示延迟执行的时间(毫秒);
如果是周期性定时器,此属性表示间隔时间(毫秒)。
- active : const bool
如果值为true,则表示定时器正在运行;
如果值为false,则定时器不在运行。
QTimer的常用函数如下所示:
// 构造函数
QTimer::QTimer(QObject * parent = 0)
// 启动定时器,调用此函数如果定时器正在运行,则会停止定时器并重新运行
void QTimer::start() [slot]
// 如果是一次性定时器,倒计时结束后,发射此信号;
// 如果是周期性定时器,每次到达触发时间后,发射一次此信号。
void QTimer::timeout() [signal]
// 停止定时器
void QTimer::stop() [slot]
一次性定时器示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
以上代码的析构函数增加如下代码
Dialog::~Dialog()
{
// 如果定时器正在运行,则先停止
if(timer->isActive())
timer->stop();
// 销毁定时器对象
delete timer;
delete ui;
}
练习:做一个电子表,实时显示当前的时间。
提示:创建一个周期性的QTimer,间隔时间1s左右,每次到点后使用QDateTime类获取当前的时间并设置在组件上。
七、多窗口编程
截止目前,所有的Qt项目只有一个窗口。本章讲解一些出窗口类的细节以及多窗口编程。
1. QMessageBox 消息对话框(掌握)
QMessageBox继承自QDialog,是一个用于显示信息或提问接受一个询问用户的问题的模态对话框。
预设了四种类型的QMessageBox效果。
Qt中自带的QDialog派生类通常使用静态成员函数直接弹出。
// 参数1:parent参数,先传this,后面再说
// 参数2:窗口标题
// 参数3:显示的信息
// 返回值:点击的按钮类型
StandardButton information|question|warning|critical(
QWidget * parent,
const QString & title,
const QString & text) [static]
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
2. 窗口类的继承关系(掌握)
QWidget作为所有窗口的基类,本身具备了很多窗口类共有的属性和函数,例如:
- windowTitle : QString
窗口标题
// 设置窗口状态
// 参数为窗口状态
void QWidget::setWindowState(Qt::WindowStates windowState)
- windowFlags : Qt::WindowFlags
窗口标记
可以通过setter设置窗口标记值,如果要同时设置多个标记,可以使用|分割,但是要注意多个标记之间不能有冲突。
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
QWidget类的构造函数如下所示。
// 当parent参数使用默认值时,创建的QWidget对象就是一个窗口;
// 当parent参数使用某个窗口对象时,创建的QWidget对象就是这个窗口对象中的一个组件对象。
QWidget::QWidget(QWidget * parent = 0)
QWidget作为组件,内部也可以包含其它布局或子组件,可以把这种QWidget对象当做是一个内嵌的窗口。
3. QMainWindow 主窗口(掌握)
QMainWindow是最合适作为主窗口的类型,因为包含了很多组成部分。
创建一个QMainWindow作为主窗口的Qt项目,进入.ui文件后,可以看到窗口对象自带四个子对象,如下所示。
需要注意的是,四个子对象也归ui指针管理。
本节所有Designer中的操作,都可以找到对应的C++代码。
3.1 QMenuBar 菜单栏
菜单栏由QMenu菜单组成,QMenu中又可以包含QMenu和实际触发的动作QAction,如下所示。
如果QMenu和QAction是通过Designer添加的,则可以使用UI指针管理。如果使用Designer添加QMenu,一级菜单中的内容不支持中文输入,此时可以复制粘贴。
点击QAction后通常要触发槽函数,QAction常用的信号函数如下所示。
QAction更多属性值,可以在下面的面板中进行配置:
3.2 QToolBar 工具栏
工具栏中通常使用若干个工具栏按钮实现,工具按钮几乎就是QAction的图标。只需要给工具栏添加QAction对象即可。
如果使用的Designer进行设计,只需要把QAction拖入工具栏即可。
3.3 QWidget 中心组件
中心组件就相当于之前的QDialog的内容,正常设计即可。
3.4 QStatusBar 状态栏
状态栏通常用于显示一些与应用程序相关的信息。
可以使用下面的两个槽函数设置显示内容和清空显示内容。
// 参数1:显示的信息内容
// 参数2:显示的时长,单位毫秒,默认值0表示持续显示
void QStatusBar::showMessage(const QString & message,
int timeout = 0) [slot]
// 清空显示的内容
void QStatusBar::clearMessage() [slot]
QMainWindow示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
4. parent参数(掌握)
parent参数不传递时,必须手动控制对象的销毁,否则会造成内存泄漏的问题。
parent参数传递时,parent参数传递的对象会成为当前创建对象的父对象,当父对象销毁时,一并销毁子对象。
建议尽量给创建的堆内存对象传递parent参数,以防止内存泄漏的产生。
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
5. 自定义窗口类(掌握)
本节自己创建一个窗口类,包含头文件、源文件和界面文件。
创建自定义窗口类的操作步骤如下所示。
1. 在Qt Creator中选中项目名称,鼠标右键,点击“添加新文件”。
2. 在弹出的窗口中,按照下图所示进行操作。
3. 在弹出的窗口中选择想要创建的类型和参数,直接点击“下一步”。
4. 在弹出的窗口中,给类命名后(大驼峰命名法、帕斯卡命名法),点击“下一步”。
5. 在项目管理界面,直接点击“完成”。可以看到新创建的窗口类的头文件、源文件与界面文件。
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
6. 对象传值(掌握)
6.1 父对象→子对象
【例子】转动粉色的QDial,使蓝色的QDial同步转动。
在子对象中公开一个调用接口,主对象直接通过接口函数进行传参。
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
6.2 子对象→父对象
【例子】转动蓝色的QDial,使粉色的QDial同步转动。
在子对象中发射带参数的自定义信号,在父对象的槽函数中接受信号函数的参数。
链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
练习:做一个系统登录页面。
要求
1. 点击注册按钮后,验证用户输入的格式是否符合电子邮箱的格式(有@,@前后有内容,以.com结尾等)。
2. 点击注册按钮后,随机显示红绿蓝三个颜色。
3. 点击注册按钮后,在新弹出的窗口中显示注册的时间。
7. 事件机制
7.1 原理(熟悉)
事件是Qt内部的机制,以用户操作(不仅仅有用户操作)为例,来说明事件在计算机中的传递,如下所示。
上图中应用内部是Qt负责进行事件分发和传递的部门,把Qt内部的逻辑放大,可以得到下面的图片。
7.2 事件函数(掌握)
事件不同于信号槽,无需连接即可使用,最常见的方式就是实现事件函数,常用的事件函数如下所示。
通过覆盖保护权限的事件虚函数,起到回调函数的效果,常见的事件如下:
// 绘制事件
void QWidget::paintEvent(QPaintEvent * event) [virtual protected]
// 大小改变事件
void QWidget::resizeEvent(QResizeEvent * event) [virtual protected]
// 鼠标按压事件
void QWidget::mousePressEvent(QMouseEvent * event) [virtual protected]
// 鼠标释放事件
void QWidget::mouseReleaseEvent(QMouseEvent * event) [virtual protected]
// 鼠标双击事件
void QWidget::mouseDoubleClickEvent(QMouseEvent * event) [virtual protected]
// 鼠标移动事件
void QWidget::mouseMoveEvent(QMouseEvent * event) [virtual protected]
// 移动事件
void QWidget::moveEvent(QMoveEvent * event) [virtual protected]
// 按键按压事件
void QWidget::keyPressEvent(QKeyEvent * event) [virtual protected]
// 按键释放事件
void QWidget::keyReleaseEvent(QKeyEvent * event) [virtual protected]
// 焦点获取事件
void QWidget::focusInEvent(QFocusEvent * event) [virtual protected]
// 焦点丢失事件
void QWidget::focusOutEvent(QFocusEvent * event) [virtual protected]
// 关闭事件
void QWidget::closeEvent(QCloseEvent * event) [virtual protected]
// 进入事件
void QWidget::enterEvent(QEvent * event) [virtual protected]
// 离开事件
void QWidget::leaveEvent(QEvent * event) [virtual protected]
事件函数的使用步骤比较固定,如下所示:
1. 找到要使用的事件函数。
2. 在头文件中声明虚函数。
3. 在源文件中定义事件函数。
4. 在事件函数函数体中编写事件触发时要执行的代码。
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
7.3 事件过滤器(熟悉)
事件过滤器可以在子组件向父组件传递事件的过程中,增加过滤器,过滤器一方面可以起到检测事件的作用;另一方面可以拦截事件,使事件不再向后传递。但是后者会破坏Qt本身规定的事件传送机制,新手不建议使用。
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
八、文件IO
1. QFileDialog 文件对话框(熟悉)
QFileDialog类继承自QDialog,是QMessageBox的“兄弟”。这些类都是直接调用自带样式的对话框窗口,用法固定:使用静态成员函数调用,返回值是结果。
QFileDialog通过下面的函数调用:
// 参数1:父对象
// 参数2:窗口标题,即windowTitle属性
// 参数3:基于哪个目录打开,默认值为当前项目的工作目录
// 参数4:文件格式过滤器,默认值表示不过滤
// 返回值:选择的路径,选择失败返回QString()
QString QFileDialog::getOpenFileName|getSaveFileName(
QWidget * parent = 0,
const QString & caption = QString(),
const QString & dir = QString(),
const QString & filter = QString()) [static]
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
2. QFileInfo 文件信息类(熟悉)
用于获取文件或目录的信息,常用函数如下:
// 构造函数,参数为文件路径,参数不是有效路径格式仍然可以创建对象
QFileInfo::QFileInfo(const QString & file)
// 文件或目录是否存在
bool QFileInfo::exists() const
// 创建时间
QDateTime QFileInfo::created() const
// 是否可读
bool QFileInfo::isReadable() const
// 获取文件大小,单位字节,如果文件不存在或无法访问,则返回0
qint64 QFileInfo::size() const
3. QFile 文件IO类(掌握)
之前的代码不会对本地文件做任何IO操作,QFile类可以对文件做IO操作,因为QFile类继承自QIODevice类,QIODevice类是Qt所有具有IO能力的类的基类。一些最基础的IO接口都在QIODevice类中规定了。
本节学习的基础接口很多在后续网络操作中通用,反之亦然。
// 参数为文件读写的路径
QFile::QFile(const QString & name)
// 打开文件,参数为打开的模式
// 返回值为打开的结果
bool QIODevice::open(OpenMode mode) [virtual]
// 是否读到数据流尾部
bool QIODevice::atEnd() const [virtual]
// 读取数据
// 参数:最大读取的字节数
// 返回值:保存了读取数据的字节数组对象
QByteArray QIODevice::read(qint64 maxSize)
// 写出数据
// 参数:携带写出数据的字节数组
// 返回值:实际的数据写出量,-1表示写出时发生错误
qint64 QIODevice::write(const QByteArray & byteArray)
// 清空缓存区
bool QFileDevice::flush()
// 关闭数据流
void QIODevice::close() [virtual]
// 获取可读的字节数
qint64 QIODevice::size() const [virtual]
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
4. UI与耗时操作(掌握)
在默认的情况下,一个Qt应用程序只有一个线程(主线程),这个线程主要负责Qt的UI逻辑与人机交互,因此主线程也被称作UI线程。
但是一些耗时操作(例如IO或复杂算法)在主线程中执行,会导致主线程原有的逻辑被阻塞,无法及时响应。
操作系统检测到主线程特殊操作(例如尝试关闭程序)无法及时响应时,会弹出下面的窗口引导用户做出选择。
正确的解决方案是引入多线程。
5. 多线程(掌握)
Qt的线程类是QThread,包含了很多与线程相关的API。
5.1 复现主线程阻塞
QThread类中有一个静态成员函数,可以使CPU强行休眠一段时间:
// 当前线程睡眠一段时间
// 参数是睡眠的时间,单位毫秒
void QThread::msleep(unsigned long msecs) [static]
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
5.2 子线程的创建与启动
主线程阻塞的解决方法是新开启一个子线程,子线程执行耗时操作,主线程执行UI操作。
创建并启动一个子线程的步骤如下所示:
1. 在Qt Creator中选中项目名称,鼠标右键,点击“添加新文件”。
2. 在弹出的窗口中,按照下图所示进行操作。
3. 在弹出的窗口中,先输入类名(大驼峰),再输入基类名(QThread),再选择类型信息为“继承自QObject”,最后点击“下一步”。
4. 在项目管理界面,直接点击“完成”。可以看到新创建的线程类的头文件和源文件就已经添加到项目中了。
5. 在自定义线程类的头文件中声明下面的函数(函数覆盖):
// 相当于子线程的main函数,是子线程执行的起始点和终止点
// 需要注意的是,此函数不能直接调用
void QThread::run() [virtual protected]
6. 在自定义线程类的源文件中定义run函数,并在函数体中添加子线程要执行的代码,例如:
7. 在主线程中创建一个自定义线程类对象。
8. 自定义线程类对象调用start函数开启子线程的执行。
// 开始执行子线程,调用此函数后,内部会在子线程中开始执行run函数。
// 参数是子线程的优先级。
void QThread::start(Priority priority = InheritPriority) [slot]
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
5.3 异步刷新
在实际开发中子线程经常用于执行耗时操作,主线程用于执行UI操作,有时需要在UI上显示耗时操作的数据,此时需要子线程向主线程更新数据,主线程的UI对子线程的数据进行异步刷新显示。
实际上子线程向主线程发送数据就是子对象向父对象发送数据,因此使用信号槽处理;反之,主线程传递数据到子线程可以直接调用子线程公开的函数接口。
需要注意的是,子线程不能执行UI操作,主线程不能执行耗时操作,这也是异步刷新的原因。
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
5.4 停止线程
耗时操作往往伴随着循环,因此可以给线程循环条件增加一个标志位,通过设置标准位的数值控制循环的执行,使线程停止。这种停止的方式可保证run函数正常执行完,是比较推荐的方式。
如果不能使用标志位停止线程,可以考虑使用下面的函数强行停止线程。
void QThread::terminate() [slot]
不推荐使用此函数,因为比较危险。
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
6. 多线程文件IO(掌握)
给本章第3节的单线程文件拷贝器,增加多线程功能。
子线程执行文件拷贝的耗时操作,主线程执行人机交互逻辑与UI显示,线程通信使用异步刷新。
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
九、数据库(掌握)
1. 简介
Qt本身不具有数据库功能,但是支持间接操作数据库产品,支持以下数据库的接入:
嵌入式的主要开发场景并非服务器开发,因此不需要太高性能的数据库产品,通常采用小巧的SQLite作为嵌入式软硬件本地数据存储的方式。
SQLite数据库因为体积较小,在Qt5之后已经内置了,因此在Qt中可以直接连接SQLite并使用。
2. 准备工作
在Qt中使用数据库相关的头文件需要再.pro项目配置文件中添加sql模块。
别忘了保存!
主要使用三个类:
- QSqlDatabase
数据库连接类:表示一个数据库连接。
- QSqlError
数据错误信息类:内部包含数据库返回的错误信息。
- QSqlQuery
数据操作类:用于执行SQL语句。
3. 连接数据库
相关函数如下:
// 获得数据库连接对象,参数为数据类型(本章第一节表格中的Driver Name)
QSqlDatabase QSqlDatabase::addDatabase(const QString & type) [static]
// 设置数据库名称
// 参数:当使用SQLite数据库时,此参数表示数据库文件的名称,此文件会在构建目录中生成。
void QSqlDatabase::setDatabaseName(const QString & name)
// 打开数据库连接,返回值是打开的结果,如果失败可以通过lastError函数获得错误信息
bool QSqlDatabase::open()
// 返回上一次数据库连接的错误信息,返回值是错误信息类
QSqlError QSqlDatabase::lastError() const
// 获得错误信息类的错误信息
QString QSqlError::text() const
4. 建表
先来编写建表的SQL语句:
CREATE TABLE laptop(
id INTEGER PRIMARY KEY,
brand TEXT,
price INTEGER,
model TEXT);
相关函数如下:
// 执行SQL语句
// 参数是要执行的SQL语句
// 返回值为语句本身执行是否成功
bool QSqlQuery::exec(const QString & query)
// 用法与本章第3节的同名函数相同,略
QSqlError QSqlQuery::lastError() const
建表成功后,可以到构建目录中找到数据库文件,使用SQLiteSpy打开数据库,查看新建的表。
5. 增删改
这几个操作需要录入用户输入的参数,之前把参数直接与SQL进行语句进行拼接,这种做法在Qt中不推荐使用,因为可能会导致安全性问题。
在Qt使用推荐使用占位符+预处理+数据绑定的方式解决此问题。
1. 先编写要预处理的SQL语句,把参数位使用占位符替换。
占位符有两种写法:
- ODBC 风格
使用 ? 表示占位符,优点是书写简单,缺点是数据绑定时顺序不可变。
- Oracle 风格
使用 :列名 表示占位符,优点是数据绑定时顺序可变,缺点是书写繁琐。
2. 把上一步写的SQL语句送入Qt中进行预处理,Qt内部就知道大概要执行一个什么样的SQL语句。
// 预处理
// 参数是包含占位符的要处理的SQL语句
// 返回值是预处理的结果
bool QSqlQuery::prepare(const QString & query)
3. 数据绑定,替换原来占位符的参数。
// ODBC风格的数据绑定函数,不能乱序
// 要绑定的参数,QVariant支持各种常见类型
void QSqlQuery::addBindValue(const QVariant & val)
// Oracle风格的数据绑定函数,可乱序
// 参数1:占位符,即:列名
// 参数2:要绑定的参数,QVariant支持各种常见类型
void QSqlQuery::bindValue(const QString & placeholder,
const QVariant & val)
4. 真正执行SQL语句。
// 执行预处理的SQL语句,注意不要给此函数输入参数!
// 返回值是执行的结果
bool QSqlQuery::exec()
插入成功之后,可以使用SQLiteSpy打开db文件,双击表名,查看表中的数据。
6. 查询
模糊查询是一种特殊的条件查询,使用LIKE关键字配合两个占位符完成:
- %
匹配任一多个(0,1,......,n)字符
- _
匹配任意一个字符
-- 查询姓张的同学
SELECT * FROM class WHERE name LIKE "张%";
-- 查询令字辈的老师
SELECT * FROM teacher WHERE name LIKE "_令%"
在Qt中使用模糊查询要注意占位符不要在预处理的SQL语句中编写,而是应该在绑定时增加到参数中。
相关函数如下:
// 向后移动并取出一行数据,取出成功返回true;如果取出失败,则移动到之前为止,并返回false
bool QSqlQuery::next()
// 取出当前位置的记录的某一列数据
// 参数为列名
// 返回值是QVariant对象,可以直接转换为所需类型
QVariant QSqlQuery::value(const QString & name) const
// 取出当前位置的记录的某一列数据
// 参数为列序号,第一列序号为0,以此类推
// 返回值是QVariant对象,可以直接转换为所需类型
QVariant QSqlQuery::value(int index) const
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
十、网络编程
1. 基础知识回顾(重点)
1.1 UDP和TCP
UDP TCP 协议相同点:都存在于传输层
1.1.1 TCP(传输控制协议)
是一种面向连接的传输层协议,它能提供高可靠性通信(即数据无误、数据无丢失、
数据无失序、数据无重复到达的通信),因此传输效率较低。
适用情况:
1、适合于对传输质量或可靠性要求较高。
2、MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议
3、上位机命令发送与数据接收。
Qt的网络编程以TCP进行讲解。
1.1.2 UDP(用户数据报协议)
UDP(User Datagram Protocol)用户数据报协议,是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。
适用情况:
1、发送小尺寸数据(如对DNS服务器进行IP地址查询时)
2、在接收到数据,给出应答较困难的网络中使用UDP。
3、适合于广播/组播式通信中。
4、MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议
5、流媒体、VOD、VoIP、IPTV等网络多媒体服务中通常采用UDP方式进行实时数据传输
1.2 IP地址与端口号
1.2.1 IP地址
思考:现实生活中怎样唯一标定一个人?
基本概念
●IP地址是Internet中主机的标识
●Internet中的主机要与别的机器通信必须具有一个IP地址
●IP地址为32位(IPv4)或者128位(IPv6)
●表示形式:常用点分十进制形式,如202.38.64.10,最后都会转换为一个32位的无符号整数。
1.2.2 端口号
●为了区分一台主机接收到的数据包应该转交给哪个进程来进行处理,使用端口号来区分
●TCP端口号与UDP端口号独立
●端口号一般由IANA (Internet Assigned Numbers Authority) 管理
●端口用两个字节来表示
常见端口:1~1023(1~255为众所周知端口,256~1023端口通常由UNIX系统占用)
注册端口:1024~49151(尽量用5000以上且不为豹子号)
动态或私有端口:49152~65535
2. 准备工作
如果在Qt项目中使用网络相关头文件,需要在.pro项目配置文件中增加network模块:
增加network模块后才能使用网络相关头文件,本次课程使用的类有:
- QTcpServer
基于TCP的服务器管理类,用于管理服务器端链接,本身不具备IO能力,因为不继承QIODevice,直接继承QObject类。
- QTcpSocket
基于TCP的IO类,用于IO操作,因为间接继承了QIODevice。
- QTextStream
文本流类,用于高效进行文本数据IO传输。
3. 相关函数
// QTcpServer类的构造函数
QTcpServer::QTcpServer(QObject * parent = 0)
// 服务器开启监听,表示客户端可以发起请求了
// 参数1:哪些IP可以向服务器发起连接请求,默认值为所有IP
// 参数2:服务器占用的端口号
// 返回值:监听服务开启是否成功
bool QTcpServer::listen(
const QHostAddress & address = QHostAddress::Any,
quint16 port = 0)
// QTcpSocket类的构造函数
QTcpSocket::QTcpSocket(QObject * parent = 0)
// 连接到主机
// 参数1:主机的IP地址
// 参数2:主机的端口号
// 参数3:读写模式
void QAbstractSocket::connectToHost(
const QString & hostName,
quint16 port,
OpenMode openMode = ReadWrite) [virtual]
// 服务器接受到新连接请求发射的信号
void QTcpServer::newConnection() [signal]
// QTcpSocket对象连接成功时发射的信号
void QAbstractSocket::connected() [signal]
// QTcpSocket对象连接失败时发射的信号
void QAbstractSocket::disconnected() [signal]
// 每当一个客户端连接时,此函数将返回当前连接的对象,这个对象会跟随服务器管理类对象一并销毁
QTcpSocket * QTcpServer::nextPendingConnection() [virtual]
// 获取对面的IP地址
QHostAddress QAbstractSocket::peerAddress() const
// QHostAddress可以转换为字符串
QString QHostAddress::toString() const
// 获取对面的端口号
quint16 QAbstractSocket::peerPort() const
// IO连接是否还在打开
bool QIODevice::isOpen() const
// 关闭IO
void QIODevice::close() [virtual]
// 文本流类构造函数
QTextStream::QTextStream(QIODevice * device)
// 写出字符串
// 参数是要写出的内容 ,注意使用QString类型
// 返回值是当前类型的引用,表示支持链式调用
QTextStream & QTextStream::operator<<(const QString & string)
// 新数据可读时发射的信号
void QIODevice::readyRead() [signal]
// 读取maxlen个字符,返回值是读取的内容
QString QTextStream::read(qint64 maxlen)
// 读取所有内容,返回值是读取的内容
// 数据量很大或不清楚数据量的情况下避免使用此函数
QString QTextStream::readAll()
// 一次读取一行文本
// 参数是一行文本的最大字符数
// 返回值是读取的内容
QString QTextStream::readLine(qint64 maxlen = 0)
// 服务器关闭监听与连接
void QTcpServer::close()
示例代码下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
十一、项目打包
开发者编写的程序应该能在任意一台计算机中运行,哪怕这台计算机并没有安装Qt的开发环境,为了把开发的Qt项目制作成常见软件的安装包的形式,因此进行本章的学习。
1. 设置全局应用程序图标(熟悉)
设置的操作步骤如下:
1. 设计或下载一款图标图片,建议分辨率使用256x256
2. 把图片转换为.ico图标格式
PNG转ICO, 在线转换器 - 转换视频, 音乐, 图像, PDF - Office-Converter.com
Convertio — 文件转换器
3. 转换完成后,把不带中文字符的图标文件放置到项目的工作目录中。
4. 在Qt Creator中选中项目名称,鼠标右键,点击“添加新文件”。
5. 在弹出的窗口中,按照下图所示进行操作。
6. 在弹出的窗口中,输入图标配置文件的名称(例如 icon_config.rc),一定要输入.rc扩展名,然后点击“下一步”。
7. 在项目管理界面点击“完成”,此时可以看到.rc图标配置文件已经添加到项目中了。
8. 在.rc文件中增加一句代码,如下所示。
IDI_ICON1 ICON DISCARDABLE "XXX.ico"
上面的XXX表示图标文件的名称
9. 在.pro项目配置文件中增加下面的代码。
RC_FILE += XXX.rc
XXX是.rc文件的名称
10. 编译并运行项目观察图标(标题栏、任务栏、可执行文件图标等)。
2. Debug模式与Release模式(掌握)
Qt项目运行有两种模式:Debug模式与Release模式
默认的模式是Debug模式,这种模式下生成的exe可执行文件中包含很多与开发相关的信息(例如调试信息、冗余代码等),这样的程序运行比较慢、体积比较大,但是方便开发者进行开发和调试。
Release模式生成的可执行文件一般不包含调试信息,虽然这样的程序不方便被调试,但是体积小,并且编译器会对代码做出一定程度的优化,因此运行速度有一定的提升,适合发布给用户使用。
选择了不同的模式后,点击
,可以看到当前模式下的构建目录,不同构建模式下,构建目录也不同。
进入构建目录后,在构建目录中分别有debug和release两个文件夹,进入当前构建模式所属的文件夹,就可以看到当前项目生成的exe可执行文件。
无论双击哪个模式下的exe文件,发现均无法正常运行,并弹出critical的QMessageBox。提示丢失各种dll文件。
3. dll动态链接库(熟悉)
程序的本体是exe文件,但是exe文件的运行需要调用Qt的API,可以把exe所需的Qt API封装成dll格式的动态链接库文件,使dll文件与exe文件(Release版本)处于同一个目录,此时即可正常运行。
如果项目中包含.qrc、图片、数据库等文件,建议也一并放入到exe所在的目录,这个目录后期会充当开发阶段的工作目录和构建目录。
补充dll文件有两种方法:
- 补充所有常用的dll文件
可以把老师提炼的当前Qt版本所有常用的dll文件直接补充,这种方法优点是操作简单,缺点是体积较大。
下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
- 使用Qt自带的windeployqt工具提取exe所需dll
优点是没有冗余的dll文件,缺点是操作非常复杂,操作步骤如下:
1. 在开始菜单中找到Qt文件夹中的
2. 在命令行中定位到exe所在的文件夹。
常用命令:
dir 当前目录文件列表
cd .. 返回上一级
cd 文件夹 进入子文件夹
C: 切换到C盘
3. 定位到exe所属的文件夹后,执行下面命令提取dll。
windeployqt 可执行文件名称.exe
4. 提取完成后,虽然exe所在的文件夹中多了很多dll文件,但是直接运行还是报错,因为除了Qt的dll文件外,还需要一些其他的dll文件,此时手动补充。
4. 打包(了解)
Qt不具备打包功能,需要借助第三方软件实现,本次课程使用isetup软件。
下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
安装一直下一步,只要不使用中文路径即可,软件的完整名称和如下。
打包流程如下:
编译完成后,得到软件安装包。
5. 补充:5.14版本Qt环境安装
下载链接:百度网盘 请输入提取码
提取码:hqyj
--来自百度网盘超级会员V6的分享
安装教程:
Qt 5.14版本安装教程_哔哩哔哩_bilibili
6. 项目
1. 制作一个多人聊天室
2. 要求同时容纳10人以上。
3. 半双工通信。
4. 聊天记录使用数据库保存,可查询历史聊天记录。
5. 美化样式。
6. 其它功能可选:头像选择、文件传输等。
提交要求:
1. 源代码
2. 安装包
3. 项目讲解视频(5分钟以内)
包含技术模块的讲解以及软件运行的演示与讲解