在前面的《玩转 Qt 2D 绘图》系列文章中,已经介绍了 2D 绘图中常用的知识,本文主要通过一个汽车仪表盘的实例来综合应用前面所介绍知识点,并且还介绍了一些 Qt 绘图的常用技巧。

相关文章:

实例效果图如下:

本实例主要涉及如下知识点:

  • 坐标系的缩小、旋转、位移
  • 绘制线条、圆、饼状图、多边形、文本
  • 画笔、画刷的应用
  • 三角函数的应用

废话不多说,下面开始分布讲解仪表盘的绘制步骤。为了便于后面的描述,我们先将控件上的各个元素进行标注:

1. 坐标系设置

1.1 圆的半径

仪表盘是一个圆,因此需要先确定圆的中心点和半径,中心点位于 QWidget 的中心,而 QWidget 可能并不是正方形,因此只能以最短边来确定圆的半径。

1
2
3
4
const qreal width = this->width();
const qreal height = this->height();
const qreal side = qMin(width, height); // 最短边
const qreal radius = side / 2.0; // 半径

1.2 坐标系缩放

假设当外圆的半径为 100 时,我们将内圆的半径设计为 85,中心圆的半径设计为 45,刻度数字字体设计为 10px,等等。

由于仪表板控件是一个通用型的控件,因此在实际应用中,其长宽并不固定为100px,其可以为任意值,为了保持美观和协调性,外圆、内圆、中心圆、数字字体等元素的大小都需要根据控件的大小而动态的改变。我们可以通过百分比的方式来设置各个元素的大小,如:

1
2
3
4
5
6
qreal r1 = side / 2.0; // 外圆半径
qreal r2 = r1 * 0.85; // 内圆半径为外圆的85%
qreal r3 = r1 * 0.45; // 中心圆的半径

// 字体大小则根据 半径100 -> 10px 这个基础来缩小和放大
qreal fz = 10 * (r1 / 100.0);

看了上面的代码,相信大家已经有了明显的感受:需要手动计算百分比,太繁琐了,而且字体大小的缩放也不够线性。

的确如此,针对这些问题,我们可以通过下面的方案来解决。

在实际项目开发中,控件的样式通常不是程序员自己凭空想象的,而是经过设计师或美工设计出来的,设计师通过蓝湖、figma 等工具将效果图交付给开发人员,这些工具都带有尺寸标注功能,开发人员可以方便的获取设计尺寸。我们在开发时可以直接以设计尺寸来进行开发,不再计算百分比,改为动态的对坐标系进行缩放来适应实际尺寸。

1
2
3
4
const qreal width = this->width();
const qreal height = this->height();
const qreal side = qMin(width, height);
painter.scale(side / 200.0, side / 200.0); // 设计师所设计的的长宽为200*200

1.3 坐标系原点

为了方便绘制,我们通常还将坐标系的原点移动到圆的中心点位置,当然这不是必须的。

1
painter.translate(width / 2, height / 2);

2. 绘制外圆

控件是包含各种不同的元素,如外圆、内圆、刻度、指针等,一层一层的叠加在 QWidget 上面,从而组成了一个完整的控件。在绘制时,我们通常从最底层元素开始绘制,然后再一层一层地绘制上层元素。

本实例中,我们先绘制外圆,然后绘制内圆,一层一层的叠加(本文的章节顺序即为元素的绘制顺序)。

1
2
3
4
5
6
7
8
// 绘制外圆
painter.save();
{
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(80, 80, 80));
painter.drawEllipse(QPointF(0, 0), 100.0, 100.0);
}
painter.restore();

3. 绘制内圆

1
2
3
4
5
6
7
painter.save();
{
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(60, 60, 60));
painter.drawEllipse(QPointF(0, 0), 85.0, 85.0);
}
painter.restore();

4. 绘制三色饼状图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// startAngle_ 和 endAngle_ 起始和结束角度
// qreal startAngle_ = -30;
// qreal endAngle_ = 210;

painter.save();
{
qreal end = endAngle_;
qreal spanAngles = (endAngle_ - startAngle_) * 0.7; // 0 ~ 70%
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(24, 189, 155));
painter.drawPie(-55, -55, 110, 110, (end - spanAngles) * 16, spanAngles * 16);

end -= spanAngles;
spanAngles = (endAngle_ - startAngle_) * 0.15; // 70% ~ 85%
painter.setBrush(QColor(218, 218, 0));
painter.drawPie(-55, -55, 110, 110, (end - spanAngles) * 16, spanAngles * 16);

end -= spanAngles;
spanAngles = (endAngle_ - startAngle_) * 0.15; // 85% ~ 100%
painter.setBrush(QColor(255, 107, 107));
painter.drawPie(-55, -55, 110, 110, (end - spanAngles) * 16, spanAngles * 16);
}
painter.restore();

5. 绘制中心圆

绘制中心圆,用于覆盖饼圆的中心。

