Iot dashboard: Make circular progress bar a QskControl (#124)

* IOT example, circular progress bar: Use a pen instead of a brush

That way we don't have to draw two circles, and we can in addition
use a conical gradient.

* IOT example: Make circular progress bar a QskControl

... and internally use a QskPaintedNode for now. By doing this we
already have the API ready (similar to QskProgressBar) and can
swap the QskPaintedNode with an arc renderer at a later point in
time.
This commit is contained in:
Peter Hartmann 2021-08-24 08:46:26 +02:00 committed by GitHub
parent 279ec9537c
commit 3a1a7c635c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 490 additions and 104 deletions

View File

@ -5,107 +5,190 @@
#include "CircularProgressBar.h"
#include <QPainter>
#include <QskAnimator.h>
#include <QskFunctions.h>
CircularProgressBar::CircularProgressBar( const QskGradient& gradient, int progress, QQuickItem* parent )
: QQuickPaintedItem( parent )
, m_progress( progress )
QSK_SUBCONTROL( CircularProgressBar, Groove )
QSK_SUBCONTROL( CircularProgressBar, Bar )
namespace
{
// This is a bit hackish, but let's do this properly
// once QSkinny has an arc renderer in place
QLinearGradient g( 0, 0, 30, 0 );
QGradientStop stop1( 0.0, gradient.colorAt( 0 ) );
QGradientStop stop2( 1.0, gradient.colorAt( 1 ) );
g.setStops( {stop1, stop2} );
m_gradient = g;
connect( this, &QQuickPaintedItem::contentsSizeChanged, [this]()
class PositionAnimator : public QskAnimator
{
auto size = contentsSize();
QRadialGradient ringGradient( size.width() / 2, size.height() / 2, 45 );
QGradientStop stop1( 0.0, "#c0c0c0" );
QGradientStop stop2( 0.5, "#f0f0f0" );
QGradientStop stop3( 1.0, "#c0c0c0" );
ringGradient.setStops( {stop1, stop2, stop3} );
public:
PositionAnimator( CircularProgressBar* progressBar )
: m_progressBar( progressBar )
{
setAutoRepeat( true );
setDuration( 1300 );
m_ringGradient = ringGradient;
} );
setWindow( progressBar->window() );
}
double CircularProgressBar::width() const
void advance( qreal value ) override
{
return m_width;
const auto aspect = CircularProgressBar::Bar | QskAspect::Position;
m_progressBar->setMetric( aspect, value );
m_progressBar->update();
}
void CircularProgressBar::setWidth( double width )
{
m_width = width;
private:
CircularProgressBar* m_progressBar;
};
}
QColor CircularProgressBar::backgroundColor() const
class CircularProgressBar::PrivateData
{
return m_backgroundColor;
public:
void updateIndeterminateAnimator( CircularProgressBar* progressBar )
{
if ( !isIndeterminate )
{
delete animator;
animator = nullptr;
return;
}
void CircularProgressBar::setBackgroundColor( const QColor& color )
if ( progressBar->window() && progressBar->isVisible() )
{
m_backgroundColor = color;
if ( animator == nullptr )
animator = new PositionAnimator( progressBar );
animator->start();
}
else
{
if ( animator )
animator->stop();
}
}
QRadialGradient CircularProgressBar::ringGradient() const
PositionAnimator* animator = nullptr;
qreal value = 0.0;
qreal origin = 0.0;
bool hasOrigin = false;
bool isIndeterminate = false;
};
CircularProgressBar::CircularProgressBar( qreal min, qreal max, QQuickItem* parent )
: QskBoundedControl( min, max, parent )
, m_data( new PrivateData )
{
return m_ringGradient;
m_data->value = minimum();
initSizePolicy( QskSizePolicy::MinimumExpanding, QskSizePolicy::MinimumExpanding );
connect( this, &QskBoundedControl::boundariesChanged,
this, &CircularProgressBar::adjustValue );
}
void CircularProgressBar::setRingGradient( const QRadialGradient& gradient )
CircularProgressBar::CircularProgressBar( QQuickItem* parent )
: CircularProgressBar( 0.0, 100.0, parent )
{
m_ringGradient = gradient;
}
void CircularProgressBar::paint( QPainter* painter )
bool CircularProgressBar::isIndeterminate() const
{
const auto size = contentsSize();
const int startAngle = 1440;
const int endAngle = -16 * ( m_progress / 100.0 ) * 360;
painter->setRenderHint( QPainter::Antialiasing, true );
#if 1
QRectF outerRect( {0, 0}, size );
painter->setBrush( m_ringGradient );
painter->setPen( m_backgroundColor );
painter->drawEllipse( outerRect );
painter->setBrush( m_gradient );
painter->drawPie( outerRect, startAngle, endAngle );
painter->setBrush( m_backgroundColor );
painter->setPen( m_backgroundColor );
QRectF innerRect( width() / 2, width() / 2, size.width() - width(), size.height() - width() );
painter->drawEllipse( innerRect );
#else
const qreal w = 10;
const QRectF r( 0.5 * w, 0.5 * w, size.width() - w, size.height() - w );
const QColor c0 ( Qt::lightGray );
QRadialGradient g1( r.center(), qMin( r.width(), r.height() ) );
g1.setColorAt( 0.0, c0 );
g1.setColorAt( 0.5, c0.lighter( 120 ) );
g1.setColorAt( 1.0, c0 );
painter->setPen( QPen( g1, w, Qt::SolidLine, Qt::FlatCap ) );
painter->drawArc( r, startAngle, 16 * 360 );
QConicalGradient g2( r.center(), 0 );
g2.setColorAt( 0.0, Qt::red );
g2.setColorAt( 0.5, Qt::blue );
g2.setColorAt( 1.0, Qt::red );
painter->setPen( QPen( g2, w, Qt::SolidLine, Qt::FlatCap ) );
painter->drawArc( r, startAngle, endAngle );
#endif
return m_data->isIndeterminate;
}
void CircularProgressBar::setIndeterminate( bool on )
{
if ( on == m_data->isIndeterminate )
return;
m_data->isIndeterminate = on;
m_data->updateIndeterminateAnimator( this );
update();
Q_EMIT indeterminateChanged( on );
}
void CircularProgressBar::resetOrigin()
{
if ( m_data->hasOrigin )
{
m_data->hasOrigin = false;
update();
Q_EMIT originChanged( origin() );
}
}
qreal CircularProgressBar::origin() const
{
if ( m_data->hasOrigin )
{
return boundedValue( m_data->origin );
}
return minimum();
}
qreal CircularProgressBar::value() const
{
return m_data->value;
}
qreal CircularProgressBar::valueAsRatio() const
{
return QskBoundedControl::valueAsRatio( m_data->value );
}
void CircularProgressBar::setValue( qreal value )
{
if ( isComponentComplete() )
value = boundedValue( value );
setValueInternal( value );
}
void CircularProgressBar::setValueAsRatio( qreal ratio )
{
ratio = qBound( 0.0, ratio, 1.0 );
setValue( minimum() + ratio * boundaryLength() );
}
void CircularProgressBar::setOrigin( qreal origin )
{
if ( isComponentComplete() )
origin = boundedValue( origin );
if( !m_data->hasOrigin || !qskFuzzyCompare( m_data->origin, origin ) )
{
m_data->hasOrigin = true;
m_data->origin = origin;
update();
Q_EMIT originChanged( origin );
}
}
void CircularProgressBar::componentComplete()
{
Inherited::componentComplete();
adjustValue();
}
void CircularProgressBar::setValueInternal( qreal value )
{
if ( !qskFuzzyCompare( value, m_data->value ) )
{
m_data->value = value;
Q_EMIT valueChanged( value );
update();
}
}
void CircularProgressBar::adjustValue()
{
if ( isComponentComplete() )
setValueInternal( boundedValue( m_data->value ) );
}
#include "moc_CircularProgressBar.cpp"

View File

@ -5,31 +5,59 @@
#pragma once
#include <QskBoundedControl.h>
#include <QskGradient.h>
#include <QGradient>
#include <QQuickPaintedItem>
class CircularProgressBar : public QQuickPaintedItem
class CircularProgressBar : public QskBoundedControl
{
Q_OBJECT
Q_PROPERTY( bool indeterminate READ isIndeterminate
WRITE setIndeterminate NOTIFY indeterminateChanged )
Q_PROPERTY( qreal origin READ origin
WRITE setOrigin RESET resetOrigin NOTIFY originChanged )
Q_PROPERTY( qreal value READ value WRITE setValue NOTIFY valueChanged )
Q_PROPERTY( qreal valueAsRatio READ valueAsRatio
WRITE setValueAsRatio NOTIFY valueChanged )
using Inherited = QskBoundedControl;
public:
CircularProgressBar( const QskGradient&, int progress, QQuickItem* parent = nullptr );
QSK_SUBCONTROLS( Groove, Bar )
virtual void paint( QPainter* painter ) override;
CircularProgressBar( qreal min, qreal max, QQuickItem* parent = nullptr );
CircularProgressBar( QQuickItem* parent = nullptr );
double width() const;
void setWidth( double width );
bool isIndeterminate() const;
void setIndeterminate( bool on = true );
QColor backgroundColor() const;
void setBackgroundColor( const QColor& );
void resetOrigin();
qreal origin() const;
QRadialGradient ringGradient() const;
void setRingGradient( const QRadialGradient& );
qreal value() const;
qreal valueAsRatio() const; // [0.0, 1.0]
public Q_SLOTS:
void setValue( qreal );
void setValueAsRatio( qreal );
void setOrigin( qreal );
Q_SIGNALS:
void indeterminateChanged( bool );
void valueChanged( qreal );
void originChanged( qreal );
protected:
void componentComplete() override;
private:
QGradient m_gradient;
QColor m_backgroundColor;
QRadialGradient m_ringGradient;
double m_width = 20;
int m_progress;
void setValueInternal( qreal value );
void adjustValue();
class PrivateData;
std::unique_ptr< PrivateData > m_data;
};

View File

@ -0,0 +1,220 @@
/******************************************************************************
* QSkinny - Copyright (C) 2021 Uwe Rathmann
* This file may be used under the terms of the QSkinny License, Version 1.0
*****************************************************************************/
#include "CircularProgressBarSkinlet.h"
#include "CircularProgressBar.h"
#include <QskPaintedNode.h>
#include <QEasingCurve>
#include <QPainter>
namespace {
class ArcNode : public QskPaintedNode
{
public:
ArcNode() : QskPaintedNode()
{
}
void setGradient( const QskGradient& gradient )
{
m_gradient = gradient;
}
void setGradientType( QGradient::Type type )
{
m_gradientType = type;
}
void setWidth( double width )
{
m_width = width;
}
void setValue( double value )
{
m_value = value;
}
void setOrigin( double origin )
{
m_origin = origin;
}
void setMaximum( double maximum )
{
m_maximum = maximum;
}
void setIndeterminate( bool isIndeterminate )
{
m_isIndeterminate = isIndeterminate;
}
void setPosition( double position )
{
m_position = position;
}
virtual void paint( QPainter* painter, const QSizeF& size ) override
{
int startAngle;
int spanAngle;
if( m_isIndeterminate )
{
static const QEasingCurve curve( QEasingCurve::Linear );
startAngle = -1 * m_position * 360;
// the other option is to just set a fixed value for the
// span angle (or do some advanced stuff with easing curves)
spanAngle = qAbs( 0.5 - m_position ) * 360;
}
else
{
startAngle = 90 + -1 * ( m_origin / m_maximum ) * 360;
spanAngle = -1 * ( m_value / m_maximum ) * 360;
}
painter->setRenderHint( QPainter::Antialiasing, true );
const QRectF r( 0.5 * m_width, 0.5 * m_width, size.width() - m_width, size.height() - m_width );
QGradientStops stops;
for( const QskGradientStop& stop : m_gradient.stops() )
{
QGradientStop s( stop.position(), stop.color() );
stops.append( s );
}
if( m_gradientType == QGradient::RadialGradient )
{
QRadialGradient radialGradient( r.center(), qMin( r.width(), r.height() ) );
radialGradient.setStops( stops );
painter->setPen( QPen( radialGradient, m_width, Qt::SolidLine, Qt::FlatCap ) );
painter->drawArc( r, startAngle * 16, spanAngle * 16 );
}
else
{
QConicalGradient conicalGradient( r.center(), startAngle );
conicalGradient.setStops( stops );
painter->setPen( QPen( conicalGradient, m_width, Qt::SolidLine, Qt::FlatCap ) );
painter->drawArc( r, startAngle * 16, spanAngle * 16 );
}
}
virtual uint hash() const override
{
uint h = qHash( m_gradientType );
h = qHash( m_width, h );
h = qHash( m_value, h );
h = qHash( m_origin, h );
h = qHash( m_maximum, h );
h = qHash( m_isIndeterminate, h );
h = qHash( m_position, h );
for( const QskGradientStop& stop : m_gradient.stops() )
{
h = stop.hash( h );
}
return h;
}
private:
QskGradient m_gradient;
QGradient::Type m_gradientType;
double m_width;
double m_value;
double m_origin;
double m_maximum;
bool m_isIndeterminate;
double m_position;
};
}
CircularProgressBarSkinlet::CircularProgressBarSkinlet( QskSkin* skin )
: QskSkinlet( skin )
{
setNodeRoles( { GrooveRole, BarRole } );
}
CircularProgressBarSkinlet::~CircularProgressBarSkinlet()
{
}
QRectF CircularProgressBarSkinlet::subControlRect(
const QskSkinnable* /*skinnable*/, const QRectF& contentsRect,
QskAspect::Subcontrol /*subControl*/ ) const
{
return contentsRect;
}
QSGNode* CircularProgressBarSkinlet::updateSubNode(
const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const
{
const auto bar = static_cast< const CircularProgressBar* >( skinnable );
switch( nodeRole )
{
case GrooveRole: // fall through
case BarRole:
{
return updateBarNode( bar, nodeRole, node );
}
}
return Inherited::updateSubNode( skinnable, nodeRole, node );
}
QSGNode* CircularProgressBarSkinlet::updateBarNode( const CircularProgressBar* bar, quint8 nodeRole, QSGNode* node ) const
{
auto arcNode = static_cast< ArcNode* >( node );
if( arcNode == nullptr )
{
arcNode = new ArcNode();
}
const auto subControl = ( nodeRole == GrooveRole ) ? CircularProgressBar::Groove
: CircularProgressBar::Bar;
const QskGradient gradient = bar->gradientHint( subControl );
const QGradient::Type type = ( nodeRole == GrooveRole ) ?
QGradient::RadialGradient : QGradient::ConicalGradient;
const double width = bar->metric( subControl | QskAspect::Size );
const double value = ( nodeRole == GrooveRole ) ? bar->maximum() : bar->value();
arcNode->setGradient( gradient );
arcNode->setGradientType( type );
arcNode->setWidth( width );
arcNode->setOrigin( bar->origin() );
arcNode->setMaximum( bar->maximum() );
arcNode->setIndeterminate( bar->isIndeterminate() );
if( bar->isIndeterminate() )
{
const double position = bar->metric( CircularProgressBar::Bar | QskAspect::Position );
arcNode->setPosition( position );
}
else
{
arcNode->setValue( value );
}
QQuickWindow* window = bar->window();
const QRect rect = bar->contentsRect().toRect();
arcNode->update( window, QskTextureRenderer::AutoDetect, rect );
return arcNode;
}
#include "moc_CircularProgressBarSkinlet.cpp"

View File

@ -0,0 +1,39 @@
/******************************************************************************
* QSkinny - Copyright (C) 2021 Uwe Rathmann
* This file may be used under the terms of the QSkinny License, Version 1.0
*****************************************************************************/
#pragma once
#include <QskSkinlet.h>
class CircularProgressBar;
class CircularProgressBarSkinlet : public QskSkinlet
{
Q_GADGET
using Inherited = QskSkinlet;
public:
enum NodeRole
{
GrooveRole,
BarRole,
RoleCount,
};
Q_INVOKABLE CircularProgressBarSkinlet( QskSkin* = nullptr );
~CircularProgressBarSkinlet() override;
QRectF subControlRect( const QskSkinnable*,
const QRectF&, QskAspect::Subcontrol ) const override;
protected:
QSGNode* updateSubNode( const QskSkinnable*,
quint8 nodeRole, QSGNode* ) const override;
private:
QSGNode* updateBarNode( const CircularProgressBar*, quint8 nodeRole, QSGNode* ) const;
};

View File

@ -47,7 +47,6 @@ class ProgressBarAnimator : public QskAnimator
void setup() override
{
m_backgroundColor = m_pieChart->color( PieChartPainted::Panel );
m_ringGradient = m_progressBar->ringGradient();
}
void advance( qreal value ) override
@ -68,7 +67,6 @@ class ProgressBarAnimator : public QskAnimator
newGradient.setColorAt( stop.first, newColor );
}
m_progressBar->setRingGradient( newGradient );
m_progressBar->update();
}
@ -83,13 +81,16 @@ PieChartPainted::PieChartPainted( const QColor& color, const QskGradient& gradie
: QskControl( parent )
, m_color( color )
, m_gradient( gradient )
, m_progressBar( new CircularProgressBar( gradient, progress, this ) )
, m_progressBar( new CircularProgressBar( this ) )
, m_progressLabel( new QskTextLabel( this ) )
, m_animator( new ProgressBarAnimator( this, m_progressBar ) )
{
setAutoLayoutChildren( true );
setSubcontrolProxy( QskBox::Panel, PieChartPainted::Panel );
m_progressBar->setGradientHint( CircularProgressBar::Bar, gradient );
m_progressBar->setValue( progress );
auto progressText = QString::number( progress ) + " %";
m_progressLabel->setText( progressText );
m_progressLabel->setFontRole( QskSkin::SmallFont );
@ -109,7 +110,6 @@ QSizeF PieChartPainted::contentsSizeHint( Qt::SizeHint /*sizeHint*/, const QSize
void PieChartPainted::updateLayout()
{
m_progressBar->setContentsSize( size().toSize() );
m_progressBar->update();
const auto rect = layoutRect();

View File

@ -7,6 +7,8 @@
#include "Box.h"
#include "BoxWithButtons.h"
#include "CircularProgressBar.h"
#include "CircularProgressBarSkinlet.h"
#include "Diagram.h"
#include "DiagramSkinlet.h"
#include "LightIntensity.h"
@ -48,6 +50,7 @@ namespace
Skin::Skin( const Palette& palette, QObject* parent )
: QskSkin( parent )
{
declareSkinlet< CircularProgressBar, CircularProgressBarSkinlet >();
declareSkinlet< Diagram, DiagramSkinlet >();
initHints( palette );
@ -104,10 +107,15 @@ void Skin::initHints( const Palette& palette )
ed.setColor( TopBarItem::Item3 | QskAspect::TextColor, "#f99055" );
ed.setColor( TopBarItem::Item4 | QskAspect::TextColor, "#6776ff" );
ed.setGradient( TopBarItem::Item1, { Qt::Horizontal, "#FF5C00", "#FF3122" } );
ed.setGradient( TopBarItem::Item2, { Qt::Horizontal, "#6776FF", "#6100FF" } );
ed.setGradient( TopBarItem::Item3, { Qt::Horizontal, "#FFCE50", "#FF3122" } );
ed.setGradient( TopBarItem::Item4, { Qt::Horizontal, "#6776FF", "#6100FF" } );
// conical gradients are counterclockwise, so specify the 2nd color first:
ed.setGradient( TopBarItem::Item1, { Qt::Horizontal, "#FF3122", "#FF5C00" } );
ed.setGradient( TopBarItem::Item2, { Qt::Horizontal, "#6100FF", "#6776FF" } );
ed.setGradient( TopBarItem::Item3, { Qt::Horizontal, "#FF3122", "#FFCE50" } );
ed.setGradient( TopBarItem::Item4, { Qt::Horizontal, "#6100FF", "#6776FF" } );
// the bar gradient is defined through the top bar items above
ed.setMetricHint( CircularProgressBar::Groove | QskAspect::Size, 8.53 );
ed.setMetricHint( CircularProgressBar::Bar | QskAspect::Size, 8.53 );
ed.setFontRole( TimeTitleLabel::Text, Skin::TitleFont );
@ -180,4 +188,5 @@ void Skin::initHints( const Palette& palette )
ed.setColor( QskTextLabel::Text, palette.text );
ed.setColor( UsageDiagramBox::DayText, palette.text );
ed.setColor( ShadowPositioner::Panel, palette.shadow );
ed.setGradient( CircularProgressBar::Groove, palette.circularProgressBarGroove );
}

View File

@ -17,7 +17,8 @@ class Skin : public QskSkin
Palette( const QskGradient& menuBar, const QskGradient& mainContent,
const QskGradient& box, const QColor& lightDisplay, const QColor& pieChart,
const QskGradient& roundButton, const QColor& weekdayBox,
const QColor& text, const QColor& shadow )
const QColor& text, const QColor& shadow,
const QskGradient& circularProgressBarGroove )
: menuBar( menuBar )
, mainContent( mainContent )
, box( box )
@ -27,6 +28,7 @@ class Skin : public QskSkin
, weekdayBox( weekdayBox )
, text( text )
, shadow( shadow )
, circularProgressBarGroove( circularProgressBarGroove )
{
}
QskGradient menuBar;
@ -38,6 +40,7 @@ class Skin : public QskSkin
QColor weekdayBox;
QColor text;
QColor shadow;
QskGradient circularProgressBarGroove;
};
Skin( const Palette& palette, QObject* parent = nullptr );
@ -59,7 +62,8 @@ class DaytimeSkin : public Skin
: Skin(
Skin::Palette( {"#6D7BFB"}, {"#fbfbfb"}, {"#ffffff"},
"#ffffff", "#ffffff", {"#f7f7f7"},
{"#f4f4f4"}, Qt::black, Qt::black )
{"#f4f4f4"}, Qt::black, Qt::black,
{ Qt::Horizontal, { { 0.0, 0xc4c4c4 }, { 0.5, 0xf8f8f8 }, { 1.0, 0xc4c4c4 } } } )
, parent )
{
}
@ -72,7 +76,8 @@ class NighttimeSkin : public Skin
: Skin(
Skin::Palette( {"#2937A7"}, {"#040404"}, {"#000000"},
"#000000", "#000000", {"#0a0a0a"},
{"#0c0c0c"}, Qt::white, Qt::white )
{"#0c0c0c"}, Qt::white, Qt::white,
{ Qt::Horizontal, { { 0.0, 0x666666 }, { 0.5, 0x222222 }, { 1.0, 0x333333 } } } )
, parent )
{
}

View File

@ -4,6 +4,7 @@ SOURCES += \
Box.cpp \
BoxWithButtons.cpp \
CircularProgressBar.cpp \
CircularProgressBarSkinlet.cpp \
Diagram.cpp \
DiagramSkinlet.cpp \
LightIntensity.cpp \
@ -30,6 +31,7 @@ HEADERS += \
Box.h \
BoxWithButtons.h \
CircularProgressBar.h \
CircularProgressBarSkinlet.h \
Diagram.h \
DiagramSkinlet.h \
LightIntensity.h \