Busy Indicator

Introduction

Certain graphical assets may take a while to load or you may wish to show that some other processing is going on. This custom BusyIndicator shows one way in which visual feedback can be provided. This busy indicator has been implemented as a custom QDeclarativeItem in C++ since it uses a conical gradient which it is not possible to represent in an SVG (which only have support for linear and radial gradients). We do take care to minimise the amount of expensive imperative drawing operations. My inspiration for this design comes from StarCraft 2 ;-)

Busy Indicator

Implementation

First, here is the class declaration:

  1. #ifndef BUSYINDICATOR_H
  2. #define BUSYINDICATOR_H
  3.  
  4. #include <QDeclarativeItem>
  5.  
  6. class BusyIndicator : public QDeclarativeItem
  7. {
  8.     Q_OBJECT
  9.     Q_PROPERTY( qreal innerRadius READ innerRadius WRITE setInnerRadius NOTIFY innerRadiusChanged )
  10.     Q_PROPERTY( qreal outerRadius READ outerRadius WRITE setOuterRadius NOTIFY outerRadiusChanged )
  11.     Q_PROPERTY( QColor backgroundColor READ backgroundColor WRITE setBackgroundColor NOTIFY backgroundColorChanged )
  12.     Q_PROPERTY( QColor foregroundColor READ foregroundColor WRITE setForegroundColor NOTIFY foregroundColorChanged )
  13.     Q_PROPERTY( qreal actualInnerRadius READ actualInnerRadius NOTIFY actualInnerRadiusChanged )
  14.     Q_PROPERTY( qreal actualOuterRadius READ actualOuterRadius NOTIFY actualOuterRadiusChanged )
  15.  
  16. public:
  17.     explicit BusyIndicator( QDeclarativeItem* parent = 0 );
  18.  
  19.     void setInnerRadius( const qreal& innerRadius );
  20.     qreal innerRadius() const;
  21.  
  22.     void setOuterRadius( const qreal& outerRadius );
  23.     qreal outerRadius() const;
  24.  
  25.     void setBackgroundColor( const QColor& color );
  26.     QColor backgroundColor() const;
  27.  
  28.     void setForegroundColor( const QColor& color );
  29.     QColor foregroundColor() const;
  30.  
  31.     qreal actualInnerRadius() const;
  32.     qreal actualOuterRadius() const;
  33.  
  34.     virtual void paint( QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = 0 );
  35.  
  36. signals:
  37.     void innerRadiusChanged();
  38.     void outerRadiusChanged();
  39.     void backgroundColorChanged();
  40.     void foregroundColorChanged();
  41.     void actualInnerRadiusChanged();
  42.     void actualOuterRadiusChanged();
  43.  
  44. protected slots:
  45.     virtual void updateSpinner();
  46.  
  47. private:
  48.     // User settable properties
  49.     qreal m_innerRadius; // In range (0, m_outerRadius]
  50.     qreal m_outerRadius; // (m_innerRadius, 1]
  51.     QColor m_backgroundColor;
  52.     QColor m_foregroundColor;
  53.  
  54.     // The calculated size, inner and outer radii
  55.     qreal m_size;
  56.     qreal m_actualInnerRadius;
  57.     qreal m_actualOuterRadius;
  58.  
  59.     QString m_cacheKey;
  60. };
  61.  
  62. #endif // BUSYINDICATOR_H

This is quite a simple sub-class of QDeclarativeItem with only a handful of properties for setting the inner and outer radii of the busy indicator’s ring as a fraction of the item’s size. In this case the item’s size is defined to be min( width, height ) so as to preserve the 1:1 aspect ratio of the ring.

