学习视频链接

完整项目链接:

https://github.com/lejunXTS/QT.git

https://github.com/lejunXTS/coinfilp/tree/main

此系列博客代码是我在学习过程中自己手敲实现一遍的,作为一个学习过程的记录。为了防止误解,我在此再次说明一下。如有侵权,请私信我。

概述

什么是QT

QT是一个跨平台的C++图像用户界面应用程序框架

为应用程序开发者提供建立艺术级图形界面所需的所有功能,它是完全面向对象的,很容易扩展,并且允许真正的组件编程

Qt的发展史

1991年 Qt最早由奇趣科技开发

1996年 进入商业领域,它也是目前流行的Linux桌面环境KDE的基础

2008年 奇趣科技被诺基亚公司收购,Qt称为诺基亚旗下的编程语言

2012年 Qt又被Digia公司收购

2014年4月 跨平台的集成开发环境Qt Creator3.1.0发布,同年5月20日配发了Qt5.3正式版,至此Qt实现了对iOS、Android、WP等各平台的全面支持

当前Qt最新版本为 Qt 6

支持的平台

Windows – XP、Vista、Win7、Win8、Win2008、Win10、Win11

Uinux/X11 – Linux、Sun Solaris、HP-UX、Compaq Tru64 UNIX、IBM AIX、SGI IRIX、FreeBSD、BSD/OS、和其他很多X11平台

Macintosh – Mac OS X

Embedded – 有帧缓冲支持的嵌入式Linux平台,Windows CE

Qt版本

Qt按照不同的版本发行,分为商业版和开源版

商业版:

为商业软件提供开发,他们提供传统商业软件发行版,并且提供在商业有效期内的免费升级和技术支持服务。

开源的LGPL版本:

为了开发自有而设计的开放源码软件,它提供了和商业版本同样的功能,在GNU通用公共许可下,它是免费的。

QT的优点

跨平台,几乎支持所有的平台

接口简单,容易上手

一定程度上简化了内存回收机制

开发效率高,能够快速的构建应用程序

有很好的社区氛围

可以进行嵌入式开发

成功案例

Linux桌面环境KDE

WPS Office 办公软件

Skype 网络电话

Google Earth 谷歌地图

VLC多媒体播放器

VirtualBox虚拟机软件

创建项目

Qt中的构建⼯具有三种:qmake CMake Qbs

qmake:qmake是⼀个构建⼯具(build tool),⽤于⾃动⽣成makefile⽂件。qmake⽀持跨平台构建。qmake编辑的是⼀个后缀名为.pro的⽂件。

CMake:CMake是⼀个跨平台的构建⼯具。CMake本⾝不是⼀个编译器,其实就是⽣成⼀个让编译器能读懂编译流程的⽂件⼯具。让CMake⾃动⽣成构建系统,例如Makefile和Visual Studio项⽬⽂件。CMake是⼀个第三⽅⼯具,有⾃⼰的⽂档。

Qbs:Qbs(Qt Build Suite:Qt构建套件)同qmake、CMake⼀样都是构建⼯具。Qbs号称是新⼀代的构建⼯具,⽐qmake编译速度更快。Qbs没有绑定Qt版本,它从项⽬⽂件的⾼级项⽬描述中⽣成⼀个正确的依赖表。⽽传统的MakeFile⽣成⼯具如qmake和CMake,其在⽣成MakeFile⽂件后将实际的命令交给Make⼯具去执⾏。

基类选择

QWidget(父类)——最简单、最基本的窗体程序,里面可以放置多个控件实现程序功能

QMainWindow(子类)——主窗口类,一般用于较为复杂的应用程序,除了中央客户区界面,还包括菜单栏、工具栏、状态栏以及多个可停靠的工具对话框等

QDialog(子类)——基于对话框的程序,对话框一般用于弹窗,也可以用于主界面显示,对话框是从QWidget继承而来,并丰富了一些功能,如模态显示和返回值等

版本控制系统:

svn vss git

创建QT项目

第一个QT项目

#include "mywidget.h"

#include <QApplication> //包含一个应用程序类的头文件

//main程序入口 argc 命令行变量的数量 argv 命令行变量的数组
int main(int argc, char *argv[])
{
//a应用程序对象,在qt中,应用程序对象有且仅有一个
QApplication a(argc, argv);
//窗口对象 myWidget的父类——>QWidget
myWidget w;
//调用show方法 窗口对象默认不会显示,必须要调用show方法显示窗口
w.show();
//让应用程序对象进入消息循环
//让代码阻塞到这行
return a.exec();
}

Qt系统提供的标准类名声明头文件没有.h后缀

Qt一个类对应一个头文件,类名就是头文件名

QApplication应用程序类:

管理图形用户界面应用程序的控制流和主要设置。

是Qt的整个后台管理的命脉它包含主事件循环,在其中来自窗口系统和其它资源的所有事件处理和调度。它也处理应用程序的初始化和结束,并且提供对话管理。

对于任何一个使用Qt的图形用户界面应用程序,都正好存在一个QApplication对象,而不论这个应用程序在同一时间内是不是有0、1、2或更多个窗口。

a.exec()
程序进入消息循环,等待对用户输入进行响应。这里main()把控制权转交给Qt,Qt完成事件处理工作,当应用程序退出的时候exec()的值就会返回。在exec()中,Qt接受并处理用户和系统的事件并且把它们传递给适当的窗口部件。

#ifndef MYWIDGET_H //防止头文件重复包含
#define MYWIDGET_H

#include <QWidget>//包含QWidget 窗口类

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

public:
myWidget(QWidget *parent = nullptr);//构造函数
~myWidget();//析构函数
};
#endif // MYWIDGET_H
#include "mywidget.h"

//命名规范
//类名:首字母大写,单词和单词之间首字母大写(大驼峰)
//函数名 变量名称,首字母小写,单词和单词之间字母大写(小驼峰)

