Qt实现炫酷启动图-动态进度条
目录
一、简述
最近接到一个新需求,让做一个动效进度条。
由于我们的产品比较大,在软件启动的时候会消耗比较长的时间,原生的进度条已经不能满足我们的需求,这里我们就需要一个会动的进度条,效果如下图所示。
光效进度条主要是做了一个进度动画,在已完成的部分上进行快速的迭代渲染,给用户一种直观感受,我们的软件一直努力加载,它还活着。
有了这个进度条之后,当我们的进度从40%到50%这个持续的过程中,界面再也不会出现假死的情况,是不是很完美呢。。。
下面我就来分析下这个动效进度条是怎么做的
二、动效进度条
如效果图所示,光效进度条不同于一般的进度条,他在基础的任务进度之上还添加了一层光效,主要是想告诉用户我们的软件一直在努力运行,请在耐心的等待一下下。
我自己在做功能的时候,往往喜欢先做一个测试demo,然后在把做好的功能集成在正式环境,这个功能也不列外,如第一节中展示的效果图,就是测试demo的样子,虽然很丑,但是基础功能是有的。
现在的很多软件,在进度展示上都有了比较绚丽的效果,比如压缩软件,解压文件的时候都会有动效进度条,用过的同学应该都知道长啥样,而我们的光效进度条跟这个效果差不多,除此之外我们还提供了另一种动效,延迟动效,他们两个在一定程度上都展示了更友好的进度效果。
在开始分析功能前,首先我们先来考虑下我们的需求:动效进度条,也就是说在原来的进度条基础上需要添加实时动画,让进度条看起来更炫酷一些,除了光效进度条以外,还有一种延迟到达进度条,也属于动态进度条。
延迟动效、说直白一点儿就是延迟到达,当我们设置了进度从10%到20%时,程序模拟了一个渐进的过程,使用一个时间段走完了这10%的进度。
下面我们分别来介绍这两种进度条的实现
实现炫酷的进度条我们可以从qwidget自定义开始写,也就是说从头开始写,但通常我们不这样干,因为这样可能会写出无穷无尽的bug,而且现有的*已经很稳定了,为什么还要造呢。
1、光效进度条
光效进度条我们使用了一个小技巧,采用一个简单的办法实现,我们的光效进度条控件继承自qt原生进度条类qprogressbar,在新类中我们只需要在qt绘制完原生进度条之后,补画动效即可。
a、paintevent函数
paintevent函数是qt的绘制函数,当界面刷新的时候,这个接口函数就会被调用,因此我们需要重写这个接口,首先调用父窗口的绘制方法,然后我们在绘制我们自己的动效,代码如下所示
qprogressbar::paintevent(event); drawcache绘制动效
b、drawcache绘制动效
绘制动效的时候,我们需要知道动效的绘制区域,这个地方我们需要主动去解析qss的一些参数,qt的style()->subelementrect这个接口刚好可以拿到我们需要的信息
下面简单描述下我们的实现流程
- 首先我们获取进度条的几何大小和中间进度的几何大小,这样的话我们就可以计算出来各border的数值
- 然后根据我们当前的value值就可以计算出进度条已经走过(就是值小于我们设定的区域)的几何大小
- 我们的光效将是跑在第二步计算出来的区域上,一直循环迭代
- 内存里我们维护一个cachevalue,这个值在每次界面刷新的时候递增,但是不能大于第二步的value值,cachevalue将是我们动效绘制的一个关键参数,他表示了动效绘制的长度
- 构造一个渐变刷子,设置给qpainter
- 绘制动效
上下大致描述了下绘制动效的一个流程,下面送上具体代码,由于篇幅原因,代码我进行了部分伪代码处理。
void gmpprogressbar::drawcache() { qstyleoptionprogressbarv2 opt; qrect outerrect = style()->subelementrect(qstyle::se_progressbargroove, &opt, this); qrect innerrect = style()->subelementrect(qstyle::se_progressbarcontents, &opt, this); qmargins borders(构造一个qmargins); qrectf rect(动效绘制区域); if (m_icachevalue != 0) { qpainter painter(this); qlineargradient gradient(构造绘图刷子); painter.setbrush(gradient); painter.drawroundedrect(rect, 2, 2); } }
c、定时刷新
由于我们的动效是需要主动去刷新的,因此我们需要声明一个定时器,然后定时去刷新,实现代码可能像下面这样
connect(m_pcachetimer, &qtimer::timeout, this, [this]{ if (tm_cache == m_mode) { ++m_icachevalue; repaint(); }else { m_pcachetimer->stop(); } });
定时器只需要在我们第一次设置进度条值的时候启动,或者当我们设置一个新的值,而定时器没有启动,我们就需要去激活定时器。
tm_cache模式即是我们的动效模式,tm_smooth模式则是我们的延迟到达模式
connect(this, &qprogressbar::valuechanged, [this](int value){//tm_cache模式下 启动动画时机 if (!m_pcachetimer->isactive() && value != 0 && tm_cache == m_mode) { m_pcachetimer->start(m_irefreshleveling); } });
动效进度条效果如下图所示
2、延迟到达进度条
动效进度条可能更适用于启动界面,但是也有一些时候,我们可能需要更平缓的一个加载曲线,例如安装软件、卸载软件的时候。
a、setvalue
延迟到达进度条和动效进度条的实现方式就有所差别了,对于实现延迟到达进度条,我们这里重写了setvalue函数,当外部调用该接口设置value值时,我们并没有立即去设置当前值,而是使用了一个时间段去完成这个值得刷新。
- 外部调用setvalue时,我们首先计算出我们应该绘制的最大宽度pixelmax、当前已经绘制到的最大宽度cachevalue和我们的步长
- 设置定时器刷新频率,并重启定时器
- 定时器刷新时,cachevalue自增我们的步长
- 调用父类的qprogressbar::setvalue接口设置值
b、定时器
延迟达到功能的的定时器和之前我们什么的动效定时器可以混用一个,我们定时器刷新的时候,针对不同的动画模式,我们执行不同的的代码即可,实现代码可能像下面这样
connect(m_pcachetimer, &qtimer::timeout, this, [this]{ if (tm_cache == m_mode) { ++m_icachevalue; repaint(); } else if (tm_smooth == m_mode) { changesmooth(); } else { m_pcachetimer->stop(); } });
延迟到达进度条效果如下图所示
3、接口说明
光效进度条类对外只暴露了3个接口,分别是设置动画模式、动画时长和刷新频率
特别需要注意的是,我们这里重写了父类的setvalue接口,这意味着我们不能使用多态来操作这个接口
void settransitionmode(transitionmode mode);//设置动画模式 void setsmoothduration(int duration);//设置刷新总时长 模式为tm_smooth时有效 void setrefreshleveling(int rate);//设置刷新频率 每次更改transitionmode之后会变为默认值
a、修改动画模式
修改动画模式的时候,我们需要清空内存中的所有数据,并把value值设为0。
void gmpprogressbar::settransitionmode(transitionmode mode) { if (m_mode == mode) { return; } m_mode = mode; cleardata(); qprogressbar::setvalue(0); }
b、其他接口
设置刷新时长和频率接口都比较简单,不做特别说明
特别注意:这个3个接口最好是在动画启动前设置,动画开始后尽量不要去调用
三、启动图
第二节我们主要是讲述了怎么做一个动效进度条,这一节我们来做一个启动图页面,把这个动效使用进去。
启动图不是我们主要分析的内容,这个我就简单说下这个类的实现方式和一些借口说明
1、实现思路
qt已经给我们提供了一个qsplashscreen,但是使用起来还是特别有限,因此这里我把qt的源码直接进行了二次开发
- 首先qt的原生实现方式基本都被移植了出来
- 启动图使用了简单的上下布局,上面是一张我自绘制的图片,放在了一个qlabel上,下面是动效进度条
- 自绘制的图片上包括了,产品名称、logo、背景图等等
2、背景图切换
当我们调用setpixmap设置背景图时,如果我们指定了多张图,我将会启动一个定时器,在指定时长后重新构造一张大的背景图,并添加到启动窗口上
这里主要说明下背景图是怎么构造出来的,代码如下所示
a、根据背景图构造启动图大小,并移动到屏幕中间
m_currentpixmap = m_lstpixmaps.at(m_icurrentindex); qrect size(qpoint(), m_currentpixmap.size() / m_currentpixmap.devicepixelratio()); size.setheight(size.height() + statusbarheight); setfixedsize(size.size()); m_pprogressbar->setfixedwidth(size.width() / 8 * 3); move(qapplication::desktop()->screengeometry().center() - size.center());
b、绘制程序logo
qpainter painter(&m_currentpixmap); painter.drawpixmap(m_startpos, m_logo);
c、绘制标题栏
painter.save(); painter.setfont(m_titlefont); qfontmetrics fontmetrics(m_titlefont); int textwidth = fontmetrics.width(m_strwindowtitle); int texthegith = m_logo.height(); qrect texttect = qrect(m_startpos + qpoint(13 + m_logo.width(), 0), qsize(textwidth, texthegith)); painter.drawtext(texttect, m_strwindowtitle, qtextoption(qt::aligncenter)); painter.restore();
d、设置给qlabel背景图
m_pwindowbackground->setpixmap(m_currentpixmap);
启动图的效果这里就不在贴图了,第三节上的两个gif图都是最终的启动图效果
四、测试
最后就是测试代码了,主要是模拟了程序的一个加载过程
1、构造启动图
首先我们构造一个启动图对象,并设置程序logo和动画模式
gmpsplashscreen * screen = new gmpsplashscreen(qpixmap(":/splashscreen/start.png")); screen->show(); screen->setlogo(qicon("logo.ico").pixmap(48, 48)); screen->settransitionmode(gmpprogressbar::tm_cache);
2、背景图
设置背景图,并设置背景图更换时间间隔
qlist<qpixmap> lstpixmap; lstpixmap.append(qpixmap(":/splashscreen/start.png")); lstpixmap.append(qpixmap(":/splashscreen/start.jpg")); screen->setpixmap(lstpixmap, 2000);
3、其他信息
设置程序的提示信息和标题栏
screen->showmessage("established connections", 0); screen->settitle(qstringliteral("广联达bim土建计量gtj2018"));
4、事件循环
这里写了一个死循环,主要是为了模拟程序的一个加载过程,每隔10ms处理下界面刷新事件
a.processevents(); while (1) { qtest::qsleep(10); a.processevents(); } splashscreen w; w.show(); screen->finish(&w);
五、源码
需要源码的留邮箱,现在的csdn简直太坑爹了。。。