C语言学了这么久是否还跟我一样对着黑窗口体验与printf的极致交流,学了Python后似乎忘了C/C++也能开发图形化界面,直到揭开Qt神秘的面纱,才有相见恨晚的感觉。在此之前,都用Python开发上位机软件,语言简洁精炼,入门简单高效。但世界总是公平的,有舍就有的,有好处必然有坏处。其一,多次碰到程序卡顿的情况。其二,使用串口时若没有在退出前关闭,就会一直占用。其三,打包出来的可执行程序体积太大。以上三宗罪并不是不可解决,但纠其根本,在于脚本语言本身没有低级语言的稳定性和速度。个人开发者倒是无所谓,对于工程实践更多需要是稳定和流畅。
难学的东西学会了肯定是有好处的,于是开始探索C/C++开发图像化界面的道路。了解了MFC,是微软公司提供的一个类库,只针对Windows平台开发可视化界面,许多windows程序都是由它开发,入门了一两天,用起来还是挺痛苦的。知道被Qt吸引,以其跨平台,文档丰富而完善,入门轻松高效,界面优美大方等优势,无可挑剔。
不过对于初学者,C语言学得多,C++部分看得懂,学起来还是有一些门槛的。本文基于三天入门过程中走过的弯路,以先成功后学习的理念,总结出一篇完整开发串口调试助手的简单实践,让初学者少走弯路,快速体验完整开发流程,再去深入了解或研究其他高深内容。
提示:之所以写这篇文章,也是因为看了许多其他作者的文章大受益处,分享带来共赢
此串口调试助手功能还是比较完善的,使用起来丝毫不卡顿,启动速度也相当优秀,可打包成绿色版和安装版,用来代替市面上的串口调试助手是个不错的选择。可最重要的是有了开发技术,随意添加任何其他功能或对界面进行优化。给它取名为QComTool。
从官网下载安装包:https://download.qt.io/archive/qt/5.14/5.14.2/ 本文使用5.14.2版本,其他版本或许略有不同,尽量别下最新版,用最新版会走更多弯路。下载后直接运行安装。刚进入安装程序会要求输入注册账户,如果闲麻烦可以先断网,再运行安装程序,会自动跳过这一步,再去联网即可。
组件选择需特别注意,本文只需选择MinGW 7.30 64位系统,用来编译运行,其他组件可以根据需要勾选,由于篇幅限制不展开描述。
之后根据提示一路下一步即可。
安装成功后打开开始菜单栏打开Qt Creator软件,是用于应用程序开发的跨平台IDE。
选择Qt Widgets应用程序
设置项目路径和名称
编译系统默认
基类选择QWidget
然后一路下一步。创建成功后,直接Ctrl + R编译运行,或者点击左下角小锤子即可出现空白窗口。
工程项目目录很简洁,*。pro包含一些配置脚本,一个基类头文件和源文件再加主程序文件。最下面的是设计UI的文件。
打开UI设计文件,可以很方便的通过拖拽方式设计界面,并且配置属性和名称。
先添加三个主体框架,修改属性让边框更清晰,再点击阵列分布自动适应窗口大小并对齐。
其他部分同理,根据需要设计自己的界面,特别注意修改各个控件的名称方便写程序,可在右侧层次结构框架中直接单击修改。设计出以下界面。
若对这一步有困难,可以直接用记事本打开这个ui文件,复制以下代码,自动生成该界面。
<ui version="4.0">
<class>Widgetclass>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0x>
<y>0y>
<width>757width>
<height>529height>
rect>
property>
<property name="windowTitle">
<string>Widgetstring>
property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::Boxenum>
property>
<property name="frameShadow">
<enum>QFrame::Raisedenum>
property>
<property name="lineWidth">
<number>2number>
property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="formLayout_2">
<property name="verticalSpacing">
<number>20number>
property>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>串口号:string>
property>
widget>
item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox_1"/>
item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>波特率:string>
property>
widget>
item>
<item row="1" column="1">
<widget class="QComboBox" name="comboBox_2"/>
item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>校验位:string>
property>
widget>
item>
<item row="2" column="1">
<widget class="QComboBox" name="comboBox_3"/>
item>
<item row="3" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>数据位:string>
property>
widget>
item>
<item row="3" column="1">
<widget class="QComboBox" name="comboBox_4"/>
item>
<item row="4" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>停止位:string>
property>
widget>
item>
<item row="4" column="1">
<widget class="QComboBox" name="comboBox_5"/>
item>
<item row="5" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>流控制:string>
property>
widget>
item>
<item row="5" column="1">
<widget class="QComboBox" name="comboBox_6"/>
item>
<item row="6" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>字编码:string>
property>
widget>
item>
<item row="6" column="1">
<widget class="QComboBox" name="comboBox_7"/>
item>
layout>
item>
<item>
<widget class="QPushButton" name="Btn_Serial_Open">
<property name="text">
<string>打开串口string>
property>
widget>
item>
layout>
widget>
item>
<item row="0" column="1">
<widget class="QFrame" name="frame_2">
<property name="frameShape">
<enum>QFrame::Boxenum>
property>
<property name="frameShadow">
<enum>QFrame::Raisedenum>
property>
<property name="lineWidth">
<number>2number>
property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTextEdit" name="RX_Text">
<property name="tabChangesFocus">
<bool>falsebool>
property>
<property name="readOnly">
<bool>truebool>
property>
widget>
item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="Btn_Clear">
<property name="text">
<string>清空所有string>
property>
<property name="checkable">
<bool>falsebool>
property>
<property name="autoDefault">
<bool>falsebool>
property>
<property name="default">
<bool>falsebool>
property>
<property name="flat">
<bool>falsebool>
property>
widget>
item>
<item>
<layout class="QHBoxLayout" name="RX_Box">
<item>
<widget class="QCheckBox" name="RX_CheckBox_Hex">
<property name="text">
<string>Hexstring>
property>
<property name="checked">
<bool>falsebool>
property>
widget>
item>
<item>
<widget class="QCheckBox" name="RX_CheckBox_Format">
<property name="text">
<string>日志格式string>
property>
<property name="checked">
<bool>truebool>
property>
widget>
item>
layout>
item>
layout>
item>
layout>
widget>
item>
<item row="1" column="0" colspan="2">
<widget class="QFrame" name="frame_Send">
<property name="frameShape">
<enum>QFrame::Boxenum>
property>
<property name="frameShadow">
<enum>QFrame::Raisedenum>
property>
<property name="lineWidth">
<number>2number>
property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTextEdit" name="TX_Text"/>
item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QPushButton" name="Btn_Send">
<property name="text">
<string>发送string>
property>
<property name="checkable">
<bool>falsebool>
property>
<property name="checked">
<bool>falsebool>
property>
<property name="autoDefault">
<bool>falsebool>
property>
<property name="default">
<bool>falsebool>
property>
<property name="flat">
<bool>falsebool>
property>
widget>
item>
<item>
<widget class="QFrame" name="frame_4">
<property name="frameShape">
<enum>QFrame::StyledPanelenum>
property>
<property name="frameShadow">
<enum>QFrame::Raisedenum>
property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QCheckBox" name="Auto_Send">
<property name="text">
<string>自动发送string>
property>
widget>
item>
<item>
<widget class="QSpinBox" name="Wait_ms">
<property name="minimum">
<number>1number>
property>
<property name="maximum">
<number>10000number>
property>
<property name="value">
<number>100number>
property>
widget>
item>
<item>
<widget class="QLabel" name="label_9">
<property name="text">
<string>msstring>
property>
widget>
item>
layout>
widget>
item>
<item>
<layout class="QHBoxLayout" name="TX_Box">
<item>
<widget class="QCheckBox" name="TX_CheckBox_Hex">
<property name="text">
<string>Hexstring>
property>
widget>
item>
<item>
<widget class="QLabel" name="TX_Num">
<property name="text">
<string>已发送:0string>
property>
widget>
item>
<item>
<widget class="QLabel" name="RX_Num">
<property name="text">
<string>已接收:0string>
property>
widget>
item>
<item>
<widget class="QPushButton" name="Btn_Clear_Num">
<property name="text">
<string>清零string>
property>
<property name="checkable">
<bool>falsebool>
property>
<property name="autoDefault">
<bool>falsebool>
property>
<property name="default">
<bool>falsebool>
property>
<property name="flat">
<bool>falsebool>
property>
widget>
item>
layout>
item>
layout>
item>
layout>
widget>
item>
layout>
widget>
<resources/>
<connections/>
ui>
widget.h文件中加入以下方法和属性,其中,方法作为自定义槽函数,信号和槽是Qt中的核心概念,通俗的讲就是一个控件状态的改变(如按钮被点击)会发出特定信号,通过connect函数将信号连接到指定的槽函数中,有信号就能触发槽函数运行。
#ifndef WIDGET_H
#define WIDGET_H
#include
#include
#include
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
QSerialPort Serial;
QTimer time;
quint64 RX_Cnt;
quint64 TX_Cnt;
void Set_TX_Hex(bool checked);
void Check_TX_Text();
void Clear_Num();
void ComboBox_AddItems();
void Serial_Receive();
void Serial_Ready_Send();
void Serial_Send();
void Serial_Tab();
bool Serial_Open();
bool Serial_Close();
void OnTimeOut();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
这里添加函数实现以及配置各个信号与槽相连接
#include "widget.h"
#include "ui_widget.h"
#include
#include
#include
#include
#include
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
this->setWindowTitle("QComTool");
ui->frame_Send->setMaximumHeight(300);
RX_Cnt = TX_Cnt = 0;//计数清零
ui->Btn_Send->setEnabled(false);
//添加项目
ComboBox_AddItems();
//连接信号槽 当下位机发送数据QSerialPor会发送readyRead信号,定义槽Serial_Receive解析数据
connect(&Serial,&QSerialPort::readyRead,this,&Widget::Serial_Receive);
//打开串口按钮
connect(ui->Btn_Serial_Open,&QPushButton::clicked,this,&Widget::Serial_Tab);
//发送数据按钮
connect(ui->Btn_Send,&QPushButton::clicked,this,&Widget::Serial_Ready_Send);
//清空接收按钮
connect(ui->Btn_Clear,&QPushButton::clicked, ui->RX_Text,&QTextEdit::clear);
//数据清零按钮
connect(ui->Btn_Clear_Num,&QPushButton::clicked,this,&Widget::Clear_Num);
//切换发送为Hex
connect(ui->TX_CheckBox_Hex, &QCheckBox::toggled, this,&Widget::Set_TX_Hex);
//发送框文本改变
connect(ui->TX_Text,&QTextEdit::textChanged,this,&Widget::Check_TX_Text);
//自动发送
connect(&time,&QTimer::timeout,this,&Widget::Serial_Send);
//自动发送的循环时间
connect(ui->Wait_ms, QOverload<int>::of(&QSpinBox::valueChanged),
[=](int i){time.setInterval(i);});//匿名函数
}
Widget::~Widget()
{
delete ui;
}
//切换发送为Hex
void Widget::Set_TX_Hex(bool checked)
{
if (checked)
{
//编码格式
QByteArray code_name;
code_name.append(ui->comboBox_7->currentText());
QTextCodec *codec = QTextCodec::codecForName(code_name);
//数据转换
QByteArray Encoded_data = codec->fromUnicode(ui->TX_Text->toPlainText());
QString ret(Encoded_data.toHex().toUpper());
if(ret.length()%2 == 1)
{
ret.insert(ret.length()-1,"0");
}
int len = ret.length()/2;
for(int i=0;i<len;i++)
{
ret.insert(2*i+i+2," ");
}
ui->TX_Text->setText(ret);
}
else
{
QString ret = ui->TX_Text->toPlainText();
ret = ret.toUpper();//全部大写
ret.remove(QRegExp("[^A-F0-9]"));//正则匹配删除非法字符
QByteArray Data_Hex;
Data_Hex.append(ret);
QByteArray Data_Ascii = QByteArray::fromHex(Data_Hex);
//编码格式
QByteArray code_name;
code_name.append(ui->comboBox_7->currentText());
QTextCodec *codec = QTextCodec::codecForName(code_name);
//数据转换
QString Unicode_data = codec->toUnicode(Data_Ascii);
ui->TX_Text->setText(Unicode_data);
}
}
//校验发送框字符是否非法
void Widget::Check_TX_Text()
{
if(ui->TX_CheckBox_Hex->isChecked())
{
QString ret1 = ui->TX_Text->toPlainText(),ret2;
ret2 = ret1.toUpper();
ret1.remove(QRegExp("[A-F0-9][A-F0-9]\\s"));//先去除合法Hex字符
if(ret1.length() > 1)
{
ret2.remove(QRegExp("[^A-F0-9]"));//删除非法字符
if(ret2.length()%2 == 1)
{
ret2.insert(ret2.length()-1,"0");
}
int len = ret2.length()/2;
for(int i=0;i<len;i++)
{
ret2.insert(2*i+i+2," ");
}
ui->TX_Text->setText(ret2);
ui->TX_Text->moveCursor(QTextCursor::End, QTextCursor::MoveAnchor);
}
}
}
//下拉列表框添加项目
void Widget::ComboBox_AddItems()
{
QMetaEnum metaEnum;
QStringList strList;
//comboBox_1 可用串口
foreach(const QSerialPortInfo &info,QSerialPortInfo::availablePorts())
{
ui->comboBox_1->addItem(info.portName());
}
//comboBox_2 波特率
metaEnum = QMetaEnum::fromType<QSerialPort::BaudRate>();
for (int i = 0; i < metaEnum.keyCount()-1; i++)
{
ui->comboBox_2->addItem(QString::number(metaEnum.value(i)));
}
ui->comboBox_2->setCurrentText(QString::number(QSerialPort::Baud115200));
//comboBox_3 校验位
strList<<"无"<<"奇校验"<<"偶校验"<<"为0"<<"为1";
ui->comboBox_3->addItems(strList);
strList.clear();
//comboBox_4 数据位
metaEnum = QMetaEnum::fromType<QSerialPort::DataBits>();
for (int i = 0; i < metaEnum.keyCount()-1; i++)
{
ui->comboBox_4->addItem(QString::number(metaEnum.value(i)));
}
ui->comboBox_4->setCurrentText(QString::number(QSerialPort::Data8));
//comboBox_5 停止位
strList<<"1"<<"1.5"<<"2";
ui->comboBox_5->addItems(strList);
strList.clear();
//comboBox_6 流控
strList<<"无"<<"硬件"<<"软件";
ui->comboBox_6->addItems(strList);
strList.clear();
//字符编码格式
strList<<"GBK"<<"UTF-8";
ui->comboBox_7->addItems(strList);
strList.clear();
}
//数据清零
void Widget::Clear_Num()
{
RX_Cnt = TX_Cnt = 0;
ui->RX_Num->setText("已接收:"+QString::number(RX_Cnt));
ui->TX_Num->setText("已发送:"+QString::number(TX_Cnt));
}
//接收数据
void Widget::Serial_Receive()
{
QByteArray data = Serial.readAll();
RX_Cnt += data.length();
ui->RX_Num->setText("已接收:"+QString::number(RX_Cnt));
if(ui->RX_CheckBox_Format->isChecked())//添加日志
{
QDateTime time = QDateTime::currentDateTime();
QString current_date = time.toString("\n【yyyy.MM.dd hh:mm:ss.zzz 接收】\n");
ui->RX_Text->insertPlainText(current_date);
}
if(ui->RX_CheckBox_Hex->isChecked())//Hex模式
{
uchar temp;
QString Hex;
for (int i = 0; i < data.size(); i++)
{
temp = data.at(i);
if(temp < 0x10)Hex += "0";
Hex += QString::number(temp,16).toUpper()+" ";
}
ui->RX_Text->insertPlainText(Hex);
}
else
{
//编码格式
QByteArray code_name;
code_name.append(ui->comboBox_7->currentText());
QTextCodec *codec = QTextCodec::codecForName(code_name);
QString Unicode_data = codec->toUnicode(data);
ui->RX_Text->insertPlainText(Unicode_data);
}
ui->RX_Text->moveCursor(QTextCursor::End);
}
//发送数据准备工作
void Widget::Serial_Ready_Send()
{
if(ui->Auto_Send->isChecked())//自动发送使能
{
if(ui->Btn_Send->text().operator==("发送"))//开始发送
{
ui->Btn_Send->setText("停止发送");
time.setInterval(ui->Wait_ms->value());//循环时间
time.start();
}
else
{
ui->Btn_Send->setText("发送");
time.stop();
}
}
else
{
Serial_Send();
}
}
//发送数据
void Widget::Serial_Send()
{
if(ui->Auto_Send->isChecked() == false &&
ui->Btn_Send->text().operator==("停止发送"))//自动发送使能
{
ui->Btn_Send->setText("发送");
time.stop();
return;
}
QByteArray Data_QByteArray;
if(ui->TX_CheckBox_Hex->isChecked())//Hex格式发送
{
QString ret = ui->TX_Text->toPlainText();
ret = ret.toUpper();//全部大写
ret.remove(QRegExp("[^A-F0-9]"));//正则匹配删除非法字符
QByteArray Hex_Srting;
Hex_Srting.append(ret);
Data_QByteArray = QByteArray::fromHex(Hex_Srting);
}
else//字符串格式发送
{
//编码格式
QByteArray code_name;
code_name.append(ui->comboBox_7->currentText());
QTextCodec *codec = QTextCodec::codecForName(code_name);
//数据发送
Data_QByteArray = codec->fromUnicode(ui->TX_Text->toPlainText());
}
if(Data_QByteArray.length() > 0)
{
if(Serial.write(Data_QByteArray) >= 0)
{
if(ui->RX_CheckBox_Format->isChecked())//如果开启日志模式
{
QDateTime time = QDateTime::currentDateTime();
QString current_date = time.toString("\n【yyyy.MM.dd hh:mm:ss.zzz 发送】\n");
current_date += ui->TX_Text->toPlainText();
ui->RX_Text->insertPlainText(current_date);
ui->RX_Text->moveCursor(QTextCursor::End);
}
TX_Cnt += Data_QByteArray.length();
ui->TX_Num->setText("已发送:"+QString::number(TX_Cnt));
}
else
{
QMessageBox::critical(this, tr("错误"), tr("串口发送失败"));
}
}
}
//切换串口状态
void Widget::Serial_Tab()
{
if(ui->Btn_Serial_Open->text().operator==("打开串口"))
{
if(Serial_Open())
{
ui->Btn_Serial_Open->setText("关闭串口");
ui->Btn_Send->setEnabled(true);
ui->Btn_Send->setText("发送");
time.stop();
}
else
{
QMessageBox::critical(this, tr("错误"), tr("串口打开失败"));
}
}
else
{
if(Serial_Close())
{
ui->Btn_Serial_Open->setText("打开串口");
ui->Btn_Send->setEnabled(false);
}
}
}
//打开串口
bool Widget::Serial_Open()
{
Serial.setParent(this);
QString PortName = ui->comboBox_1->currentText();
QString Temp_String;
//这样我们就获取到 可用的串口名字了
Serial.setPortName(PortName);
if(Serial.isOpen())//如果串口已经打开了 先给他关闭了
{
Serial.clear();
Serial.close();
}
if(Serial.open(QIODevice::ReadWrite))//用ReadWrite 的模式尝试打开串口
{
//波特率设置
Serial.setBaudRate(ui->comboBox_2->currentText().toInt(),QSerialPort::AllDirections);//设置波特率和读写方向
//校验位设置
Temp_String = ui->comboBox_3->currentText();
QSerialPort::Parity Serial_Parity;
if(Temp_String.operator == ("无" ))
Serial_Parity = QSerialPort::NoParity;
else if (Temp_String.operator == ("奇校验"))
Serial_Parity = QSerialPort::EvenParity;
else if (Temp_String.operator == ("偶校验"))
Serial_Parity = QSerialPort::OddParity;
else if (Temp_String.operator == ("为0"))
Serial_Parity = QSerialPort::SpaceParity;
else if (Temp_String.operator == ("为1"))
Serial_Parity = QSerialPort::MarkParity;
else
Serial_Parity = QSerialPort::UnknownParity;
Serial.setParity(Serial_Parity);
//数据位设置
Temp_String = ui->comboBox_4->currentText();
QSerialPort::DataBits Serial_DataBits;
if(Temp_String.operator == ("5"))
Serial_DataBits = QSerialPort::Data5;
else if (Temp_String.operator == ("6"))
Serial_DataBits = QSerialPort::Data6;
else if (Temp_String.operator == ("7"))
Serial_DataBits = QSerialPort::Data7;
else if (Temp_String.operator == ("8"))
Serial_DataBits = QSerialPort::Data8;
else
Serial_DataBits = QSerialPort::UnknownDataBits;
Serial.setDataBits(Serial_DataBits); //数据位
//停止位
Temp_String = ui->comboBox_5->currentText();
QSerialPort::StopBits Serial_StopBits;
if(Temp_String.operator == ("1"))
Serial_StopBits = QSerialPort::OneStop;
else if (Temp_String.operator == ("1.5"))
Serial_StopBits = QSerialPort::OneAndHalfStop;
else if (Temp_String.operator == ("2"))
Serial_StopBits = QSerialPort::TwoStop;
else
Serial_StopBits = QSerialPort::UnknownStopBits;
Serial.setStopBits(Serial_StopBits);
//流控制
Temp_String = ui->comboBox_6->currentText();
QSerialPort::FlowControl Serial_FlowControl;
if(Temp_String.operator == ("无"))
Serial_FlowControl = QSerialPort::NoFlowControl;
else if (Temp_String.operator == ("硬件"))
Serial_FlowControl = QSerialPort::HardwareControl;
else if (Temp_String.operator == ("软件"))
Serial_FlowControl = QSerialPort::SoftwareControl;
else
Serial_FlowControl = QSerialPort::UnknownFlowControl;
Serial.setFlowControl(Serial_FlowControl);
return true;
}
return false;
}
//关闭串口
bool Widget::Serial_Close()
{
if(Serial.isOpen())//关闭旧串口
{
Serial.clear();
Serial.close();
return true;
}
return false;
}
到这里可能还有一些列报错,不过没关系,只要按照以上步骤,再点击编译后就不会报错。先给它添加一个图标,可将以下图片通过这个网站https://www.aconvert.com/cn/icon/jpg-to-ico/生成ICO图标,命名为QComTool.ico放到项目文件夹中,
再Pro文件中加入这一行代码添加图标资源RC_ICONS = QComTool.ico
选择发布版进行编译,同样Ctrl+R
于是正常了发布版程序
但此时这个程序还不能运行,需要链接动态库文件,也就是传说中的dll.
新建一个目录,命名为QComTool
将exe文件放到这个目录中
windeployqt.exe 文件绝对路径\QComTool.exe
程序会自动将所有动态链接库复制到这个目录中
这样就得到了一个绿色版的程序,大功告成,点击exe即可运行。
如果还需要打包成安装版,可以使用工具 innosetup-6.2.1.exe
进行打包,官网下载链接:
https://jrsoftware.org/isinfo.php,只需通过这个工具把绿色版整个文件夹打包成安装包程序即可。
通过本文实现了串口调试助手功能,但篇幅限制没有展开描述,相信有C++基础的基本能够看懂程序,更多功能等待探索。