项目实战:Qt+OSG教育学科工具之地理三维星球
若该文为原创文章,未经允许不得转载
原博主博客地址:https://blog.csdn.net/qq21497936
原博主博客导航:https://blog.csdn.net/qq21497936/article/details/102478062
本文章博客地址:https://blog.csdn.net/qq21497936/article/details/105372492
各位读者,知识无穷而人力有穷,要么改需求,要么找专业人士,要么自己研究
红胖子(红模仿)的博文大全:开发技术集合(包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软硬结合等等)持续更新中...(点击传送门)
Qt开发专栏:项目实战(点击传送门)
OSG开发专栏(点击传送门)
需求
使用Qt开发内嵌的三维地理学科工具。
原理
使用Qt+Osg三维研发,依托Qt内嵌OSG。
相关博客
《OSG三维开发专栏》:循序渐进学习OSG
《OSG开发笔记(一):OSG介绍、编译》:OSG介绍与编译
《OSG开发笔记(四):OSG不使用osgQt重写类嵌入Qt应用程序》:OSG源码嵌入Qt
《OSG开发笔记(十):OSG模型的变换之平移、旋转和缩放》:对于模型结点的基本操作
《OSG开发笔记(十四):OSG交互》:按键消息和鼠标消息的交互
《OSG开发笔记(十五):OSG光照》:光影的学习,产生立体感
《OSG开发笔记(十八):OSG鼠标拾取pick、拽托球体以及多光源》:pick拾取三维物体交互
《OSG开发笔记(二十一):OSG使用HUD绘制图形以及纹理混合模式》:hud绘制背景和前景
《OSG开发笔记(二十三):Qt使用QOpenGLWidget渲染OSG和地球仪》:基础版本的地球仪开发关键
(以上是支撑该需求的三维技术博客)
Demo v3.1.0
相比于v2.0.0版本:修复了星球纹理贴图存在缝隙的问题;修复了缩放无限制的bug;对球体、贴图、2d/3d切换、缩放、旋转增加了序列化接口(demo为启动应用后恢复之前关闭的状态)。



下载地址
Demo v3.1.0运行包下载地址:https://download.csdn.net/download/qq21497936/12542665
QQ群:1047134658(点击“文件”搜索“教育学科工具”,群内与博文同步更新)
Demo v2.0.0
相比于v1.0.0版本,增加了地球以外的八大行星,对布局进行了调整,适配了多种分辨率,并且优化了部分代码;




下载地址
Demo v2.0.0运行包下载地址:https://download.csdn.net/download/qq21497936/12312105
QQ群:1047134658(点击“文件”搜索“教育学科工具”,群内与博文同步更新所有可开源的源码模板)
Demo v1.0.0
完成地理星球中地球的研发,包括基本操作、鼠标pick旋转、缩放等,包含海洋分布、人口分布、气候分布、海平线等等功能;



