自定义QScrollBar而不使用样式表

huangapple go评论73阅读模式
英文:

Customizing a QScrollBar without stylesheet

问题

我正在尝试学习如何在不使用样式表的情况下自定义QScrollArea滚动条。

我的目标是排除顶部和底部箭头,只绘制带有圆角矩形的滑块。部分实现成功,但我遇到了两个问题,滑块周围有黑色边框,而页面控制没有正确更新/绘制:

#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:
        // 特殊的子控件,包含滑块手柄可以移动的区域
        if (cc == CC_ScrollBar || sc == QStyle::SC_ScrollBarGroove)
        {
            QRect rect = QProxyStyle::subControlRect(cc, opt, sc, widget);
            // 排除顶部和底部箭头区域
            rect.setTop(0);
            rect.setBottom(rect.bottom() + 17);
            return rect;
        }

        // 顶部和底部箭头的矩形区域
        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 
    {
        QPainter painter(this);
        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));
    }
};
英文:

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:

自定义QScrollBar而不使用样式表

#include &quot;scrollarea.h&quot;
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::dellClass())
{
ui-&gt;setupUi(this);
ScrollArea* scrollArea = new ScrollArea(this);
scrollArea-&gt;setFrameShape(QFrame::NoFrame);
scrollArea-&gt;setLineWidth(0);
scrollArea-&gt;setWidgetResizable(true);
QWidget* widget = new QWidget(scrollArea);
widget-&gt;setObjectName(&quot;w&quot;);
widget-&gt;setStyleSheet(&quot;#w { background-color: rgba(40, 80, 120, 50); }&quot;);
QVBoxLayout* layout = new QVBoxLayout(widget);
for (int i = 0; i &lt; 20; i++)
{
QPushButton* btn = new QPushButton(widget);
btn-&gt;setFixedHeight(32);
layout-&gt;insertWidget(i, btn);
}
scrollArea-&gt;setWidget(widget);
ui-&gt;centralWidget-&gt;layout()-&gt;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-&gt;rect());
painter.setRenderHint(QPainter::Antialiasing);
QRect sliderRect = sliderHandleRect();
painter.setBrush(Qt::red);
painter.drawRoundedRect(sliderRect, 8, 8);
}
QRect sliderHandleRect() const 
{
QStyleOptionSlider opt;
initStyleOption(&amp;opt);
return style()-&gt;subControlRect(QStyle::CC_ScrollBar, &amp;opt, QStyle::SC_ScrollBarSlider, this);
}
};
class ScrollArea : public QScrollArea
{
Q_OBJECT
public:
ScrollArea(QWidget* parent = 0) : QScrollArea(parent) {
setVerticalScrollBar(new VScrollBar(Qt::Vertical, this));
}
};

答案1

得分: 1

拉伸问题:

在你的VProxyStyle::subControlRect中,你有rect.setTop(0);(一个常数值),这会将滑块手柄固定在顶部,并从底部拉伸。

解决方案:

使用一个非常数值,即rect.top()-17,其中17是滚动条箭头在你的情况下引起的偏移量。

if (cc == CC_ScrollBar || sc == QStyle::SC_ScrollBarGroove)
{
    QRect rect = QProxyStyle::subControlRect(cc, opt, sc, widget);
    // 排除顶部和底部箭头区域。
    
    rect.setTop(rect.top()-17); // 在这里进行更改
    rect.setBottom(rect.bottom() + 17);
    return rect;
}

对于更一般的用法,以下是如何计算该偏移量的方法,即上下滚动条箭头的宽度:

  • 在你的VProxyStyle(派生自QProxyStyle)中添加一个新成员,我们称之为offset,它是一个int

  • 在你的ScrollBar的构造函数中计算offset

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

    // 这是我添加的部分
    QStyleOptionSlider newScrollbar;
    newScrollbar.initFrom(this);
    // 访问offset,这是VProxyStyle的成员
    proxyStyle->offset = this->style()->subControlRect(QStyle::CC_ScrollBar,
                                                       &newScrollbar,
                                                       QStyle::SC_ScrollBarAddLine,
                                                       this).width();
}
  • QProxyStyle::subControlRect中使用offset如下:
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);
        // 排除顶部和底部箭头区域。

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

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

我从这里得到了灵感:Qt Forum How to get width and height of QScrollbars arrow widgets

边框问题:

在你的VScrollBar::paintEvent中,你只填充了滑块手柄的矩形,而没有填充其边界矩形,因为你使用了刷子,这就是为什么边框显示为黑色的原因,它没有被绘制。

解决方案1:

为了绘制边框,你可以使用笔来绘制边界矩形,如果你不希望它具有边框外观,可以使用红色笔来匹配你的矩形:

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

    painter.setRenderHint(QPainter::Antialiasing);
    QRect sliderRect = sliderHandleRect();
    
    painter.setBrush(Qt::red);
    painter.drawRoundedRect(sliderRect, 8, 8);
    
    // 使用笔而不是刷子来仅绘制边界矩形(边框)
    painter.setPen(Qt::red);
    painter.drawRoundedRect(sliderRect, 8, 8);
}

解决方案2:

使用QPainterPath来填充滚动手柄并绘制其边框:

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);
}

我从这里得到了灵感:Qt drawing a filled rounded rectangle with border

结果:

自定义QScrollBar而不使用样式表

英文:

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-&gt;offset = this-&gt;style()-&gt;subControlRect(QStyle::CC_ScrollBar,
&amp;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:

自定义QScrollBar而不使用样式表

huangapple
  • 本文由 发表于 2023年5月30日 03:49:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/76359991.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定