1

I'm trying to learn how to customize a QScrollArea scrollbar without using stylesheets.

My goal is to exclude the top and bottom arrows and draw only the slider with a rounded rect.

I partially got it working but I'm struggling with two things, there's a black border around the slider, and the page control is not being updated/drawn correctly:

enter image description here

#include "scrollarea.h"

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::dellClass())
{
    ui->setupUi(this);

    ScrollArea* scrollArea = new ScrollArea(this);
    scrollArea->setFrameShape(QFrame::NoFrame);
    scrollArea->setLineWidth(0);
    scrollArea->setWidgetResizable(true);

    QWidget* widget = new QWidget(scrollArea);
    widget->setObjectName("w");
    widget->setStyleSheet("#w { background-color: rgba(40, 80, 120, 50); }");
    QVBoxLayout* layout = new QVBoxLayout(widget);

    for (int i = 0; i < 20; i++)
    {
        QPushButton* btn = new QPushButton(widget);
        btn->setFixedHeight(32);
        layout->insertWidget(i, btn);
    }

    scrollArea->setWidget(widget);
    ui->centralWidget->layout()->addWidget(scrollArea);
}

scrollarea.h

class VProxyStyle : public QProxyStyle
{
public:
    VProxyStyle(QStyle *style = nullptr) : QProxyStyle(style) { }

    QRect subControlRect(QStyle::ComplexControl cc, const QStyleOptionComplex* opt, QStyle::SubControl sc, const QWidget *widget = nullptr) const override
    {        
        // https://doc.qt.io/qt-6/qstyle.html
        // SC_ScrollBarGroove:
        // Special sub-control which contains the area in which the slider handle may move
        if (cc == CC_ScrollBar || sc == QStyle::SC_ScrollBarGroove)
        {
            QRect rect = QProxyStyle::subControlRect(cc, opt, sc, widget);
            // Exclude the top and bottom arrows area.
            rect.setTop(0);
            rect.setBottom(rect.bottom() + 17);
            return rect;
        }

        // The rect of top and bottom arrows.
        if (sc == QStyle::SC_ScrollBarAddLine || sc == QStyle::SC_ScrollBarSubLine) {
            return QRect();
        }

        return QProxyStyle::subControlRect(cc, opt, sc, widget);
    }
};



class VScrollBar : public QScrollBar 
{
    Q_OBJECT

public:
    VScrollBar(Qt::Orientation orientation, QWidget *parent) : QScrollBar(orientation, parent) 
    {
        VProxyStyle *proxyStyle = new VProxyStyle(style());
        setStyle(proxyStyle);
    }

    void paintEvent(QPaintEvent *event) override 
    {
        //QScrollBar::paintEvent(event);

        QPainter painter(this);
        //painter.eraseRect(event->rect());

        painter.setRenderHint(QPainter::Antialiasing);
        QRect sliderRect = sliderHandleRect();
        painter.setBrush(Qt::red);
        painter.drawRoundedRect(sliderRect, 8, 8);
    }

    QRect sliderHandleRect() const 
    {
        QStyleOptionSlider opt;
        initStyleOption(&opt);
        return style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSlider, this);
    }
};

class ScrollArea : public QScrollArea
{
    Q_OBJECT
public:
    ScrollArea(QWidget* parent = 0) : QScrollArea(parent) {
        setVerticalScrollBar(new VScrollBar(Qt::Vertical, this));
    }
};
Cesar
  • 41
  • 2
  • 5
  • 16

1 Answers1

1

Stretching problem:

In your VProxyStyle::subControlRect, you have rect.setTop(0); (a const value), which will pin your slider handle to the top while stretching from the bottom.

Solution:

Use a non const value, which is rect.top()-17, 17 being the offset caused by scrollbar's arrows in your case.

if (cc == CC_ScrollBar || sc == QStyle::SC_ScrollBarGroove)
{
    QRect rect = QProxyStyle::subControlRect(cc, opt, sc, widget);
    // Exclude the top and bottom arrows area.
    
    rect.setTop(rect.top()-17); //make the change here
    rect.setBottom(rect.bottom() + 17);
    return rect;
}

For a more general use, here's how you can calculate that offset, which is the up and down scroll bar arrows width:

  • Add a new member to your VProxyStyle (derived from QProxyStyle), and let's call it offset which is an int.

  • Calculate offset in your ScrollBar's ctor:

VScrollBar(Qt::Orientation orientation, QWidget *parent) : QScrollBar(orientation, parent)
{
    VProxyStyle *proxyStyle = new VProxyStyle(style());
    setStyle(proxyStyle);

    //This is what I added
    QStyleOptionSlider newScrollbar;
    newScrollbar.initFrom(this);
    //access offset, which is a member of VProxyStyle
    proxyStyle->offset = this->style()->subControlRect(QStyle::CC_ScrollBar,
                                                       &newScrollbar,
                                                       QStyle::SC_ScrollBarAddLine,
                                                       this).width();
}
  • Use offset in QProxyStyle::subControlRect as follows:
QRect subControlRect(QStyle::ComplexControl cc, const QStyleOptionComplex* opt, QStyle::SubControl sc, const QWidget *widget = nullptr) const override
{
    if (cc == CC_ScrollBar || sc == QStyle::SC_ScrollBarGroove)
    {
        QRect rect = QProxyStyle::subControlRect(cc, opt, sc, widget);
        // Exclude the top and bottom arrows area.

        rect.setTop(rect.top() - offset);
        rect.setBottom(rect.bottom() + offset);
        return rect;
    }

    return QProxyStyle::subControlRect(cc, opt, sc, widget);
}

I got the idea from here: Qt Forum How to get width and height of QScrollbars arrow widgets


Border problem:

In your VScrollBar::paintEvent, you're only filling the slider handle rect without its bounding rect because you're using a brush, which is why the border appears black, it is not being drawn.

Solution 1:

To paint that border, you could use a pen to draw the bounding rect, a red pen to match your rect if you don't want to make it appear having a border:

void paintEvent(QPaintEvent *event) override
{
    QPainter painter(this);

    painter.setRenderHint(QPainter::Antialiasing);
    QRect sliderRect = sliderHandleRect();
    
    painter.setBrush(Qt::red);
    painter.drawRoundedRect(sliderRect, 8, 8);
    
    //use a pen instead of brush to only draw the bounding rect (border)
    painter.setPen(Qt::red);
    painter.drawRoundedRect(sliderRect, 8, 8);
}

Solution 2:

Use QPainterPath to fill your scroll handle and draw its border:

void paintEvent(QPaintEvent *event) override
{
    QPainter painter(this);
    QPainterPath path;
    QRect sliderRect = sliderHandleRect();
        
    painter.setRenderHint(QPainter::Antialiasing);
        
    path.addRoundedRect(sliderRect, 8, 8);
    painter.setPen(Qt::red);
    painter.fillPath(path, Qt::red);
    painter.drawPath(path);
}

I got the idea from here: Qt drawing a filled rounded rectangle with border


Result:

Custom red scroller handle