Qt learning custom control 1: draw a simple to use dashboard

Keywords: Qt Python JSP Programming

Qt learning custom control 1: draw a simple to use dashboard

Opening words

I worked as an electrical engineer of new energy bus for several years before, and the platform was OK. All I had been in contact with were electrical principles, harnesses, batteries and various vehicle controllers, etc. all day long, it was basically those tedious and repetitive work, which fortunately created a lot of profits for the enterprise, but I had a great limitation on personal ability improvement and insight.

Later, I used my spare time to look up the information on the Internet and learned a little basic knowledge of Python, VBA, JSP, HTML, Qt and so on. I had a basic understanding of programming.

Later, due to various reasons, the previous platform began to collapse, coupled with their own educational background problems, so after being recommended by friends who do vehicle controller hardware, they entered the current company, slowly began to change their business to start controller software development, and began to self-study Qt.

Due to my poor memory, I forget what I have done and learned easily. I think it's still on my blog, and it's also convenient for me to browse. Don't talk too much, just get to the point.

Method 1: load SVG (if the SVG file cannot be uploaded, the screenshot will be displayed)

Dial screenshots

Pointer screenshots

widget.h:

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <qtimer.h>
#include <qsvgrenderer.h>

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = nullptr);
    ~Widget();

    inline qreal value() {return m_value;}
    inline void setValue(qreal val)
    {
        m_value = val > m_max ? m_max : (val < m_min ? m_min : val);
    }
    inline qreal min() {return m_min;}
    inline qreal max() {return m_max;}
    inline qreal startAngle() {return m_startAngle;}
    inline qreal endAngle() {return m_endAngle;}

protected:
    void paintEvent(QPaintEvent *);
    qreal calcAngle(qreal val);

public slots:
    void changeValue();
    
private:
    Ui::Widget *ui;
    
    qreal m_value = 0.0;
    qreal m_min = 0.0;
    qreal m_max = 240.0;
    qreal m_startAngle = -120;
    qreal m_endAngle = 120.0;
    
    QTimer *m_timer;
    QSvgRenderer* boardRender;
    QSvgRenderer* needleRender;
};

#endif // WIDGET_H

widget.cpp:

#include "widget.h"
#include "ui_widget.h"
#include <qpainter.h>
#include <qmath.h>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    resize(300, 300);

    boardRender = new QSvgRenderer();
    boardRender->load(QString("F:/QtPrj/VSG/speedometer.svg"));

    needleRender = new QSvgRenderer();
    needleRender->load(QString("F:/QtPrj/VSG/needel.svg"));

    m_timer = new QTimer();
    m_timer->setTimerType(Qt::PreciseTimer);
    m_timer->setInterval(50);
    connect(m_timer, SIGNAL(timeout()), this, SLOT(changeValue()));
    m_timer->start();
}

Widget::~Widget()
{
    delete ui;
}

void Widget::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setRenderHints(QPainter::Antialiasing |
                           QPainter::TextAntialiasing |
                           QPainter::SmoothPixmapTransform);

    qreal size = qMin(width(), height());

    painter.save();
    boardRender->render(&painter, QRectF((width()-size)/2, (height()-size)/2, size, size));
    painter.restore();

    painter.save();
    QTransform trans;
    trans.translate(width()/2, height()/2);
    trans.rotate(calcAngle(m_value), Qt::ZAxis);
    trans.translate(-width()/2, -height()/2);
    painter.setWorldTransform(trans);
    needleRender->render(&painter, QRectF((width()-size)/2, (height()-size)/2, size, size));
    painter.restore();

    painter.save();
    QFont textFont("Microsoft YaHei", static_cast<int>(12*size/200), QFont::Bold);
    painter.setFont(textFont);
    QFontMetricsF fm(textFont);
    QString text = QString::number(static_cast<int>(m_value), 10);
    qreal pixelWidth = fm.horizontalAdvance(text);
    qreal pixelHeight = fm.height();
    const QRectF rect = QRectF((width()-pixelWidth)/2, height()/2+height()/5, pixelWidth, pixelHeight);
    painter.setPen(Qt::white);
    painter.drawText(rect, Qt::AlignCenter, text);
    painter.restore();
}