//快捷键
//注释 ctrl+/
//运行 ctrl+r
//编译 ctrl+b
//字体缩放 Ctrl+鼠标滚轮
//查找 Ctrl+f
//整行移动 Ctrl+shift+上或者下
//帮助文档 F1
//自动对齐 Ctrl+i
//同名之间的.h和.cpp切换 F4



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

myWidget::~myWidget() {}

.pro 文件介绍

//QT包含的模块
QT += core gui
//大于4版本以上 包含widget模块
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
//这一行指定了C++标准为C++17,用于告诉编译器使用C++17标准编译代码
CONFIG += c++17

# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
//源文件
SOURCES += \
main.cpp \
mywidget.cpp
//头文件
HEADERS += \
mywidget.h
//项目的部署规则,根据目标平台将可执行文件安装到不同的目录。
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

按钮控件的常用API

myWidget::myWidget(QWidget *parent)
: QWidget(parent)//初始化列表
{
//创建一个按钮
QPushButton * btn = new QPushButton;
//btn->show();//show以顶层方式单出窗口控件
//让btn对象依赖在myWidget窗口中
btn->setParent(this);
//显示文本
btn->setText("the first button");



//创建第二个按钮 按照控件的大小创建窗口
QPushButton * btn2 = new QPushButton("the second button",this);

//移动btn2按钮
btn2->move(100,100);

// //重置按钮大小
// btn2->resize(60,60);


//重置窗口大小
resize(500,300);

//设置固定窗口大小
setFixedSize(500,300);

//设置窗口标题
setWindowTitle("the first window");
}

对象树

当创建的对象在堆区的时候,如果指定的父类是QObject派生下来的类或者QObject子类派生下来的类,可以不用管理释放的操作,对象会放入到对象树中。

一定的程度上简化了内存回收机制

窗口坐标体系

坐标体系:

以左上角为原点(0,0),X向右增加,Y向下增加

对于嵌套窗口,其坐标是相对于父窗口来说的

信号和槽机制

连接方式

connect(信号的发送者,发送的具体信号,信号的接受者,信号的处理(槽函数))

信号槽的优点:松散耦合,信号发送端和接收端本身是没有关联的,通过connect连接将两端耦合在一起

//创建一个自己的按钮对象
MyPushButton * myBtn = new MyPushButton;
myBtn->setText("my own button");

myBtn->move(100,200);
myBtn->setParent(this);//设置到对象树中


//需求——点击我的按钮,关闭窗口
//参数1 信号的发送者 参数二 发送的信号(函数的地址)
//参数3 信号的接受者 参数4 处理的槽函数(函数的地址)
//connect(myBtn, &MyPushButton::clicked, this, &myWidget::close);
connect(myBtn, &QPushButton::clicked, this, &QWidget::close);

自定义信号和槽

#ifndef TEACHER_H
#define TEACHER_H

#include <QObject>

class Teacher : public QObject
{
Q_OBJECT
public:
explicit Teacher(QObject *parent = nullptr);

signals:
//自定义信号 写到signals下
//返回值是void 只需要声明 不需要实现
//可以有参数 可以重载
void hungry();
};

#endif // TEACHER_H
#include "teacher.h"

Teacher::Teacher(QObject *parent)
: QObject{parent}
{}
#ifndef STUDENT_H
#define STUDENT_H

#include <QObject>

class Student : public QObject
{
Q_OBJECT
public:
explicit Student(QObject *parent = nullptr);

//早起版本槽函数写在public slots下,5.4版本后写在public或者全局下
//返回值void 需要声明 需要实现
//可以有参数 可以重载
void treat();

signals:

};

#endif // STUDENT_H
#include "student.h"
#include <QDebug>

Student::Student(QObject *parent)
: QObject{parent}
{}

void Student::treat()
{
qDebug() << "please eat teacher";
}
#ifndef WIDGET_H
#define WIDGET_H

#include "teacher.h"
#include "student.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();

private:
Ui::Widget *ui;

Teacher * cc;
Student * ll;

void classIsOver();
};
#endif // WIDGET_H
#include "widget.h"
#include "./ui_widget.h"

//Teacher 类 老师类
//Student 类 学生类
//下课后,老师会触发一个信号,饿了,学生相应信号,请客吃饭

Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);

//创建老师对象
this->cc = new Teacher(this);

//创建学生对象
this->ll = new Student(this);

//老师饿了 学生请老师吃饭
connect(cc, &Teacher::hungry, ll, &Student::treat);

//调用下课函数
classIsOver();
}

void Widget::classIsOver()
{
//下课函数 调用后 触发老师饿了的信号
emit cc->hungry();
}

Widget::~Widget()
{
delete ui;
}
#include "widget.h"

#include <QApplication>

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}

重载

当自定义信号和槽出现重载,需要利用函数指针明确指向函数的地址

void hungry();

void hungry(QString foodName);
void treat();

void treat(QString foodName);
emit cc->hungry("sandwich");
//连接带参数的信号和槽
//指针->地址
//函数指针->函数地址
void(Teacher:: *teacherSignal)(QString) = &Teacher::hungry;
void(Student:: *studentSlot)(QString) = &Student::treat;
connect(cc, teacherSignal, ll, studentSlot);

classIsOver();

点击一个下课的按钮再触发下课 & 信号连接信号

Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);

//创建老师对象
this->cc = new Teacher(this);

//创建学生对象
this->ll = new Student(this);

// //老师饿了 学生请老师吃饭
// connect(cc, &Teacher::hungry, ll, &Student::treat);

// //调用下课函数
// classIsOver();


//连接带参数的信号和槽
//指针->地址
//函数指针->函数地址
void(Teacher:: *teacherSignal)(QString) = &Teacher::hungry;
void(Student:: *studentSlot)(QString) = &Student::treat;
connect(cc, teacherSignal, ll, studentSlot);