Now for the implementation:

  1. #include "busyindicator.h"
  2.  
  3. #include <QConicalGradient>
  4. #include <QPainter>
  5. #include <QPainterPath>
  6. #include <QPixmapCache>
  7.  
  8. BusyIndicator::BusyIndicator( QDeclarativeItem* parent )
  9.     : QDeclarativeItem( parent ),
  10.       m_innerRadius( 0.8 ),
  11.       m_outerRadius( 1.0 ),
  12.       m_backgroundColor( 177, 210, 143, 70 ),
  13.       m_foregroundColor( 119, 183, 83, 255 ),
  14.       m_actualInnerRadius( 90.0 ),
  15.       m_actualOuterRadius( 100.0 ),
  16.       m_cacheKey()
  17. {
  18.     setFlag( QGraphicsItem::ItemHasNoContents, false );
  19.     setWidth( 100.0 );
  20.     setHeight( 100.0 );
  21.  
  22.     updateSpinner();
  23.  
  24.     connect( this, SIGNAL( widthChanged() ), SLOT( updateSpinner() ) );
  25.     connect( this, SIGNAL( heightChanged() ), SLOT( updateSpinner() ) );
  26. }
  27.  
  28. void BusyIndicator::setInnerRadius( const qreal& innerRadius )
  29. {
  30.     if ( qFuzzyCompare( m_innerRadius, innerRadius ) )
  31.         return;
  32.     m_innerRadius = innerRadius;
  33.     updateSpinner();
  34.     emit innerRadiusChanged();
  35. }
  36.  
  37. qreal BusyIndicator::innerRadius() const
  38. {
  39.     return m_innerRadius;
  40. }
  41.  
  42. void BusyIndicator::setOuterRadius( const qreal& outerRadius )
  43. {
  44.     if ( qFuzzyCompare( m_outerRadius, outerRadius ) )
  45.         return;
  46.     m_outerRadius = outerRadius;
  47.     updateSpinner();
  48.     emit outerRadiusChanged();
  49. }
  50.  
  51. qreal BusyIndicator::outerRadius() const
  52. {
  53.     return m_outerRadius;
  54. }
  55.  
  56. void BusyIndicator::setBackgroundColor( const QColor& color )
  57. {
  58.     if ( m_backgroundColor == color )
  59.         return;
  60.     m_backgroundColor = color;
  61.     updateSpinner();
  62.     emit backgroundColorChanged();
  63. }
  64.  
  65. QColor BusyIndicator::backgroundColor() const
  66. {
  67.     return m_backgroundColor;
  68. }
  69.  
  70. void BusyIndicator::setForegroundColor( const QColor& color )
  71. {
  72.     if ( m_foregroundColor == color )
  73.         return;
  74.     m_foregroundColor = color;
  75.     updateSpinner();
  76.     emit foregroundColorChanged();
  77. }
  78.  
  79. QColor BusyIndicator::foregroundColor() const
  80. {
  81.     return m_foregroundColor;
  82. }
  83.  
  84. qreal BusyIndicator::actualInnerRadius() const
  85. {
  86.     return m_actualInnerRadius;
  87. }
  88.  
  89. qreal BusyIndicator::actualOuterRadius() const
  90. {
  91.     return m_actualOuterRadius;
  92. }
  93.  
  94. void BusyIndicator::updateSpinner()
  95. {
  96.     // Calculate new inner and outer radii
  97.     m_size = qMin( width(), height() );
  98.     qreal nCoef = 0.5 * m_size;
  99.     m_actualInnerRadius = nCoef * m_innerRadius;
  100.     m_actualOuterRadius = nCoef * m_outerRadius;
  101.  
  102.     // Calculate a new key
  103.     m_cacheKey = m_backgroundColor.name();
  104.     m_cacheKey += "-";
  105.     m_cacheKey += m_foregroundColor.name();
  106.     m_cacheKey += "-";
  107.     m_cacheKey += QString::number(m_actualOuterRadius);
  108.     m_cacheKey += "-";
  109.     m_cacheKey += QString::number(m_actualInnerRadius);
  110.  
  111.     emit actualInnerRadiusChanged();
  112.     emit actualOuterRadiusChanged();
  113. }
  114.  
  115. void BusyIndicator::paint( QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget )
  116. {
  117.     Q_UNUSED( option );
  118.     Q_UNUSED( widget );
  119.  
  120.     QPixmap pixmap;
  121.     if ( !QPixmapCache::find( m_cacheKey, pixmap ) )
  122.     {
  123.         // Set up a convenient path
  124.         QPainterPath path;
  125.         path.setFillRule( Qt::OddEvenFill );
  126.         path.addEllipse( QPointF( m_actualOuterRadius, m_actualOuterRadius ), m_actualOuterRadius, m_actualOuterRadius );
  127.         path.addEllipse( QPointF( m_actualOuterRadius, m_actualOuterRadius ), m_actualInnerRadius, m_actualInnerRadius );
  128.  
  129.         qreal nActualDiameter = 2 * m_actualOuterRadius;
  130.         pixmap = QPixmap( nActualDiameter, nActualDiameter );
  131.         pixmap.fill( Qt::transparent );
  132.         QPainter p( &pixmap; );
  133.  
  134.         // Draw the ring background
  135.         p.setPen( Qt::NoPen );
  136.         p.setBrush( m_backgroundColor );
  137.         p.setRenderHint( QPainter::Antialiasing );
  138.         p.drawPath( path );
  139.  
  140.         // Draw the ring foreground
  141.         QConicalGradient gradient( QPointF( m_actualOuterRadius, m_actualOuterRadius ), 0.0 );
  142.         gradient.setColorAt( 0.0, Qt::transparent );
  143.         gradient.setColorAt( 0.05, m_foregroundColor );
  144.         gradient.setColorAt( 0.8, Qt::transparent );
  145.         p.setBrush( gradient );
  146.         p.drawPath( path );
  147.         p.end();
  148.  
  149.         QPixmapCache::insert( m_cacheKey, pixmap );
  150.     }
  151.  
  152.     // Draw pixmap at center of item
  153.     painter->drawPixmap( 0.5 * ( width() - m_size ), 0.5 * ( height() - m_size ), pixmap );
  154. }