qreal Widget::calcAngle(qreal val)
{
    return (val * (m_endAngle - m_startAngle) / (m_max - m_min) + m_startAngle);
}

void Widget::changeValue()
{
    static bool dirChange = false;

    if (!dirChange)
    {
        m_value += 2;
        if (m_value >= m_max)
        {
            m_value = m_max;
            dirChange = true;
        }
    }
    else
    {
        m_value -= 2;
        if (m_value <= m_min)
        {
            m_value = m_min;
            dirChange = false;
        }
    }
    
    int size = qMin(width(), height());
    update(QRect((width()-size)/2, (height()-size)/2, size, size));
}

Effect map 1:

Method 2: draw directly with code

qmgauge.h:

#ifndef QMGAUGE_H
#define QMGAUGE_H

#include <qwidget.h>
#include <qpainter.h>

class QmGauge : public QWidget
{
    Q_OBJECT

    Q_PROPERTY(qreal m_minimum READ minimum WRITE setMinimum)
    Q_PROPERTY(qreal m_maximum READ maximum WRITE setMaximum)
    Q_PROPERTY(qreal m_value READ value WRITE setValue)
    Q_PROPERTY(qreal m_startAngle READ startAngle WRITE setStartAngle)
    Q_PROPERTY(qreal m_endAngle READ endAngle WRITE setEndAngle)
    Q_PROPERTY(int m_majorScaleLines READ majorScaleLines WRITE setMajorScaleLines)
    Q_PROPERTY(int m_minorScaleLines READ minorScaleLines WRITE setMinorScaleLines)
    Q_PROPERTY(QString m_unitLabel READ unitLabel WRITE setUnitLabel)

public:
    explicit QmGauge(QWidget *parent = nullptr);
    ~QmGauge();

    inline qreal minimum() {return m_minimum;}
    inline void setMinimum(qreal val) {m_minimum = val;}
    inline qreal maximum() {return m_maximum;}
    inline void setMaximum(qreal val) {m_maximum = val;}
    inline qreal value() {return m_value;}
    inline void setValue(qreal val) {
        m_value = val > m_maximum ? m_maximum : (val < m_minimum ? m_minimum : val);}
    inline qreal startAngle() {return m_startAngle;}
    inline void setStartAngle(qreal angle) {m_startAngle = angle;}
    inline qreal endAngle() {return m_endAngle;}
    inline void setEndAngle(qreal angle) {m_endAngle = angle;}
    inline int majorScaleLines() {return m_majorScaleLines;}
    inline void setMajorScaleLines(int lines) {m_majorScaleLines = lines;}
    inline int minorScaleLines() {return m_minorScaleLines;}
    inline void setMinorScaleLines(int lines) {m_minorScaleLines = lines;}
    inline QString unitLabel() {return m_unitLabel;}
    inline void setUnitLabel(QString label) {m_unitLabel = label;}

public slots:
    void replot();

protected:
    void drawPlate(QPainter *painter);
    void drawScaleLines(QPainter *painter);
    void drawScaleLabels(QPainter *painter);
    void drawScalLabelByChar(QPainter *painter);
    void drawNeedle(QPainter *painter);
    void drawUnitLabel(QPainter *painter);
    virtual void paintEvent(QPaintEvent *);

private:
    qreal m_minimum = 0.0;
    qreal m_maximum = 240.0;
    qreal m_value = 0.0;
    qreal m_startAngle = -120.0;
    qreal m_endAngle = 120.0;
    int m_majorScaleLines = 13;
    int m_minorScaleLines = 4;
    QString m_unitLabel = QString(tr("km/h"));
};

#endif // QMGAUGE_H

qmgauge.cpp:

#include "qmgauge.h"
#include <QLinearGradient>
#include <qmath.h>
#include <qdebug.h>

/**
 * @brief QmGauge::QmGauge
 * @param parent
 */
QmGauge::QmGauge(QWidget *parent) :
    QWidget(parent)
{
    //Set minimum size
    setMinimumSize(QSize(100, 100));
}

/**
 * @brief QmGauge::~QmGauge
 */
QmGauge::~QmGauge()
{
}

