背景:
项目需要,可能需要做一些仪表显示。此篇除了介绍实现方法,还要说明心路历程。对我而言,重要的是心理,而不是技术。写下来也是自勉。
本人起初心里是比较抵触的,从业20多年了,深知所谓界面,实际上最根本还是代码实现的,操作系统层面多少年来一步步发展更新,是靠不断封装演变来的。所谓的可视化开发,主要还是靠ide提供的插件控件,通常只是应用。如果要按照自己的想法做个什么,那就是绘图机制,把代码变成图不用想就知道麻烦。基于这种心理,就是再说qt可以做得很炫,个人表示一看就够那种,根本看不进去。
终究为了活下去,所以要保住工作,也是对技术的不服输,所以不忙时打开qt示例,看它的文档。其实看过很多遍了,主要是看不进去。终于要善于提炼代码,看它的主要部分,缩小规模,然后研读手册,必要时看看别人怎么说的,逐个尝试,最后形成自己的总结,就学会了。
下面只介绍干货。
用法总结:
所谓绘图机制,肯定用到paintEvent函数,每一个能看到的窗体都是使用操作系统的重绘机制显示的。说人话就是,绘图的过程要写在paintEvent函数里,只要做了更新显示操作,记着调用update函数,它会调用paintEvent函数重绘界面。
qt绘图就一个QPainter,所有事都是它干的。QPen用于设置边框边线,QBrush用于设置填充。
painter画任何东西都是有个draw***函数。
画任何东西都基于坐标系,所以要先操作坐标系,再画。比如想画仪表盘刻度,设定好间隔(循环),转一次坐标系,画一个短线,最后转完一圈,也就画了一圈刻度。
painter就是画画,肯定后画的覆盖先画的,跟图层似的。
因为能旋转坐标系,所以要记住前后的坐标系状态,所以前面调用QPainter::save(),最后调用QPainter::restore()。比如画仪表,开始画背景是正常坐标系,接着通过旋转坐标系画刻度,最后要在正常坐标系下画表轴或者其它东西。这中间,旋转坐标系那部分,就需要暂存painter状态,画完这部分再恢复。
具体下面例子说明。
下面无论我画什么,都不再贴mainwindow代码了,没有必要,每个仪表都是一个类,使用时,在界面画一个label,提升为这个类即可。
手绘仪表盘:
思路是:
先画个黑色矩形当背景;
再旋转坐标系画刻度;
通过一个全局的成员变量值,作为指针旋转角度值,通过旋转坐标系把指针画到相应位置;
最后画一个转轴盖住指针(其实也可以把指针别画那么长,留出圆心位置即可)。
我鸟语不行,注释部分,看官您忽略就好。至于表芯转轴,你们说叫hand heart还是center?要不咱别较真的了,主要看代码。
guage_watertemp.h
/**************************************************************************************************
** File name: guage_watertemp.h (Guage_WaterTemp)
** Created by: Henrick.Nie at 2024-9-8
** Used for: The car water temprature guage.
**************************************************************************************************/
#ifndef GUAGE_WATERTEMP_H
#define GUAGE_WATERTEMP_H
#include <QLabel>
#include <QPainter>
#include <QDebug>
class Guage_WaterTemp : public QLabel
{
Q_OBJECT
public:
explicit Guage_WaterTemp(QWidget *parent = nullptr);
void f_SetTemp(const qreal &realTemp);
private:
qreal m_realTemp, m_realMin = 25, m_realMax = 155;
void paintEvent(QPaintEvent *event);
void f_Draw(QPainter *p);
};
#endif // GUAGE_WATERTEMP_H
guage_watertemp.cpp
#include "guage_watertemp.h"
Guage_WaterTemp::Guage_WaterTemp(QWidget *parent) :
QLabel(parent)
{
}
void Guage_WaterTemp::paintEvent(QPaintEvent *event)
{
QLabel::paintEvent(event);
QPainter painter(this);
f_Draw(&painter);
}
void Guage_WaterTemp::f_Draw(QPainter *p)
{
QColor colorHand = QColor(Qt::red);
//Init the size data.
int iHeartSize = width() / 5;
int iX0 = width() / 2, iY0 = height() - iHeartSize;
int iBackWidth = width(), iBackHeight = width() / 2 + iHeartSize * 1.5 ;
int iBackLeft = 0 - iBackWidth / 2, iBackTop = 0 - iBackWidth / 2 - iHeartSize / 2;
int iMarkSize = iBackWidth / 40, iMarklength = iBackWidth / 20;
int iHandSize = iBackWidth / 25, iHandLeft = iBackLeft + width() / 20, iHandLength = iBackWidth / 2 - iBackWidth / 20;
int iText30Left = iBackLeft + width() / 28, iText30Top = 0 - width() / 15;
int iText90Left = 0 - width() / 18, iText90Top = 0 - width() / 4;
int iText150Left = width() / 3.5, iText150Top = iText30Top;
//Init the origin.
p->setRenderHint(QPainter::Antialiasing);
p->translate(iX0, iY0);
//Draw the black rect as the background.
p->setPen(Qt::NoPen);
p->setBrush(QBrush(Qt::black));
p->drawRect(iBackLeft, iBackTop, iBackWidth, iBackHeight);
//Draw the hand heart on the origin.
p->setPen(QPen(QBrush(colorHand), iHeartSize, Qt::SolidLine, Qt::RoundCap));
p->drawPoint(0, 0);
//Draw the mark. (From 30 degrees to 150 degrees, 150-30=120, 120/10=12)
p->save();
p->rotate(30);
for(int i = 30; i <= 150; ++i)
{
QBrush brush = (i < 140) ? QBrush(Qt::white) : QBrush(Qt::red);
p->setPen(QPen(brush, iMarkSize));
p->drawPoint(iBackLeft, 0);
p->rotate(1);
}
p->restore();
QList<int> iList_Big = { 30, 90, 150 };
p->save();
p->rotate(30);
for (int i = 30; i <= 150; i += 12)
{
int iSize = iList_Big.contains(i) ? iMarkSize * 2 : iMarkSize;
int iLeft = iList_Big.contains(i) ? iBackLeft + iMarkSize / 2 : iBackLeft;
int iLength = iList_Big.contains(i) ? iMarklength + 10 : iMarklength;
QBrush brush = (i < 130) ? QBrush(Qt::white) : QBrush(Qt::red);
p->setPen(QPen(brush, iSize));
p->drawLine(iLeft, 0, iLeft + iLength, 0);
p->rotate(12);
}
p->restore();
//Draw the mark text.
p->setFont(QFont("", width() / 10));
p->setPen(QPen(QBrush(Qt::white), width() / 18));
p->drawText(iText30Left, iText30Top, "30");
p->drawText(iText90Left, iText90Top, "90");
p->drawText(iText150Left, iText150Top, "150");
//Draw hands.
p->save();
p->setPen(QPen(QBrush(colorHand), iHandSize, Qt::SolidLine, Qt::RoundCap));
if (m_realTemp < m_realMin)
{
m_realTemp = m_realMin;
}
if (m_realTemp > m_realMax)
{
m_realTemp = m_realMax;
}
p->rotate(m_realTemp);
p->drawLine(iHandLeft, 0, iHandLeft + iHandLength, 0);
p->restore();
p->end();
}
void Guage_WaterTemp::f_SetTemp(const qreal &realTemp)
{
m_realTemp = realTemp;
update();
}
效果:
其实我是仿照大众polo劲情的水温表画的。至于指针,既然是旋转肯定是角度。看到表盘的样式时,要大致估计一下,最左侧和左右侧的刻度大概是坐标轴上的多少度。算一下它的跨度,和实际需要的水温数值要有个转换。比方说,这个水温表的刻度刚好是水温和角度一一对应,所以不用转换。
图片仪表盘:
上面看到效果后会很有成就感,但要把仪表盘画得逼真可不容易,还要考虑缩放和字体,细节太多。因此有了这种方法,从网上找图片素材,自己加工仪表盘。直接先把图片画上去当背景,然后代码只控制表针即可。以后修改背景表盘,photoshop修图即可。
先看我找的素材:
再看我修好的效果:
上代码:
polo_base.h
/**************************************************************************************************
** File name: polo_baes.h (Polo_Base)
** Created by: Henrick.Nie at 2024-9-9
** Used for: The base class of the polo guage.
**************************************************************************************************/
#ifndef POLO_BASE_H
#define POLO_BASE_H
#include <QLabel>
#include <QPainter>
#include <QDebug>
class Polo_Base : public QLabel
{
Q_OBJECT
public:
explicit Polo_Base(QWidget *parent = nullptr);
protected:
QString m_sPicPath;
virtual void f_SetValue(const qreal &realValue);
private:
qreal m_realDegree, m_realMin = 40, m_realMax = 140;
void paintEvent(QPaintEvent *event);
void f_Draw(QPainter *p);
};
#endif // POLO_BASE_H
polo_base.cpp
#include "polo_base.h"
Polo_Base::Polo_Base(QWidget *parent) :
QLabel(parent)
{
}
void Polo_Base::paintEvent(QPaintEvent *event)
{
QLabel::paintEvent(event);
QPainter painter(this);
f_Draw(&painter);
}
void Polo_Base::f_Draw(QPainter *p)
{
QColor colorHand = QColor(Qt::red);
//Init the origin.
int iX0 = width() / 2, iY0 = height() - width() / 4;
p->setRenderHint(QPainter::Antialiasing);
p->translate(iX0, iY0);
//Draw the background picture.
QPixmap pic(m_sPicPath);
int iPicWidth = width(), iPicHeight = width() * pic.height() / pic.width();
int iLeft = 0 - iPicWidth / 2, iTop = 0 - iPicHeight + width() / 4;
p->drawPixmap(iLeft, iTop, width(), width() * pic.height() / pic.width(), pic);
//Draw hands.
int iHandSize = width() / 25, iHandLeft = iLeft - width() / 15, iHandLength = width() / 2 - width() / 20;
p->save();
p->setPen(QPen(QBrush(colorHand), iHandSize, Qt::SolidLine, Qt::RoundCap));
if (m_realDegree < m_realMin)
{
m_realDegree = m_realMin;
}
if (m_realDegree > m_realMax)
{
m_realDegree = m_realMax;
}
p->rotate(m_realDegree);
p->drawLine(iHandLeft, 0, iHandLeft + iHandLength, 0);
p->restore();
//Draw the hand heart on the origin.
int iHeartSize = width() / 4;
p->setPen(QPen(QBrush(QColor(Qt::darkGray)), iHeartSize, Qt::SolidLine, Qt::RoundCap));
p->drawPoint(0, 0);
p->end();
}
void Polo_Base::f_SetValue(const qreal &realValue)
{
m_realDegree = realValue;
update();
}
polo_watertemp.h
/**************************************************************************************************
** File name: polo_watertemp.h (Polo_WaterTemp)
** Created by: Henrick.Nie at 2024-9-9
** Used for: The water temprature guage of polo.
**************************************************************************************************/
#ifndef POLO_WATERTEMP_H
#define POLO_WATERTEMP_H
#include "polo_base.h"
class Polo_WaterTemp : public Polo_Base
{
Q_OBJECT
public:
explicit Polo_WaterTemp(QWidget *parent = nullptr);
void f_SetValue(const qreal &realValue) override;
};
#endif // POLO_WATERTEMP_H
polo_watertemp.cpp
#include "polo_watertemp.h"
Polo_WaterTemp::Polo_WaterTemp(QWidget *parent) :
Polo_Base(parent)
{
m_sPicPath = ":/images/polo_watertemp.jpg";
}
void Polo_WaterTemp::f_SetValue(const qreal &realValue)
{
//The given water temprature value, it equals the degrees.
Polo_Base::f_SetValue(realValue);
}
polo_fuel.h
/**************************************************************************************************
** File name: polo_fuel.h (Polo_Fuel)
** Created by: Henrick.Nie at 2024-9-9
** Used for: The fuel guage of polo.
**************************************************************************************************/
#ifndef POLO_FUEL_H
#define POLO_FUEL_H
#include "polo_base.h"
class Polo_Fuel : public Polo_Base
{
Q_OBJECT
public:
explicit Polo_Fuel(QWidget *parent = nullptr);
void f_SetValue(const qreal &realValue) override;
};
#endif // POLO_FUEL_H
polo_fuel.cpp
#include "polo_fuel.h"
Polo_Fuel::Polo_Fuel(QWidget *parent) :
Polo_Base(parent)
{
m_sPicPath = ":/images/polo_fuel.jpg";
}
void Polo_Fuel::f_SetValue(const qreal &realValue)
{
Polo_Base::f_SetValue(40 + realValue * 2.2);
}
燃料表这个要注意数值换算了。下面转速表也是。
tachometer.h
/**************************************************************************************************
** File name: tachometer.h (TachoMeter)
** Created by: Henrick.Nie at 2024-9-9
**************************************************************************************************/
#ifndef TACHOMETER_H
#define TACHOMETER_H
#include <QLabel>
#include <QPainter>
#include <QDebug>
class TachoMeter : public QLabel
{
Q_OBJECT
public:
explicit TachoMeter(QWidget *parent = nullptr);
void f_SetValue(const qreal &realValue);
private:
qreal m_realValue = -45, m_realMin = -45, m_realMax = 225;
void paintEvent(QPaintEvent *event);
void f_Draw(QPainter* p);
};
#endif // TACHOMETER_H
tachometer.cpp
#include "tachometer.h"
TachoMeter::TachoMeter(QWidget *parent) :
QLabel(parent)
{
}
void TachoMeter::paintEvent(QPaintEvent *event)
{
QLabel::paintEvent(event);
QPainter painter(this);
f_Draw(&painter);
}
void TachoMeter::f_Draw(QPainter *p)
{
//Init the size data.
int iSideSize = qMin(width(), height());
int iX0 = width() / 2, iY0 = height() / 2;
//Init the origin.
p->setRenderHint(QPainter::Antialiasing);
p->translate(iX0, iY0);
//Draw the background.
p->setPen(Qt::NoPen);
p->setBrush(QBrush(Qt::black));
p->drawRect(0 - width() / 2, 0 - height() / 2, width(), height());
QPixmap pic(":/images/tacho1.jpg");
int iLeft = 0 - iSideSize / 2, iTop = 0 - iSideSize / 2;
p->drawPixmap(iLeft, iTop, iSideSize, iSideSize, pic);
//Draw the hand.
p->save();
p->setPen(QPen(QBrush(Qt::red), iSideSize / 40, Qt::SolidLine, Qt::RoundCap));
if (m_realValue < m_realMin)
{
m_realValue = m_realMin;
}
if (m_realValue > m_realMax)
{
m_realValue = m_realMax;
}
p->rotate(m_realValue);
p->drawLine(0 - iSideSize / 2 + iSideSize / 20, 0, 0, 0);
p->restore();
//center
p->setPen(Qt::NoPen);
p->setBrush(QBrush(QColor(Qt::darkGray)));
p->drawEllipse(QPoint(0, 0), iSideSize / 20, iSideSize / 20);
p->end();
}
void TachoMeter::f_SetValue(const qreal &realValue)
{
//value: 0 to 800
//degree: -45 to 225 (225-(-45))=270
qreal realScale = 270.0 / 800.0; //per 10 rpm
m_realValue = 0 - 45 + realValue * realScale;
update();
}
效果:
效果解析:
上面效果图中,我在界面上放置了滑动条,实际操作时,可以改变滑块位置,直接看到仪表动作。
本文完。