In the constructor we set up a default size of 100×100 pixels for the indicator and call the updateSpinner() function. This function is also called whenever one of the affecting properties changes. These are:

  • Height
  • Width
  • Inner radius
  • Outer radius
  • Background color
  • Foreground color

The implementation of the updateSpinner() function only calculates a new QString value which is later used in paint() as a key in the global QPixmapCache. In the paint() function we check to see if the QPixmapCache already contains a matching pixmap or not. If it does we paint it. If it does not we first generate it, store it in the cache and then paint it.

This approach minimises the amount of expensive painting calls and key constructions.

Usage

Before we can use our custom item in any QML scene we need to expose it to the QML world. We do this with something along these lines:

  1. qmlRegisterType<BusyIndicator>( "ZapBsComponents", 1, 0, "BusyIndicator" );

Then in your QML scene you need to instruct the QML backend to import this collection (of 1) components with:

  1. import ZapBsComponents 1.0

You are now ready to roll.

Independent Usage

  1. import QtQuick 1.0
  2. import ZapBsComponents 1.0
  3.  
  4. Rectangle {
  5.     id: root
  6.     width: 640
  7.     height: 360
  8.  
  9.     // Trying out the new BusyIndicator custom item
  10.     BusyIndicator {
  11.         id: busy1
  12.         anchors.centerIn: parent
  13.  
  14.         // Make the ring do something interesting
  15.         RotationAnimation
  16.         {
  17.             target: busy1
  18.             property: "rotation" // Suppress a warning
  19.             from: 0
  20.             to: 360
  21.             direction: RotationAnimation.Clockwise
  22.             duration: 1000
  23.             loops: Animation.Infinite
  24.             running: true
  25.         }
  26.     }
  27. }

The above should provide you with a nicely spinning busy indicator. Obviously the size and colors can be varied using the properties we declared in the header file.

Compound Usage Within Another Component

It is also easy to include the BusyIndicator into compound components. One example might be for slow to load images:

  1. import QtQuick 1.0
  2. import ZapBsComponents 1.0
  3.  
  4. Item {
  5.     id: container
  6.     property alias source: image.source
  7.     property alias fillMode: image.fillMode
  8.  
  9.     Image {
  10.         id: image
  11.         anchors.fill: parent
  12.     }
  13.  
  14.     BusyIndicator {
  15.         id: busyIndicator
  16.         anchors.fill: parent
  17.         visible: image.status != Image.Ready
  18.  
  19.         RotationAnimation
  20.         {
  21.             target: busyIndicator
  22.             property: "rotation" // Suppress a warning
  23.             from: 0
  24.             to: 360
  25.             direction: RotationAnimation.Clockwise
  26.             duration: 1000
  27.             loops: Animation.Infinite
  28.             running: image.status != Image.Ready
  29.         }
  30.     }
  31. }

