Raymii.org
Quis custodiet ipsos custodes?Home | About | All pages | Cluster Status | RSS Feed
Drawing a Circle in Qt QML three different ways
Published: 05-07-2023 23:59 | Author: Remy van Elst | Text only version of this article
❗ This post is over one years old. It may no longer be up to date. Opinions may have changed.
Table of Contents
Qt has no Circle
built in to QML as a basic type, as for example the Rectangle
or the Button
control. This post shows you how to get a Circle
in QML, from the most basic method (a Rectangle
with a radius
of 180) to more advanced methods, using the Canvas
JavaScript API (which allows us to draw a partially filled Circle, for a Pie Chart) and a c++
control based on QQuickPaintedItem
. I wanted to experiment with the Canvas
QML control and the QQuickPaintedItem
C++ interface to get a better understanding of Qt and QML drawing interfaces, this post reflects that journey including showing your grouped QML properties exposed from C++.
Recently I removed all Google Ads from this site due to their invasive tracking, as well as Google Analytics. Please, if you found this content useful, consider a small donation using any of the options below:
I'm developing an open source monitoring app called Leaf Node Monitoring, for windows, linux & android. Go check it out!
Consider sponsoring me on Github. It means the world to me if you show your appreciation and you'll help pay the server costs.
You can also sponsor me by getting a Digital Ocean VPS. With this referral link you'll get $200 credit for 60 days. Spend $25 after your credit expires and I'll get $25!
With all the controls in it, the program looks like:
In this post I'm going to show you QML Profiler screenshots. These are shown as a comparison between the 3 methods on my specific machine. Don't treat them as a benchmark, do that yourself if you experience performance issues.
The QML program has a grid with all the controls in it. Per section I'm commenting out all but one and then taking a screenshot. Without any controls, so just the grid, the QML profiler looks like this:
This article also shows how to expose a Grouped QML Property. This allows
you to set border.width
and border.color
in your custom C++ exposed QML
control.
For this example I'm using Qt 5.15, the code also works with Qt 6.4.
QML Rectangle Circle
The simplest and most basic method to get a Circle
in QML is a Rectangle
with a radius
property set to width / 2
or 180
:
Rectangle {
width: 150
height: 150
color: "dodgerblue"
radius: 180
}
Which looks like:
The QML Profiler shows that this is quite fast:
The rectangle drawing method has a few advantages, namely that you get a
border
, a fill color and all the other advantages that this QML Control gives
you. Later on in the C++
example you'll see that we need to provide that all
ourselves.
QML Canvas Circle
Most of the examples you'll find online regarding a Circle in QML refer to the
Canvas
or Shapes
API. The Canvas
API is a JavaScript way to draw stuff on
screen. The disadvantage is that this requires a lot more memory and resources,
but you do get a lot of flexibility.
My QMLCircle.qml
file is pasted at the end of this post and supports being
used as a Pie chart. One section is filled in with one color and a different
section is filled with another color, including an Animation
so that it looks
nice. Useful for a Chart
-like graphics.
The basic circle looks the same as the Rectangle
-with-radius circle:
QMLCircle {
primaryColor: "skyblue"
}
The QML profiler shows that a lot more is happening:
Is this slower? Probably, and almost certainly on embedded deviced
When using the 'pie-chart' feature it looks like this:
QMLCircle {
primaryColor: "skyblue"
secondaryColor: "tomato"
value: 0.87
}
Using it combined with the Animation
is also quite cool:
property int timerDuration: 10
property int timerSecDone: 0
QMLCircle {
value: (timerSecDone * 100 / timerDuration) / 100
primaryColor: "skyblue"
secondaryColor: "tomato"
}
Timer {
interval: 1000
running: true
triggeredOnStart: true
repeat: true
onTriggered: {
timerSecDone++
if (timerSecDone > timerDuration+1) {
timerSecDone = 0;
}
}
}
Using Canvas
gives you a bunch of flexibility, this Animation
was really
easy and quick to add. (Especially in comparison to the C++ style in the next
section). In one of the Coffee Machines we make at work almost this exact code
is used in one of the UI's to show a progress bar of the consumption status. It
includes a bit more 'fancyness', styling, animation, but it boils down to the
same code. I know because I wrote it.
C++ QML Circle
The last example I want to show is a C++ based Circle
. It uses the Qt
Drawing API
I've coded up a basic QQuickPaintedItem
which draws a Circle. It has two
properties, color
and antialiassing
. The latter is to make it look smooth,
the first is for the fill color. It looks like this:
CppCircle {
width: 150
height: 150
color: "greenyellow"
antialiasing: true
border.color: "black"
border.width: 1
}
CppCircle {
width: 150
height: 150
color: "hotpink"
antialiasing: false
border.color: "black"
border.width: 5
}
The second (hotpink
) circle is not anti-aliassed. The basic code is a class derived from QQuickPaintedItem
. The most important method is the paint
method:
void CppCircle::paint(QPainter *painter)
{
// make it smooth
if(antialiasing())
painter->setRenderHint(QPainter::Antialiasing);
// create rect which will be used to draw circle in
QRectF rect(0 + border()->width(), 0 + border()->width(), width() - 1 - (border()->width()*2), height() - 1 - (border()->width()*2));
// create brush based on QML color property
QBrush brush(m_color);
// use brush to fill figures
painter->setBrush(brush);
// create pen
QPen pen;
if(border()->width() > 0) {
pen.setBrush(border()->color());
pen.setWidth(border()->width());
pen.setStyle(Qt::SolidLine);
}
else {
pen.setStyle(Qt::NoPen);
}
painter->setPen(pen);
// Draw the circle
painter->drawEllipse(rect);
}
The full code is at the end of the article but the gist should be clear. A Pen
is used to draw lines and outlines, a Brush
is used to fill figures. The full
code at the end of this page shows border
as a QML Property Group so
that I can set border.width
and border.color
.
The QML profiler seems to show that this custom control is even faster than the
Rectangle
(which was 28 microseconds
compared to 21
):
The C++ method is the fastest of the bunch, but it is also the most limited.
Adding the border
property took way more time than the Animation
in the
Javascript QML Control, so this is a tradeoff you need to make for yourself.
Full source code
Here below you'll find the full source code for the program. First an animated gif of the full program. Any artifacts or stuttering are caused by the recording software. It's super smooth on my local machine.
main.qml
/*
* Copyright (c) 2023 Remy van Elst
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Window 2.15
import org.raymii.shapes 1.0
Window {
width: 640
height: 480
visible: true
title: qsTr("QML Circles demo by Raymii.org")
property int timerDuration: 10
property int timerSecDone: 0
Grid {
anchors.fill: parent
anchors.margins: 10
spacing: 10
columns: 3
rows: 3
Rectangle {
id: rectCircle
width: 150
height: 150
color: "dodgerblue"
radius: 180
}
QMLCircle {
primaryColor: "skyblue"
secondaryColor: "tomato"
value: 0.87
}
CppCircle {
width: 150
height: 150
color: "greenyellow"
antialiasing: true
border.color: "black"
border.width: 1
}
CppCircle {
width: 150
height: 150
color: "hotpink"
antialiasing: false
border.color: "black"
border.width: 5
}
QMLCircle {
value: (timerSecDone * 100 / timerDuration) / 100
primaryColor: "skyblue"
secondaryColor: "tomato"
}
QMLCircle {
value: 0.15
}
}
Timer {
interval: 1000
running: true
triggeredOnStart: true
repeat: true
onTriggered: {
timerSecDone++
if (timerSecDone > timerDuration+1) {
timerSecDone = 0;
}
}
}
}
main.cpp
/*
* Copyright (c) 2023 Remy van Elst
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "cppcircle.h"
#include <QGuiApplication>
#include <QQmlApplicationEngine>
int main(int argc, char *argv[])
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
qmlRegisterType<CppCircle>("org.raymii.shapes", 1, 0, "CppCircle");
// You MUST make this type know to QML otherwise you'll receive an error:
// Invalid grouped property access: Property "border" with type "BorderGroupedProperty*", which is not a value type
qmlRegisterType<BorderGroupedProperty>("org.raymii.shapes", 1, 0, "BorderGroupedProperty");
const QUrl url(QStringLiteral("qrc:/main.qml"));
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
&app, [url](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl)
QCoreApplication::exit(-1);
}, Qt::QueuedConnection);
engine.load(url);
return app.exec();
}
cppCircle.h
/*
* Copyright (c) 2023 Remy van Elst
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#pragma once
#include <QObject>
#include <QQuickPaintedItem>
#include <QPainter>
class BorderGroupedProperty : public QObject
{
Q_OBJECT
Q_PROPERTY(int width READ width WRITE setWidth NOTIFY widthChanged)
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
public:
BorderGroupedProperty(QObject* parent = nullptr);
int width() const;
void setWidth(int newWidth);
QColor color() const;
void setColor(const QColor &newColor);
signals:
void widthChanged();
void colorChanged();
private:
int m_width = 0;
QColor m_color = QColor(0,0,0);
};
class CppCircle : public QQuickPaintedItem
{
Q_OBJECT
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
Q_PROPERTY(bool antialiasing READ antialiasing WRITE setAntialiasing NOTIFY antialiasingChanged)
Q_PROPERTY(BorderGroupedProperty* border READ border)
public:
explicit CppCircle(QQuickItem *parent = nullptr);
virtual void paint(QPainter *painter);
QColor color() const;
void setColor(const QColor &newColor);
bool antialiasing() const;
void setAntialiasing(bool newAntialiasing);
BorderGroupedProperty *border() const;
signals:
void colorChanged();
void antialiasingChanged();
private:
QColor m_color;
bool m_antialiasing;
BorderGroupedProperty *m_border = nullptr;
};
cppCircle.cpp
/*
* Copyright (c) 2023 Remy van Elst
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "cppcircle.h"
#include <QPen>
CppCircle::CppCircle(QQuickItem *parent)
: QQuickPaintedItem{parent}, m_border(new BorderGroupedProperty(this))
{
}
QColor CppCircle::color() const
{
return m_color;
}
void CppCircle::setColor(const QColor &newColor)
{
if (m_color == newColor)
return;
m_color = newColor;
emit colorChanged();
}
bool CppCircle::antialiasing() const
{
return m_antialiasing;
}
void CppCircle::setAntialiasing(bool newAntialiasing)
{
if (m_antialiasing == newAntialiasing)
return;
m_antialiasing = newAntialiasing;
emit antialiasingChanged();
}
void CppCircle::paint(QPainter *painter)
{
// make it smooth
if(antialiasing())
painter->setRenderHint(QPainter::Antialiasing);
// create rect which will be used to draw circle in
QRectF rect(0 + border()->width(), 0 + border()->width(), width() - 1 - (border()->width()*2), height() - 1 - (border()->width()*2));
// create brush based on QML color property
QBrush brush(m_color);
// use brush to fill figures
painter->setBrush(brush);
// create pen
QPen pen;
if(border()->width() > 0) {
pen.setBrush(border()->color());
pen.setWidth(border()->width());
pen.setStyle(Qt::SolidLine);
}
else {
pen.setStyle(Qt::NoPen);
}
painter->setPen(pen);
// Draw the circle
painter->drawEllipse(rect);
}
BorderGroupedProperty::BorderGroupedProperty(QObject *parent) : QObject(parent)
{
}
int BorderGroupedProperty::width() const
{
return m_width;
}
void BorderGroupedProperty::setWidth(int newWidth)
{
if (m_width == newWidth)
return;
m_width = newWidth;
emit widthChanged();
}
QColor BorderGroupedProperty::color() const
{
return m_color;
}
void BorderGroupedProperty::setColor(const QColor &newColor)
{
if (m_color == newColor)
return;
m_color = newColor;
emit colorChanged();
}
BorderGroupedProperty *CppCircle::border() const
{
return m_border;
}
QMLCircle.qml
/*
* Copyright (c) 2023 Remy van Elst
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import QtQuick 2.15
Item {
id: root
property int size: 150
property real value: 0
property color primaryColor: "#ff6725"
property color secondaryColor: "#52adff"
property int animationTime: 1000
width: size
height: size
onValueChanged: c.degree = value * 360
Canvas {
id: c
property real degree: 0
anchors.fill: parent
antialiasing: true
onDegreeChanged: requestPaint()
onPaint: {
var ctx = getContext("2d");
var x = root.width / 2;
var y = root.height / 2;
var radius = root.size / 2
var startAngle = (Math.PI / 180) * 270;
var fullAngle = (Math.PI / 180) * (270 + 360);
var progressAngle = (Math.PI / 180) * (270 + degree);
ctx.reset()
ctx.fillStyle = root.secondaryColor;
ctx.beginPath();
ctx.moveTo(x,y);
ctx.arc(x, y, radius-1, startAngle, fullAngle);
ctx.lineTo(x, y)
ctx.fill();
ctx.fillStyle = root.primaryColor;
ctx.beginPath();
ctx.moveTo(x,y);
ctx.arc(x, y, radius, startAngle, progressAngle);
ctx.lineTo(x, y)
ctx.fill();
}
Behavior on degree {
NumberAnimation {
duration: root.animationTime
}
}
}
}
Tags: articles
, c++
, cpp
, debugging
, development
, javascript
, performance
, qml
, qt
, qt5
, qt6