一、效果图
二、效果图拆解
根据效果图上显示,最外层一共是13个小点,这些小点有点像子弹头一样,头部是尖的,底部是平的。靠近一层是表盘码值表,数字是的底部朝向表盘圆心。再近一层是一个渐变圆环,颜色有黑色渐变成白蓝色。里面是121个刻度线,最里面是长刻度线和短刻度线,这些刻度线都是头尖底圆。最后内部正上方是绘制的文字,表盘中的指针也是头头尖底圆,指向0值。
其中主题颜色为蓝白色,指针颜色为暗红色。
三、绘制过程拆解
①首先是要解决里面图形(子弹头和数字)旋转的问题,如果直接计算旋转之后的图形再绘制,可能真的比较麻烦,不过三角函数比较优秀的人可以尝试直接旋转图形再绘制。这里采用的方法是使用QPainter自带的旋转绘图坐标的方式,先将坐标旋转之后,绘制图像,再将坐标还原回来,结果图看上去就是绘制的图形被旋转了。
②首先绘制最外层的13个子弹头,这些子弹头不是规则的图形(不是正方形,长方形之类的),但是可以用一个长方形+一个三角形构成,如下图所示:
这里绘制的是等比例放大的图像,将这个图像缩小之后,看上去菱角不那么明显时,就会误以为是圆角的(实际是眼睛欺骗了大脑的结果,当初为了实现菱角圆滑,真的费了很多心思没结果)。
这个图形虽然可以用正方形+三角形的方式构成,但是在绘图的时候比较麻烦,需要绘制两次,或者可以用QPainterPath计算两个图形的并集,但是都比较麻烦,不如直接用QPainterPath将路径添加在集合中,一次绘制即可。
③绘制13个数字码值表。这里也是先将坐标旋转之后,绘制数字,最后还原坐标。
④绘制渐变圆环。这里没有使用绘制两个扇形圆的方式(内部绘制一个遮罩圆),这里是先将圆环的路径计算出来,然后直接绘制路径所在的区域。
⑤绘制内部121个短刻度线。这里的效果图上看着短刻度线头尖底圆,这里和绘制子弹头相同,实际也是因为眼睛欺骗了大脑,实际这个子弹头放大之后是下面这样的:
实际上就是两个等腰梯形合并在一起的,由于效果图上点线距离太小,所以看上去就像是圆的。
⑥绘制长短刻度线。绘制方法和上面绘制短刻度线异样,只是梯形的长度不一样。
⑦绘制表盘上的文字。这里和绘制表盘数字一样。
⑧绘制表盘指针。这里由于指针图像比较大,不能再用梯形的方式绘制指针底部,所以这里指针底部是一个半圆。
四、绘制前准备
这里根据效果图表盘一共被分成12个区间,一共13个点,在坐标上,为了计算方便,本文中设置每两个相邻的点之间的度数为20°(圆形是360°),每两个小刻度之间的角度为2°,这样整个表盘的其实角度就是-30°,终点角度为210°,表盘扇形区域总度数为240°。
再说Qt坐标系,Qt坐标系是向右为x轴,右为正,左为负,向下为y轴,下为正,上为负。所以这样表盘的相对Qt坐标系的起始角度为-120°,终点角度为120°。
五、详细绘制过程
①准备工作,先设置对话框窗口大小,设置窗口背景颜色等,代码如下所示:
//设置窗口大小
setFixedSize(640,480);
//去掉问号
Qt::WindowFlags flags= this->windowFlags();
setWindowFlags(flags&~Qt::WindowContextHelpButtonHint);
//背景设置成黑色
QPalette bgpal=palette();
bgpal.setColor(QPalette::Background,QColor(0,0,0));
setPalette(bgpal);
②重载窗口绘图函数,初始化QPainter对象,移动绘图坐标中心,设置窗口画笔画刷等,代码如下所示:
void Dashboard::paintEvent(QPaintEvent*)
{QPainter painter(this);int width=this->width();int height=this->height();int radius=((width>height)?height:width)/2;//将画笔移动到中下方painter.translate(width>>1,height*0.6);//启用反锯齿painter.setRenderHint(QPainter::Antialiasing, true);//取消画笔painter.setPen(Qt::NoPen);//设置画刷颜色painter.setBrush(QColor(98,246,255));
}
当前设置完成后,界面一片纯黑,效果图如下所示:
③绘制13个子弹头。这里分成几步一步步实现,描述清楚坐标计算、坐标移动等,后续步骤中也会用到,不再进行描述。
A.首先,组装子弹头的效果路径,代码如下所示:
//组装点的路径图
QPainterPath pointPath;
pointPath.moveTo(-2,-2);
pointPath.lineTo(2,-2);
pointPath.lineTo(2,2);
pointPath.lineTo(0,4);
pointPath.lineTo(-2,2);
在Qt坐标系中绘制这个闭合图形,得到的效果图如下所示:
这里可能会有疑问,为什么moveTo坐标是负数,而不是0。这里这样设置主要是将坐标原点绘制在被绘制图形的中心,这样图形旋转之后,图形的中点还是在坐标的中心点。
B.然后计算这些子弹头的旋转坐标值,以左下角的子弹头为例。根据Qt坐标系计算,这个子弹头的旋转角度是-120°(别问我怎么知道的,这个旋转一下就知道了)。下一个子弹头,旋转角度是在-120°的基础上加上20°,依次类推,就可以绘制出所有子弹头的被旋转的图像。
这里用到了Qt坐标系的旋转,函数是rotate函数,是QPainter下的函数。使用方法如下代码所示:
painter.rotate(-120);
旋转之后的子弹头效果图如下所示:
C.这里既然绘制出了第一个,那剩下的12个绘制就很方便了,因为旋转角度是逐渐递增的,递增角度为20°。所以旋转代码如下:
painter.rotate(-120+i*20);
绘制13个子弹头的代码如下所示:
for(int i=0;i<13;++i){painter.save();//计算并选择绘图对象坐标painter.rotate(-120+i*20);//绘制路径painter.drawPath(pointPath);painter.restore();
}
这里用到QPainter函数的save函数和restore函数,这两个函数一般是成对出现的,这里是先将绘图对象保存一下,因为后面需要旋转坐标,绘制完成之后,恢复绘图对象,被旋转的坐标自然就被恢复了,不然就需要将坐标旋转回来。
这一步因为所有子弹头的绘制坐标都在中心点,所以所有子弹头都重叠在一起,如下图所示:
D.将子弹头分开。当前由于坐标系原点在控件中间,要将子弹头绘制在圆环上(意识里的圆环上),就需要计算每一个子弹头的坐标。计算每一个子弹头的坐标代码如下所示:
for(int i=0;i<13;++i){QPointF point(0,0);painter.save();//计算绘图对象中心点point.setX(radius*qCos(((210-i*20)*M_PI)/180));point.setY(radius*qSin(((210-i*20)*M_PI)/180));painter.restore();
}
其中point就是子弹头绘制的坐标。为了方便绘图,直接将绘图坐标的中心点移动到计算出来的坐标点上,代码如下所示:
painter.translate(point.x(),-point.y());
这里为什么y坐标是负数?因为三角函数是基于常规的坐标系计算的(y轴向上为正),而Qt坐标系刚好相反(y轴向下为正),所以这里需要y坐标转换一下。
E.完整的绘制13个子弹头的代码如下:
void Dashboard::DrawPoint(QPainter& painter,int radius)
{//组装点的路径图QPainterPath pointPath;pointPath.moveTo(-2,-2);pointPath.lineTo(2,-2);pointPath.lineTo(2,2);pointPath.lineTo(0,4);pointPath.lineTo(-2,2);//绘制13个小点for(int i=0;i<13;++i){QPointF point(0,0);painter.save();//计算并移动绘图对象中心点point.setX(radius*qCos(((210-i*20)*M_PI)/180));point.setY(radius*qSin(((210-i*20)*M_PI)/180));//计算并移动绘图对象的中心点painter.translate(point.x(),-point.y());//计算并选择绘图对象坐标painter.rotate(-120+i*20);//绘制路径painter.drawPath(pointPath);painter.restore();}
}
效果如下图:
④绘制表盘数字。坐标旋转移动中心点的方法和③中相同,后续不在详细描述,直接上代码:
void Dashboard::DrawDigital(QPainter& painter,int radius)
{//绘制13个小点for(int i=0;i<13;++i){QPointF point(0,0);painter.save();//计算并移动绘图对象中心点point.setX(radius*qCos(((210-i*20)*M_PI)/180));point.setY(radius*qSin(((210-i*20)*M_PI)/180));//计算并移动绘图对象的中心点painter.translate(point.x(),-point.y());//计算并选择绘图对象坐标painter.rotate(-120+i*20);//绘制路径painter.drawText(-15, 0, 30, 20,Qt::AlignCenter,QString::number(i*20));painter.restore();}
}
效果图如下:
很惊奇的发现居然什么都没有?对,确实什么都没有,原因是绘制文字,需要设置画笔,在初始化的时候,将画笔去掉了,这里需要设置画笔,代码如下所示:
painter.setPen(QColor(98,246,255));
效果图如下所示:
界面看上去比较丑陋,那是因为这里字体字号等都是系统默认的,需要自己设置字体,本工程中字体设置为黑体,字号为16,代码如下所示:
QFont font;
font.setFamily("SimHei");
font.setPointSize(16);
painter.setFont(font);
效果图如下所示:
⑤绘制渐变圆环。这里采用的饭饭是先计算扇形圆弧的扇形区域,再计算内圆(圆环内测圆)的路径,两个路径求差集(路劲差集就是圆环的路径),绘制锥形渐变,外测为黑色,内测为蓝白色,代码如下所示:
void Dashboard::DrawCircle(QPainter& painter,int radius)
{//保存绘图对象painter.save();//计算大小圆路径QPainterPath outRing;QPainterPath inRing;outRing.moveTo(0,0);inRing.moveTo(0,0);outRing.arcTo(-radius,-radius, 2*radius,2*radius,-31,242);inRing.addEllipse(-radius+20,-radius+20,2*(radius-20),2*(radius-20));outRing.closeSubpath();//设置渐变色QRadialGradient radialGradient(0,0,radius,0,0);radialGradient.setColorAt(0.95,QColor(98,246,255));radialGradient.setColorAt(1,QColor(0,0,0));//设置渐变画刷painter.setBrush(radialGradient);//大圆减去小圆得到圆环painter.drawPath(outRing.subtracted(inRing));//恢复绘图对象painter.restore();
}
这里有一个问题需要说明,outRing.arcTo函数中,扇形圆的其实角度应该是-31,结束角度应该是240,但是这里偏移了一点点,是因为后续绘制刻度尺的时候,刻度尺的中心点坐标对应的是-30°和240°,刻度尺有宽度,所以看上去就没有对齐,为了让界面看上去对齐,这里扇形圆做了一定的偏移。
效果图如下所示:
⑥绘制小刻度尺。这里的每个小刻度的绘制方法和上面子弹头的绘制方法一样,唯一不一样的是小刻度的底部不是平的,有一点点突出,为了实现这个效果,这里处理方法是两个梯形合并在一起。路径代码和绘制代码如下所示:
void Dashboard::DrawSmallScale(QPainter& painter,int radius)
{//组装点的路径图QPainterPath pointPath;pointPath.moveTo(-2,-2);pointPath.lineTo(-1,-4);pointPath.lineTo(1,-4);pointPath.lineTo(2,-2);pointPath.lineTo(1,8);pointPath.lineTo(-1,8);//绘制121个小点for(int i=0;i<121;++i){QPointF point(0,0);painter.save();//计算并移动绘图对象中心点point.setX(radius*qCos(((210-i*2)*M_PI)/180));point.setY(radius*qSin(((210-i*2)*M_PI)/180));//计算并移动绘图对象的中心点painter.translate(point.x(),-point.y());//计算并选择绘图对象坐标painter.rotate(-120+i*2);//绘制路径painter.drawPath(pointPath);painter.restore();}
}
效果图如下所示:
⑦绘制大刻度。方法和绘制小刻度一样,只是长短刻度需要区分一下。代码如下所示:
void Dashboard::DrawBigScale(QPainter& painter,int radius)
{//组装点的路径图QPainterPath pointPath1;pointPath1.moveTo(-2,-2);pointPath1.lineTo(-1,-4);pointPath1.lineTo(1,-4);pointPath1.lineTo(2,-2);pointPath1.lineTo(1,8);pointPath1.lineTo(-1,8);QPainterPath pointPath2;pointPath2.moveTo(-2,-2);pointPath2.lineTo(-1,-4);pointPath2.lineTo(1,-4);pointPath2.lineTo(2,-2);pointPath2.lineTo(1,15);pointPath2.lineTo(-1,15);//绘制25个刻度for(int i=0;i<25;++i){QPointF point(0,0);painter.save();//计算并移动绘图对象中心点point.setX(radius*qCos(((210-i*10)*M_PI)/180));point.setY(radius*qSin(((210-i*10)*M_PI)/180));//计算并移动绘图对象的中心点painter.translate(point.x(),-point.y());//计算并选择绘图对象坐标painter.rotate(-120+i*10);//绘制路径if(i%2){painter.drawPath(pointPath1);}else{painter.drawPath(pointPath2);}painter.restore();}
}
效果图如下所示:
⑧绘制表盘上的文字。这里又需要将画笔设置出来。代码如下所示:
void Dashboard::DrawText(QPainter& painter,int radius)
{painter.save();//设置画笔painter.setPen(QColor(98,246,255));//设置字体QFont font;font.setFamily("Microsoft YaHei");font.setPointSize(16);painter.setFont(font);painter.drawText(-25, -radius, 50, 20,Qt::AlignCenter,QString("km/h"));painter.restore();
}
效果图如下所示:
⑨最后一步是绘制一个指针。废话不多说,直接上代码。如果前面的代码都搞明白了,最后这个指针也就没啥难度了,最可能出问题就是指针底部的半圆路径绘制。
void Dashboard::DrawPointer(QPainter& painter,int radius)
{//组装点的路径图QPainterPath pointPath;pointPath.moveTo(10,0);pointPath.lineTo(1,-radius);pointPath.lineTo(-1,-radius);pointPath.lineTo(-10,0);pointPath.arcTo(-10,0,20,20,180,180);QPainterPath inRing;inRing.addEllipse(-5,-5,10,10);painter.save();//计算并选择绘图对象坐标painter.rotate(-120);//设置画刷painter.setBrush(QColor(255,0,0,200));//绘制路径painter.drawPath(pointPath.subtracted(inRing));painter.restore();
}
这里指针加了一点点透明度,看上去有点点红黑的感觉,效果图如下所示:
六、最后总结
①这里只是绘制了一个静态表盘,如果要让指针动起来,修改指针的旋转角度即可。如果要有动画过度效果,那就得加定时器,定时调用绘制代码,让指针一点点的旋转过去,这里没有实现这个功能。
②上面描述的是绘制的整个过程,源码中,设置开始角度、结束角度、颜色,渐变颜色等一系列参数设置函数,可以根据自身需要设置不同的值,实现不同的效果。
七、代码获取
从Git下载,地址为:https://github.com/youyicc/Dashboard1.git