[Qt]QListView 重绘实例之三:滚动条覆盖的问题处理

0 环境

  1. Windows 11
  2. Qt 5.15.2 MinGW x64

1 系列文章

简介:本系列文章,是以纯代码方式实现 Qt 控件的重构,尽量不使用 Qss 方式。

《[Qt]QListView 重绘实例之一:背景重绘》

《[Qt]QListView 重绘实例之二:列表项覆盖的问题处理》

《[Qt]QListView 重绘实例之三:滚动条覆盖的问题处理》

《[Qt]QListView 重绘实例之四:效果一讲解》

《[Qt]QListView 重绘实例之五:效果二讲解》

2 问题开始

继上文《之二》,继续处理绘制圆角矩形背景时,遗留了另一个主要问题:滚动条覆盖的问题。

实际上,本文不仅仅只是解决滚动条覆盖的问题,还会进一步重绘一个简单的滚动条,以实现较好的整体效果。

QListView-items

补充内容:

QListView 设置代理样式,并不会对其子控件生效,如水平/垂直滚动条不会受代理样式影响,即使其中实现了滚动条的新样式。必须单独给 QListView 的自带滚动条设置代理样式。

3 重绘滚动条

关于滚动条,需要先对一下暗号!啊不!是术语,抱歉!

QScrollBar

参考上图所示,Qt 中滚动条的相关定义如下:

  • 滑块 Slider
  • 滚动区域 Groove
  • 上一行 SubLine
  • 下一行 AddLine
  • 上一页 SubPage
  • 下一页 AddPage

3.1 开始重绘滚动条

关于滚动条的重绘,直观的感觉就是尝试重构一个新的滚动条类,然后设置为 QListView 的滚动条,以此来解决滚动条覆盖的问题。

void PScrollBar::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event)

    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
    painter.setPen(Qt::green);
    painter.setBrush(Qt::NoBrush);
    painter.drawRoundedRect(rect(), 15, 15);
}

只打算给滚动条先画一个圆角矩形外边框,但却出现了如下图所示的效果。

QListView-scrollbarbg

无填充的圆角矩形是绘制出来了,可以看到绿色的边框。但是,背景却成了黑色。而右图中,则是将填充色设置成白色,可以看到四个角上仍旧有黑色的区域。

补充说明:

关于滚动条背景黑色的问题,从上文代码中可以看出,在 paintEvent() 方法中重绘肯定是无法解决。

尝试过 setAttribute(Qt::WA_TranslucentBackground) 设置,也没有效果。

尝试过在国内/国外网站查找资料,也没有找到相关的内容。

(这过程中,还是花不不少的时间/精力的。)

……

最后,只好在 QScrollBar 的源码中寻找答案。发现 QScrollBar 内部初始化时,有给其添加 Qt::WA_OpaquePaintEvent 属性。然后果断去除此属性,测试效果即可满足要求。万幸啊!~~

……

其实,是有尝试 Qss 的,最开始确实也一直没有找到合适的纯代码解决方案。相对来说,使用 Qss 确实简单、直接,可以很方便的设置滚动条的各个元素。而纯代码方式却没办法,甚至有些地方都做不到。而修改源码,或者复制/修改源码为另外的版本,都比 Qss 方案开销大。(可谁让 Qss 会有性能问题呢!当然,少量嵌入式代码应用还好。)

归根结底,还是对源码、对 Qt 的底层实现不熟吧!而我的思路更多的是想在尽量不动 Qt 底层的基础上,实现自己想要的功能。也有相互隔离的意思吧。(额,略过略过……)

3.2 滚动条透明背景

Qt::WA_OpaquePaintEvent

Qt 帮助文档说明:

Indicates that the widget paints all its pixels when it receives a paint event. Thus, it is not required for operations like updating, resizing, scrolling and focus changes to erase the widget before generating paint events. The use of WA_OpaquePaintEvent provides a small optimization by helping to reduce flicker on systems that do not support double buffering and avoiding computational cycles necessary to erase the background prior to painting. Note: Unlike WA_NoSystemBackground, WA_OpaquePaintEvent makes an effort to avoid transparent window backgrounds. This flag is set or cleared by the widget’s author.

表示当控件收到绘制事件时,绘制控件的所有像素。因此,在生成绘制事件之前,诸如更新、调整大小、滚动和更改焦点等操作则不再要求需要擦除控件。使用此标志可以提供一些小优化,有助于减少在不支持双缓冲的系统上会出现的界面闪烁问题,还能减少在绘制前擦除背景所需的计算时间。注意:与 WA_NoSystemBackground 不同,WA_OpaquePaintEvent 应尽量避免窗口需要透明背景的情况。此标志由控件的作者设置或清除。

理解:

这里的重点是控件收到绘制事件时,会绘制控件的所有像素。如此,就像本文代码实现一样,在矩形圆角外侧,也要进行绘制。那么,绘制成什么样呢?大概就绘制成了黑色了吧!(就像,对 QPainter 设置 Qt::NoBrush 也是这样的黑色效果。)

又明确说明此标志应该避免使用在需要窗口透明背景的情况。而在 QScrollBar 中刚好设置了该标志。所以,应用于透明背景的情况,就应该删除此标志。

PScrollBar::PScrollBar(QWidget *parent) : QScrollBar(parent)
{
    /* 设置透明背景,因为 QScrollBar 源码中设置了此标志 */
    setAttribute(Qt::WA_OpaquePaintEvent, false);
}

再来看一下效果图,这下背景黑框问题解决了。

QListView-scrollbarbg2

3.3 绘制滚动条元素

有了上面的基础,现在开始就需要绘制滚动条的各个组成元素了。虽然绘制滚动条背景时,只画了一个白底绿边的圆角矩形,但也同时说明了几个问题点:

  • 只绘制背景,又没有调用父类函数绘制默认的滚动条效果,则只能看到一个背景,所以,滚动条的各个元素都必须重绘;
  • 经过鼠标点击验证,虽然没有绘制滚动条各个元素,但这些元素仍在原地方,鼠标点击后还是会响应对应的事件。如上一行/下一行的两个按钮;
  • 而且,好像还没有办法知道,上一行/下一行的按钮具体是多大。对滚动条的重绘,只知道整个滚动条的大小,并不知道其各个元素的大小;

本文打算绘制如下图所示的效果,这种效果也是现今比较常见的效果,也正好不需要滚动条两端的上一行/下一行按钮,方便利用滚动条的整个区域。

QListView-scrollbarstyle

然后,就是选择重绘的实现方式了。

本文一开始采用了重写 paintEvent() 的方法,但后来发现,这样就只能通过 Qss 才能隐藏上一行/下一行两个按钮,没办法通过纯代码的方式实现。

相反,采用 QProxyStyle 代码样式的方法,却可以很好的控制滚动条的各个元素,以及绘制效果。最后还是改为了代理样式的方法。

3.3.1 设置滚动条元素

对滚动条各个元素作如下设置:

  • 上一行 SC_ScrollBarSubLine:设置大小为 0
  • 下一行 SC_ScrollBarAddLine:设置大小为 0
  • 上一页 SC_ScrollBarSubPage:上移/左移 1 个按钮的高度/宽度
  • 下一而 SC_ScrollBarAddPage:下称/右移 1 个按钮的高度/宽度
  • 滑块 SC_ScrollBarSlider:上称/左移 1 个按钮的高度/宽度,增加 2 个按钮的高度/宽度
  • 滑轨 SC_ScrollBarGroove:上称/左移 1 个按钮的高度/宽度,增加 2 个按钮的高度/宽度

这样做,主要是在原样式的基础上进行调整,可以确保 Qt 计算滚动时,滚动条依旧可以正常响应。

QListView-elements1

但是,这个实现还是使用了一组经验数据。暂时没有找到更合适的解决办法,如果更改了滚动条的默认宽度/高度,估计会出问题,需要同步修改这些经验值。

这样做主要是为了解决初始状态下,获取不到上一行/下一行按钮大小的问题。因为实测发现,subControlRect() 函数在滚动条初始化时,根本不会调用 SC_ScrollBarSubLineSC_ScrollBarAddLine 两处。

