【北京迅为】《STM32MP157开发板嵌入式开发指南》-第七十六章 C++入门
iTOP-STM32MP157开发板采用ST推出的双核cortex-A7+单核cortex-M4异构处理器,既可用Linux、又可以用于STM32单片机开发。开发板采用核心板+底板结构,主频650M、1G内存、8G存储,核心板采用工业级板对板连接器,高可靠,牢固耐用,可满足高速信号环境下使用。共240PIN,CPU功能全部引出:底板扩展接口丰富底板板载4G接口(选配)、千兆以太网、WIFI蓝牙模块HDMI、CAN、RS485、LVDS接口、温湿度传感器(选配)光环境传感器、六轴传感器、2路USB OTG、3路串口,CAMERA接口、ADC电位器、SPDIF、SDIO接口等
第六篇 嵌入式GUI开发篇
在嵌入式上,我们少不了界面的开发,一种是用安卓,一种是用QT,那么安卓对CPU的性能要求比较高,不是所有的CPU都可以运行,但是QT对CPU要求不高,甚至可以在单片机上来运行,而且QT是一个非常优秀的跨平台工具,一套代码我们可以在多个平台上来运行,比如Windows,Android,Linux等,换一套编译器即可更换不同的平台。所以非常的方便和有趣。接下来我们就来学习上嵌入式上的QT开发,因为QT开发需要C++基础,不过大家不用担心,QT上用的C++并不多,手册上的C++知识大家掌握了即可开始QT的学习,大家不必担心。
第七十六章 C++入门
C++基础(上) → C++基础(上)_哔哩哔哩_bilibili
C++基础(下) → https://www.bilibili.com/video/BV1tp4y1i7EJ?p=4
76.1 c++基础
c++是c语言的升级版,在c的基础上增加了很多功能。是一种高级语言,常见后缀:cpp,c++,cc等。
g++编译:gcc是一个通用命令,它会根据不同的参数调用不同的编译器或链接器,GCC 为了让操作简单推出g++命令用来编译 C++。
C++命名空间:中大型软件项目在多人合作开发时会出现变量或函数的命名冲突,C++ 引入了命名空间来解决变量重复定义。声明命名空间 std:using namespace std,后续如果有未指定命名空间的符号,那么默认使用 std,cin、cout ,endl都位于命名空间 std。
#include <iostream>
namespace A{
int num = 1;
}
namespace B{
int num = 2;
}
int main(int argc, const char *argv[])
{
int m = A::num;
std::cout << m << std::endl;
m = B::num;
std::cout << m << std::endl;
return 0;
}
76.2 c++类和对象
1.什么是面向对象,什么又是面向过程
c语言就是面向过程的,c++就是面向对象的,面向对象特点:封装,继承,多态。
举例:a+b
直接计算a+b就是面向过程。
面向对象就是给a+b穿上了一层衣服。不是直接计算a+b。
2.c++的类
类大家可以把他看成c语言结构体的升级版。类的成员不仅可以是变量,也可以是函数,类可以看作是一种数据类型,这种数据类型是一个包含成员变量和成员函数的集合。
如:
class student
{
public:
//成员函数
char name[64];
char age;
//成员函数
void do(){
Cout << “function” <<endl;
}
};
对象:类的实例化
直接定义:
student my; // student就是类 my就是对象
在堆里面定义。
student *my = new student;
删除对象
delete my; 目的是释放堆里面的内存
my = NULL; 变成空指针,杜绝成为野指针。
访问类的成员
student my;
student *my1 = new student;
my.age = 18;
my1->age =19;
cout << my.age << endl;
cout << my1->age << endl;
访问方法和C语言结构体是一样的,普通变量通过“.” 指针通过“->”。
4.类的函数成员
因为类里面的成员不仅可以是变量,也可以是函数。
第一步:在类里面声明
第二步:实现这个函数。我们可以直接在类里面写,也可以写在类的外面。
直接写在类的里面
class student
{
public:
char name[64];
int age;
void test(){
cout << 123 << endl;
};
};
写到类的外面
class student
{
public:
char name[64];
int age;
void test();
};
void student::test(){ //student:表示属于这个类里面的函数,不加的话会被识别成普通函数。
cout << 123 << endl;
};
};
5.访问修饰符
类的访问修饰符就是对类的成员进行权限管理。
public: 表示函数和变量是公开的,任何人都可以访问。
private: 表示函数和变量只能在自己的类里面自己访问自己,不能通过对象来访问。
能不能强行访问?可以的。
protected:表示函数和变量只能在自己的类里面自己访问自己,但是可以被派生类来访问的。
76.3 函数的重载
1.类函数的重载特性
类函数的重载特性就是说我们可以在类里面定义同名的函数,但是参数不同的函数。
class student
{
public:
char name[64];
int age;
void test();
void test(int a);
private:
int haha;
};
重载函数在调用的时候,会根据参数的类型,然后去匹配相应的函数进行调用。
76.4 构造函数和析构函数
析构函数:假如我们定义了析构函数,当对象被删除或者生命周期结束的时候,就会触发析构函数。
构造函数:假如我们定义了构造函数,就会触发这个构造函数。
我们要怎么定义析构函数和构造函数?
1.析构函数和构造函数的名字必须和类名一模一样。
2.析构函数要在前面加上一个~
举例:
class student
{
public:
student();
~student();
char name[64];
int age;
void test();
void test(int a);
private:
int haha;
};
student::student(){
cout << "hello" << endl;
}
student::~student(){
cout << "bye" << endl;
}
构造函数是可以被重载的。
析构函数不能被重载。
3.类的继承
什么是类的继承?
类的继承允许我们在新的类里面继承父类的public还有protected部分,private是不能被继承的。
当我们觉得这个类不好的时候,可以使用类的继承,添加我们需要的功能。·
格式:
class 儿子:public 爸爸{
public:
........
Protected:
}
例子:
class mystudent:public student
{
public:
int grade;
};
如果在子类里面去访问父类的成员?
也是通过.和->来访问的。
76.5 虚函数和纯虚函数
虚函数:有实际定义的,允许派生类对他进行覆盖式的替换,virtual来修饰。
纯虚函数:没有实际定义的虚函数就是纯虚函数。
举例:
virtual void test(); //虚函数
virtual void testa(){} //纯虚函数
怎么定义一个虚函数?
用virtual来修饰,虚函数是用在类的继承上的。
虚函数的优点?
76.1 安装Qtcreator
本章内容对应视频讲解链接(在线观看):
在Windows上搭建QT开发环境 → https://www.bilibili.com/video/BV1tp4y1i7EJ?p=2
Qtcreator下载地址:https://download.qt.io/archive/qt/5.12/5.12.10/,进入选择版本号界面,本教程使用的是5.12.10,也推荐读者选择此版本。进入如下图界面后,选择安装包,我们在windows下学习Qt,所以选择qt-opensource-windows-x86-5.11.1.exe,点击即可下载。
下载后右键点击exe文件,选择以管理员身份运行。注册账号,点击下一步(或next),选择安装路径。选择下一步,勾选需要用到的组件,本阶段教程需要勾选以下七个选项:
选择完后一直点下一步,安装完成后如下图:
76.2 创建工程
创建一个QT工程步骤:
步骤一:
步骤二:
填写工程名字,不要有中文路径:
步骤三:填写类名:
创建成功后如下图:
工程目录下的.pro工程文件分析:
点击forms,然后双击ui文件,就可以进入ui编辑器。
ui编辑器面板介绍:
76.3 信号和槽
本章内容对应视频讲解链接(在线观看):
QT信号和槽 → https://www.bilibili.com/video/BV1tp4y1i7EJ?p=6
信号就是指控件发出的特定的信号。槽就是槽函数的意思,信号和槽都位于类中,不是C++标准代码。我们可以把槽函数绑定在某一个控件的信号上。当需要调用外部函数时,发送一个信号,此时与该信号相关联的槽便会被调用,槽其实就是一个函数,槽与信号的关联要由程序员来完成,关联方法有自动关联和手动关联。
76.3.1 自动关联
使用Qt信号和槽的自动关联,可加快开发速度,一般用于同一个窗体之间的控件关联,槽函数格式非常关键,格式为:
void on_<窗口部件名称>_<信号名称>(<signal parameters>);
自动关联步骤:
步骤一:手动选择相应的控件,然后右键->转到槽。
选择信号类型:
自动关联会在.h文件声明槽函数。槽函数只能声明到private slots或者public slots 下面。按住Ctrl+鼠标左键,跳转到.cpp文件对应的函数功能实现部分。填写功能代码,我们在槽函数内用qDebug打印信息。
添加完成qDebug内容之后,点击构建,运行:
每次点击,按钮都会发信号,对应的槽函数就会执行,结果如下图:
76.3.2 手动关联
信号和槽机制是QT的核心机制,要精通QT编程就必须对信号和槽有所了解。信号和槽是一种高级接口,应用于对象之间的通信,它是QT的核心特性,也是QT区别于其它工具包的重要地方。此外如果遇到不懂的函数或类,可以先选中,然后按F1键,即可查看介绍。
虽然Qt有自动关联功能,但涉及到多个窗体和复杂事件的时候,只能使用手动关联,手动关联使用connect这个函数。
函数定义:
connect(const QObject *sender, const char *signal,
const QObject *receiver, const char *member,
Qt::ConnectionType = Qt::AutoConnection);
参数含义:
sender:发送对象;
singal:发送对象里面的一个信号,格式一般为SIGNAL(信号);
receiver:接收对象;
member:接收对象里面的槽函数,格式一般为SLOT(信号)。
ConnectionType:设置信号和槽的同步异步,一般使用默认值Qt::AutoConnection,可不填。
通常只传递前四个参数,如下所示:
connect(A,SIGNAL(B),C,SLOT(D));
当对象A发出B信号时候,就
会触发对象C的槽函数D
signals 是 QT 的关键字,而非 C/C++ 的。signals 关键字指出进入了信号声明区,随后即可声明自己的信号。
slots 槽函数是普通的 C++ 成员函数,当与其关联的信号被发射时,这个槽函数就会被调用。槽函数有的参数个数和类型,在对应的信号函数中必须一一对应,即信号函数的参数个数必须多于或等于槽函数的个数。
emit Qt定义的一个宏,作用是发射信号,与此信号关联的槽函数就会被调用。
例程:我们在widget.h中添加以下内容自定义一个信号和一个槽函数
signals:
void my_Signal(void); //自定义的信号
private slots:
void my_Solt(void); //自定义的槽函数
添加完成如下图所示:
并在widget.cpp实现槽函数:
void Widget::my_Solt(void)
{
qDebug("按下");
}
然后在widget.cpp中绑定信号和槽:
connect(this,SIGNAL(my_Signal()),this,SLOT(my_Solt()));
在widget.ui中创建按钮,并转到槽,自动关联的槽函数如下图:
发射信号
这样,点击按钮,就会发射自定义的信号my_Signal(),与my_Signal()相关联的this对象槽函数my_Solt就会被调用,槽函数就会输出打印信息,如下图:
部分核心代码如下:
Widget.h:
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
signals:
void my_Signal(void); //自定义的信号
private slots:
void my_Solt(void); //自定义的槽函数
void on_pushButton_clicked();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
Widget.cpp:
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
connect(this,SIGNAL(my_Signal()),this,SLOT(my_Solt()));
}
Widget::~Widget()
{
delete ui;
}
void Widget::my_Solt(void)
{
qDebug("按下");
}
void Widget::on_pushButton_clicked()
{
emit my_Signal();
}
76.4 给界面添加图片
本章内容对应视频讲解链接(在线观看):
仿写一个智能家居界面(上) → https://www.bilibili.com/video/BV1tp4y1i7EJ?p=7
76.4.1 添加资源
选中项目名称,右键单击—>选择添加新文件
在弹出窗口中选择Qt—>Qt Resource File ,选择Choose
填写写资源名称,
例如填写 picture后,在工程下的Resources会出现picture.qrc文件,成功后如下图。
双击picture.qrc,点击“Add Frefix”,如下图所示:
76.4.2 添加图片
我们首先将要添加的图片复制到工程目录下。然后点击点击“Add files”,
选中图片,点击“打开”,进入资源编辑器,在资源编辑器中会看到添加的图片,然后保存。
以此点开Resources下的各个文件夹,即可看到添加的图片,此时图片已经添加到工程。
76.4.3 Label添加图片
在ui文件添加Label组件,添加完成如下图所示:
然后我们右击该组件->选择改变样式表,
弹出对话框,选择添加资源的小三角->border image,
选择要添加的图片,如下图:
点击OK,apply,OK,即可完成添加,如下图:
76.5 界面布局
本章内容对应视频讲解链接(在线观看):
仿写一个智能家居界面(中) → https://www.bilibili.com/video/BV1tp4y1i7EJ?p=8
76.5.1 水平布局
Horizontal Layout 水平方向布局,组件自动在水平方向上分布
使用时先选中组件,然后点击水平布局即可完成,可看到组件变为水平排列。如下图:
76.5.2 垂直布局
Vertical Layout 垂直方向布局,组件自动在垂直方向上分布,操作方法和水平布局一致,在布局之后组件垂直排列。
我们点击打破布局按钮,重新选择要布局的组件,然后点击垂直布局按钮,如下图:
76.5.3 栅格布局
Grid Layout 网格状布局,网状布局大小改变时,每个网格的大小都改变
我们发现布局之后各个组件都是紧挨着的,这时候可以用“弹簧”控件来控制组件位置。
Horizontal Spacer 一个用于水平分隔的空格
完成后如下图:
Vertical Spacer 一个用于垂直分隔的空格,拖拽组件如下图:
选中点击垂直布局,完成后如下图:
76.6 界面切换
本章内容对应视频讲解链接(在线观看):
仿写一个智能家居界面(下) → https://www.bilibili.com/video/BV1tp4y1i7EJ?p=9
本节通过实验介绍通过创建窗口对象的方式实现界面切换:
步骤一:
在主界面ui文件添加pushButton按钮,然后右击转到槽函数。
然后右击工程,点击新建一个窗口如下图所示:
随后在,工程下创建新的Qt设计师界面类,如下图:
我们选择Widget,用户可以根据需要选择,然后输入类名windowRun。
创建完成后如下图:
步骤二:在widget窗口的头文件widget.h添加windowrun头文件,并创建win窗口,添加内容如下:
#include "windowrun.h"
windowRun *win;
添加完成如下图所示(注意,这两个添加的位置不在同一处):
步骤三:在widget.cpp的按钮槽函数on_pushButton_clicked()中添加以下内容:
win = new windowRun(); //显示新窗口
win->setGeometry(this->geometry());//设置win窗口尺寸与此窗口尺寸相同
win->show();//显示
添加完成之后如下图所示:
运行程序后,点击按钮后即可跳转到第二个界面。
76.7 Qt串口编程
本章内容对应视频讲解链接(在线观看):
QT上位机开发之串口助手(上) → https://www.bilibili.com/video/BV1tp4y1i7EJ?p=10
QT上位机开发之串口助手(下) → https://www.bilibili.com/video/BV1tp4y1i7EJ?p=11
本节我们使用Qt来编写一个简单的上位机。
实验介绍:组装ui界面,使用Qt提供的串口类来实现串口收发功能,需要掌握的相关 Qt知识有以下几点:
QSerialPort是 Qt5中的附加模块,提供了基本的功能,包括配置、I/O操作、获取和设置RS-232引脚的信号,要链接QtSerialPort模块,需要在.pro文件中添加+=serialport。
QSerialPort封装了丰富的成员变量来对应串口属性,还有很多操作串口的成员函数,常用的成员函数有setPort()或setPortName(),setBaudRate(),setDataBits(),setStopBits(),setParity()等,可以用这些函数设置要访问的串口设备。本实验使用了readyRead()信号,当有数据到来时会触发类对象的readyRead()信号,然后利用它的成员函数 readAll()读取。
类QSerialPortInfo可以获取可用的串口信息,如端口名称,系统位置,产品号,描述,制造商等信息。我们把它获取到的端口信息交给QSerialPort类对象。
76.7.1 界面布局
步骤一:将控件拖到ui界面上
接收框使用Plain Text Edit,发送框使用lineEdit,属性选择组件使用Combo Box。
步骤二:属性设置栏布局,以串口号为例,依次水平布局属性选择位。
然后全部选中属性选择框,点垂直布局
效果如下图:
步骤三:功能栏布局,在按钮间添加弹簧,点击水平布局。
选中Lbel,发送框和功能按钮,点击垂直布局:
如下图:
选中属性栏和右侧组件,然后点击水平布局,如下图:
完成后:
再仿照上边的方法将下方的功能部分和接收框垂直布局:
添加完组件后,更改接收框为只读:点击接收框,在QTextEdit里标记readOnly。
在右上角更改ui界面对象名,界面组装完成后可以根据需要自行修改,
双击属性选择框添加属性:
如下图:
76.7.2 实现串口功能
1.编辑工程文件(后缀为 .pro的文件)在QT += core gui后添加 serialport。
2.自动获取串口
使用QSerialPortInfo:::availablePorts()获取当前串口,该函数返回容器类Qlist<QSerialPortInfo>,用Qt定义的关键字foreach遍历容器Qlist里的串口信息,并将串口信息放到QStringList的类对象serialNamePort,显示到ui的串口组件。
{
{
ui->setupUi(this);
QStringList serialNamePort;
//遍历:availablePorts()返回的串口信息
foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()){
serialNamePort << info.portName();
}
ui->serialCb->addItems(serialNamePort);
}
编译后点击串口选择框,会出现已经连接的串口。
3.打开串口功能和属性设置
步骤一:实例化串口类QSerialPort对象serialPort,对串口的操作就是对serialPort对象的操作,调用QSerialPort封装的成员变量(属性)和成员函数(功能)就能控制串口。
class Example : public QMainWindow
{
public:
..........
QSerialPort * serialPort;
..........
};
ui(new Ui::Example)
{
ui->setupUi(this);
......
serialPort = new QSerialPort;
......
}
步骤二:填充波特率,数据位,停止位,校验位等属性。获取ui组件传递过来的串口信息,将串口属性填充到serialPort对象。
步骤三:打开串口,判断是否打开成功。
/*打开按钮*/
void Example::on_openCb_clicked()
{
QSerialPort::BaudRate bauRate; //波特率
QSerialPort::DataBits dataBits; //数据位
QSerialPort::StopBits stopBits; //停止位
QSerialPort::Parity checkBits; //校验位
//设置波特率
if (ui->baudCb->currentText() == "4800" ) { bauRate = QSerialPort::Baud4800; }
else if(ui->baudCb->currentText() == "9600" ) { bauRate = QSerialPort::Baud9600; }
else if(ui->baudCb->currentText() == "115200") { bauRate = QSerialPort::Baud115200;}
//设置数据位
if (ui->dataCb->currentText() == "5") { dataBits = QSerialPort::Data5;}
else if(ui->dataCb->currentText() == "6") { dataBits = QSerialPort::Data6;}
else if(ui->dataCb->currentText() == "7") { dataBits = QSerialPort::Data7;}
else if(ui->dataCb->currentText() == "8") { dataBits = QSerialPort::Data8;}
//设置停止位
if (ui->stopCb->currentText() == "1" ) { stopBits = QSerialPort::OneStop; }
else if(ui->stopCb->currentText() == "1.5" ) { stopBits = QSerialPort::OneAndHalfStop; }
else if(ui->stopCb->currentText() == "2" ) { stopBits = QSerialPort::TwoStop; }
//设置校验位
if(ui->checkCb->currentText() == "none" ) { checkBits = QSerialPort::NoParity; }
//填充串口对象的属性值
serialPort->setPortName(ui->serialCb->currentText());
serialPort->setBaudRate(bauRate);
serialPort->setDataBits(dataBits);
serialPort->setStopBits(stopBits);
serialPort->setParity(checkBits);
//设置好属性后打开串口
if(serialPort->open(QIODevice::ReadWrite) == true){
QMessageBox::information(this,"提示","成功");
}else{
QMessageBox::critical(this,"提示","失败");
}
}
4.收发串口数据功能
读数据:每当数据流从串口到达系统一次,就会传到Qt应用程序一次,readyRead信号就会触 发一次,所以可以用前面章节讲的信号和槽机制将readyRead信号和槽函数绑定,然后 就可以在槽函数中读取串口数据。槽函数中使用readAll()读取数据,使用带换行功能的appendPlainText()显示到ui的接收窗口。
//类中声明槽函数
private slots:
void serialPortReadyRead_Solt(void);
//readyRead信号和槽函数绑定
connect(serialPort,SIGNAL(readyRead()),this,SLOT(serialPortReadyRead_Solt()));
//读串口
void Example::serialPortReadyRead_Solt(void)
{
QString buf;
buf = QString(serilaPort->readAll());
ui->recvEdit->appendPlainText(buf);
}
写数据:获取ui 界面填写的信息,ui->sendEdit->text(),使用QSerialPort的成员函数 write将数据写到串口。
void Widget::on_sendBt_clicked()
{
serilaPort->write(ui->sendEdit->text().toLocal8Bit().data());
}
5.关闭串口功能
使用QSerialPort的成员函数close()关闭串口。
void Widget::on_closeBt_clicked()
{
serilaPort->close();
}
6.清空发送栏数据
调用ui组件lineEdit的成员函数clear即可清空数据
void Widget::on_clearBt_clicked()
{
ui->recvEdit->clear();
}
编译测试,结果如下图:
76.8 Qt程序打包和部署
本章内容对应视频讲解链接(在线观看):
把QT程序打包成Windows软件 → https://www.bilibili.com/video/BV1tp4y1i7EJ?p=12
因为我们要把写好的程序发给用户来用,写好的源码也不方便给别人看,所以要把程序进行打包部署。
步骤一:点击左下角的电脑图标将Debug模式切换到Release模式。
release模式:发布版本,不对源代码进行调试,基本没有调试信息。
debug模式:调试版本,有很多调试信息。
步骤二:找到release模式构建的文件夹。
步骤三:修改可执行程序图标。先把图标加到工程所在文件夹。然后在pro文件里面添加
RC_ICONS=serial_iocn.ico
注意:图标的格式必须为.ico这个格式的,其他格式不行。
步骤四:封包操作。需要用到QT的控制台,点击电脑左下角,在搜索栏搜索qt,即可看到 qt控制台,双击即可打开,如下图 :
我们需要电脑桌面上创建一个新的文件夹,注意千万不要有中文路径。然后把exe文件拷贝到我们新创建的文件夹里面,在控制台进入可执行文件所在的目录,如下图:
步骤五:使用windeployqt工具把库加到我们新创建的这个文件夹里面。
格式:windeployqt exe 文件的名称,如下图:
打包成功后,进入执行文件路径,双击程序就可以打开,如下图:
76.9 Qt网络编程
本章内容对应视频讲解链接(在线观看):
QT网络编程之TCP通信 → https://www.bilibili.com/video/BV1tp4y1i7EJ?p=13
QT网络编程之UDP通信 → https://www.bilibili.com/video/BV1tp4y1i7EJ?p=14
网络编程有TCP和UDP,TCP编程需要用到两个类:QTcpServer和QTcpSocket。
区别 | ||
TCP | UDP | |
是否连接 | 面向连接 | 面向非连接 |
传输可靠性 | 可靠 | 不可靠 |
应用场合 | 少量数据,如传输文件 | 传输大量数据,如传输视频语音 |
76.9.1 TCP实现服务器和客户端
TCP协议(Transmission Control Protocol)是一种面向连接的,可靠的,基于字节流的传输层通信协议,传输数据稳定可靠。
在 help索引中搜索到如下图两个重要类:
服务器编程中两个类都会用到,客户端编程中只会用到QTcpSocket对象。
本实验中对QTcpServer类的基本使用:
(1)监听客户端连接。
(2)每当有新的客户端连接服务器的时候,会自动触发信号,
(3)根据当前连接上的新的客户端创建一个Socket对象,将数据的收发动作交给socket套 接字去处理。
(4)关闭服务器close();
对QTcpSocket类的基本使用:
(1)服务器端:有新连接时获取连接状态,绑定socket 。
(2)客户端:通过socket连接服务器,连接成功触发信号。
(3)当有数据到来时会触发信号,用readAll()读取。
(4)通过读写socket收发数据。
具体步骤:
步骤一:创建工程,在工程文件.pro中添加network,如下图:
步骤二:设计ui界面,
- 在属性编辑栏设置主窗口大小:
- 添加组件
接收窗口: Plain Text Edit
发送窗口,IP地址窗口,端口号窗口:Line Edit
打开服务器,关闭服务器:Push Button
拖拽完成后逐个布局,根据需要设置组件大小,这里端口号框设置成了最小200
按钮布局:拖拽按钮和弹簧,然后点击水平布局。
然后选中全部组件,点击栅格布局:
最后更改组件名称注释,完成后如图:
步骤三:服务器端编程:
1.创建QTcpServer对象
2.创建监听端口,使得客户端可以使用这个端口访问服务器,使用listen函数。
bool listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0);
第一个参数是绑定指定的地址(即本机服务器IP),第二个参数是绑定的本服务器端口号。
监听某个端口时,如果有新连接进来就发出newConnection()信号。
3.当服务器对象被访问时,会发出newConnection()信号,所以为该信号添加槽函数并用一个QTcpSocket对象接受客户端的访问。
4.当socket接收缓冲区有新数据到来时,会发出readyRead()信号,为该信号添加槽函数,使用readyRead()读取。
5.socket发送数据可直接调用write()成员函数。
6.关闭端口号。
代码如下:
#include <QMainWindow>
#include <QTcpServer>
#include <QTcpSocket>
namespace Ui {
class TcpServer;
}
class TcpServer : public QMainWindow
{
Q_OBJECT
public:
explicit TcpServer(QWidget *parent = 0);
~TcpServer();
QTcpServer * tcpServer;
QTcpSocket * tcpSocket;
public slots:
void newConnection_Slot(void);
void readyRead_Solt(void);
private slots:
void on_openBu_clicked();
void on_sendBu_clicked();
void on_closeBu_clicked();
private:
Ui::TcpServer *ui;
};
#include "tcpserver.h"
#include "ui_tcpserver.h"
#include <QTcpServer>
#include <QTcpSocket>
#include <QString>
TcpServer::TcpServer(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::TcpServer)
{
ui->setupUi(this);
tcpServer = new QTcpServer(this);
tcpSocket = new QTcpSocket(this);
//连接信号与槽函数进行绑定
connect(tcpServer,SIGNAL(newConnection()),SLOT(newConnection_Slot()));
}
//连接信号槽函数
void TcpServer::newConnection_Slot(void)
{
//连接客户端后socket
tcpSocket = tcpServer->nextPendingConnection();
//套接字的接收数据信号与都数据槽函数连接
connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(readyRead_Solt()));
}
//读取数据
void TcpServer::readyRead_Solt(void)
{
QString buf;
//读取
buf = tcpSocket->readAll();
ui->recvEdit->appendPlainText(buf);
}
TcpServer::~TcpServer()
{
delete ui;
}
//打开
void TcpServer::on_openBu_clicked()
{
//监听
tcpServer->listen(QHostAddress::Any,ui->portEdit->text().toUInt());
}
//发送数据
void TcpServer::on_sendBu_clicked()
{
tcpSocket->write(ui->sendEdit->text().toLocal8Bit().data());
}
//关闭
void TcpServer::on_closeBu_clicked()
{
tcpSocket->close();
}
步骤四:客户端编程
1.创建QTcpSocket套接字对象
2.使用套接字对象的成员函数去请求连接服务器。
void connectToHost(const QHostAddress &address, quint16 port, openMode mode = ReadWrite);
第一个参数为服务器IP地址,第二个参数为服务器端口号。第三个参数为打开方式,默认为可读可写
函数功能:请求连接服务器连接成功后发出connected()信号,绑定槽函数connected_Solt()去操作socket。
3.使用write函数向服务器发送数据,当socket接收缓冲区有新数据到来时
会发出readyRead()信号,为该信号添加槽函数以读取数据。
4.断开与服务器的连接。
class TcpClient : public QMainWindow
{
.......
private slots:
void on_openBt_clicked();
void connected_Solt(void);
void readyRead_Solt(void);
void on_sendEdit_2_clicked();
void on_closeBt_clicked();
};
TcpClient::TcpClient(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::TcpClient)
{
ui->setupUi(this);
//创建socket 对象
tcpSocket = new QTcpSocket(this);
}
TcpClient::~TcpClient()
{
delete ui;
}
//打开(连接服务器)
void TcpClient::on_openBt_clicked()
{
tcpSocket->connectToHost(ui->ipEdit->text(),ui->portEdit->text().toUInt());
connect(tcpSocket,SIGNAL(connected()),this,SLOT(connected_Solt()));
}
//等待数据到来
void TcpClient::connected_Solt(void)
{
connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(readyRead_Solt()));
}
//读取数据
void TcpClient::readyRead_Solt(void)
{
ui->recvEdit->appendPlainText(tcpSocket->readAll());
}
//发送
void TcpClient::on_sendEdit_2_clicked()
{
tcpSocket->write(ui->sendEdit->text().toLocal8Bit().data());
}
//关闭
void TcpClient::on_closeBt_clicked()
{
tcpSocket->close();
}
编译运行成功,使用服务器和客户端通信如下图:
76.9.2 UDP实现服务器和客户端
UDP协议是开放式,无连接,不可靠的传输层通信协议,但它收发数据的速度相对于TCP快很多,常用在传输音视频等数据量非常大的场合。
udp网络编程只需要使用一个类QUdpSocket。
本实验中对QUdpSocket的基本使用:
1.创建QUdpSocket对象。
2.绑定端口号
3.数据到来触发readyRead()信号。
4.读取发送数据。
5.关闭。
具体步骤:
步骤一:组装ui界面,和TCP章节搭建UI界面方法一致。
步骤二:编写代码
1.创建QUdpSocket对象,使用bind函数绑定端口号和套接字,数据报到来后会发出信 号readyRead(),在绑定的槽函数内去读取数据。
2.读取数据,数据到来hasPendingDatagrams()返回true,再用pendingDatagramSize()获取数据报的长度,如果数据没有被读取
完,hasPendingDatagrams()就会返回true,直至数据都被读取完。
readDatagram(data,size);
参数data为读取的数据,size为数据长度。
3.发送数据,使用writeDatagram函数,
writeDatagram(const char *data, qint64 len, const QHostAddress &host, quint16 port);
Data:发送的数据。
Len:发送的数据长度。
Host:目标IP地址。
Port:目标端口号。
4.关闭socket套接字。
代码如下:
udp.h
#include <QMainWindow>
#include <QUdpSocket>
namespace Ui {
class Udp;
}
class Udp : public QMainWindow
{
Q_OBJECT
public:
explicit Udp(QWidget *parent = 0);
~Udp();
QUdpSocket * udpSocket;
private slots:
void on_pushButton_clicked();
void readyRead_Slot(void);
void on_pushButton_3_clicked();
void on_pushButton_2_clicked();
private:
Ui::Udp *ui;
};
udp.cpp:
Udp::Udp(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::Udp)
{
ui->setupUi(this);
udpSocket = new QUdpSocket(this);
}
Udp::~Udp()
{
delete ui;
}
/*
* 打开按钮
*/
void Udp::on_pushButton_clicked()
{
//绑定本端口的端口号
if(udpSocket->bind(ui->cliEdit->text().toUInt()) == true){
QMessageBox::information(this,"提示","成功");
}else{
QMessageBox::information(this,"提示","失败");
}
//绑定数据信号和槽函数
connect(udpSocket,SIGNAL(readyRead()),this,SLOT(readyRead_Slot()));
}
/*
*读取数据槽函数
*/
void Udp::readyRead_Slot()
{
QString buf;
QByteArray array;
//hasPendingDatagrams()返回true时表示至少有一个数据报在等待被读取
while(udpSocket->hasPendingDatagrams()){
//获取数据
array.resize(udpSocket->pendingDatagramSize());
udpSocket->readDatagram(array.data(),array.size());
buf = array.data();
ui->recvEdit->appendPlainText(buf);
}
}
/*
* 发送数据
*/
void Udp::on_pushButton_3_clicked()
{
quint16 port;
QString sendBuff;
QHostAddress address;
address.setAddress(ui->ipEdit->text());//目标机地址
port = ui->portEdit->text().toInt();//目标机端口号
sendBuff = ui->sendEdit->text();//发送的数据
//发送
udpSocket->writeDatagram(sendBuff.toLocal8Bit().data(),sendBuff.length(),address,port);
}
/*
*关闭
*/
void Udp::on_pushButton_2_clicked()
{
udpSocket->close();
}
步骤三:运行测试,收发功能正常如下图:
76.10 Qt定时器
本章内容对应视频讲解链接(在线观看):
QT时间编程之QT时钟 → https://www.bilibili.com/video/BV1tp4y1i7EJ?p=15
实验目标:实现计时器功能,并且点击打点按钮将当前时间打印出来。
用到的类有QTimer和QTime,QTimer是一个计时器类,相当于秒表,QTimer是一个时间类,相当于手表。
76.10.1 实验步骤
步骤一:界面布局:
拖拽组件,在属性编辑栏设置大小,然后选中按钮,点击水平布局;
在属性编辑栏设置Label的最小高度为50,选中全部组件,点击栅格布局,如下图:
根据实际情况调整大小,更改对象名后如下图:
步骤二:创建计时器类对象timer和时间类time,设置初始时间为0。
class TimerP : public QMainWindow
{
Q_OBJECT
public:
explicit TimerP(QWidget *parent = 0);
~TimerP();
QTimer timer;
QTime time;
..........
};
步骤三:开启计时器对象,设置定时时间,时间到后会发出 timeout() 信号,绑定此信号和自定义的槽函数timeOut_Slot()。
void start(int msec);
函数功能:开启定时器,时间到后发出timeout信号,并重新计时。
参数msec含义:定时时间,单位毫秒。
TimerP::TimerP(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::TimerP)
{
ui->setupUi(this);
//信号timeout与槽函数绑定
connect(&timer,SIGNAL(timeout()),this,SLOT(timeOut_Slot()));
time.setHMS(0,0,0,0);
ui->showTime->setText("00:00:00:000");
}
/*
*开始定时
*/
void TimerP::on_starBu_clicked()
{
timer.start(3);
}
步骤四:槽函数timeOut_Slot()内处理时间类对象,使每次计时时间结束后,时间对象能增加 相同的时间,实现计时功能。
QTime addMSecs(int ms) const;
参数msec含义:增加的时间值,单位毫秒。
函数功能:返回一个当前时间对象之后ms毫秒之后的时间对象。
/*
* 计时
*/
void TimerP::timeOut_Slot()
{
//qDebug("timt out");
time = time.addMSecs(3);
ui->showTime->setText(time.toString("hh:mm:ss.zzz"));
}
步骤五:打点记录功能,使用全局变量记录排名,并显示到界面。
/*
* 记录
*/
void TimerP::on_bitBu_clicked()
{
QString temp;
i=i+1;
temp.sprintf("%d: ",i);
ui->bitTime->append(temp);
ui->bitTime->append(time.toString("hh:mm:ss.zzz"));
}
76.10.2 部分代码
timerp.h:
class TimerP : public QMainWindow
{
Q_OBJECT
public:
explicit TimerP(QWidget *parent = 0);
~TimerP();
QTimer timer;
QTime time;
private slots:
void on_starBu_clicked();//开始计时按钮槽函数
void timeOut_Slot();//定时时间到槽函数
void on_closeBu_clicked();//关闭按钮槽函数
void on_resetBu_clicked();//重置按钮槽函数
void on_bitBu_clicked();//打点记录按钮槽函数
private:
Ui::TimerP *ui;
};
timerp.cpp:
#include <QTimer>
#include <QTime>
static int i;
TimerP::TimerP(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::TimerP)
{
ui->setupUi(this);
connect(&timer,SIGNAL(timeout()),this,SLOT(timeOut_Slot()));
time.setHMS(0,0,0,0);
ui->showTime->setText("00:00:00:000");
}
TimerP::~TimerP()
{
delete ui;
}
void TimerP::on_starBu_clicked()
{
timer.start(3);
}
/*
* 处理时间类对象
*/
void TimerP::timeOut_Slot()
{
//qDebug("timt out");
time = time.addMSecs(3);
ui->showTime->setText(time.toString("hh:mm:ss.zzz"));
}
/*
* 关闭
*/
void TimerP::on_closeBu_clicked()
{
timer.stop();
i=0;
}
/*
* 重置
*/
void TimerP::on_resetBu_clicked()
{
timer.stop();
time.setHMS(0,0,0,0);
ui->showTime->setText("00:00:00:000");
ui->bitTime->clear();
i=0;
}
/*
* 记录
*/
void TimerP::on_bitBu_clicked()
{
QString temp;
i=i+1;
temp.sprintf("%d: ",i);
ui->bitTime->append(temp);
ui->bitTime->append(time.toString("hh:mm:ss.zzz"));
}
运行结果: