英文:
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:
#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));
}
};
答案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
结果:
英文:
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 fromQProxyStyle
), and let's call itoffset
which is anint
. -
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:
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论