Qt自定义控件:汽车仪表盘

之前在网上看到一个用Qt设计的界面

gaugecar

作者免费提供了dll库以供使用,但是作为学习者,我们需要研究其代码以便了解大佬的开发技巧。当然花钱是不可能花钱的,这辈子都不可能花钱的。

我根据作者提供的核心代码脑补了剩余的部分。

作者源文章:Qt编写自定义控件1-汽车仪表盘

分析

根据我的理解,绘制过程如下:

  • 绘制一个黑色实心圆作为底部
  • 绘制第二个黑色实现圆,因为两个圆颜色不一样,形成最外圈的圆环
  • 绘制彩色圆饼图
  • 绘制灰色实心圆将彩色圆饼图裁成圆环
  • 绘制刻度线
  • 绘制刻度数值
  • 绘制数值指示指针
  • 绘制淡红色实心圆
  • 绘制白色实心圆
  • 绘制数值文字
  • 绘制遮罩,形成反光效果

开发

首先创建一个空白Qt Widget工程,基类为QWidget,不需要ui文件。

default

Qt使用QPainter在paintEvent()函数中绘图。

1
2
3
4
5
6
7
8
9
int width = this->width();
int height = this->height();
int side = qMin(width, height);

//绘制准备工作,启用反锯齿,平移坐标轴中心,等比例缩放
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
painter.translate(width / 2, height / 2);
painter.scale(side / 200.0, side / 200.0);

在正式绘图前先进行简单处理。

先获取窗口长、宽中比较小的一个值slide;设置反采样;将painter的坐标系平移到窗口中心;设置画笔缩放因子

这样当窗口大小调整时,图像也会自动调整位置和大小

背景圆

先绘制背景圆

1
2
3
4
5
6
7
8
9
void GaugeCar::drawOuterCircle(QPainter *painter)
{
int radius = 99;
painter->save();
painter->setPen(Qt::NoPen);
painter->setBrush(outerCircleColor);
painter->drawEllipse(-radius, -radius, radius * 2, radius * 2);
painter->restore();
}

将代码模块化,在paintEvent()中调用这个函数就可以了。

Qt的坐标圆点在窗口左上角,因为我们把painter的坐标系移至窗口中央,所以绘制圆时为-radius,-radius。当然圆的绘制方法是长方形的内接圆,所以绘制圆的后两个参数w/h为radius * 2。

效果为

outtercircle

绘制圆的坐标系为

coord

水平为x轴,向右为正,垂直为y轴,向下为正,所以左上角点(x,y)坐标为(-r,-r)。圆所在长方形的宽高为2r,2r。

颜色就选0x505050

内圆

在背景圆内部绘制一个浅色圆形成圆环。

1
2
3
4
5
6
7
8
9
void GaugeCar::drawInnerCircle(QPainter *painter)
{
int radius = 90;
painter->save();
painter->setPen(Qt::NoPen);
painter->setBrush(innerCircleColor);
painter->drawEllipse(-radius, -radius, radius * 2, radius * 2);
painter->restore();
}

圆半径要比背景圆小,颜色选0x404040

效果为

innercircle

彩色饼图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
void GaugeCar::drawColorPie(QPainter *painter)
{
int radius = 60;
painter->save();
painter->setPen(Qt::NoPen);

QRectF rect(-radius, -radius, radius * 2, radius * 2);

if (pieStyle == PieStyle_Three) {
//计算总范围角度,根据占比例自动计算三色圆环范围角度
//可以更改比例
double angleAll = 360.0 - startAngle - endAngle;
double angleStart = angleAll * 0.7;
double angleMid = angleAll * 0.15;
double angleEnd = angleAll * 0.15;

//增加偏移量使得看起来没有脱节
int offset = 3;

//绘制开始饼圆
painter->setBrush(pieColorStart);
painter->drawPie(rect, (270 - startAngle - angleStart) * 16, angleStart * 16);

//绘制中间饼圆
painter->setBrush(pieColorMid);
painter->drawPie(rect, (270 - startAngle - angleStart - angleMid) * 16 + offset, angleMid * 16);

//绘制结束饼圆
painter->setBrush(pieColorEnd);
painter->drawPie(rect, (270 - startAngle - angleStart - angleMid - angleEnd) * 16 + offset * 2, angleEnd * 16);
} else if (pieStyle == PieStyle_Current) {
//计算总范围角度,当前值范围角度,剩余值范围角度
double angleAll = 360.0 - startAngle - endAngle;
double angleCurrent = angleAll * ((currentValue - minValue) / (maxValue - minValue));
double angleOther = angleAll - angleCurrent;

//绘制当前值饼圆
painter->setBrush(pieColorStart);
painter->drawPie(rect, (270 - startAngle - angleCurrent) * 16, angleCurrent * 16);

//绘制剩余值饼圆
painter->setBrush(pieColorEnd);
painter->drawPie(rect, (270 - startAngle - angleCurrent - angleOther) * 16, angleOther * 16);
}

painter->restore();
}