/**
 * @brief QmGauge::replot
 */
void QmGauge::replot()
{
    //Update drawing area only, reduce calculation
    int minWidth = qMin(width(), height());
    update((width()-minWidth)/2, (height()-minWidth)/2, minWidth, minWidth);
}

/**
 * @brief QmGauge::drawPlate
 * @param painter
 */
void QmGauge::drawPlate(QPainter *painter)
{
    //Save drawing device current state
    painter->save();

    painter->setPen(Qt::NoPen);
    qreal radius = qMin(width(), height())/2;
    //Draw the dial strand skeleton
    QLinearGradient lg1 = QLinearGradient(-radius, -radius, radius, radius);
    lg1.setColorAt(0.0, QColor(240, 240, 240));
    lg1.setColorAt(1.0, QColor(200, 200, 200));
    lg1.setSpread(QGradient::RepeatSpread);
    painter->setBrush(lg1);
    painter->drawEllipse(QPointF(0, 0), radius, radius);

    QLinearGradient lg2 = QLinearGradient(-radius, -radius, radius, radius);
    lg2.setColorAt(0.0, QColor(210, 210, 210));
    lg2.setColorAt(1.0, QColor(170, 170, 170));
    lg2.setSpread(QGradient::RepeatSpread);
    painter->setBrush(lg2);
    painter->drawEllipse(QPointF(0, 0), radius-radius/20, radius-radius/20);

    //Draw Dial Black disc
    painter->setBrush(QColor(30, 30, 30));
    painter->drawEllipse(QPointF(0, 0), radius-radius/10, radius-radius/10);
    //Restore drawing device to previous state
    painter->restore();
}

/**
 * @brief QmGauge::drawScaleLines
 * @param painter
 */
void QmGauge::drawScaleLines(QPainter *painter)
{
    //Save drawing device current state
    painter->save();
    qreal currentWidth = qMin(width(), height())/2;
    qreal startPosX = -currentWidth + currentWidth/4;
    qreal minorEndPosX = startPosX + currentWidth/32;
    qreal majorEndPosX = startPosX + currentWidth/16;
    qreal angleInc = (m_endAngle - m_startAngle) / (m_majorScaleLines-1);
    angleInc /= m_minorScaleLines + 1;

    //Rotate to the angle corresponding to the minimum value
    painter->rotate(m_startAngle + 90);
    qreal minWidth = qMin(minimumWidth(), minimumHeight());
    QPen pen;
    pen.setColor(Qt::white);
    //Draw tick marks
    for (int i = 0; i < m_majorScaleLines; i++)
    {
        pen.setWidthF(2.0*currentWidth*2/minWidth); //Let lineweight scale with window
        painter->setPen(pen);
        painter->drawLine(QPointF(startPosX, 0), QPointF(majorEndPosX, 0));
        if (i < m_majorScaleLines-1)
        {
            pen.setWidthF(1.0*currentWidth*2/minWidth);
            painter->setPen(pen);
            for (int j = 0; j < m_minorScaleLines; j++)
            {
                painter->rotate(angleInc);
                painter->drawLine(QPointF(startPosX, 0), QPointF(minorEndPosX, 0));
            }
            painter->rotate(angleInc);
        }
    }
    //Restore drawing device to previous state
    painter->restore();
}

/**
 * @brief QmGauge::drawScaleLabels
 * @param painter
 * @note The idea of drawing around text labels is as follows:
 *       1. First, each text label is drawn to the corresponding pixmap memory block;
 *       2. Then draw pixmap to the interface.
 */
