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:
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 ▼

👆🏻👆🏻👆🏻Scan to follow👆🏻👆🏻👆🏻
▼Click “
” to like, it is my motivation to keep updating ▼
Blog address: codeceo.net