效果为

pie

饼图分为两种:两种色、三种色

三种色取值为0x17bb99 0xd8d800 0xfd6969

三色

三色分别占比:0.7 0.15 0.15

扇形绘制方法和实心圆类似,不过在长方形内接圆的基础上增加了起始和结束角度。

代码中*16是Qt的角度固定表示方式。

角度以x轴正方向为0度,逆时针绕一圈回来后为360度。而270度是y轴正方向,也就是说此圆饼的起始结束角度为正值,比如本图所示的起始结束角度都为60度。

两色

和三色差不多,只不过颜色少一点。

裁成圆环

在圆饼上绘制一个实心圆将饼圆裁成圆弧

1
2
3
4
5
6
7
8
9
void GaugeCar::drawCoverCircle(QPainter *painter)
{
int radius = 50;
painter->save();
painter->setPen(Qt::NoPen);
painter->setBrush(coverCircleColor);
painter->drawEllipse(-radius, -radius, radius * 2, radius * 2);
painter->restore();
}

arc

刻度线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void GaugeCar::drawScale(QPainter *painter)
{
int radius = 72;
painter->save();

painter->rotate(startAngle);
int steps = (scaleMajor * scaleMinor);
double angleStep = (360.0 - startAngle - endAngle) / steps;

QPen pen;
pen.setColor(scaleColor);
pen.setCapStyle(Qt::RoundCap);

for (int i = 0; i <= steps; i++) {
if (i % scaleMinor == 0) {
pen.setWidthF(1.5);
painter->setPen(pen);
painter->drawLine(0, radius - 10, 0, radius);
} else {
pen.setWidthF(0.5);
painter->setPen(pen);
painter->drawLine(0, radius - 5, 0, radius);
}

painter->rotate(angleStep);
}

painter->restore();
}

效果为

scale

刻度线分为粗线和细线,没遇到整值就绘制粗线。使用变量控制刻度线的数量,一般每10根细线绘制一根粗线。先决定一根线作为基准,然后使用画笔的rotate选择功能绕坐标原点旋转。

刻度数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void GaugeCar::drawScaleNum(QPainter *painter)
{
int radius = 82;
painter->save();
painter->setPen(scaleColor);

double startRad = (360 - startAngle - 90) * (M_PI / 180);
double deltaRad = (360 - startAngle - endAngle) * (M_PI / 180) / scaleMajor;

for (int i = 0; i <= scaleMajor; i++) {
double sina = qSin(startRad - i * deltaRad);
double cosa = qCos(startRad - i * deltaRad);
double value = 1.0 * i * ((maxValue - minValue) / scaleMajor) + minValue;

QString strValue = QString("%1").arg((double)value, 0, 'f', precision);
double textWidth = fontMetrics().horizontalAdvance(strValue);
double textHeight = fontMetrics().height();
int x = radius * cosa - textWidth / 2;
int y = -radius * sina + textHeight / 4;
painter->drawText(x, y, strValue);
}

painter->restore();
}

scale number

此处不能用rotate,因为在100处会显示完全倒过来的数字。

使用一个变量控制数值精度,也就是小数点位数。

数值指示指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
void GaugeCar::drawPointerCircle(QPainter *painter)
{
int radius = 6;
int offset = 30;
painter->save();
painter->setPen(Qt::NoPen);
painter->setBrush(pointerColor);

painter->rotate(startAngle);
double degRotate = (360.0 - startAngle - endAngle) / (maxValue - minValue) * (currentValue - minValue);
painter->rotate(degRotate);
painter->drawEllipse(-radius, radius + offset, radius * 2, radius * 2);

painter->restore();
}

void GaugeCar::drawPointerIndicator(QPainter *painter)
{
int radius = 75;
painter->save();
painter->setOpacity(0.8);
painter->setPen(Qt::NoPen);
painter->setBrush(pointerColor);

QPolygon pts;
pts.setPoints(3, -5, 0, 5, 0, 0, radius);

painter->rotate(startAngle);
double degRotate = (360.0 - startAngle - endAngle) / (maxValue - minValue) * (currentValue - minValue);
painter->rotate(degRotate);
painter->drawConvexPolygon(pts);

painter->restore();
}

void GaugeCar::drawPointerIndicatorR(QPainter *painter)
{
int radius = 75;
painter->save();
painter->setOpacity(1.0);

QPen pen;
pen.setWidth(1);
pen.setColor(pointerColor);
painter->setPen(pen);
painter->setBrush(pointerColor);

QPolygon pts;
pts.setPoints(3, -5, 0, 5, 0, 0, radius);

painter->rotate(startAngle);
double degRotate = (360.0 - startAngle - endAngle) / (maxValue - minValue) * (currentValue - minValue);
painter->rotate(degRotate);
painter->drawConvexPolygon(pts);

//增加绘制圆角直线,与之前三角形重叠,形成圆角指针
pen.setCapStyle(Qt::RoundCap);
pen.setWidthF(4);
painter->setPen(pen);
painter->drawLine(0, 0, 0, radius);

painter->restore();
}

void GaugeCar::drawPointerTriangle(QPainter *painter)
{
int radius = 10;
int offset = 38;
painter->save();
painter->setPen(Qt::NoPen);
painter->setBrush(pointerColor);

QPolygon pts;
pts.setPoints(3, -5, 0 + offset, 5, 0 + offset, 0, radius + offset);

painter->rotate(startAngle);
double degRotate = (360.0 - startAngle - endAngle) / (maxValue - minValue) * (currentValue - minValue);
painter->rotate(degRotate);
painter->drawConvexPolygon(pts);

painter->restore();
}

就是绘制不同形状的指示针,然后根据数值计算旋转角度,使用rotate绘图

pointer

淡红色实心圆

绘制圆就简单了

1
2
3
4
5
6
7
8
9
10
void GaugeCar::drawRoundCircle(QPainter *painter)
{
int radius = 18;
painter->save();
painter->setOpacity(0.8);
painter->setPen(Qt::NoPen);
painter->setBrush(pointerColor);
painter->drawEllipse(-radius, -radius, radius * 2, radius * 2);
painter->restore();
}

颜色和指针一样

round

中心圆

1
2
3
4
5
6
7
8
9
void GaugeCar::drawCenterCircle(QPainter *painter)
{
int radius = 15;
painter->save();
painter->setPen(Qt::NoPen);
painter->setBrush(centerCircleColor);
painter->drawEllipse(-radius, -radius, radius * 2, radius * 2);
painter->restore();
}

center

数值文字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void GaugeCar::drawValue(QPainter *painter)
{
int radius = 100;
painter->save();
painter->setPen(textColor);

QFont font;
font.setPixelSize(18);
painter->setFont(font);

QRectF textRect(-radius, -radius, radius * 2, radius * 2);
QString strValue = QString("%1").arg((double)currentValue, 0, 'f', precision);
painter->drawText(textRect, Qt::AlignCenter, strValue);

painter->restore();
}

text

反光效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void GaugeCar::drawOverlay(QPainter *painter)
{
if (!showOverlay) {
return;
}

int radius = 90;
painter->save();
painter->setPen(Qt::NoPen);

QPainterPath smallCircle;
QPainterPath bigCircle;
radius -= 1;
smallCircle.addEllipse(-radius, -radius, radius * 2, radius * 2);
radius *= 2;
bigCircle.addEllipse(-radius, -radius + 140, radius * 2, radius * 2);

//高光的形状为小圆扣掉大圆的部分
QPainterPath highlight = smallCircle - bigCircle;

QLinearGradient linearGradient(0, -radius / 2, 0, 0);
overlayColor.setAlpha(100);
linearGradient.setColorAt(0.0, overlayColor);
overlayColor.setAlpha(30);
linearGradient.setColorAt(1.0, overlayColor);
painter->setBrush(linearGradient);
painter->rotate(-20);
painter->drawPath(highlight);

painter->restore();
}

这里有一个绘制图像的方法,先设计一个大圆,然后设计一个小圆,使用QPainterPath中的-或者QRegion的subtracted方法,把小圆对应的部分从大圆中减掉,就获得了一个圆环。当然还有并、取反、异或操作。

效果为

overlay

控件

上面的部分只是绘制仪表盘,如果需要把它变成独立的控件还需要把一些接口添加上。

设置颜色的接口、设置数值的接口等等,同时还有获取属性的接口。

将这些接口API添加完毕后只能称为独立的模块。我们想要的是在Designer中拖拽使用。

新建一个自定义控件工程

new

设置工程名称

name

设置属性

setup

然后保持默认一路点下去。

然后将我们开发好的类直接替换控件的文件

再添加个图片作为控件的图标

icon

编译得到一个动态链接库文件

so

然后将此动态链接库复制到Designer插件目录,Manjaro Linux命令

1
sudo cp libgaugecarplugin.so /usr/lib/qt/plugins/designer

再次打开Designer

result

这样就开发出了自己的控件。

完整代码工程在QtApps Github中的GaugeCar/GaugeCarPlugin中


Qt自定义控件:汽车仪表盘
https://feater.top/qt/qt-custom-widget-of-car-gauge/
作者
JackeyLea
发布于
2022年5月26日
许可协议