Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Qt Custom Widget: Draggable Progress Ruler (QWidget + QPainter)

Tech 1

Qt Custom Widget: Draggable Progress Ruler (QWidget + QPainter)

A QWidget-based progress indicator that combines a filled progress track with a graduated ruler. The scale can be rendered on the top or bottom edge, supports click/drag positioning, integer or fractional labels, negative ranges, and optional animated transitions.

Features

  • Adjustable range (min/max) and current value
  • Configurable decimal precision for tick labels
  • Independent major/minor tick spacing
  • Customizable colors for backgroudn, ticks, and fill
  • Ruler can be placed at the top or bottom
  • Optional animation with tunable step size
  • Supports negative values and arbitrary ranges
  • Direct press/drag to set progress

Header

#ifndef PROGRESSRULER_H
#define PROGRESSRULER_H

#include <QWidget>
#include <QColor>

class QTimer;

class ProgressRuler : public QWidget {
    Q_OBJECT

    Q_PROPERTY(double minimum READ minimum WRITE setMinimum)
    Q_PROPERTY(double maximum READ maximum WRITE setMaximum)
    Q_PROPERTY(double value READ value WRITE setValue NOTIFY valueChanged)

    Q_PROPERTY(int precision READ precision WRITE setPrecision)
    Q_PROPERTY(int majorStep READ majorStep WRITE setMajorStep)
    Q_PROPERTY(int minorStep READ minorStep WRITE setMinorStep)

    Q_PROPERTY(bool rulerOnTop READ rulerOnTop WRITE setRulerOnTop)

    Q_PROPERTY(bool animationEnabled READ animationEnabled WRITE setAnimationEnabled)
    Q_PROPERTY(double animationStep READ animationStep WRITE setAnimationStep)

    Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor)
    Q_PROPERTY(QColor tickColor READ tickColor WRITE setTickColor)
    Q_PROPERTY(QColor fillColor READ fillColor WRITE setFillColor)

public:
    explicit ProgressRuler(QWidget* parent = nullptr);
    ~ProgressRuler() override;

    // Value range
    double minimum() const { return m_min; }
    double maximum() const { return m_max; }
    double value()   const { return m_target; }

    // Appearance/behavior
    int precision()       const { return m_precision; }
    int majorStep()       const { return m_majorStep; }
    int minorStep()       const { return m_minorStep; }
    bool rulerOnTop()     const { return m_rulerOnTop; }

    bool animationEnabled() const { return m_animation; }
    double animationStep()   const { return m_animStep; }

    QColor backgroundColor() const { return m_bg; }
    QColor tickColor()       const { return m_ticks; }
    QColor fillColor()       const { return m_fill; }

    QSize sizeHint() const override;
    QSize minimumSizeHint() const override;

public slots:
    void setRange(double min, double max);
    void setMinimum(double min);
    void setMaximum(double max);

    void setValue(double v);            // clamps to [min, max]

    void setPrecision(int p);
    void setMajorStep(int s);
    void setMinorStep(int s);
    void setRulerOnTop(bool onTop);

    void setAnimationEnabled(bool on);
    void setAnimationStep(double step);

    void setBackgroundColor(const QColor& c);
    void setTickColor(const QColor& c);
    void setFillColor(const QColor& c);

signals:
    void valueChanged(double value);

protected:
    void paintEvent(QPaintEvent*) override;
    void mousePressEvent(QMouseEvent*) override;
    void mouseMoveEvent(QMouseEvent*) override;

private slots:
    void advanceAnimation();

private:
    // helpers
    double pixelsPerUnit() const;
    double xToValue(int x) const;
    int valueToX(double v) const;
    void applyPressed(const QPoint& pos);

    void drawBackground(QPainter& p) const;
    void drawFill(QPainter& p) const;
    void drawRuler(QPainter& p, bool top) const;

private:
    // range/value
    double m_min {0.0};
    double m_max {100.0};
    double m_target {0.0};
    double m_current {0.0};

    // ruler
    int  m_precision {0};
    int  m_majorStep {10};
    int  m_minorStep {5};
    bool m_rulerOnTop {true};

    // animation
    bool   m_animation {true};
    double m_animStep {1.0};
    bool   m_descending {false};
    QTimer* m_timer {nullptr};

    // colors
    QColor m_bg {Qt::white};
    QColor m_ticks {Qt::black};
    QColor m_fill {QColor(0, 160, 230)};
};

#endif // PROGRESSRULER_H

Implementation (key parts)

#include "ProgressRuler.h"

#include <QPainter>
#include <QMouseEvent>
#include <QTimer>
#include <QtMath>

ProgressRuler::ProgressRuler(QWidget* parent)
    : QWidget(parent) {
    setMouseTracking(true);

    m_timer = new QTimer(this);
    m_timer->setInterval(15); // ~60 FPS
    connect(m_timer, &QTimer::timeout, this, &ProgressRuler::advanceAnimation);

    m_current = m_min;
}

ProgressRuler::~ProgressRuler() = default;

QSize ProgressRuler::sizeHint() const { return {360, 60}; }
QSize ProgressRuler::minimumSizeHint() const { return {160, 40}; }

void ProgressRuler::setRange(double min, double max) {
    if (qFuzzyCompare(min, max)) return;
    if (min > max) std::swap(min, max);

    m_min = min;
    m_max = max;

    // clamp state
    m_target = qBound(m_min, m_target, m_max);
    m_current = qBound(m_min, m_current, m_max);

    update();
}

void ProgressRuler::setMinimum(double min) { setRange(min, m_max); }
void ProgressRuler::setMaximum(double max) { setRange(m_min, max); }