/* 水平滚动条的高度,垂直滚动条的宽度 */
const int ScrollBarExtent = 21;
PScrollBarStyle::PScrollBarStyle()
{
    /* TODO: 经验值,调试 QProxyStyle 过程可以获取滚动条两个按钮的默认大小的 21x21 */
    /* NOTED: 必须设置一个合适值,否则滚动条初始化状态可能效果不正常 */
    mSubLineRect = QRect(0, 0, ScrollBarExtent, ScrollBarExtent);
    mAddLineRect = QRect(0, 0, ScrollBarExtent, ScrollBarExtent);
}
QRect PScrollBarStyle::subControlRect(QStyle::ComplexControl cc,
                                      const QStyleOptionComplex *option,
                                      QStyle::SubControl sc,
                                      const QWidget *widget) const
{
    int x, y, w, h;
    QRect rect = QProxyStyle::subControlRect(cc, option, sc, widget);
    rect.getRect(&x, &y, &w, &h);

    switch(cc)
    {
    case QStyle::CC_ScrollBar:
    {
        const QStyleOptionSlider *opt = qstyleoption_cast<const QStyleOptionSlider *>(option);
        if(nullptr == opt) { break; }

        switch(sc)
        {
        /* NOTED: 实测发现,初始状态,滚动条不会触发这两个消息 */
        case QStyle::SC_ScrollBarSubLine:
            mSubLineRect = rect;
            return QRect(x, y, 0, 0);
        case QStyle::SC_ScrollBarAddLine:
            mAddLineRect = rect;
            return QRect(x, y, 0, 0);
        case QStyle::SC_ScrollBarSubPage:
        {
            if(Qt::Horizontal == opt->orientation)
            {
                rect = QRect(x - mSubLineRect.width(), y, w, h);
            }
            else
            {
                rect = QRect(x, y - mSubLineRect.height(), w, h);
            }
            return rect;
        }
        case QStyle::SC_ScrollBarAddPage:
        {
            if(Qt::Horizontal == opt->orientation)
            {
                rect = QRect(x + mAddLineRect.width(), y, w, h);
            }
            else
            {
                rect = QRect(x, y + mAddLineRect.height(), w, h);
            }
            return rect;
        }
        case QStyle::SC_ScrollBarSlider:
        {
            if(Qt::Horizontal == opt->orientation)
            {
                rect = QRect(x - mSubLineRect.width(), y,
                             w + mSubLineRect.width() + mAddLineRect.width(), h);
            }
            else
            {
                rect = QRect(x, y - mSubLineRect.height(),
                             w, h + mSubLineRect.height() + mAddLineRect.height());
            }
            return rect;
        }
        case QStyle::SC_ScrollBarGroove:
        {
            if(Qt::Horizontal == opt->orientation)
            {
                rect = QRect(x - mSubLineRect.width(), y,
                             w + mSubLineRect.width() + mAddLineRect.width(), h);
            }
            else
            {
                rect = QRect(x, y - mSubLineRect.height(),
                             w, h + mSubLineRect.height() + mAddLineRect.height());
            }
            return rect;
        }
        default:
            break;
        }
        break;
    }
    default:
        break;
    }

    return rect;
}

效果图如下。可以看到,滚动条的两侧按键都被移除掉了,使用鼠标测试滚动功能也都正常。

QListView-element2

3.3.2 绘制滚动条效果

需要绘制两个部分:

  • 以整个滚动条大小绘制滑轨;
  • 以获取的滑块大小绘制滑块;

其中,利用滑块大小判断,还实现了滚动条无效时不绘制滑块的效果。这一点与默认滚动条表现一致。

void PScrollBarStyle::drawComplexControl(QStyle::ComplexControl control,
                                         const QStyleOptionComplex *option,
                                         QPainter *painter,
                                         const QWidget *widget) const
{
    switch(control)
    {
    case QStyle::CC_ScrollBar:
    {
        const QStyleOptionSlider *opt = qstyleoption_cast<const QStyleOptionSlider *>(option);
        if(nullptr == opt) { break; }

        painter->save();
        painter->setRenderHint(QPainter::Antialiasing);
        /* Track */
        painter->setPen(Qt::NoPen);
        painter->setBrush(QBrush(QColor(0xCE, 0xCE, 0xCE)));
        painter->drawRoundedRect(opt->rect, Radius, Radius);
        /* Slider */
        QRect sliderRect = subControlRect(control, opt, SC_ScrollBarSlider, widget);
        /* 不需要滚动条时,滑块大小等于滑轨大小,或滚动条大小 */
        /* 滚动条无效时,不绘制滑块,只绘制背景/滑轨,与滚动条默认行为一致 */
        if((Qt::Horizontal == opt->orientation && opt->rect.width() != sliderRect.width())
        || (Qt::Vertical == opt->orientation && opt->rect.height() != sliderRect.height()))
        {
            painter->setBrush(QBrush(QColor(0, 0xBC, 0xD4)));
            painter->drawRoundedRect(sliderRect, Radius, Radius);
        }
        painter->restore();

        return;
    }
    default:
        break;
    }

    QProxyStyle::drawComplexControl(control, option, painter, widget);
}