//classIsOver();


//点击一个下课的按钮再触发下课
QPushButton * btn = new QPushButton("class over", this);

//重置窗口大小
resize(600,400);

//点击按钮出发下课
//connect(btn, &QPushButton::clicked, this, &Widget::classIsOver);


//无参信号和槽连接
void(Teacher:: *teacherSignal2)(void) = &Teacher::hungry;
void(Student:: *studentSlot2)(void) = &Student::treat;
connect(cc, teacherSignal2, ll, studentSlot2);

//信号连接信号
connect(btn, &QPushButton::clicked, cc, teacherSignal2);

//断开信号
//disconnect(cc, teacherSignal2, ll, studentSlot2)

}

其他

QString->char * 先转成QByteArray (.toUtf8()) 再转成char * ()

  • 一个信号可以连接多个槽函数

  • 多个信号可以连接同一个槽函数

  • 信号和槽函数的参数必须类型一一对应

  • 信号和槽的参数个数不需要一致,信号的参数个数可以多于槽函数的参数个数

//QT4版本以前的信号和槽连接方式
//利用QT4信号槽连接无参版本
//QT4版本底层SIGNAL("hungry") SLOT("treat")
connect(cc, SIGNAL(hungry()), ll, SLOT(treat()));
//QT4版本优点:参数直观 缺点:类型不做检测
//QT5以上版本支持QT4版本写法,反之不支持

Lambda表达式

[]标识符 匿名函数

= 值传递

& 引用传递

() 参数 {}实现体

mutable 修饰值传递变量,可以修改拷贝出的数据,改变不了本体

返回值 ->int{}

最常用 ={}

C++11中的Lambda表达式(这个再找资料看一下)

//利用lambda表达式实现点击按钮关闭窗口
QPushButton * btn2 = new QPushButton;
btn2->setText("close");
btn2->move(100,0);
btn2->setParent(this);

connect(btn2, &QPushButton::clicked, this, [=](){
this->close();
emit cc->hungry("sandwich");

//btn2->setText("aaa");
});

QMainWindow

简介

QMainWindow是一个为用户提供主窗口程序的类,包含一个菜单栏(menu bar)、多个工具栏(tool bars)、多个锚接部件(dock widgets)(浮动窗口)、一个状态栏(status bar)及一个中心部件(central widget),是许多应用程序的基础,如文本编辑器,图片编辑器等。

基础&常用操作

#include "mainwindow.h"
#include <QMenuBar>
#include <QToolBar>
#include <QDebug>
#include <QPushButton>
#include <QStatusBar>
#include <QLabel>
#include <QDockWidget>
#include <QTextEdit>

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
//重置窗口大小
resize(600,400);


//菜单栏--------------------------------------------------------


//菜单栏 最多只能有一个
//菜单栏创建
QMenuBar * bar = menuBar();

//将菜单栏放入到 窗口中
setMenuBar(bar);

//创建菜单
QMenu * fileMenu = bar->addMenu("文件");
QMenu * editMenu = bar->addMenu("编辑");

//创建菜单项
QAction * newAction = fileMenu->addAction("新建");
//添加分隔符
fileMenu->addSeparator();
QAction * openAction = fileMenu->addAction("打开");


//工具栏--------------------------------------------------------


//工具栏 可以有多个
QToolBar * toolBar = new QToolBar(this);
addToolBar(Qt::LeftToolBarArea, toolBar);

//后期设置只允许左右停靠
toolBar->setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea);

//设置浮动
//toolBar->setFloatable(false);

//设置移动(总开关)
//toolBar->setMovable(false);

//在工具栏中设置内容
toolBar->addAction(newAction);
//添加分割线
toolBar->addSeparator();
toolBar->addAction(openAction);

//工具栏中添加控件
QPushButton * btn = new QPushButton("aa", this);
toolBar->addWidget(btn);


//状态栏--------------------------------------------------------


//状态栏 最多有一个
QStatusBar * stBar = statusBar();

//设置到窗口中
setStatusBar(stBar);

//放标签控件
QLabel * label = new QLabel("提示信息", this);
stBar->addWidget(label);

QLabel * label2 = new QLabel("右侧提示信息", this);
stBar->addPermanentWidget(label2);


//锚接部件--------------------------------------------------------


//锚接部件 (浮动窗口) 可以有多个
QDockWidget * dockWidget = new QDockWidget("浮动", this);
addDockWidget(Qt::BottomDockWidgetArea, dockWidget);

//设置后期停靠区域,只允许上下
dockWidget->setAllowedAreas(Qt::TopDockWidgetArea | Qt::BottomDockWidgetArea);



//中心部件--------------------------------------------------------


//设置中心部件(核心部件) 只能有一个
QTextEdit * edit = new QTextEdit(this);
setCentralWidget(edit);

}

MainWindow::~MainWindow() {}

资源文件

  1. 将图片文件拷贝到项目文件下

  2. 创建新文件->QT->QT resource file->给资源文件起名

  3. res生成res.qrc

  4. open in editor 编辑资源

  5. 添加前缀 添加文件

  6. 使用”: + 前缀名 + 文件名”

#include "mainwindow.h"
#include "./ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);

//ui->actionnew->setIcon(QIcon("D:/喜欢/罗小黑&其他/001VEnKKgy1gsu6mbym6vj60t41dan4j02.jpg"));

//使用添加QT资源 ": + 前缀名 + 文件名"
ui->actionnew->setIcon(QIcon(":/image/001VEnKKgy1gsu6mbym6vj60t41dan4j02.jpg"));
ui->actionopen->setIcon(QIcon(":/image/005y0Ylbly1hhdrfsc6pbj32p51pinpe.jpg"));


}

对话框QDialog

自定义对话框

模态与非模态对话框的创建

