Search code examples
qtqtstylesheetsqslider

Change QSlider's add/sub page colors from the centre


I want to design a QSlider, which in the design sheet looks like this:

enter image description here

The slider groove color is the same for all slider parts, regardless of the slider handle being in its centre. In the design sheet, if the slider handle is moved to the left or right, it looks like this:

enter image description here

enter image description here

AFAIK, it is possible to change the slider's add/sub-page colors using style sheets.

However, I do not know how to change it in such a way that the change is done from the center, both to the left and right part, instead of the slider's leftmost point.

How could I achieve this?


Solution

  • Using A Stylesheet

    While this is achievable using stylesheets, it is not recommended, the effort and complexity of the implementation is not worth the results, which are fragile and jagged. There are too many moving pieces for stylesheets to handle effectively while keeping a smooth, coherent look/behavior.

    The basic idea is to use QLinearGradient for the sub-page/add-page sub controls, and separating the color stops with a small fraction (e.g, 0.00001). This achieves a segmented look.

    The heavy work is done when trying to adjust the values each time the handle is moved.

    #include <QApplication>
    #include <QtWidgets>
    
    class StyleSheetCenteredSlider : public QSlider
    {
    public:
        StyleSheetCenteredSlider(QWidget *parent = nullptr) : QSlider(parent)
        {
            setOrientation(Qt::Horizontal);
            setMinimum(0);
            setMaximum(100);
            setValue(50);
    
            styleSheet = QString(
                "QSlider::sub-page:horizontal { background: black;}"
                "QSlider::add-page:horizontal { background: black;}"
                "QSlider::groove:horizontal {"
                    "border: 1px solid black;"
                "}");
    
            connect(this, &QSlider::valueChanged, this, &StyleSheetCenteredSlider::updateSliderStyle);
        }
    
        void updateSliderStyle(int value)
        {
            qreal stopPosition = static_cast<qreal>(value) / maximum();
    
            qreal progressScaledDownStartingPosition;
            qreal backgroundColorPosition;
    
            if(stopPosition < 0.5)
            {
                //scale down from the whole groove to the add page
                /*
                    Black is half of the whole slider : 0.5
                    EXAMPLE
                        0      0.3     0.5             1
                        |-Black-|-white-|----Black-----|
                                0       x              1
                                |-white-|----Black-----|
                    The white part above = 0.2, which is 0.5 - 0.3
                    but 0.2 is taken from the whole groove,
                    so scale down to the add page (devide by the length of the add page, which is (1 - 0.3 (stopPosition)))
                */
                progressScaledDownStartingPosition = ((qreal)0.5 - stopPosition) / (1 - stopPosition);
                backgroundColorPosition = progressScaledDownStartingPosition + (qreal)0.000001;
                styleSheet.replace(QRegularExpression("QSlider::add-page:horizontal \\{([^}]*)\\}"),
                                   QString("QSlider::add-page:horizontal {"
                                       "background: qlineargradient(x1:0, y1:0,"
                                       "x2:1, y2:0,"
                                       "stop:0 white,"
                                       "stop:%1 white,"
                                       "stop:%2 black,"
                                       "stop:1 black);"
                                       "}").arg(progressScaledDownStartingPosition)
                                       .arg(backgroundColorPosition));
            }
            else
            {
                if(stopPosition > 0.5)
                {
                    /*
                        Black is half of the whole slider : 0.5
                        EXAMPLE
                            0           0.5     0.6            1
                            |---Black----|-white--|---Black----|
                            0            x        1
                            |---Black----|-white--|
                        The white part above = 0.1, which is 0.6 - 0.5
                        but 0.1 is taken from the whole groove,
                        so again, scale down to the sub page (devide by the length of the sub page)
                        the scaled down position is to be subtracted from 1, which is the full length of the sub page
                    */
                    progressScaledDownStartingPosition = 1 - ((stopPosition - 0.5) / stopPosition);
                    backgroundColorPosition = progressScaledDownStartingPosition + 0.000001;
    
                    styleSheet.replace(QRegularExpression("QSlider::sub-page:horizontal \\{([^}]*)\\}"),
                                       QString(
                                           "QSlider::sub-page:horizontal { "
                                               "background: qlineargradient(x1:0, y1:0,"
                                               "x2:1, y2:0,"
                                               "stop:0 black,"
                                               "stop:%1 black,"
                                               "stop:%2 white,"
                                               "stop:1 white);"
                                           "}").arg(progressScaledDownStartingPosition)
                                           .arg(backgroundColorPosition));
                }
                else
                {
                    styleSheet.replace(QRegularExpression("QSlider::sub-page:horizontal \\{([^}]*)\\}"),
                                       QString("QSlider::sub-page:horizontal { background: black;}"));
                    styleSheet.replace(QRegularExpression("QSlider::add-page:horizontal \\{([^}]*)\\}"),
                                       QString("QSlider::add-page:horizontal { background: black;}"));
                }
            }
    
            setStyleSheet(styleSheet);
        }
    
        QString styleSheet;
    };
    
    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
    
        StyleSheetCenteredSlider styleSheetSlider;
        styleSheetSlider.show();
        styleSheetSlider.resize(500,100);
    
        return a.exec();
    }
    

    Notice how the center is shaky:

    That's without the handle, adding it makes things even more complicated:

    #include <QApplication>
    #include <QtWidgets>
    
    class StyleSheetCenteredSlider : public QSlider
    {
    public:
        StyleSheetCenteredSlider(QWidget *parent = nullptr) : QSlider(parent)
        {
            setOrientation(Qt::Horizontal);
            setMinimum(0);
            setMaximum(100);
            setValue(50);
    
            styleSheet = QString(
                "QSlider::sub-page:horizontal { background: black;}"
                "QSlider::add-page:horizontal { background: black;}"
                "QSlider {"
                    "height: 20px;"
                    "background: rgb(50,50,50);"
                "}"
                "QSlider::groove:horizontal {"
                    "border: 1px solid #262626;"
                    "height: 5px;"
                    "margin: 0 12px;"
                "}"
                "QSlider::handle:horizontal {"
                    "background: cyan;"
                    "border: 1px solid black;"
                    "width: 23px;"
                    "border-radius: 12px;"
                    "height: 100px;"
                    "margin: -24px -12px;"
                "}");
    
            connect(this, &QSlider::valueChanged, this, &StyleSheetCenteredSlider::updateSliderStyle);
        }
    
        void updateSliderStyle(int value)
        {
            qreal stopPosition = static_cast<qreal>(value) / maximum();
    
            qreal progressScaledDownStartingPosition;
            qreal backgroundColorPosition;
    
            if(stopPosition < 0.5)
            {
                //scale down from the whole groove to the add page
                /*
                    Black is half of the whole slider : 0.5
                    EXAMPLE
                        0      0.3     0.5             1
                        |-Black-|-white-|----Black-----|
                                0       x              1
                                |-white-|----Black-----|
                    The white part above = 0.2, which is 0.5 - 0.3
                    but 0.2 is taken from the whole groove,
                    so scale down to the add page (devide by the length of the add page, which is (1 - 0.3 (stopPosition)))
                */
                qreal whateverMargin = (8/(qreal)width());
                progressScaledDownStartingPosition = std::max(((qreal)0.5 - stopPosition) / (1 - stopPosition) - whateverMargin, (qreal)0);
                backgroundColorPosition = progressScaledDownStartingPosition + (qreal)0.000001;
                styleSheet.replace(QRegularExpression("QSlider::add-page:horizontal \\{([^}]*)\\}"),
                                   QString("QSlider::add-page:horizontal {"
                                       "background: qlineargradient(x1:0, y1:0,"
                                       "x2:1, y2:0,"
                                       "stop:0 white,"
                                       "stop:%1 white,"
                                       "stop:%2 black,"
                                       "stop:1 black);"
                                       "}").arg(progressScaledDownStartingPosition)
                                       .arg(backgroundColorPosition));
            }
            else
            {
                if(stopPosition > 0.5)
                {
                    /*
                        Black is half of the whole slider : 0.5
                        EXAMPLE
                            0           0.5     0.6            1
                            |---Black----|-white--|---Black----|
                            0            x        1
                            |---Black----|-white--|
                        The white part above = 0.1, which is 0.6 - 0.5
                        but 0.1 is taken from the whole groove,
                        so again, scale down to the sub page (devide by the length of the sub page)
                        the scaled down position is to be subtracted from 1, which is the full length of the sub page
                    */
                    qreal whateverMargin = (8/(qreal)width());
                    progressScaledDownStartingPosition = std::min(1 - ((stopPosition - 0.5) / stopPosition) + whateverMargin, (qreal)0.999999);
                    backgroundColorPosition = progressScaledDownStartingPosition + 0.000001;
    
                    styleSheet.replace(QRegularExpression("QSlider::sub-page:horizontal \\{([^}]*)\\}"),
                                       QString(
                                           "QSlider::sub-page:horizontal { "
                                               "background: qlineargradient(x1:0, y1:0,"
                                               "x2:1, y2:0,"
                                               "stop:0 black,"
                                               "stop:%1 black,"
                                               "stop:%2 white,"
                                               "stop:1 white);"
                                           "}").arg(progressScaledDownStartingPosition)
                                           .arg(backgroundColorPosition));
                }
                else
                {
                    styleSheet.replace(QRegularExpression("QSlider::sub-page:horizontal \\{([^}]*)\\}"),
                                       QString("QSlider::sub-page:horizontal { background: black;}"));
                    styleSheet.replace(QRegularExpression("QSlider::add-page:horizontal \\{([^}]*)\\}"),
                                       QString("QSlider::add-page:horizontal { background: black;}"));
                }
            }
    
            setStyleSheet(styleSheet);
        }
    
        QString styleSheet;
    };
    
    
    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
    
        StyleSheetCenteredSlider styleSheetSlider;
        styleSheetSlider.show();
        styleSheetSlider.resize(500,100);
    
        return a.exec();
    }
    

    Now there's a margin to take into account, and the center is off.

    The details could possibly be corrected and finalised, but this approach is not worth it considering the results.


    Using The Paint Event

    A far better approach (in terms of implementation ease and end results) is using the paint event:

    #include <QApplication>
    #include <QtWidgets>
    
    class PaintEventCenteredSlider : public QSlider 
    {
    public:
        PaintEventCenteredSlider(QWidget *parent = nullptr) : QSlider(parent) {}
    
    protected:
        void paintEvent(QPaintEvent *event) override
        {
            QPainter painter(this);
            painter.setRenderHint(QPainter::Antialiasing, true);
    
            QStyleOptionSlider option;
            option.initFrom(this);
    
            QRect grooveRect;
            QRect handleRect;
            QRect progressRect;
    
            QPoint handlePoint;
            int handlePos = 0;
    
            if(orientation() == Qt::Horizontal)
            {
                QRect grooveRect = style()->subControlRect(QStyle::CC_Slider, &option, QStyle::SC_SliderGroove, this);
                QRect handleRect = style()->subControlRect(QStyle::CC_Slider, &option, QStyle::SC_SliderHandle, this);
    
                QPoint grooveCenter = grooveRect.center();
                QRect progressRect = grooveRect;
                //example
                //440 = 0 + (( 500 - 15 ) * ( 90 - 0 ) / ( 99 - 0 ) )
                //(value() - minimum()) / (maximum() - minimum()) -Example> ( 90 - 0 ) / ( 99 - 0 ) ) = 99/90 = 0.9
                //basically just the percentage of progress
                //grooveRect.left() is just in case we have a margin or something like that, it's usually 0
                //grooveRect.width() is obvious, the full length, the distance
                //handleRect.width() this is used to take into account the handle rect, without it, it will "go past the groove beyond the border"
                //try to comment `- handleRect.width()`
                handlePos = grooveRect.left() + ((grooveRect.width() - handleRect.width()) * (value() - minimum()) / (maximum() - minimum()));
                //use the below to get more output in order to understand the above
                qDebug()<<handlePos<< "=" << grooveRect.left() << "+" << "((" << grooveRect.width() << "-" << handleRect.width()<<")"<< "*"<< "("<<value() <<"-"<< minimum()<<")"<< "/"<< "("<<maximum() <<"-"<< minimum()<<")"<<")";
    
                handlePoint = QPoint(handlePos, grooveCenter.y());
                handleRect.moveLeft(handlePoint.x() );
    
                //from the center
                progressRect.setLeft(grooveCenter.x());
                //to the handle's position
                progressRect.setRight(handlePoint.x());
            }
            else
            {
                //the default value for QStyleOptionSlider::orientation is Qt::Horizontal
                //we have to adjust it here to get the correct rects
                option.orientation = Qt::Vertical;
                grooveRect = style()->subControlRect(QStyle::CC_Slider, &option, QStyle::SC_SliderGroove, this);
                handleRect = style()->subControlRect(QStyle::CC_Slider, &option, QStyle::SC_SliderHandle, this);
    
                QPoint grooveCenter = grooveRect.center();
                progressRect = grooveRect;
    
                handlePos = grooveRect.top() + ((grooveRect.height() - handleRect.height()) * (value() - minimum()) / (maximum() - minimum()));
                qDebug()<<handlePos<< "=" << grooveRect.left() << "+" << "((" << grooveRect.width() << "-" << handleRect.width()<<")"<< "*"<< "("<<value() <<"-"<< minimum()<<")"<< "/"<< "("<<maximum() <<"-"<< minimum()<<")"<<")";
    
                handlePoint = QPoint(grooveCenter.x(), handlePos);
                handleRect.moveTop(handlePoint.y() );
    
                //from the center
                progressRect.setTop(grooveCenter.y());
                //to the handle's position
                progressRect.setBottom(handlePoint.y());
            }
    
            //draw groove
            painter.setBrush(Qt::black);
            painter.setPen(Qt::black);
            painter.drawRoundedRect(grooveRect, 3,3);
    
            //draw progress
            painter.setBrush(Qt::white);
            painter.setPen(Qt::white);
            painter.drawRect(progressRect);
    
            //draw handle
            painter.setBrush(Qt::cyan);
            painter.setPen(Qt::cyan);
            painter.drawEllipse(handleRect);
        }
    };
    
    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
    
        PaintEventCenteredSlider paintEventSlider;
        paintEventSlider.setOrientation(Qt::Horizontal);
        paintEventSlider.show();
        paintEventSlider.resize(500,100);
    
        return a.exec();
    }
    

    The center is stable, the margins are easier to calculate, and it works when added to layouts, unlike the stylesheets approach, which only gets worse:


    For more about customizing QSlider: