Custom Rating Plugin in Qt C++

In practical projects, when we need to implement interfaces such as “five-star rating”, “user scoring”, and “evaluation star level”, the built-in controls of Qt often cannot directly meet the requirements: they lack animation effects, styles are difficult to fully customize, and icon replacement is not flexible. To achieve a more flexible and controllable UI, we usually need to build a custom rating widget. By inheriting from QWidget, handling mouse events, and drawing star-shaped icons or custom graphics, we can create a rating component that is both aesthetically pleasing and easily extensible, ensuring consistent performance across different platforms and themes. This article will detail how to implement a Qt C++ rating plugin from scratch, supporting adjustable star counts, half-star displays, mouse hover previews, and configurable styles, making your Qt UI more professional and refined.Let’s first take a look at the effect diagram:Custom Rating Plugin in Qt C++Header file:

#pragma once
#include <QWidget>
#include <QPainter>
#include <QMouseEvent>
class StarRatingWidget : public QWidget{    Q_OBJECT
public:    explicit StarRatingWidget(QWidget* parent = nullptr);
    double rating() const { return m_rating; }    void setRating(double rating);    // Color differences    void setColors(const QStringList& colors) {        if (colors.size() > STAR_COUNT)return;        this->colors = colors;    }    // Support for half stars    void setHalfStar(bool halfStar) {        isHalfStar = halfStar;    }    // Score color mapping    int mapScoreToLevel(int score) {        score = qBound(1, score, 5);        int n = qBound(1, colors.size(), 5);        if (n == 1) {            return 0;        }        else if (n == 2) {            return (score <= 2) ? 0 : 1;        }        else if (n == 3) {            if (score <= 2) return 0;            if (score == 3) return 1;            return 2; // score 4,5        }        else if (n == 4) {            if (score == 1) return 0;            if (score == 2) return 1;            if (score == 3) return 2;            return 3; // 4,5        }        else { // n == 5            return score - 1;        }    }
signals:    void ratingChanged(double rating);
protected:    void paintEvent(QPaintEvent* event) override;    void mousePressEvent(QMouseEvent* event) override;    void mouseMoveEvent(QMouseEvent* event) override;    void leaveEvent(QEvent* event) override;    void showEvent(QShowEvent* event) override;
private:    void drawStar(QPainter& painter, const QRectF& rect, bool filled, bool half = false);    QPainterPath drawStarPath();    double calculateRatingFromPos(const QPoint& pos);
    double m_rating = 0.0;      // Current rating [0.0, 5.0]    double m_hoverRating = -1.0; // Temporary rating when hovering (for hover effect)    static constexpr int STAR_COUNT = 5;    static constexpr double STAR_SPACING = 2.0;    bool isHalfStar = false; // Half star    QStringList colors = {"#f7ba2a"};};

cpp:

#include "starratingwidget.h"
#include <QPainter>
#include <QPainterPath>
#include <QtMath>
#include <QtGlobal>
//#f7ba2a
StarRatingWidget::StarRatingWidget(QWidget* parent)    : QWidget(parent){    setMouseTracking(true);    setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);}
void StarRatingWidget::showEvent(QShowEvent* event){    QWidget::showEvent(event);    int width = STAR_COUNT * 30 + (STAR_COUNT - 1) * STAR_SPACING;    setFixedSize(width, 30);}
void StarRatingWidget::setRating(double rating){     /*  Limit value within [min, max]:        If value < min, return min        If value > max, return max        If min <= value <= max, return value*/    rating = qBound(0.0, rating, 5.0); // Limit range    if (qFuzzyCompare(m_rating, rating)) return; // Compare two floating-point numbers for "approximate equality"    m_rating = rating;    update();    emit ratingChanged(m_rating);// Rating changed}
void StarRatingWidget::paintEvent(QPaintEvent*){    // If half stars are not supported, convert the score to an integer    if (!isHalfStar) {        m_rating = qFloor(m_rating);    }    QPainter painter(this);    painter.setRenderHint(QPainter::Antialiasing);
    double width = height(); // Square stars    double totalWidth = STAR_COUNT * width + (STAR_COUNT - 1) * STAR_SPACING;    double startX = (rect().width() - totalWidth) / 2.0;
    double currentRating = (m_hoverRating >= 0) ? m_hoverRating : m_rating;    // Get the maximum integer    int level = qFloor(currentRating);    QColor fillColor = colors[mapScoreToLevel(level)];    QColor borderColor = QColor("#999999");    for (int i = 0; i < STAR_COUNT; ++i) {        QRectF starRect(startX + i * (width + STAR_SPACING), 0, width, height());        // Directly fill the rectangle background color        /*painter.setBrush(Qt::red);        painter.setPen(Qt::NoPen);        painter.drawRect(starRect);*/        double starValue = currentRating - i;        if (starValue >= 1.0) {            // Fully filled            drawStar(painter, starRect, true, false);            painter.setBrush(fillColor);            painter.setPen(Qt::NoPen);            drawStar(painter, starRect, true, false);        }        else if (starValue > 0.0) {            // Half star            // First draw the hollow outline            painter.setBrush(Qt::transparent);            painter.setPen(QPen(borderColor, 1));            drawStar(painter, starRect, false);            painter.restore();            // Partially filled (half star or any ratio)            double fillWidth = starRect.width() * starValue; // Note: not 1 - starValue!            QRectF clipRect(starRect.left(), starRect.top(), fillWidth, starRect.height());            painter.save();            painter.setClipRect(clipRect, Qt::IntersectClip); // Only allow drawing in the left area            painter.setBrush(fillColor);            painter.setPen(Qt::NoPen);            drawStar(painter, starRect, true); // Draw a complete solid star, but clipped            painter.restore();        }        else {            // Hollow            painter.setBrush(Qt::transparent);            painter.setPen(QPen(borderColor, 1));            drawStar(painter, starRect, false);        }    }}
void StarRatingWidget::drawStar(QPainter& painter, const QRectF& rect, bool filled, bool half){    // Build the star path (normalized to [-0.5, 0.5] range)    QPainterPath starPath = drawStarPath();    // Scale and translate to rect    QTransform transform;    transform.translate(rect.center().x(), rect.center().y());    transform.scale(rect.width(), rect.height());    starPath = transform.map(starPath);
    if (filled) {        painter.drawPath(starPath);    }    else {        painter.drawPath(starPath);    }}
QPainterPath StarRatingWidget::drawStarPath(){    // Return a standard star path (for clipping half stars)    QPainterPath path;    path.moveTo(0, 0.382);    path.lineTo(0.118, 0.118);    path.lineTo(0.382, 0.118);    path.lineTo(0.191, -0.056);    path.lineTo(0.276, -0.324);    path.lineTo(0, -0.154);    path.lineTo(-0.276, -0.324);    path.lineTo(-0.191, -0.056);    path.lineTo(-0.382, 0.118);    path.lineTo(-0.118, 0.118);    path.closeSubpath();    return path;}
double StarRatingWidget::calculateRatingFromPos(const QPoint& pos){    double width = height();    //qDebug() << "calculateRatingFromPos-width:"  << width;    double totalWidth = STAR_COUNT * width + (STAR_COUNT - 1) * STAR_SPACING;    double startX = (rect().width() - totalWidth) / 2.0;    //qDebug() << "calculateRatingFromPos-startX:" << startX;    double localX = pos.x() - startX;    //qDebug() << "calculateRatingFromPos-localX:" << localX;    if (localX <= 0) return 0.0; // Less than 0 is 0 points    if (localX >= totalWidth) return 5.0; // Greater than is full score
    int starIndex = static_cast<int>(localX / (width + STAR_SPACING));    starIndex = qBound(0, starIndex, STAR_COUNT - 1);// Less than 0 returns 0, greater than 4 returns 4, otherwise returns the corresponding index    //qDebug() << "calculateRatingFromPos-starIndex:" << starIndex;    double offsetInStar = localX - starIndex * (width + STAR_SPACING);    double ratio = offsetInStar / width;// Current star's ratio    //qDebug() << "calculateRatingFromPos-ratio:" << ratio;    if (isHalfStar) {        double rating = starIndex + (ratio > 0.5 ? 1.0 : (ratio > 0 ? 0.5 : 0.0));        //qDebug() << "calculateRatingFromPos-rating:" << rating;        return qBound(0.0, rating, 5.0);// Less than 0 returns 0, greater than 5 returns 5, otherwise returns the corresponding score    }    else {        double rating = starIndex + (ratio > 0 ? 1.0 : 0);        //qDebug() << "calculateRatingFromPos-rating:" << rating;        return qBound(0.0, rating, 5.0);// Less than 0 returns 0, greater than 5 returns 5, otherwise returns the corresponding score    }}
void StarRatingWidget::mousePressEvent(QMouseEvent* event){    if (event->button() == Qt::LeftButton) {        double newRating = calculateRatingFromPos(event->pos());        setRating(newRating);    }}
void StarRatingWidget::mouseMoveEvent(QMouseEvent* event){    m_hoverRating = calculateRatingFromPos(event->pos());    update();}
void StarRatingWidget::leaveEvent(QEvent*){    m_hoverRating = -1.0;    update();}

Usage:

    // Rating    // Default style    ui->widget_4->setRating(2);    ui->widget_4->setColors(QStringList() << "#99a9bf" << "#f7ba2a" << "#ff9900"); // Up to 5 colors    connect(ui->widget_4, &StarRatingWidget::ratingChanged, [this](double rating) {        qDebug() << "rating:" << rating;    });    // Half star    ui->widget_5->setHalfStar(true); // Allow half stars    ui->widget_5->setRating(3.5); // Score    ui->widget_5->setColors(QStringList() << "#f7ba2a" << "#2196f3" ); // Up to 5 colors    connect(ui->widget_5, &StarRatingWidget::ratingChanged, [this](double rating) {        qDebug() << "rating:" << rating;    });                        

Through this article, we have fully implemented a rating plugin that can be used independently in Qt applications. From basic control drawing, mouse event handling, to the extension design of star counts, icons, and styles, we have made the rating control highly configurable and provided a good user interaction experience. Such components can not only be reused directly in practical projects but can also be further extended according to product needs, such as adding animations, supporting half-star ratings, and incorporating theme switching.

The true value of custom controls lies in: allowing the expressiveness of the interface to no longer be limited by the styles provided by the framework, but to create a visual language unique to your application. I hope this content can help you advance further in Qt UI development, and you are also welcome to continue improving on this basis to build a richer component library.

Follow me for more basic programming knowledge

Custom Rating Plugin in Qt C++

👆🏻👆🏻👆🏻Scan to follow👆🏻👆🏻👆🏻

Click “Custom Rating Plugin in Qt C++” to like, it is my motivation to keep updating

Blog address: codeceo.net

Leave a Comment