#include "mainwindow.h"
#include "./ui_mainwindow.h"
#include <QDialog>
#include <QDebug>

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);

//点击新建按钮,弹出一个对话框
connect(ui->actionnew,&QAction::triggered, [=](){
//对话框 分类
//模态对话框(不可以对其他窗口进行操作) 非模态对话框(可以对其他窗口进行操作)

// //模态创建 阻塞
// QDialog dlg(this);
// dlg.resize(300,200);
// dlg.exec();

// exec():是 QDialog 类的成员函数,用于以模态(Modal)的方式显示对话框。
// 模态对话框会阻塞程序的执行,直到用户关闭对话框并返回结果。

// qDebug() << "模态对话框弹出了";


//非模态对话框
QDialog * dlg2 = new QDialog(this);
dlg2->resize(300,200);
dlg2->show();
dlg2->setAttribute(Qt::WA_DeleteOnClose);//55号属性

qDebug() << "非模态对话框弹出了";

});
}

MainWindow::~MainWindow()
{
delete ui;
}

标准对话框

消息对话框

#include "mainwindow.h"
#include "./ui_mainwindow.h"
#include <QDialog>
#include <QDebug>
#include <QMessageBox>

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);

//点击新建按钮,弹出一个对话框
connect(ui->actionnew,&QAction::triggered, [=](){
//对话框 分类
//模态对话框(不可以对其他窗口进行操作) 非模态对话框(可以对其他窗口进行操作)

// //模态创建 阻塞
// QDialog dlg(this);
// dlg.resize(300,200);
// dlg.exec();

// exec():是 QDialog 类的成员函数,用于以模态(Modal)的方式显示对话框。
// 模态对话框会阻塞程序的执行,直到用户关闭对话框并返回结果。

// qDebug() << "模态对话框弹出了";


// //非模态对话框
// QDialog * dlg2 = new QDialog(this);
// dlg2->resize(300,200);
// dlg2->show();
// dlg2->setAttribute(Qt::WA_DeleteOnClose);//55号属性

// qDebug() << "非模态对话框弹出了";


//消息对话框-----------------------------------------
// //错误对话框
// QMessageBox::critical(this, "critical", "error");

// //信息对话框
// QMessageBox::information(this, "info", "information");

//提问对话框
//参数1 父亲 参数2 标题 参数3 提示内容 参数4 按键类型 参数5 默认关联回车按键
//QMessageBox::question(this, "ques", "question");
//QMessageBox::question(this, "ques", "question", QMessageBox::Save | QMessageBox::Cancel);
//QMessageBox::question(this, "ques", "question", QMessageBox::Save | QMessageBox::Cancel, QMessageBox::Cancel);

// if (QMessageBox::Save == QMessageBox::question(this, "ques", "question", QMessageBox::Save | QMessageBox::Cancel))
// {
// qDebug() << "select save";
// }
// else
// {
// qDebug() << "select cancel";
// }


//警告对话框
QMessageBox::warning(this, "war", "waring");

});
}

MainWindow::~MainWindow()
{
delete ui;
}

其他标准对话框

QColorDialog: 选择颜色

//颜色对话框
//QColorDialog::getColor(QColor(255,0,0));
QColor color = QColorDialog::getColor(QColor(255,0,0));
qDebug() << "r = " << color.red() << "g = " << color.green() << "b = "
<< color.blue();

QFileDialog: 选择文件或者目录

//文件对话框
//参数1 父亲 参数2 标题 参数3 默认打开路径 参数4 过滤文件格式
//返回值是选取的路径
QString str = QFileDialog::getOpenFileName(this, "open file", "C:\\Users\\****\\Desktop", "(*.txt)");
qDebug() << str;

QFontDialog: 选择字体

//字体对话框
bool flag;
QFont font = QFontDialog::getFont(&flag, QFont("华文彩云", 36));
qDebug() << "字体:" << font.family().toUtf8().data() << "字号:" << font.pointSize()
<< "是否加粗:" << font.bold() << "是否倾斜:" << font.italic();

QInputDialog: 允许用户输入一个值,并将其值返回

QMessageBox: 模态对话框,用于显示信息、询问问题等

QPageSetupDialog: 为打印机提供纸张相关的选项

QPrintDialog: 打印机配置

QPrintPreviewDialog:打印预览

QProgressDialog: 显示操作过程

界面布局

实现登录窗口

利用布局方式给窗口进行美化

选取widget进行布局,水平布局,垂直布局,栅格布局

给用户名、密码、登录、退出按钮进行布局

默认窗口和控件之间有6像素,可以调整layoutLeftMargin

利用弹簧进行布局

调整固定高度

常用控件

按钮组

QPushButton 常用按钮

QToolButton 工具按钮 用于显示图片 显示文字

toolButtonStyle 凸起风格 autoRaise

radioButton 单选按钮 默认ui->radioButtonMan->setChecked(true);

checkBox 多选按钮 监听状态

//设置单选按钮 男默认选中
ui->radioButtonMan->setChecked(true);

//选中女后打印信息
connect(ui->radioButtonWoman, &QRadioButton::clicked, [=](){
qDebug() << "select woman!";
});


//多选按钮 2是选中 0是未选中 1是半选
connect(ui->checkBox_4, &QCheckBox::stateChanged, [=](int state){
qDebug() << state;
});

QListWidgetItem 列表容器

QListWidgetItem * item 一行内容
ui->listWidget->addItem(item)

可以利用additems一次性添加整个内容

// //利用listWidget写诗
// QListWidgetItem * item = new QListWidgetItem("人生天地间");

// //将一行诗放入到listWidget控件中
// ui->listWidget->addItem(item);
//设置居中方式
// item->setTextAlignment(Qt::AlignCenter);


//QStringList QList<QString>
QStringList list;
list << "人生天地间" << "忽如远行客" << "斗酒相娱乐" << "聊厚不为薄";
ui->listWidget->addItems(list);