1
2
3
4
5
6
7
painter.save();
{
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(100, 100, 100));
painter.drawEllipse(QPointF(0, 0), 45, 45);
}
painter.restore();

6. 绘制刻度线

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
// majorScaleNum_ 指定有多少个大刻度线
// scaleNumPerMajor_ 指定每2个大刻度线间隔内有多少个小刻度线
// int majorScaleNum_ = 10;
// int scaleNumPerMajor_ = 10;

painter.save();
{
QPen pen;
pen.setColor(QColor(255, 255, 255));
pen.setCapStyle(Qt::RoundCap);
pen.setWidthF(1.0);
painter.setPen(pen);

int totalScaleNum = majorScaleNum_ * scaleNumPerMajor_;
qreal angleStep = (endAngle_ - startAngle_) / (qreal)totalScaleNum;

painter.rotate(360 - endAngle_); // rotate 按顺时针方向旋转

for (int i = 0; i <= totalScaleNum; i++) {
if (i % scaleNumPerMajor_ == 0) {
// 绘制大刻度
painter.drawLine(QPointF(57.0, 0.0), QPointF(72.0, 0.0));
}
else {
// 绘制小刻度
painter.drawLine(QPointF(57.0, 0.0), QPointF(64.0, 0.0));
}
painter.rotate(angleStep);
}
}
painter.restore();

7. 绘制刻度数字

因为数字始终是从左到右的方向绘制的,而绘制刻度时会改变坐标轴的方向,因此数字不能与刻度一起绘制。

使用三角函数计算文字矩形区域下横线中间点位置。

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
painter.save();
{
QPen pen;
pen.setColor(QColor(255, 255, 255));
painter.setPen(pen);

QFont font;
font.setPixelSize(10);
painter.setFont(font);

QFontMetrics fm(font);
int txtHeight = fm.height();

qreal angleStep = (endAngle_ - startAngle_) / majorScaleNum_;
qreal stepNum = (maxValue_ - minValue_) / majorScaleNum_;
for (int i = 0; i <= majorScaleNum_; i++) {
qreal angle = startAngle_ + i * angleStep;
qreal x = qCos(qDegreesToRadians(angle)) * 74.0;
qreal y = -qSin(qDegreesToRadians(angle)) * 74.0; // 纵坐标与数学坐标系相反

QString txt = QString::number((int)((majorScaleNum_ - i) * stepNum)); // 大数在右边,小数在左边,先绘制大数
int txtWidth = fm.horizontalAdvance(txt);
if (IS_NEARLY_EQUAL(x, 0.0)) {
painter.drawText(QRectF(x - txtWidth / 2, y - txtHeight, txtWidth, txtHeight), Qt::AlignCenter, txt);
}
else if (IS_NEARLY_EQUAL(y, 0.0)) {
painter.drawText(QRectF(x, y - txtHeight / 2, txtWidth, txtHeight), Qt::AlignVCenter | Qt::AlignLeft, txt);
}
else {
if (x > 0.0)
painter.drawText(QRectF(x, y - txtHeight / 2, txtWidth, txtHeight), Qt::AlignVCenter | Qt::AlignLeft, txt);
else
painter.drawText(QRectF(x - txtWidth, y - txtHeight / 2, txtWidth, txtHeight), Qt::AlignVCenter | Qt::AlignRight, txt);
}
}
}
painter.restore();

8. 绘制当前指针

指针样式实际为三个点组成的三角形。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// maxValue_ 和 minValue_ 为设置的刻度最大值和最小值
// qreal minValue_ = 0.0;
// qreal maxValue_ = 100.0;

const qreal anglePerValue = (endAngle_ - startAngle_) / (maxValue_ - minValue_);
painter.save();
{
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(255, 107, 107, 204));

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

qreal rotateAngle = 360.0 - endAngle_;
rotateAngle += anglePerValue * (curValue_ - minValue_);

painter.rotate(rotateAngle);

painter.drawPolygon(pts);
}
painter.restore();

9. 绘制中心小圆

1
2
3
4
5
6
7
8
9
10
11
12
painter.save();
{
painter.setPen(Qt::NoPen);
// 红色圆
painter.setBrush(QColor(255, 107, 107));
painter.drawEllipse(QPointF(0.0, 0.0), 20.0, 20.0);

// 白色圆
painter.setBrush(QColor(255, 255, 255));
painter.drawEllipse(QPointF(0.0, 0.0), 16.0, 16.0);
}
painter.restore();

10. 绘制当前值

1
2
3
4
5
6
7
8
9
10
11
12
painter.save();
{
QFont font = painter.font();
font.setPixelSize(14);
font.setBold(true);

painter.setFont(font);
painter.setPen(QColor(0, 0, 0));

painter.drawText(-16, -16, 32, 32, Qt::AlignCenter, QString::number(curValue_));
}
painter.restore();

至此,大功告成,齐活!


控件演示程序下载地址:

https://github.com/winsoft666/qt-custom-2d-controls