• [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 开始

    自定义 Qt 控件,无外乎两个主要目的:

    • 实现更漂亮的样式;
    • 实现更强大的/更合适的功能;

    要实现以上两个主要目的,基本上都需要对 Qt 原生控件进行一定的重绘,以适应需求。

    本节中,主要讲解 QListView 的背景绘制。

    QListView

    (之所以单独写一文,是因为自己动手实现时才发现:虽然最后的实现代码并不多,但要弄懂这些,还是要花费很多精力的。)

    → 解决方案直达 ←

    3 paintEvent 重绘与问题

    通常,重构一个新控件,基本上都是直接重写 void paintEvent(QPaintEvent *event) 方法。

    void PListView::paintEvent(QPaintEvent *event)
    {
        Q_UNUSED(event)
    
        QPainter painter(this);		// Error
    
        painter.setRenderHint(QPainter::Antialiasing);
        painter.setPen(QPen(Qt::red));
        painter.setBrush(QBrush(Qt::white));
    
        painter.drawRoundedRect(rect(), 5, 5);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    3.1 问题 1 —— 绘图对象

    通常,进行重绘时,新建 QPainter 对象都是以父控件为对象,意即在父控件中进行绘制。

    但是,如果这样直接对 QListView 进行重绘,是会出错的:

    QWidget::paintEngine: Should no longer be called
    QPainter::begin: Paint device returned engine == 0, type: 1
    QPainter::setRenderHint: Painter must be active to set rendering hints
    QPainter::setPen: Painter not active
    QPainter::setBrush: Painter not active
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (猜测)原因大致应该是:QListView 是由多个子控件组成,实际负责显示内容的只是其中的一个子控件,所以绘制对象需要具体指定到负责显示的对象。

    QListView 继承树如下:

    QListView-inherittree

    而一个默认 QListView 对象包含的子控件如下:

    (QWidget(0x1eb4600, name = "qt_scrollarea_viewport"),
    QStyledItemDelegate(0x1eb1840),
    QItemSelectionModel(0x1eb1ba0),
    QWidget(0x1eb1010, name = "qt_scrollarea_hcontainer"),
    QWidget(0x1eb1150, name = "qt_scrollarea_vcontainer"))
    
    • 1
    • 2
    • 3
    • 4
    • 5

    其中,实际显示内容的对象就是 “qt_scrollarea_viewport”,也就是 QListView 的视口(viewport)。这样做的主要原因,是要实现对 QListView 内容的滚动显示(显示部分内容)。

    所以,对于 QListView 重绘,必须要针对视口 viewport()

    void PListView::paintEvent(QPaintEvent *event)
    {
        Q_UNUSED(event)
    
        QPainter painter(viewport());
    
        painter.setRenderHint(QPainter::Antialiasing);
        painter.setPen(QPen(Qt::red));
        painter.setBrush(QBrush(Qt::white));
    
        painter.drawRoundedRect(rect(), 5, 5);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    效果如下图示:

    QListView-paint1

    3.2 问题 2 —— 外边线框

    从上图看,这次倒是绘制出背景框。但首先注意到的问题是 QListView 默认外连线框,非常显眼。因此,首要目的是要去掉这个外连线框。

    上文实现代码中的重绘过程,仅做了两件事:

    • 绘制了一个圆角矩形;
    • 阻止了 QListView 的其它默认绘制;

    因此 ,基本可以肯定,外连线框并不是由 paintEvent() 绘制过程中引起的。看来原因得到 QListView 里层查找。

    原因查找的具体过程略过不述,QListView 的外边线框其实就是其父类 QFrame 的边框(可以理解为一个底层,其它内容都绘制在这个底层之上,毕竟 QListView 是 UI 控件)。

    只需要对 QListView 进行如下设置,改变一下 QFrame 样式即可去掉外边线框:

    PListVeiw::PListView(QWidget *parent) : QListView(parent)
    {
        setFrameStyle(QFrame::NoFrame);
    }
    
    • 1
    • 2
    • 3
    • 4

    效果如下:

    QListView-noframe

    强制隐藏/关闭垂直滚动条,效果如下:

    QListView-noscrollbar

    3.3 问题 3 —— 绘制区域

    从上图可知,绘制的背景效果基本出来了。但是,也被垂直滚动条挡住了一部分。

    再回来看一看绘图代码,其中有一行如下:

    	painter.drawRoundedRect(rect(), 5, 5);
    
    • 1

    此时,指定的绘图区域为 rect(),即针对控件的整个显示区域。而我们指定的绘图对象是 QListView 的视口,原则上为了保证一致性,在什么上绘图,就应该在该对象的区域内进行绘制。所以,修改以上那行的代码:

    	painter.drawRoundedRect(viewport()->rect(), 5, 5);
    
    • 1

    效果如下:

    QListView-viewport

    这种效果,也还可以。一些样式也确实是将滚动条置于控件之外的。

    本文不针对此样式进行讲解,主要考虑滚动条内含在列表内的样式。

    滚动条的问题,先按下不提,具体详见本系列后文说明。参考《[Qt]QListView 重绘实例之三:滚动条覆盖的问题处理》。

    3.4 问题 4 —— 滚动时残留

    先前为了重点显示 QListView 的背景绘制效果,所以没有绘制 QListView 的内容。

    现在,加上内容的绘制代码:

    void PListView::paintEvent(QPaintEvent *event)
    {
        QPainter painter(viewport());
    
        painter.setRenderHint(QPainter::Antialiasing);
        painter.setPen(QPen(Qt::red));
        painter.setBrush(QBrush(Qt::white));
    
        painter.drawRoundedRect(rect(), 5, 5);	// 理解为视口占据整个控件区域
        
        QListView::paintEvent(event);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    说明:绘制顺序是有要求的。应该先绘制背景,然后绘制列表内容(即前景)。

    效果图如下:

    QListView-paint2

    但是,如果我们使用鼠标滚轮滚动或拖动滚动条,滚动 QListView 的内容,却出现了如下效果:

    QListView-residual

    这显然不是想要的效果。

    具体原因未深究,暂时未知,猜测应该是底层代码的原因。因为,上文中的重绘代码其实很简单,并未做多余的动作。

    但这种残留效果,显然不可接受。

    因此,至少到目前,这种方式绘制 QListView 的背景是不可行的。

    (考虑到添加委托会对列表项进行绘制,可能会影响到这个残留问题。尝试过添加委托,但这个残留问题依然存在。)

    4 解决方案

    从上文得知,采用 paintEvent()QListView 背景进行绘制的方案不可行。

    另,考虑到后来的 Qt 版本对于 Qss 的性能问题,本系列也不考虑 Qss 方案。

    于是,已知可行的方案只剩使用 QProxyStyle 代理样式定制了。

    (之前也没有实际使用过代理样式,通过学习/练习/测试得出了合适的效果。)

    关于 QProxyStyle 的具体内容,查找资料的过程中有发现,有不少介绍的好博文,请酌情参考(文末参考资料有链接),本文不另述。

    4.1 定义背景绘制样式

    /* .h */
    class PListViewStyle : public QProxyStyle
    {
    public:
        PListViewStyle();
    
        void drawControl(QStyle::ControlElement element,
                         const QStyleOption *option,
                         QPainter *painter,
                         const QWidget *widget = nullptr) const override;
    };
    
    /* .cpp */
    PListViewStyle::PListViewStyle()
    {
    }
    void PListViewStyle::drawControl(QStyle::ControlElement element,
                                      const QStyleOption *option,
                                      QPainter *painter,
                                      const QWidget *widget) const
    {
        switch(element)
        {
        case QStyle::CE_ShapedFrame:
        {
            const QStyleOptionFrame *opt = qstyleoption_cast<const QStyleOptionFrame *>(option);
            if(nullptr == opt) { return; }
    
            painter->save();
            painter->setRenderHint(QPainter::Antialiasing);
    
            painter->setPen(QPen(Qt::red));
            painter->setBrush(QBrush(Qt::white));
            painter->drawRoundedRect(opt->rect, 5, 5);
    
            painter->restore();
            return;
        }
        default:
            break;
        }
    
        QProxyStyle::drawControl(element, 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
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    4.2 使用代理样式

    PListVeiw::PListView(QWidget *parent) : QListView(parent)
    {
        // setFrameStyle(QFrame::NoFrame);	// Must delete or comment it
        setStyle(new PListViewStyle);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    注意:

    • 需要删除重写函数 void paintEvent(QPaintEvent *event),否则可能覆盖效果。
    • 需要删除对 QFrame 的样式设置,不能再设置为 QFrame::NoFrame。因为代理样式实际是对 QFrame 进行绘制的,如果设置了 QFrame::NoFrame,则绘制的样式根本就不会显示。

    效果如下:

    QListView-paintbg

    至少看上去,基本达到了预期的效果。

    但是,

    但是,

    但是,总有但是,哈哈。

    将背景的圆角矩形圆角半径加大一下,再来看看效果图:

    QListView-residual2

    从上图可以看出有几个问题:

    • 列表项在背景的上层,即背景绘制先于列表项。而列表项也是有背景的(以及高亮/选中背景),可以理解为列表项就是一个个小矩形(默认没有圆角)。由上可以看出,视口的最上/最下一行,都有矩形直角覆盖了背景(圆角矩形),因此破坏了背景的效果;
    • 同理,滚动条也在背景上层,滚动条也是一个直角矩形,矩形直角覆盖了背景,因此也破坏了背景的效果;

    其中:

    • 对于列表项产生的覆盖问题,可以通过使用委托,控制列表项背景(默认背景/高亮背景/选中背景)的绘制,使绘制视口最上/最下一行时,绘制合适的圆角效果。
    • 对于滚动条的问题,就复杂得多,具体详见本系列后文内容。参考《[Qt]QListView 重绘实例之三:滚动条覆盖的问题处理》。

    5 参考资料

    1. 《C++ GUI Qt 4编程(第二版)》,第 19 章,19.2 子类化 QStyle
    2. QStyle类用法总结(一)
    3. 绘制自定义QSlider
  • 相关阅读:
    RocketMQ学习笔记
    秒级启动的集成测试框架
    【Mysql性能优化系列】Mysql优化方案你知道哪些
    用户行为分析-如何用数据驱动增长
    如何利用Socks5代理IP提升网络安全与跨境电商业务
    MySQL常见问题汇总
    手撸代码,Redis发布订阅机制实现
    卷积神经网络(CNN)
    企业内部IM即时聊天软件WorkPlus,自主可控的信创即时通讯IM
    鸿鹄工程项目管理系统em Spring Cloud+Spring Boot+前后端分离构建工程项目管理系统
  • 原文地址:https://blog.csdn.net/kyzoon/article/details/133220006