void ProgressRuler::setValue(double v) {
    const double clamped = qBound(m_min, v, m_max);
    if (qFuzzyCompare(m_target, clamped)) return;

    m_descending = clamped < m_current;
    m_target = clamped;

    if (m_animation) {
        if (!m_timer->isActive()) m_timer->start();
    } else {
        m_current = m_target;
        update();
        emit valueChanged(m_current);
    }
}

void ProgressRuler::setPrecision(int p) { m_precision = qMax(0, p); update(); }
void ProgressRuler::setMajorStep(int s) { m_majorStep = qMax(1, s); update(); }
void ProgressRuler::setMinorStep(int s) { m_minorStep = qMax(1, s); update(); }
void ProgressRuler::setRulerOnTop(bool onTop) { m_rulerOnTop = onTop; update(); }

void ProgressRuler::setAnimationEnabled(bool on) {
    m_animation = on;
    if (!on && m_timer->isActive()) m_timer->stop();
}

void ProgressRuler::setAnimationStep(double step) { m_animStep = qMax(0.0001, step); }

void ProgressRuler::setBackgroundColor(const QColor& c) { m_bg = c; update(); }
void ProgressRuler::setTickColor(const QColor& c) { m_ticks = c; update(); }
void ProgressRuler::setFillColor(const QColor& c) { m_fill = c; update(); }

void ProgressRuler::advanceAnimation() {
    // approach target by m_animStep units per tick
    if (!m_descending) {
        m_current = qMin(m_current + m_animStep, m_target);
    } else {
        m_current = qMax(m_current - m_animStep, m_target);
    }

    update();

    if (qFuzzyCompare(m_current, m_target)) {
        m_timer->stop();
        emit valueChanged(m_current);
    }
}

void ProgressRuler::paintEvent(QPaintEvent*) {
    QPainter p(this);
    p.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);

    drawBackground(p);
    drawFill(p);
    drawRuler(p, m_rulerOnTop);
}

double ProgressRuler::pixelsPerUnit() const {
    const double span = m_max - m_min;
    if (span <= 0.0) return 0.0;
    return width() / span;
}

int ProgressRuler::valueToX(double v) const {
    return qRound((v - m_min) * pixelsPerUnit());
}

double ProgressRuler::xToValue(int x) const {
    const double ppu = pixelsPerUnit();
    if (ppu <= 0.0) return m_min;
    const double v = m_min + (x / ppu);
    return qBound(m_min, v, m_max);
}

void ProgressRuler::applyPressed(const QPoint& pos) {
    const double v = xToValue(pos.x());
    setValue(v);
}

void ProgressRuler::mousePressEvent(QMouseEvent* e) {
    if (e->buttons() & Qt::LeftButton) {
        applyPressed(e->pos());
    }
    QWidget::mousePressEvent(e);
}

void ProgressRuler::mouseMoveEvent(QMouseEvent* e) {
    if (e->buttons() & Qt::LeftButton) {
        applyPressed(e->pos());
    }
    QWidget::mouseMoveEvent(e);
}

void ProgressRuler::drawBackground(QPainter& p) const {
    p.save();
    p.setPen(m_ticks);
    p.setBrush(m_bg);
    p.drawRect(rect());
    p.restore();
}

void ProgressRuler::drawFill(QPainter& p) const {
    p.save();
    p.setPen(Qt::NoPen);
    p.setBrush(m_fill);

    const int w = qMax(0, valueToX(m_current));
    const QRect r(0, 0, w, height());
    p.drawRect(r);

    p.restore();
}

void ProgressRuler::drawRuler(QPainter& p, bool top) const {
    p.save();
    p.setPen(m_ticks);

    const int baseY = top ? 0 : height();
    const int longLen = 15;
    const int midLen  = 10;
    const int shortLen = 6;

    // baseline
    p.drawLine(QPointF(0, baseY), QPointF(width(), baseY));

    const double ppu = pixelsPerUnit();
    if (ppu <= 0.0) { p.restore(); return; }

    // We iterate by minor step in integer units; labels at major steps.
    // Supports negative ranges by mapping value->x consistently.
    const int start = static_cast<int>(std::floor(m_min));
    const int end   = static_cast<int>(std::ceil(m_max));

    for (int v = start; v <= end; v += m_minorStep) {
        const int x = valueToX(static_cast<double>(v));

        int len = shortLen;
        if (m_majorStep > 0 && (v % m_majorStep) == 0) {
            len = longLen;
        } else if (m_majorStep > 0 && (v % (m_majorStep / 2 == 0 ? 1 : m_majorStep / 2)) == 0) {
            len = midLen;
        }

        const QPointF a(x, baseY);
        const QPointF b(x, top ? baseY + len : baseY - len);
        p.drawLine(a, b);

        // draw label for major ticks (excluding endpoints)
        if (len == longLen && v != start && v != end) {
            const QString text = QString::number(static_cast<double>(v), 'f', m_precision);
            const int tw = p.fontMetrics().horizontalAdvance(text);
            const int th = p.fontMetrics().height();

            const QPointF tp(
                x - tw * 0.5,
                top ? (baseY + len + th) : (baseY - len - th * 0.5)
            );
            p.drawText(tp, text);
        }
    }

    p.restore();
}

Notes

  • The tick iteration uses integer unit steps for simplicity; precision controls label formatting, not spacing granularity. If fractional tick spacing is required, replace the integer loop with a double accumulator and use fmod for major/minor classfiication.
  • Deprecated font metrics API was replaced with horizontalAdvance for text measurement.
  • Animation is value-based (units/frame), not time-based. For time-constant motion, scale animationStep by elapsed time in advanceAnimation.
Tags: qtQWidget

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.