Sometimes it is required for us to create some of the advanced controls apart from the basics which are provided by the QML. Qt provides a class called QQuickPaintedItem which can be used to create a custom item for the QML using QPainter API in the Scene Graph.

We can see that nowadays Qt and QML used widely in the automotive industry for both infotainment and instrument cluster. Currently Qt is offering custom Dial item only in the commercial version of Qt which is part of Enterprise controls.

In the past I have worked on similar kind of project in which I have created custom dial item using QDeclarativeItem in Qt 4. here in this post I am going to recreate similar kind of Dial item with minimal functionality.

We have to follow few steps to create a custom item and to use it with QML.

  1. Create a new class by sub classing QQuickPaintedItem.
  2. Define all the properties which has to be exposed to the QML as part of this Item.
  3. Re-implement the  void paint(QPainter *painter) method and perform all the drawing for our custom dial item.
  4. Expose the new Item to the QML by using qmlRegisterType 
  5. import our new module in QML file. Use this new item like any other item in QML

The dial item which I have created only has the background dial, tick marks and numbers. It does not provide the needle. For needle I am using an image which I am placing over my Dial item in the QML and set the rotation property for it.

Here is my implementation for the custom dial item.

dialitem.h

#ifndef DIALITEM_H
#define DIALITEM_H

#include <QQuickPaintedItem>

class DialItem : public QQuickPaintedItem
{
    Q_OBJECT
    Q_PROPERTY(int startAngle READ getStartAngle WRITE setStartAngle NOTIFY startAngleChanged)
    Q_PROPERTY(qreal spanAngle READ getSpanAngle WRITE setSpanAngle NOTIFY spanAngleChanged)
    Q_PROPERTY(int dialWidth READ getDialWidth WRITE setDialWidth NOTIFY dialWidthChanged)
    Q_PROPERTY(QColor dialColor READ getDialColor WRITE setDialColor NOTIFY dialColorChanged)

public:
    DialItem(QQuickItem *parent = 0);
    void paint(QPainter *painter);
    int getStartAngle() {return m_StartAngle;}
    qreal getSpanAngle() {return m_SpanAngle;}
    int getDialWidth() {return m_DialWidth;}
    QColor getDialColor() {return m_DialColor;}
    void setStartAngle(int angle) {m_StartAngle = angle; this->update();}
    void setSpanAngle(qreal angle) {m_SpanAngle = angle; this->update();}
    void setDialWidth(int angle) {m_DialWidth = angle; this->update();}
    void setDialColor(QColor color) {m_DialColor = color; this->update();}

signals:
    void startAngleChanged();
    void spanAngleChanged();
    void dialWidthChanged();
    void dialColorChanged();

private:
    int m_StartAngle;
    qreal m_SpanAngle;
    int m_DialWidth;
    QColor m_DialColor;
};
#endif // DIALITEM_H

dialitem.cpp

#include <QTime>
#include <QPainter>

#include "dialitem.h"

#define PI 3.14159265

DialItem::DialItem(QQuickItem *parent)
    : QQuickPaintedItem(parent)
{
    m_StartAngle = 0;
    m_SpanAngle = 180;
    m_DialWidth = 4;
    m_DialColor = QColor(0, 0, 0);
    connect(this, SIGNAL(startAngleChanged()), this, SLOT(update()));
    connect(this, SIGNAL(spanAngleChanged()), this, SLOT(update()));
    connect(this, SIGNAL(dialWidthChanged()), this, SLOT(update()));
    connect(this, SIGNAL(dialColorChanged()), this, SLOT(update()));
}

void DialItem::paint(QPainter *painter)
{
    QRectF rect = this->boundingRect();
    painter->setRenderHint(QPainter::Antialiasing);
    QPen pen = painter->pen();
    pen.setColor(Qt::black);

    //Draw outer dial
    painter->save();
    pen.setWidth(m_DialWidth);
    pen.setColor(m_DialColor);
    painter->setPen(pen);
    double startAngle = -90 - m_StartAngle;
    double spanAngle = 0 - m_SpanAngle;
    qreal offset = m_DialWidth / 2;
    painter->drawArc(rect.adjusted(offset, offset, -offset, -offset), startAngle * 16, spanAngle * 16);
    painter->restore();

    //Draw major ticks
    painter->save();
    pen.setWidth(4);
    painter->translate(width() / 2, height() / 2);
    painter->rotate(-startAngle);
    painter->setPen(pen);
    qreal angleStep = m_SpanAngle / 10;
    for(int i = 0; i < 11; ++i)
    {
        painter->drawLine(rect.width()/2 - 30, 0, rect.width()/2 - 2, 0);
        painter->rotate(angleStep);
    }
    painter->translate(-width() / 2, -height() / 2);
    painter->restore();

    //Draw minor ticks
    painter->save();
    pen.setWidth(2);
    painter->translate(width() / 2, height() / 2);
    painter->rotate(-startAngle);
    painter->setPen(pen);
    int minorTicks = 8* 10;
    qreal angleStepMin = m_SpanAngle / minorTicks;
    for(int i = 0; i < minorTicks; ++i)
    {
        painter->drawLine(rect.width()/2 - 15, 0, rect.width()/2 - 2, 0);
        painter->rotate(angleStepMin);
    }
    painter->translate(-width() / 2, -height() / 2);
    painter->restore();

    //Draw numbers
    painter->save();
    pen.setColor(Qt::white);
    painter->setPen(pen);
    const int radius = rect.width()/2 - 45;
    QFontMetrics fontMat(painter->font());
    QString numbr;
    for (double i = 0; i <= m_SpanAngle; i += m_SpanAngle/10)
    {
        QPoint p(rect.center().x() + radius * cos(-90-i * PI / 180.0), rect.center().y() - radius * sin(-90-i * PI / 180.0));        numbr = QString::number(i);
        QRect bound = fontMat.boundingRect(numbr);
        painter->drawText(QPointF((p.x() - bound.width()/2), (p.y() + bound.height()/2)), numbr);    
    }
    painter->restore();
}

And in the main.cpp we will expose the new item inside the QML.

main.cpp

#include <QGuiApplication>
#include <QQmlApplicationEngine>

#include "dialitem.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    //register new item to the QML
    qmlRegisterType<DialItem>("IVIControls", 1, 0, "DialItem");

    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    return app.exec();
}

 

And here is my demo usage of my DialItem. Iam using a Slider item to modify the value of the Dial.

main.qml

import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Controls 1.2
import IVIControls 1.0

ApplicationWindow {
    visible: true
    width: 800
    height: 480

    Rectangle {
        anchors.fill: parent
        color: "#303030"
    }

    DialItem {
        id: speed
        objectName: "speed"
        width: 350
        height: width
        anchors.centerIn: parent
        startAngle: 30
        spanAngle: 300
        dialWidth: 6
        dialColor: "#4646FF"

        Image {
            id: needle
            source: "qrc:/images/needle.png"
            anchors.centerIn: parent
            rotation: 39 + 30 + slider.value
            Behavior on rotation { SpringAnimation { spring: 2; damping: 0.2 } }
        }
    }

    Slider {
        id: slider
        anchors.top: speed.bottom
        anchors.horizontalCenter: parent.horizontalCenter
        width: 400
        minimumValue: 0
        maximumValue: 300
    }
}

 

And finally here is my application running.

ividemoscreen

 

Source code:

https://github.com/arunpkqt/IVIDialDemo

 

2 thoughts on “Custom Dial Item using QQuickPaintedItem

  1. Hello Arun, is there a way to change the numbering values to a show a different scale, say
    0 – 100 while dividing itself equally and occupying the entire dial ?

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s