void QmGauge::drawScaleLabels(QPainter *painter)
{
    //Save drawing device current state
    painter->save();
    //Create a font and let the text size scale with the window
    QFont labelFont = QFont("Microsoft YaHei",
                            4*qMin(width(),height())/qMin(minimumWidth(),minimumHeight()),
                            QFont::Bold);
    /* Create a text measure, noting that if you want the label text to wrap correctly,
     * You must calculate the pixel height and width of the text maximum,
     * And draw the outer frame with the maximum value of the height and width */
    QFontMetrics tagFontMetrics = QFontMetrics(labelFont);
    int pixelWidth = tagFontMetrics.horizontalAdvance(QString::number(static_cast<int>(m_maximum), 10));
    int pixelHeight = tagFontMetrics.height();
    int maxTxtWidth = qMax(pixelWidth, pixelHeight)+1;
    //Calculate label increment
    int valueInc = static_cast<int>((m_maximum-m_minimum)/(m_majorScaleLines-1));
    // Calculate the start position of drawing label text
    qreal minWidth = qMin(width(), height())/2;
    qreal startPosX = -minWidth + minWidth/14;
    //Calculate the drawing angle increment of label text position
    qreal angleInc = (m_endAngle - m_startAngle) / (m_majorScaleLines-1);
    //Initialize drawing device
    painter->setFont(labelFont); //Set font
    painter->rotate(m_startAngle + 90); //Rotate to start drawing position
    painter->setPen(Qt::white);//Set brush color
    //Start drawing label text
    for (int i = 0; i < m_majorScaleLines; i++)
    {
        //Convert label numbers to text
        QString label = QString::number(valueInc*i, 10);
        //Create the label text pixmap, which is drawn to memory
        QPixmap pixmap(maxTxtWidth, maxTxtWidth);
        pixmap.fill(Qt::transparent); //Background transparent fill
        QPainter p(&pixmap); //Create pixmap temporary virtual drawing device
        QTransform transf; //Create virtual transformation
        transf.translate(maxTxtWidth/2, maxTxtWidth/2); //Coordinate origin transformation to pixmap Center
        transf.rotate(-90); //Rotate the coordinate system 90 degrees clockwise, that is, rotate the text 90 degrees
        transf.translate(-maxTxtWidth/2, -maxTxtWidth/2); //Restore coordinate system origin
        p.setWorldTransform(transf); //Apply virtual transformations to temporarily created drawing devices
        p.setPen(Qt::white);
        p.setFont(labelFont);
        p.drawText(QRectF(0, 0, maxTxtWidth, maxTxtWidth), Qt::AlignCenter, label); //Draw text to pixmap
        //Draw pixmap to the interface
        QRectF targetRect = QRectF(startPosX, -maxTxtWidth/2, maxTxtWidth, maxTxtWidth);
        QRectF sourceRect; //Be careful not to set the value of this rectangle, otherwise the drawing position will be incorrect
        painter->drawPixmap(targetRect, pixmap, sourceRect); //Call interface drawing device to draw memory block pixmap
        //Rotate fixed angle after drawing one label at a time
        painter->rotate(angleInc);
    }
    //Restore drawing device to previous state
    painter->restore();
}

/**
 * @brief QmGauge::drawScalLabelByChar
 * @param painter
 */