This will show a nice spinning busy indicator until the image is loaded. Of course you can expose more of the properties to the outside world if you like – this is just a simple example after all.

Another example using this Busy Indicator component along with a small progress bar in the center of the spinning ring to show loading progress for e.g. is shown in this snippet [developer.qt.nokia.com].

Independent Usage as Widget

This implementation of a busy indicator can also be used without QML. It can be added as widget through QGraphicsView [developer.qt.nokia.com] and QGraphicsScene [developer.qt.nokia.com] to a layout [developer.qt.nokia.com] and animated with QTimeLine [developer.qt.nokia.com] as shown at the following example. It is important to note that the viewport must be set in order to display the busy indicator.

Example

  • pro file

  1. QT += declarative

  • mainwindow.h
    1. #ifndef MAINWINDOW_H
    2. #define MAINWINDOW_H
    3.  
    4. #include <QtGui/QMainWindow>
    5. #include "busyindicator.h"
    6.  
    7. #include <QGraphicsScene>
    8. #include <QTimeLine>
    9.  
    10. namespace Ui {
    11.     class MainWindow;
    12. }
    13.  
    14. class MainWindow : public QMainWindow
    15. {
    16.     Q_OBJECT
    17. public:
    18.     enum ScreenOrientation {
    19.         ScreenOrientationLockPortrait,
    20.         ScreenOrientationLockLandscape,
    21.         ScreenOrientationAuto
    22.     };
    23.  
    24.     explicit MainWindow(QWidget *parent = 0);
    25.     virtual ~MainWindow();
    26.  
    27. private slots:
    28.  
    29.     void rotateSpinner(int nValue);
    30.  
    31. private:
    32.  
    33.     BusyIndicator* m_pBusyIndicator;
    34.  
    35.     QGraphicsScene* m_scene;
    36.  
    37.     QTimeLine* m_pTimeLine;
    38.  
    39. };
    40.  
    41. #endif // MAINWINDOW_H
  • mainwindow.cpp
    1. #include "mainwindow.h"
    2.  
    3. #include <QtCore/QCoreApplication>
    4.  
    5. #include <QGraphicsView>
    6. #include <QVBoxLayout>
    7.  
    8. MainWindow::MainWindow(QWidget *parent)
    9.     : QMainWindow(parent), m_pBusyIndicator(NULL), m_pTimeLine(NULL)
    10. {
    11.     QLayout* pLayout = new QVBoxLayout();
    12.  
    13.     QGraphicsScene* pScene = new QGraphicsScene(this);
    14.     m_pBusyIndicator = new BusyIndicator();
    15.     pScene->addItem(dynamic_cast<QGraphicsItem*>(m_pBusyIndicator));
    16.  
    17.     QGraphicsView* pView = new QGraphicsView(pScene, this);
    18.     pView->setViewport(this);
    19.     pView->setMinimumHeight(200);
    20.     pView->show();
    21.  
    22.     pLayout->addWidget(pView);
    23.     setLayout(pLayout);
    24.  
    25.     m_pTimeLine = new QTimeLine(1000, this);
    26.     m_pTimeLine->setLoopCount(0);
    27.     m_pTimeLine->setFrameRange(0, 36);
    28.  
    29.     connect(m_pTimeLine, SIGNAL(frameChanged(int)), this, SLOT(rotateSpinner(int)));
    30.     m_pTimeLine->start();
    31.  
    32. }
    33. //------------------------------------------------------------------------------
    34.  
    35.  
    36. MainWindow::~MainWindow()
    37. {
    38.     //Nothing to do
    39. }
    40. //------------------------------------------------------------------------------
    41.  
    42. void MainWindow::rotateSpinner(int nValue)
    43. {
    44.     qreal nTransX = m_pBusyIndicator->actualOuterRadius();
    45.     m_pBusyIndicator->setTransform(QTransform().translate(nTransX, nTransX).
    46.                         rotate(nValue*10).translate(-1*nTransX, -1*nTransX));
    47. }
    48. //------------------------------------------------------------------------------

Categories:

  • HowTo
  • snippets
  •