1
votes

How can I smoothly grow an angle/length-changing Arc with Qt QPainter? Here is minimum code I just created from Qt's Analog Clock Window Example.

The code randomly changes m_value +- 5 in 50-millisecond. This is to simulate the actual behavior I want to achieve. The arc starts at 12 O'clock position and grow counter-clockwise. m_value is scaled to fit 360 degree (12 O'clock to 12 O'clock).

My goal is to smoothly change the length of arc, in real-time, in response to the (simulated) value given, regardless of input value jitters.

I want to accomplish 2 things:

  1. Smooth redraw of the arc. The current code directly redraw the value at the time. I does not even use sub angle value. The result is visually noisy at the end of the arc.
  2. Update the drawing in along with V-Sync. So that I don't waste computation power for non-displayed redraw. I don't know how to trigger render event by V-Sync, so I've setup 33-millisecond timer. This is needed when m_value changes in less than 30 msec.

What I don't want

  • QtQuick. I'm looking for QPainter way to do it.

Platform I'm using:

  • Qt 5.x
  • on Debian Linux (If it matters)

#include <QtGui>

#include "rasterwindow.h"

class SmoothArc : public RasterWindow
{
public:
    SmoothArc();

protected:
    void timerEvent(QTimerEvent *) Q_DECL_OVERRIDE;
    void render(QPainter *p) Q_DECL_OVERRIDE;

private:
    int m_timerId;
    int m_valueTimerId;
    int m_value = 50;
};

SmoothArc::SmoothArc()
{
    setTitle("Smooth Arc");
    resize(200, 200);

    m_timerId = startTimer(33);
    m_valueTimerId = startTimer(100);
}

void SmoothArc::timerEvent(QTimerEvent *event)
{
    if (event->timerId() == m_timerId)
        renderLater();
    if (event->timerId() == m_valueTimerId) {
        m_value = m_value + (qrand() % 11 - 5);
        if (m_value > 100) m_value = 100;
        if (m_value < 0) m_value = 0;
    }
}

void SmoothArc::render(QPainter *p)
{
    p->setRenderHint(QPainter::Antialiasing);
    int side = qMin(width(), height());
    p->scale(side / 200.0, side / 200.0);

    QRectF rect(10, 10, 180, 180);
    QPen pen = p->pen();
    pen.setWidth(10);
    p->setPen(pen);
    p->drawArc(rect, 90*16, (360*(m_value/100.0))*16);
}

int main(int argc, char **argv)
{
    QGuiApplication app(argc, argv);
    SmoothArc arc;
    arc.show();
    return app.exec();
}

Complete code is at https://github.com/yashi/smooth-arc. Usual build process like the following should work.

git clone https://github.com/yashi/smooth-arc.git
cd smooth-arc
qmake
make
./smooth-arc
1
Would be a great question, if we could actually compile the code. :) - Mitch
If that helps: github.com/yashi/smooth-arc . Usual git clone https://github.com/yashi/smooth-arc.git; cd smooth-arc; qmake; make; ./smooth-arc should work - Yasushi Shoji
I want to simulate actual input data stream which comes from outside of the program. And it was easily done by qrand(). No special meaning. My goal is to smoothly change the length of arc, in real-time, in response to the current value given, regardless of input value jitters. - Yasushi Shoji
Just out of curiosity, is it necessary to use RasterWindow? - Mitch
No. It was taken from a sample I found. - Yasushi Shoji

1 Answers

2
votes

I'm not familiar with the Qt Gui-only approach to this problem, so I'll show how to do it with Qt Widgets instead.

  1. Smooth redraw of the arc. The current code directly redraw the value at the time. I does not even use sub angle value. The result is visually noisy at the end of the arc.

You can use Qt's animation framework to interpolate property changes:

#include <QtWidgets>

class SmoothArc : public QWidget
{
    Q_OBJECT
    Q_PROPERTY(qreal value READ value WRITE setValue NOTIFY valueChanged)

public:
    SmoothArc();

    qreal value() const;
    void setValue(qreal value);

signals:
    void valueChanged();

protected:
    void timerEvent(QTimerEvent *) Q_DECL_OVERRIDE;
    void paintEvent(QPaintEvent *) Q_DECL_OVERRIDE;

private:
    int m_valueTimerId;
    qreal m_value;
    QPropertyAnimation m_animation;
};

SmoothArc::SmoothArc()
{
    resize(200, 200);

    m_valueTimerId = startTimer(100);
    m_value = 50;
    m_animation.setTargetObject(this);
    m_animation.setPropertyName("value");
}

qreal SmoothArc::value() const
{
    return m_value;
}

void SmoothArc::setValue(qreal value)
{
    if (qFuzzyCompare(value, m_value))
        return;

    m_value = value;
    update();
    emit valueChanged();
}

void SmoothArc::timerEvent(QTimerEvent *event)
{
    if (event->timerId() == m_valueTimerId) {
        qreal newValue = m_value + (qrand() % 11 - 5);
        if (newValue > 100) newValue = 100;
        if (newValue < 0) newValue = 0;

        if (m_animation.state() == QPropertyAnimation::Running)
            m_animation.stop();
        m_animation.setStartValue(m_value);
        m_animation.setEndValue(newValue);
        m_animation.start();
    }
}

void SmoothArc::paintEvent(QPaintEvent *)
{
    QPainter p(this);
    p.setRenderHint(QPainter::Antialiasing);
    int side = qMin(width(), height());
    p.scale(side / 200.0, side / 200.0);

    QRectF rect(10, 10, 180, 180);
    QPen pen = p.pen();
    pen.setWidth(10);
    p.setPen(pen);
    p.drawArc(rect, 90*16, (360*(m_value/100.0))*16);
}

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    SmoothArc arc;
    arc.show();
    return app.exec();
}

#include "main.moc"

arc animation gif

  1. Update the drawing in along with V-Sync. So that I don't waste computation power for non-displayed redraw. I don't know how to trigger render event by V-Sync, so I've setup 33-millisecond timer. This is needed when m_value changes in less than 30 msec.

I think that Qt should handle this for you if you use update():

Updates the widget unless updates are disabled or the widget is hidden.

This function does not cause an immediate repaint; instead it schedules a paint event for processing when Qt returns to the main event loop. This permits Qt to optimize for more speed and less flicker than a call to repaint() does.

Calling update() several times normally results in just one paintEvent() call.