QTreeWidget 树控件

//treeWidget控件的使用

//设置水平头
ui->treeWidget->setHeaderLabels(QStringList()<< "一" << "内容");

//创建顶层节点
QTreeWidgetItem * liTtem = new QTreeWidgetItem(QStringList()<< "一");
QTreeWidgetItem * liTtem2 = new QTreeWidgetItem(QStringList()<< "二");

//加载顶层节点
ui->treeWidget->addTopLevelItem(liTtem);
ui->treeWidget->addTopLevelItem(liTtem2);

//追加子节点
//QTreeWidgetItem * ll = new QTreeWidgetItem(QStringList()<< "一帆一江一渔舟"<<"一个渔翁一个钩");
// QStringList first;
// first << "一帆一江一渔舟,一个渔翁一个钩";
// QTreeWidgetItem * ll = new QTreeWidgetItem(first);
// liTtem->addChild(ll);


// QStringList second;
// second << "一俯一仰一场笑,一江明月一江秋";
// QTreeWidgetItem * ll2 = new QTreeWidgetItem(second);
// liTtem2->addChild(ll2);

QTreeWidgetItem *childItem1 = new QTreeWidgetItem(QStringList() <<""<<"一帆一江一渔舟,一个渔翁一个钩");
QTreeWidgetItem *childItem2 = new QTreeWidgetItem(QStringList() << ""<<"一俯一仰一场笑,一江明月一江秋");
liTtem->addChild(childItem1);
liTtem2->addChild(childItem2);

QTableWidget 表格控件

//TableWidget 控件
//设置列数
ui->tableWidget->setColumnCount(3);

//设置水平表头
ui->tableWidget->setHorizontalHeaderLabels(QStringList()<<"姓名"<<"性别"<<"年龄");

//设置行数
ui->tableWidget->setRowCount(5);

//设置正文
//ui->tableWidget->setItem(0,0,new QTableWidgetItem("小李"));

QStringList nameList;
nameList<<"小李"<<"小王"<<"小杨"<<"小刘"<<"小赵";

QList<QString>sexList;
sexList<<"女"<<"女"<<"女"<<"女"<<"女";


for(int i = 0; i <5; i++) {
int col = 0;
ui->tableWidget->setItem(i,col++,new QTableWidgetItem(nameList[i]));
ui->tableWidget->setItem(i,col++,new QTableWidgetItem(sexList.at(i))); //at越界抛出异常
//int 转 QString
ui->tableWidget->setItem(i,col++,new QTableWidgetItem(QString::number(i+18)));
}

其他控件

//栈控件的使用---------------------------------------------------------

//设置默认定位Button

//Button按钮
connect(ui->Button, &QPushButton::clicked, [=](){
ui->stackedWidget->setCurrentIndex(0);
});

//buttonTabBox按钮
connect(ui->buttonTabBox, &QPushButton::clicked, [=](){
ui->stackedWidget->setCurrentIndex(1);
});

//ButtonTabWidget按钮
connect(ui->ButtonTabWidget, &QPushButton::clicked, [=](){
ui->stackedWidget->setCurrentIndex(2);
});

//--------------------------------------------------------------------

//下拉框
ui->comboBox->addItem("杜鹃");
ui->comboBox->addItem("樱花");
ui->comboBox->addItem("海棠");

//点击按钮 选中海棠
connect(ui->btnSelect, &QPushButton::clicked, [=](){
//ui->comboBox->setCurrentIndex(2);
ui->comboBox->setCurrentText("海棠");
});


//-------------------------------------------------------------------

//利用QLabel显示图片
ui->labelImage->setPixmap(QPixmap(":/image/005y0Ylbly1hj9kxm09i0j31kw1kw1ky.jpg"));

//利用QLabel显示gif动态图片
QMovie * movie = new QMovie(":/image/小太阳gif.jpg");
ui->labelMovie->setMovie(movie);
//播放动图
movie->start();

自定义控件封装

添加新文件——QT——设计师界面类(.h .cpp .ui)

.ui中设计QStringBox 和 QSlider 两个控件 组成一个控件

Widget中使用自定义控件 拖拽一个Widget控件 点击提升为 添加类名等

实现功能 改变数字 滑动条跟着移动

提供getNumber和setNumber两个对外接口

#ifndef SMALLWIDGET_H
#define SMALLWIDGET_H

#include <QWidget>

namespace Ui {
class SmallWidget;
}

class SmallWidget : public QWidget
{
Q_OBJECT

public:
explicit SmallWidget(QWidget *parent = nullptr);
~SmallWidget();

//设置数字
void setNumber(int num);

//获取数字
int getNumber();

private:
Ui::SmallWidget *ui;
};

#endif // SMALLWIDGET_H
#include "smallwidget.h"
#include "ui_smallwidget.h"

SmallWidget::SmallWidget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::SmallWidget)
{
ui->setupUi(this);

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

//QSlider滑动 QStringBox数字跟着改变
connect(ui->horizontalSlider, &QSlider::valueChanged, ui->spinBox, &QSpinBox::setValue);
}


//设置数字
void SmallWidget::setNumber(int num)
{
ui->spinBox->setValue(num);
}

//获取数字
int SmallWidget::getNumber()
{
return ui->spinBox->value();
}


SmallWidget::~SmallWidget()
{
delete ui;
}
#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();

private:
Ui::Widget *ui;
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>

Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);

//点击获取 获取控件当前的值
connect(ui->btnGet, &QPushButton::clicked, [=](){
qDebug() << ui->widget->getNumber();
});

//点击设置 设置到一半
connect(ui->btnSet, &QPushButton::clicked, [=](){
ui->widget->setNumber(50);
});
}

Widget::~Widget()
{
delete ui;
}

Qt 中的事件

#ifndef MYLABEL_H
#define MYLABEL_H

#include <QLabel>