下载地址
Demo v1.0.0运行包下载地址:https://download.csdn.net/download/qq21497936/11489564
QQ群:1047134658(点击“文件”搜索“教育学科工具”,群内与博文同步更新所有可开源的源码模板)
关键代码
获取背景Hud结点(传入文件)
osg::ref_ptr<osg::Node> OsgStarWidget::getBackgroundNode(QString imageFile)
{
osg::ref_ptr<osg::Group> pGroup = new osg::Group();
osg::ref_ptr<osg::Camera> pCamera = 0;
osg::ref_ptr<osg::Geode> pGeode = 0;
// 创建背景相机
{
// 步骤一:创建相机
pCamera = new osg::Camera();
// 步骤二:设置矩阵 显示得界面边坐标 左 右 下 上
pCamera->setProjectionMatrixAsOrtho2D(0, 1920, 0, 1080);
// 步骤三:设置视图矩阵
pCamera->setReferenceFrame(osg::Transform::ABSOLUTE_RF);
// 步骤四:不受父类矩阵影响
pCamera->setViewMatrix(osg::Matrix::identity());
// 步骤五:清除深度缓存
pCamera->setClearMask(GL_DEPTH_BUFFER_BIT);
// 步骤六:设置为不接受事件,让其得不到焦点
pCamera->setAllowEventFocus(false);
// 步骤七:设置渲染顺序
pCamera->setRenderOrder(osg::Camera::NESTED_RENDER); // 显示为背景HUD
// 步骤八:关闭光照,通过osg::StateSet设置
pGeode = new osg::Geode();
osg::ref_ptr<osg::StateSet> pStateSet = pGeode->getOrCreateStateSet();
pStateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
// 步骤九:关闭深度测试
pStateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF);
}
// 创建背景图形图片
{
// 步骤一:创建几何信息体
osg::ref_ptr<osg::Geometry> pGeometry = new osg::Geometry;
// 步骤二:绑定顶点
osg::ref_ptr<osg::Vec3Array> pVec3Array = new osg::Vec3Array;
pGeometry->setVertexArray(pVec3Array.get());
// 步骤三:设置顶点(屏幕坐标)
pVec3Array->push_back(osg::Vec3( 0, 0, 0));
pVec3Array->push_back(osg::Vec3(1920, 0, 0));
pVec3Array->push_back(osg::Vec3(1920, 1080, 0));
pVec3Array->push_back(osg::Vec3( 0, 1080, 0));
// 步骤四:读取图片(纹理图片)
osg::ref_ptr<osg::Image> pImage = new osg::Image;
pImage = osgDB::readImageFile(imageFile.toStdString());
if(!pImage || !pImage->valid())
{
LOG_WARN(QString("Failed to load image file: %1").arg(imageFile));
return 0;
}
// 步骤五:创建纹理
osg::ref_ptr<osg::Texture2D> pTexture2D = new osg::Texture2D;
pTexture2D->setImage(pImage);
pTexture2D->setUnRefImageDataAfterApply(true);
// 步骤六:绑定纹理坐标
osg::ref_ptr<osg::Vec2Array> pVec2Array = new osg::Vec2Array;
pGeometry->setTexCoordArray(0, pVec2Array.get());
// 步骤七:设置纹理坐标
pVec2Array->push_back(osg::Vec2(0.0, 0.0));
pVec2Array->push_back(osg::Vec2(1.0, 0.0));
pVec2Array->push_back(osg::Vec2(1.0, 1.0));
pVec2Array->push_back(osg::Vec2(0.0, 1.0));
// 步骤八:关联纹理状态
osg::ref_ptr<osg::StateSet> pStateSet = pGeometry->getOrCreateStateSet();
pStateSet->setTextureAttributeAndModes(0, pTexture2D.get(), osg::StateAttribute::ON);
// 步骤九:绑定法线
osg::ref_ptr<osg::Vec3Array> pVec3ArrayNormal = new osg::Vec3Array;
pGeometry->setNormalArray(pVec3ArrayNormal.get());
// 步骤十:设置法线方式
pGeometry->setNormalBinding(osg::Geometry::BIND_OVERALL);
// 步骤十一:设置法线方向
pVec3ArrayNormal->push_back(osg::Vec3f(0.0, 1.0, 0.0));
// 步骤十二:绘制四个顶点的图形
pGeometry->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS, 0, 4));
// 步骤十三:将图形添加进几何节点
pGeode->addDrawable(pGeometry.get());
}
pCamera->addChild(pGeode);
pGroup->addChild(pCamera);
return pGroup;
}
更换地球纹理
void OsgStarWidget::change3DImage(QString imageFile)
{
// 步骤一:读取图片(纹理图片)
osg::ref_ptr<osg::Image> pImage = 0;
pImage = osgDB::readImageFile(imageFile.toStdString());
if(!pImage || !pImage->valid())
{
LOG_WARN(QString("Failed to load image file: %1").arg(imageFile));
return;
}
// 步骤二:创建纹理
osg::ref_ptr<osg::Texture2D> pTexture2D = new osg::Texture2D;
pTexture2D->setImage(pImage);
pTexture2D->setUnRefImageDataAfterApply(true);
// 步骤三:球体体渲染纹理
osg::ref_ptr<osg::StateSet> pStateSet = _pGeode->getOrCreateStateSet();
pStateSet->setTextureAttribute(0, pTexture2D.get());
pStateSet->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON);
_pGeode->setStateSet(pStateSet);
}
拾取pick、缩放
MyUserPickEventHandler.h
#ifndef MYUSERPICKEVENTHANDLER_H
#define MYUSERPICKEVENTHANDLER_H
#include <osg/Node>
#include <osgViewer/Viewer>
class MyUserPickEventHandler : public osgGA::GUIEventHandler
{
public:
MyUserPickEventHandler();
public:
osg::ref_ptr<osg::MatrixTransform> getMatrixTransform() const;
void setMatrixTransform(const osg::ref_ptr<osg::MatrixTransform> &pMatrixTransform);
float getRadius() const;
void setRadius(float radius);
float getMinScale() const;
void setMinScale(float minScale);
float getMaxScale() const;
void setMaxScale(float maxScale);
float getZoomInStep() const;
void setZoomInStep(float zoomInStep);
float getZoomOutStep() const;
void setZoomOutStep(float zoomOutStep);
public:
/** Handle event. Override the handle(..) method in your event handlers to respond to events. */
// virtual bool handle(osgGA::Event* event, osg::Object* pObject, osg::NodeVisitor* pNodeVisitor);
/** Handle events, return true if handled, false otherwise. */
// virtual bool handle(const osgGA::GUIEventAdapter& guiEventAdapter, osgGA::GUIActionAdapter& guiActionAdapter, osg::Object* pObject, osg::NodeVisitor* pNodeVisitor);
/** Deprecated, Handle events, return true if handled, false otherwise. */
virtual bool handle(const osgGA::GUIEventAdapter& guiEventAdapter, osgGA::GUIActionAdapter& guiActionAdapter);
protected:
bool pick(const double x, const double y, osgViewer::Viewer *pViewer, osg::Vec3dArray *pVec3dArrayOut);
bool pick(const double x, const double y, osgViewer::Viewer *pViewer, osg::Node *pNode, osg::Vec3dArray *pVec3dArrayOut);
osg::Vec3d screen2Word(osg::Vec3d screenVec3d, osgViewer::Viewer *pViewer);
private:
bool _pickEarth;
osg::Vec3d _originVec3d;
osg::Vec3d _lastVec3d;
float _radius;
float _maxScale;
float _minScale;
float _zoomInStep;
float _zoomOutStep;
osg::ref_ptr<osg::MatrixTransform> _pMatrixTransform;
};
#endif // MYUSEREVENTHANDLER_H
MyUserPickEventHandler.cpp
#include "MyUserPickEventHandler.h"
#include "define.h"
#include "osg/MatrixTransform"
#include <QtMath>
#include "MyMath.h"
MyUserPickEventHandler::MyUserPickEventHandler()
: osgGA::GUIEventHandler(),
_pickEarth(false)
{
}
osg::ref_ptr<osg::MatrixTransform> MyUserPickEventHandler::getMatrixTransform() const
{
return _pMatrixTransform;
}
void MyUserPickEventHandler::setMatrixTransform(const osg::ref_ptr<osg::MatrixTransform> &pMatrixTransform)
{
_pMatrixTransform = pMatrixTransform;
}
float MyUserPickEventHandler::getRadius() const
{
return _radius;
}
void MyUserPickEventHandler::setRadius(float radius)
{
_radius = radius;
}
float MyUserPickEventHandler::getMaxScale() const
{
return _maxScale;
}
void MyUserPickEventHandler::setMaxScale(float maxScale)
{
_maxScale = maxScale;
}
float MyUserPickEventHandler::getMinScale() const
{
return _minScale;
}
void MyUserPickEventHandler::setMinScale(float minScale)
{
_minScale = minScale;
}
float MyUserPickEventHandler::getZoomInStep() const
{
return _zoomInStep;
}
void MyUserPickEventHandler::setZoomInStep(float zoomInStep)
{
_zoomInStep = zoomInStep;
}
//bool MyUserEventHandler::handle(osgGA::Event *event, osg::Object *object, osg::NodeVisitor *pNodeVisitor)
//{
// LOG_DEBUG("");
// return false;
//}
//bool MyUserEventHandler::handle(const osgGA::GUIEventAdapter &guiEventAdapter, osgGA::GUIActionAdapter &guiActionAdapter, osg::Object *pObject, osg::NodeVisitor *pNodeVisitor)
//{
// LOG_DEBUG("");
// return true;
//}
bool MyUserPickEventHandler::handle(const osgGA::GUIEventAdapter
&guiEventAdapter, osgGA::GUIActionAdapter &guiActionAdapter)
{
switch (guiEventAdapter.getEventType())
{
case osgGA::GUIEventAdapter::EventType::SCROLL:
case osgGA::GUIEventAdapter::EventType::PUSH:
case osgGA::GUIEventAdapter::EventType::RELEASE:
case osgGA::GUIEventAdapter::EventType::DRAG:
break;
default:
return true;
break;
}
bool flag = false;
if(_pMatrixTransform.get() == 0)
{
LOG_INFO("Fialed to handle, because it's not set the node of transform!!!");
return true;
}
// 使用智能指针就挂,原因未知
// osg::ref_ptr<osgViewer::Viewer> pViewer = dynamic_cast<osgViewer::Viewer*>(&guiActionAdapter);
osgViewer::Viewer *pViewer = dynamic_cast<osgViewer::Viewer*>(&guiActionAdapter);
if(pViewer == 0)
{
LOG_WARN("Fialed to get viewer!");
return true;
}
osg::Matrix matrix = _pMatrixTransform->getMatrix();
osg::Vec3d vec3d;
osg::ref_ptr<osg::Vec3dArray> pVec3dArray = new osg::Vec3dArray();
qreal offsetAngle;
float nowRadius = qSqrt(_pMatrixTransform->getBound().radius2() / 3.0);
switch (guiEventAdapter.getEventType())
{
case osgGA::GUIEventAdapter::EventType::SCROLL:
switch (guiEventAdapter.getScrollingMotion())
{
case osgGA::GUIEventAdapter::SCROLL_UP: // 鼠标滚轮向上放大
if(nowRadius / _radius >= _maxScale)
{
break;
}
matrix *= osg::Matrix::scale(_zoomInStep, _zoomInStep, _zoomInStep);
_pMatrixTransform->setMatrix(matrix);
flag = true;
break;
case osgGA::GUIEventAdapter::SCROLL_DOWN: // 鼠标滚轮向下缩小
if(nowRadius / _radius <= _minScale)
{
break;
}
matrix *= osg::Matrix::scale(_zoomOutStep, _zoomOutStep, _zoomOutStep);
_pMatrixTransform->setMatrix(matrix);
flag = true;
break;
default:
break;
}
break;
case osgGA::GUIEventAdapter::EventType::PUSH:
switch (guiEventAdapter.getButton())
{
case osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON:
if(pick(guiEventAdapter.getX(), guiEventAdapter.getY(), pViewer, _pMatrixTransform, pVec3dArray.get()))
{
// 拾取到物体
_pickEarth = true;
_lastVec3d = pVec3dArray->at(0);
}else{
_pickEarth = false;
}
break;
default:
break;
}
break;
case osgGA::GUIEventAdapter::EventType::DRAG:
if(pick(guiEventAdapter.getX(), guiEventAdapter.getY(), pViewer, _pMatrixTransform, pVec3dArray.get()))
{
if(_pickEarth == false)
{
// 拾取到物体
_pickEarth = true;
_lastVec3d = pVec3dArray->at(0);
break;
}
// 相交点
vec3d = pVec3dArray->at(0);
// 计算x轴方向角度
offsetAngle = MyMath::lineAngleRakeRatio(QPoint(0,0), QPointF(vec3d.y(), vec3d.z()))
-MyMath::lineAngleRakeRatio(QPoint(0,0), QPointF(_lastVec3d.y(), _lastVec3d.z()));
matrix *= osg::Matrix::rotate(osg::DegreesToRadians(-offsetAngle), 1, 0, 0);
// 计算z轴方向角度
offsetAngle = MyMath::lineAngleRakeRatio(QPoint(0,0), QPointF(vec3d.y(), vec3d.x()))
-MyMath::lineAngleRakeRatio(QPoint(0,0), QPointF(_lastVec3d.y(), _lastVec3d.x()));
matrix *= osg::Matrix::rotate(osg::DegreesToRadians(offsetAngle), 0, 0, 1);
_pMatrixTransform->setMatrix(matrix);
_lastVec3d = vec3d;
}else{
_pickEarth = false;
}
break;
case osgGA::GUIEventAdapter::EventType::RELEASE:
_pickEarth = false;
break;
default:
break;
}
return true;
}
bool MyUserPickEventHandler::pick(const double x, const double y, osgViewer::Viewer *pViewer, osg::Vec3dArray *pVec3dArrayOut)
{
bool ret = false;
// 判断场景
if(!pViewer->getSceneData())
{
return false;
}
// 判断是否拾取到物体
osgUtil::LineSegmentIntersector::Intersections intersections;
if(pViewer->computeIntersections(x, y, intersections))
{
for(auto iter = intersections.begin();
iter != intersections.end();
iter++)
{
pVec3dArrayOut->push_back(iter->getWorldIntersectPoint());
ret = true;
}
}
return ret;
}
bool MyUserPickEventHandler::pick(const double x, const double y, osgViewer::Viewer *pViewer, osg::Node *pNode, osg::Vec3dArray *pVec3dArrayOut)
{
bool ret = false;
// 判断场景
if(!pViewer->getSceneData())
{
return false;
}
// 判断是否拾取到物体
osgUtil::LineSegmentIntersector::Intersections intersections;
if(pViewer->computeIntersections(x, y, intersections))
{
for(auto iter = intersections.begin();
iter != intersections.end();
iter++)
{
for(int index = 0; index < iter->nodePath.size(); index++)
{
if(iter->nodePath.at(index)->asNode() == pNode)
{
pVec3dArrayOut->push_back(iter->getWorldIntersectPoint());
ret = true;
break;
}
}
break;
}
}
return ret;
}
osg::Vec3d MyUserPickEventHandler::screen2Word(osg::Vec3d screenVec3d, osgViewer::Viewer *pViewer)
{
osg::ref_ptr<osg::Camera> pCamera = pViewer->getCamera();
osg::Matrix matrix = pCamera->getViewMatrix() *
pCamera->getProjectionMatrix() *
pCamera->getViewport()->computeWindowMatrix();
osg::Matrix intertMatrix = osg::Matrix::inverse(matrix);
osg::Vec3d worldVec3d = screenVec3d * intertMatrix;
return worldVec3d;
}
float MyUserPickEventHandler::getZoomOutStep() const
{
return _zoomOutStep;
}
void MyUserPickEventHandler::setZoomOutStep(float zoomOutStep)
{
_zoomOutStep = zoomOutStep;
}
原博主博客地址:https://blog.csdn.net/qq21497936
原博主博客导航:https://blog.csdn.net/qq21497936/article/details/102478062
本文章博客地址:https://blog.csdn.net/qq21497936/article/details/105372492
项目实战:Qt+OSG教育学科工具之地理三维星球的更多相关文章
- 小D课堂-SpringBoot 2.x微信支付在线教育网站项目实战_1-4.在线教育后台数据库设计
笔记 4.在线教育后台数据库设计 简介:讲解后端数据库设计 ,字段冗余的好处,及常见注意事项 1.数据库设计: er图: 实体对象:矩形 ...
- 小D课堂-SpringBoot 2.x微信支付在线教育网站项目实战_1-3.在线教育站点需求分析和架构设计
笔记 3.在线教育站点需求分析和架构设计 简介:分析要开发的功能点和系统架构应该怎样架构 1.开发的功能: 首页视频列表 ...
- AJ学IOS 之微博项目实战(12)发送微博自定义工具条代理实现点击事件
AJ分享,必须精品 一:效果 二:封装好的工具条 NYComposeToolbar.h 带代理方法 #import <UIKit/UIKit.h> typedef enum { NYCom ...
- 小D课堂-SpringBoot 2.x微信支付在线教育网站项目实战_汇总
2018年Spring Boot 2.x整合微信支付在线教育网站高级项目实战视频课程 小D课堂-SpringBoot 2.x微信支付在线教育网站项目实战_1-1.SpringBoot整合微信支付开发在 ...
- Java 18套JAVA企业级大型项目实战分布式架构高并发高可用微服务电商项目实战架构
Java 开发环境:idea https://www.jianshu.com/p/7a824fea1ce7 从无到有构建大型电商微服务架构三个阶段SpringBoot+SpringCloud+Solr ...
- 项目实战:Qt+Ffmpeg+OpenCV相机程序(打开摄像头、支持多种摄像头、分辨率调整、翻转、旋转、亮度调整、拍照、录像、回放图片、回放录像)
若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...
- 【大话QT之十六】使用ctkPluginFramework插件系统构建项目实战
"使用ctkPluginFramework插件系统构建项目实战",这篇文章是写博客以来最纠结的一篇文章. 倒不是由于技术都多么困难,而是想去描写叙述一个项目架构採用ctkPlugi ...
- 项目实战:Qt手机模拟器拉伸旋转框架
若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...
- QT5 QSS QML界面美化视频课程系列 QT原理 项目实战 C++1X STL
QT5 QSS QML界面美化视频课程系列 QT原理 项目实战 C++1X STL 课程1 C语言程序设计高级实用速成课程 基础+进阶+自学 课程2 C语言程序设计Windows GDI图形绘 ...
- 项目实战10.1—企业级自动化运维工具应用实战-ansible
实战环境: 公司计划在年底做一次大型市场促销活动,全面冲刺下交易额,为明年的上市做准备.公司要求各业务组对年底大促做准备,运维部要求所有业务容量进行三倍的扩容,并搭建出多套环境可以共开发和测试人员做测 ...
随机推荐
- [转帖]a.out、coff、elf三种文件格式
补充:a.out早期并不是elf格式的,而是unix下另一种可执行格式,新的a.out是 本文讨论了 UNIX/LINUX 平台下三种主要的可执行文件格式:a.out(assembler and li ...
- 在k8s中的控制器和部署服务-ReplicationController和ReplicaSet
pod 代表了 k8s 中的基本部署单元,但是在实际应用场景中,服务不可能是单个pod运行的,否则会出现"单点".在 k8s 中对 pod 的托管部署,专门抽象成了单独的资源.其中 ...
- js加减乘除运算出现精度丢失
做乘法运算出现精度丢失 let aa= 2106.49 console.log( aa*10000 ) //21064899.999999996 console.log( Math.round(aa* ...
- 【JS 逆向百例】某公共资源交易网,公告 URL 参数逆向分析
声明 本文章中所有内容仅供学习交流,抓包内容.敏感网址.数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除! 逆向目标 目标:某地公共资 ...
- es从线上库导出数据并导入开发环境
背景 来了个需求,需要从某个线上es库查询一些数据出来并进行大屏展示.问需求方有没有开发环境的es库,答:没有,说要不直连他们的线上库. 后面想想也行吧,业务方都这么说了,结果开网络的流程被打回了,理 ...
- PDF标准详解(一)——PDF文档结构
已经很久没有写博客记录自己学到的一些东西了.但是在过去一年的时间中自己确实又学到了一些东西.一直攒着没有系统化成一篇篇的文章,所以今年的博客打算也是以去年学到的一系列内容为主.通过之前Vim系列教程的 ...
- vim 从嫌弃到依赖(5)——普通模式的一些操作
通过前面几章内容的铺垫,基本已经介绍完了普通模式的大部分内容,按照进度下面会依次介绍插入模式.命令模式.选择模式的一些操作.根据不同模式提供功能的多少和使用频率,篇幅会有长有短.本来这篇文章应该介绍插 ...
- SpringCloud之Ribbon负载均衡
上述案例中,我们启动了一个msg-service,然后通过DiscoveryClient来获取服务实例信息,然后获取ip和端口来访问. 但是实际环境中,我们往往会开启很多个user-service的集 ...
- centos多网卡时修改网卡的优先级
我有个服务器有多个网卡,分别配置了多个网段的IP地址,发现有一个网段ping不通.最后发现是路由优先级的问题. 查看路由 查看本机路由route主要看Metric的值,值越小表示优先级越高,取值范围1 ...
- laravel生成二维码,并添加背景图片,图标logo
1.安装组件 composer require simplesoftwareio/simple-qrcode 1.3.* 在 config/app.php 注册服务提供者: SimpleSoftwar ...