Qt Custom Widget: Draggable Progress Ruler (QWidget + QPainter)
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.