效果图如下:

QListView-scrollbar

3.3.3 缩小滚动条(附加)

上图的效果已经实现基本的目标,但是,滚动条还是太宽了,整体显得不太好看。所以额外实现一下缩小滚动条的效果。

QProxyStyle 代理样式中,可以使用 QStyle::PM_ScrollBarExtent 标志设置水平滚动条的高度/垂直滚动条的宽度。而且,既然要设置滚动条的宽度/高度,那么初始化时上一行/下一行按钮的大小也就不再是经验值,而是目标值了。

但是,本文中不打算使用这种方法,因为这里还有另一个重要的知识点。

处理方法:

QListView 中监听滚动条的事件,然后在 QEvent::ResizeQEvent::Move 事件下调整滚动条的位置/大小。

/* plistview.h */
class PListView : public QListView
{
private:
    /* 保存初始状态下滚动条的宽度/高度 */
    int mInitHScrollBarHieght;
    int mInitVScrollBarWidth;
}

/* plistview.cpp */
PListView::PListView()
{
    //...
    
    setVerticalScrollBar(mVScrollBar);
    mHScrollBar->setOrientation(Qt::Horizontal);
    setHorizontalScrollBar(mHScrollBar);

    setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
    setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);

    /*
     * TODO: 注意,必须在滚动条设置到列表后,才能正确地获取到滚动条的宽度/高度。
     * 此处理可能会有问题,暂未发现。
     */
    mInitHScrollBarHeight = mHScrollBar->height();
    mInitVScrollBarWidth = mVScrollBar->width();

    mVScrollBar->installEventFilter(this);
    mHScrollBar->installEventFilter(this);
    
    //...
}
bool PListView::eventFilter(QObject *obj, QEvent *event)
{
    if(obj->inherits("QScrollBar"))
    {
        QScrollBar *scrollBar = qobject_cast<QScrollBar *>(obj);
        if(nullptr == scrollBar) { return QListView::eventFilter(obj, event); }

        switch(event->type())
        {
        case QEvent::Resize:
        case QEvent::Move:
        {
            const int Margins = 5;
            QRect r(scrollBar->rect());
            if(Qt::Vertical == scrollBar->orientation())
            {
                if(mInitVScrollBarWidth == r.width())
                {
                    scrollBar->setGeometry(r.adjusted(Margins, Margins, -Margins, -Margins));
                }
            }
            else
            {
                if(mInitHScrollBarHeight == r.height())
                {
                    scrollBar->setGeometry(r.adjusted(Margins, Margins, -Margins, -Margins));
                }
            }
            break;
        }
        default:
            break;
        }
    }

    return QListView::eventFilter(obj, event);
}

注意:

经过实际测试,(在此场景下)如果尝试移动滚动条,结果可能是找不到滚动条了。

只能在滚动条的区域内,进行滚动条的大小、以及相对位置的调整。因为还在原位置处,所以做一些微调,还是可以正常被绘制出来的。

最后再来看一个效果图:

QListView-scrollbar2

滚动条的宽度/高度变小了,与边框的间距也加大了。

意思到了,具体的效果,则可以根本实际需求优化,本文不再深入了。

4 小结

到此,系列的主要内容就基本完成了。

写文章之前,其实还有一些 Qss 的代码。但是随着写文章过程中的整理,又查了一些资料,看了看源码,最终还是消除了 Qss 代码实现,达到了纯代码实现的目的。

本文实现了这个方案的整体思路与效果。对 QProxyStyle 代理应用,也进行了深入,有助于其它相似功能的扩展开发。

当然,本文肯定还有未考虑到的内容,以后有其它发现,再做补充。

系列后两文,会再写两个不一样的效果实例,具体实现有不少差别,也有不同的亮点。感兴趣的看官,还请移驾。

《[Qt]QListView 重绘实例之四:效果一讲解》

《[Qt]QListView 重绘实例之五:效果二讲解》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值