void QmGauge::drawScalLabelByChar(QPainter *painter)
{
    painter->save();
    QFont labelFont = QFont("Microsoft YaHei",
                            4*qMin(width(),height())/qMin(minimumWidth(),minimumHeight()),
                            QFont::Bold);
    QFontMetrics tagFontMetrics = QFontMetrics(labelFont);
    int pixelWidth = tagFontMetrics.horizontalAdvance(QString("0"));
    int pixelHeight = tagFontMetrics.height();
    int maxTxtWidth = qMax(pixelWidth, pixelHeight);
    int valueInc = static_cast<int>((m_maximum-m_minimum)/(m_majorScaleLines-1));
    qreal minWidth = qMin(width(), height())/2;
    qreal startPosX = -minWidth + minWidth/11;
    qreal angleInc = (m_endAngle - m_startAngle) / (m_majorScaleLines-1);
    painter->setFont(labelFont);
    painter->rotate(m_startAngle + 90);
    painter->setPen(Qt::white);
    qreal anglePerChar = qAbs(qAsin(static_cast<qreal>(maxTxtWidth)/2.0/qAbs(startPosX))*180.0/M_PI);
    QList<QString> labelList;
    labelList.clear();
    for (int i = 0; i < m_majorScaleLines; i++)
    {
        labelList.append(QString::number(valueInc*i, 10));
    }
    QList<int> indexList;
    indexList.clear();
    for(int i = 0; i < labelList.count()-1; i++)
    {
        if (labelList.at(i).size() != labelList.at(i+1).size())
        {
            indexList.append(i);
        }
    }
    for (int i = 0; i < labelList.count(); i++)
    {
        int charCount = labelList.at(i).size();
        painter->rotate(-anglePerChar*(charCount-1));
        for (int j = 0; j < charCount; j++)
        {
            QPixmap pixmap(maxTxtWidth, maxTxtWidth);
            pixmap.fill(Qt::transparent);
            QPainter p(&pixmap);
            QTransform transf;
            transf.translate(maxTxtWidth/2, maxTxtWidth/2);
            transf.rotate(-90);
            transf.translate(-maxTxtWidth/2, -maxTxtWidth/2);
            p.setWorldTransform(transf);
            p.setPen(Qt::white);
            p.setFont(labelFont);
            p.drawText(QRectF(0, 0, maxTxtWidth, maxTxtWidth), Qt::AlignCenter, labelList.at(i).at(j));
            QRectF targetRect = QRectF(startPosX, -maxTxtWidth/2, maxTxtWidth, maxTxtWidth);
            QRectF sourceRect;
            painter->drawPixmap(targetRect, pixmap, sourceRect);
            if (j < charCount-1)
            {
                painter->rotate(anglePerChar);
            }
            else
            {
                for (int k = 0; k < indexList.count(); k++)
                {
                    if (indexList.at(k) == i)
                    {
                        painter->rotate(anglePerChar/2.0);
                        indexList.removeAt(k);
                    }
                }
            }
        }
        painter->rotate(angleInc);
    }
    painter->restore();
}

/**
 * @brief QmGauge::drawNeedle
 * @param painter
 */
void QmGauge::drawNeedle(QPainter *painter)
{
    int radius;

    //Save drawing device current state
    painter->save();
    //Calculate the current angle of the pointer
    qreal currentAngle = -90 + m_startAngle + (m_value - m_minimum) * (m_endAngle - m_startAngle) / (m_maximum- m_minimum);
    //Draw pointer
    painter->rotate(currentAngle);
    radius = qMin(width(), height()) / 32;
    QRadialGradient rg1(0, 0, radius*11, 0, 0);
    rg1.setColorAt(0.0, QColor(200, 200, 200));
    rg1.setColorAt(1.0, QColor(240, 240, 240));
    painter->setPen(Qt::white);
    painter->setBrush(rg1);
    const QPointF needledPoints[3] = {QPointF(0.0, radius*0.75),
                                      QPointF(0.0, -radius*0.75),
                                      QPointF(radius*11, 0)};
    painter->drawPolygon(needledPoints, 3);
    //Restore drawing device to previous state
    painter->restore();

    //Save drawing device current state
    painter->save();
    //Calculate cap radius
    radius = qMin(width(), height()) / 20;
    //Draw pointer cap
    QRadialGradient rg2(0, 0, radius*2);
    rg2.setColorAt(0.0, Qt::darkGray);
    rg2.setColorAt(0.5, Qt::white);
    rg2.setColorAt(1.0, Qt::darkGray);
    painter->setPen(Qt::NoPen);
    painter->setBrush(rg2);
    painter->drawEllipse(QPoint(0, 0), radius, radius);
    //Restore drawing device to previous state
    painter->restore();
}

/**
 * @brief QmGauge::drawUnitLabel
 * @param painter
 */