#include <QEnterEvent>

class myLabel : public QLabel
{
Q_OBJECT
public:
explicit myLabel(QWidget *parent = nullptr);

//鼠标进入事件
void enterEvent(QEnterEvent *event);

//鼠标离开事件
void leaveEvent(QEvent *event);

signals:
};

#endif // MYLABEL_H
#include "mylabel.h"
#include <QDebug>

myLabel::myLabel(QWidget *parent)
: QLabel{parent}
{}


//鼠标进入事件
void myLabel::enterEvent(QEnterEvent *event)
{
qDebug()<< "鼠标进入了";
}

//鼠标离开事件
void myLabel::leaveEvent(QEvent *event)
{
qDebug()<< "鼠标离开了";
}

其他

//鼠标按下
virtual void mousePressEvent(QMouseEvent *ev);

//鼠标释放
virtual void mouseReleaseEvent(QMouseEvent *ev);

//鼠标移动
virtual void mouseMoveEvent(QMouseEvent *ev);
#include "mylabel.h"
#include <QDebug>
#include <QMouseEvent>

myLabel::myLabel(QWidget *parent)
: QLabel{parent}
{
//设置鼠标追踪
setMouseTracking(true);
}

//鼠标按下
void myLabel::mousePressEvent(QMouseEvent *ev)
{
//当鼠标左键按下 提示信息
if (ev->button() == Qt::LeftButton)
{
QString str = QString("鼠标按下了 x = %1 y = %2 "
"globalx = %3 globaly = %4").arg(ev->x()).arg(ev->y())
.arg(ev->globalX()).arg(ev->globalY());
//qDebug()<< "鼠标按下了";
qDebug()<< str;
}
}

//鼠标释放
void myLabel::mouseReleaseEvent(QMouseEvent *ev)
{
// if (ev->button() == Qt::LeftButton)
// {
QString str = QString("鼠标释放了 x = %1 y = %2 "
"globalx = %3 globaly = %4").arg(ev->x()).arg(ev->y())
.arg(ev->globalX()).arg(ev->globalY());
qDebug()<< str;
// }
}

//鼠标移动
void myLabel::mouseMoveEvent(QMouseEvent *ev)
{
// if (ev->buttons() & Qt::LeftButton)
// {
QString str = QString("鼠标移动了 x = %1 y = %2 "
"globalx = %3 globaly = %4").arg(ev->x()).arg(ev->y())
.arg(ev->globalX()).arg(ev->globalY());
qDebug()<< str;
// }
}


定时器

第一种定时器

#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();

//重写定时器的事件
void timerEvent(QTimerEvent *);

int id1; //定时器1的唯一标识

int id2; //定时器2的唯一标识

private:
Ui::Widget *ui;
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"

Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);

//启动定时器
//参数1 间隔(单位 毫秒)
id1 = startTimer(1000);

id2 = startTimer(2000);
}

//重写定时器的事件
void Widget::timerEvent(QTimerEvent *ev)
{
if (ev->timerId() == id1)
{
static int num = 1;

//label_2每隔1秒+1
ui->label_2->setText(QString::number(num++));
}

if (ev->timerId() == id2)
{
//label_2每隔2秒+1
static int num2 = 1;
ui->label_3->setText(QString::number(num2++));
}

}

Widget::~Widget()
{
delete ui;
}

第二种定时器

#include "widget.h"
#include "ui_widget.h"
#include <QTimer>

Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);

//启动定时器
//参数1 间隔(单位 毫秒)
id1 = startTimer(1000);

id2 = startTimer(2000);


//定时器的第二种方式
QTimer * timer = new QTimer(this);
//启动定时器
timer->start(500);

connect(timer, &QTimer::timeout, [=](){
static int num = 1;

//label_4每隔0.5秒+1
ui->label_4->setText(QString::number(num++));
});
}


//重写定时器的事件
void Widget::timerEvent(QTimerEvent *ev)
{
if (ev->timerId() == id1)
{
static int num = 1;

//label_2每隔1秒+1
ui->label_2->setText(QString::number(num++));
}

if (ev->timerId() == id2)
{
//label_2每隔2秒+1
static int num2 = 1;
ui->label_3->setText(QString::number(num2++));
}

}

Widget::~Widget()
{
delete ui;
}

event 事件

用于事件的分发

也可以做拦截操作 不建议

//通过event事件分发器拦截鼠标按下事件
bool event(QEvent *e);
//通过event事件分发器拦截鼠标按下事件
bool myLabel::event(QEvent *e)
{
//如果鼠标按下 在event事件分发钟做拦截操作
if (e->type() == QEvent::MouseButtonPress)
{
//静态类型转换
QMouseEvent * ev = static_cast<QMouseEvent *>(e);
QString str = QString("event函数中 鼠标按下了 x = %1 y = %2 "
"globalx = %3 globaly = %4").arg(ev->x()).arg(ev->y())
.arg(ev->globalX()).arg(ev->globalY());
qDebug()<< str;

return true; //true代表用户自己处理这个事件 不向下分发
}

//其他事件 交给父类处理 默认处理
return QLabel::event(e);
}

事件过滤器

通过事件过滤器,可以在程序分发到event事件之前再做一次高级拦截

两个步骤:

  1. 给控件安装事件过滤器

  2. 重写eventfilter事件

//重写事件过滤器的事件
bool eventFilter(QObject *, QEvent *);
#include "widget.h"
#include "ui_widget.h"
#include <QTimer>

Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);

//步骤一 安转事件过滤器
//label 安装事件过滤器
ui->label->installEventFilter(this);

}

