在前面的《玩转 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; qreal r3 = r1 * 0.45;
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);
|
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
|
painter.save(); { qreal end = endAngle_; qreal spanAngles = (endAngle_ - startAngle_) * 0.7; 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; painter.setBrush(QColor(218, 218, 0)); painter.drawPie(-55, -55, 110, 110, (end - spanAngles) * 16, spanAngles * 16);
end -= spanAngles; spanAngles = (endAngle_ - startAngle_) * 0.15; 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
|
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_);
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
|
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