void QmGauge::drawUnitLabel(QPainter *painter)
{
    //Save drawing device current state
    painter->save();
    //Create a font and let the text size scale with the window
    QFont unitFont = QFont("Microsoft YaHei",
                            4*qMin(width(),height())/qMin(minimumWidth(),minimumHeight()),
                            QFont::Bold);
    //Draw unit label
    QFontMetrics unitFontMetrics = QFontMetrics(unitFont);
    int pixelWidth = unitFontMetrics.horizontalAdvance(m_unitLabel);
    int pixelHeight = unitFontMetrics.height();
    painter->setFont(unitFont);
    painter->setPen(Qt::white);
    qreal limit = qMin(width(), height())/2.0/8.0;
    qreal posY = height()/18.0;
    if (posY > limit)
    {
        posY = limit;
    }
    painter->drawText(QRectF(-pixelWidth/2.0, posY, pixelWidth, pixelHeight), Qt::AlignCenter, m_unitLabel);

    //Create a font and let the text size scale with the window
    QFont valueFont = QFont("Microsoft YaHei",
                            6*qMin(width(),height())/qMin(minimumWidth(),minimumHeight()),
                            QFont::Bold);
    //Draw real time values
    QFontMetrics valueFontMetrics = QFontMetrics(valueFont);
    QString valueStr = QString::number(static_cast<int>(m_value), 10);
    pixelWidth = valueFontMetrics.horizontalAdvance(valueStr);
    pixelHeight = valueFontMetrics.height();
    painter->setFont(valueFont);
    limit = qMin(width(), height())/2.0/4.0;
    posY = height()/6.0;
    if (posY > limit)
    {
        posY = limit;
    }
    painter->drawText(QRectF(-pixelWidth/2.0, posY, pixelWidth, pixelHeight), Qt::AlignCenter, valueStr);
    //Restore drawing device to previous state
    painter->restore();
}

/**
 * @brief QmGauge::paintEvent
 */
void QmGauge::paintEvent(QPaintEvent *)
{
    //Create interface drawing device
    QPainter painter(this);
    //Set anti aliasing
    painter.setRenderHints(QPainter::Antialiasing |
                           QPainter::TextAntialiasing |
                           QPainter::SmoothPixmapTransform);
    //Transform coordinate origin to interface Center
    painter.translate(QPointF(width()/2, height()/2));
    //Call drawing function
    drawPlate(&painter);
    drawScaleLabels(&painter);
//    drawScalLabelByChar(&painter);
    drawScaleLines(&painter);
    drawNeedle(&painter);
    drawUnitLabel(&painter);
}

Be careful:
1) The drawScaleLabels function draws a numeric tag string directly on a QPixmap.
2) drawScalLabelByChar draws each character in a numeric label string onto a QPixmap.
The former function will be a little more concise. When the given interface size is updated with 50ms, it will not take up any extra CPU resources. The latter function will take up a certain amount of CPU resources with a large amount of calculation. The display effect is almost the same. The reader can test it himself.

qmwidgetsdemo.h

#ifndef QMWIDGETSDEMO_H
#define QMWIDGETSDEMO_H

#include <QWidget>
#include <QmGauge/qmgauge.h>
#include <qgridlayout.h>
#include <qtimer.h>

class QmWidgetsDemo : public QWidget
{
    Q_OBJECT

public:
    explicit QmWidgetsDemo(QWidget *parent = nullptr);
    ~QmWidgetsDemo();

public slots:
    void updateGauge();

private:
    QGridLayout *gridLayout;
    QmGauge *gauge;
    QTimer *m_timer;
    qreal m_value = 0.0;
};

#endif // QMWIDGETSDEMO_H

qmwidgetsdemo.cpp

#include "qmwidgetsdemo.h"

QmWidgetsDemo::QmWidgetsDemo(QWidget *parent) :
    QWidget(parent)
{
    resize(300, 300);

    gauge = new QmGauge();
    gridLayout = new QGridLayout();
    gridLayout->addWidget(gauge, 0, 0, 1, 1);
    setLayout(gridLayout);

    m_timer = new QTimer();
    connect(m_timer, SIGNAL(timeout()), this, SLOT(updateGauge()));
    m_timer->start(50);
}

QmWidgetsDemo::~QmWidgetsDemo()
{
}

void QmWidgetsDemo::updateGauge()
{
    static bool changeDir = false;

    if (!changeDir)
    {
        m_value += 2;
        if (m_value >= 240)
        {
            m_value = 240;
            changeDir = true;
        }
    }
    else
    {
        m_value -= 2;
        if (m_value <= 0)
        {
            m_value = 0;
            changeDir = false;
        }
    }
    gauge->setValue(m_value);
    gauge->replot();
}

Effect map 2:

The first time in CSDN, I hope to help readers!

Published 1 original article · praised 0 · visited 7
Private letter follow

Posted by mkoga on Tue, 17 Mar 2020 00:20:08 -0700