//步骤二 重写事件过滤器的事件
bool Widget::eventFilter(QObject *obj, QEvent *e)
{
if (obj == ui->label)
{
if (e->type() == QEvent::MouseButtonPress)
{
QMouseEvent * ev = static_cast<QMouseEvent *>(e);
QString str = QString("事件过滤器中 鼠标按下了 x = %1 y = %2 "
"globalx = %3 globaly = %4").arg(ev->x()).arg(ev->y())
.arg(ev->globalX()).arg(ev->globalY());
qDebug()<< str;

return true;
}
}

//其他默认处理
return QWidget::eventFilter(obj,e);
}

Widget::~Widget()
{
delete ui;
}

QPainter 绘图

基础设置

#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();

//绘图事件
void paintEvent(QPaintEvent *);

private:
Ui::Widget *ui;
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"
#include <QPainter>

Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
}

//绘图事件
void Widget::paintEvent(QPaintEvent *)
{
//实例化画家类 this指定的是绘图的设备
QPainter painter(this);

//设置画笔
QPen pen(QColor(122,168,200));

//设置画笔宽度
pen.setWidth(6);

//设置画笔风格
pen.setStyle(Qt::DotLine);

//让画家使用这个笔
painter.setPen(pen);

//设置画刷
QBrush brush(QColor(222,222,222));

//设置画刷风格
brush.setStyle(Qt::Dense3Pattern);

//让画家使用画刷
painter.setBrush(brush);

//画线
painter.drawLine(QPoint(0,0), QPoint(100,100));

//画圆 椭圆
painter.drawEllipse(QPoint(100,100),36,66);

//画矩形
painter.drawRect(QRect(20,20,50,50));

//画文字
painter.drawText(QRect(10,200,100,50),"晴天与猫");
}

Widget::~Widget()
{
delete ui;
}

高级设置

/////////////////高级设置/////////////////////////////////////////

QPainter painter(this);

// painter.drawEllipse(QPoint(100,100),50,50);

// //设置抗锯齿能力 效率低
// painter.setRenderHint(QPainter::Antialiasing);

// painter.drawEllipse(QPoint(200,200),50,50);


//画矩形
painter.drawRect(QRect(20,20,50,50));

//移动画家
painter.translate(100,0);

//保存画家状态
painter.save();

painter.drawRect(QRect(20,20,50,50));

painter.translate(100,0);

//还原画家保存状态
painter.restore();

painter.drawRect(QRect(20,20,50,50));
#include "widget.h"
#include "ui_widget.h"
#include <QPainter>

Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);

connect(ui->pushButton,&QPushButton::clicked,[=](){
posX += 20;
//如果要手动调用绘图事件 用update更新
update();
});
}

//绘图事件
void Widget::paintEvent(QPaintEvent *)
{
///////////////利用画家画资源图片//////////////////////

QPainter painter(this);

//如果超出屏幕 从0开始
if (posX > this->width())
{
posX = 0;
}

QPixmap pixmap(":/image/le.jpg");

QPixmap scaledPixmap = pixmap.scaled(200, 200, Qt::KeepAspectRatio);

painter.drawPixmap(posX,10,scaledPixmap);
}

Widget::~Widget()
{
delete ui;
}

update() 是一个 QWidget 类的成员函数,用于请求更新窗口部件的绘图

当调用 update() 函数时,Qt 将触发绘图事件,并重新绘制窗口部件

这意味着任何需要更新的视图都会被更新

绘图设备

绘图设备是指继承QPainterDevice的子类。Qt一共提供了四个这样的类,分别是QPixmap、QBitmap、QImage和 QPicture。

QPixmap专门为图像在屏幕上的显示做了优化

QBitmap是QPixmap的一个子类,它的色深限定为1,可以使用 QPixmap的isQBitmap()函数来确定这个QPixmap是不是一个QBitmap

QImage专门为图像的像素级访问做了优化。

QPicture则可以记录和重现QPainter的各条命令

#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();

//绘图事件
void paintEvent(QPaintEvent *);

private:
Ui::Widget *ui;
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"
#include <QPixmap>
#include <QPainter>
#include <QImage>
#include <QPicture>

Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);

// //pixmap绘图设备 专门为平台做了显示的优化/////////////////////////
// QPixmap pix(300,300);

// //填充颜色
// pix.fill(Qt::white);

// //声明画家
// QPainter painter(&pix);

// painter.setPen(QPen(Qt::blue));

// painter.drawEllipse(QPoint(150,150),100,100);

// //保存
// pix.save("D:\\cpp\\QT\\pix.png");



// //qimage 绘图设备 可以对像素进行访问//////////////////////////////
// QImage img(300,300,QImage::Format_RGB32);

// //填充颜色
// img.fill(Qt::white);

// //声明画家
// QPainter painter(&img);

// painter.setPen(QPen(Qt::blue));

// painter.drawEllipse(QPoint(150,150),100,100);

// //保存
// img.save("D:\\cpp\\QT\\img.png");



//qpicture 绘图设备 可以记录和重现绘图指令///////////////////////////

QPicture pic;

QPainter painter;
//开始往pic上画
painter.begin(&pic);

painter.setPen(QPen(Qt::cyan));

painter.drawEllipse(QPoint(150,150),100,100);

//结束画画
painter.end();

//保存到磁盘
pic.save("D:\\cpp\\QT\\pic.zt");
}

//绘图事件
void Widget::paintEvent(QPaintEvent *)
{
// QPainter painter(this);

// //利用qimage 对像素进行修改///////////////////////////////////
// QImage img;

// img.load(":/image/005y0Ylbly1hgh1g861whj30u00u00w5.jpg");

// img = img.scaled(400, 400, Qt::KeepAspectRatio);

// //修改像素点
// for (int i = 50; i < 100; i++)
// {
// for (int j = 50; j < 100; j++)
// {
// QRgb value = qRgb(200,0,0);
// img.setPixel(i, j, value);
// }
// }

// painter.drawImage(0,0,img);



//重现qpicture的绘图指令///////////////////////////////////////

QPainter painter(this);

QPicture pic;

pic.load("D:\\cpp\\QT\\pic.zt");

painter.drawPicture(0,0,pic);
}

