• [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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97

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

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    效果图如下:

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    注意:

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

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

    最后再来看一个效果图:

    QListView-scrollbar2

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

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

    4 小结

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

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

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

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

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

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

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

  • 相关阅读:
    左孩子右兄弟
    图片和其他数据向量化多模态融合的问题,
    R语言 求向量的距离 和 角度
    MySQL优化
    JDK9 为何要将String的底层实现由char[]改成了byte[]
    JavaScript函数七重关之函数定义
    长方柱类(类和对象)Java
    R语言做生信分析系列(一)—— R软件简单安装
    flink cdc 没有Replication client ,Replication slave权限,报错,处理
    对于Redis,如何根据业务需求配置是否允许远程访问?
  • 原文地址:https://blog.csdn.net/kyzoon/article/details/133290061