Widget::~Widget()
{
delete ui;
}

文件系统

QFile 对文件进行读写操作

#include "widget.h"
#include "ui_widget.h"
#include <QFileDialog>
#include <QFile>
#include <QString>
#include <QFileInfo>
#include <QDebug>
#include <QDateTime>

Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);

//点击选取文件按钮,弹出文件对话框

connect(ui->pushButton,&QPushButton::clicked,[=](){
QString path = QFileDialog::getOpenFileName(this,"打开文件","C:\\Users\\lyzhy\\Desktop");

//将路径放入到lineEdit中
ui->lineEdit->setText(path);

//读取内容放入到textEdit中
//QFile默认支持的格式是 UTF-8
QFile file(path);//参数为读取文件的路径
//设置打开方式
file.open(QIODevice::ReadOnly);

QByteArray array = file.readAll();

// QByteArray array;
// while(!file.atEnd())
// {
// array += file.readLine();//按行读
// }

//编码格式类
//QString content = QString::fromLocal8Bit(array);

//将读取到的数据放入到textEdit
ui->textEdit->setText(array);

//对文件对象进行关闭
file.close();

//进行写文件
file.open(QIODevice::Append);//用追加的方式进行写

file.write("茉莉生酪拿铁");

file.close();


//QFileInfo 文件信息类
QFileInfo info(path);

qDebug()<<"文件大小:"<<info.size()<<"文件后缀名:"<<info.suffix()
<<"文件名称:"<<info.fileName()<<"文件路径:"<<info.filePath();
qDebug()<<"创建日期:"<<info.birthTime().toString("yyyy/MM/dd hh:mm:ss");
qDebug()<<"最后修改日期:"<<info.lastModified().toString("yyyy-MM-dd hh:mm:ss");
});
}

Widget::~Widget()
{
delete ui;
}

翻金币游戏实战

创建项目,添加项目资源

主场景

  1. 设置游戏主场景配置

    设置背景图标

    设置固定大小

    设置项目标题

    设置背景

    背景标题

    开始菜单

    退出功能

  2. 创建开始按钮

    封装自定义按钮 MyPushButton

    构造函数 参数( 默认显示图片, 按下后显示的图片)

    开始按钮 点击开始按钮进入选择关卡场景

    开始按钮特效

    zoom1 向下跳

    zoom2 向上跳

  3. 创建选择关卡场景

    点击开始按钮后 延时进入到 选择关卡场景

选择关卡场景

  1. 场景基本设置

    背景设置 图标 标题 大小等

  2. 创建返回按钮

    选择关卡的返回按钮特效制作

     点击后切换另一个图片
    
     重写 void mousePressEvent
    
     重写 void mouseReleaseEvent
    
  3. 开始场景与选择关卡场景的切换

    点击选择关卡场景的返回按钮,发送一个自定义信号

    在主场景中监听这个信号,并且当触发信号后,重新显示主场景,隐藏掉选择关卡的场景

  4. 创建选择关卡按钮

    利用一个for循环将所有的按钮布置到场景中

    在按钮上面设置一个QLabel显示关卡数

    QLabel 设置 大小、显示文字、对齐方式、鼠标穿透

    给每个按钮 监听点击事件

翻金币场景

  1. 翻金币场景创建

    点击选择关卡按钮后,进入到翻金币游戏场景

    配置翻金币游戏场景 设置标题、图标、大小、设置背景

    实现返回按钮,可以返回到上一个场景(选关场景)

    实现三个场景之间的切换

  2. 实现显示关卡标签
    在左下角显示玩家具体的关卡标签

    QLabel创建设置 大小和位置label->setGeometry(30, this->height() - 50,120, 50);

    QFont font 设置字体以及字号

    给QLabel设置字体 setFont(font)

  3. 创建金币类
    先将金币的背景图案放入到 PlayScene 中

    创建 MyCoin 自定义金币按钮类

    MyCoin::MyCoin(QString btnImg) 构造函数中传入默认显示的图片金币

    在PlayScene创建所有的金币按钮

  4. 每个关卡的默认显示

    先引入dataConfig.h 和 dataConfig.cpp文件到项目中

    在PlayScene 中写了 int gameArray[4][4]的数组 维护每个关卡的金币状态

    初始化每个关卡的显示

  5. 金币翻转特效

    给每个硬币加属性 posX 坐标x posY 坐标y bool flag 正反面标志

    给MyCoin 加函数 changFlag改变标志,
    如果是flag为true 改为false 并且开启定时器1 (正面翻反面);
    如果flag为false ,改为true,并且开启定时器2 (反面翻正面)

    实现定时器中的内容

    测试 金币翻银币 以及 银币翻金币

  6. 解决快速点击的效果不好

    在MyCoin中加入了 isAnimation 判断 是否正在做动画条件

    当按下 MyCoin 判断是否在做动画,如果做动画,直接return,保证金币和银币动态切换的完整效果

  7. 翻转周围金币
    点击金币后 ,延时翻转周围金币实现

  8. 判断胜利

    PlayScene中 添加 isWin的标志 来判断是否胜利

    如果胜利了,打印胜利信息

    将所有按钮 屏蔽掉点击

  9. 胜利图片特效

    将胜利图片放入到游戏场景外

    当游戏胜利时,移动到屏幕中央,做胜利效果

  10. 添加音效资源

    QSoundEffect 所属模块 multimedia 需要在.pro文件中加入这个模块

    在三个场景中添加音效

    播放 s ->play()

    s->setLoop() 设置播放次数 -1代表无限循环播放

  11. 项目的优化

    将三个场景的切换位置 一致

  12. 打包

    hm nis edit

    enigma

完整项目链接:https://github.com/lejunXTS/coinfilp/tree/main

完结撒花!

完结撒花!

完结撒花!