Qt底层原理:深入解析QWidget的绘制技术细节(1)

在Qt5中,QWidget的绘制流程比较分散,网上介绍的文章也很少,因此写一篇文章总结记录一下这部分的知识点。

笔者使用的是Qt5.15.2的源码。

基本的绘制流程:从update到合成

  1. 更新请求(Invalidate):
    当一个QWidget需要被重绘时(比如大小改变、数据更新等),会调用update()方法来标记该widget为需要重绘。update一般会到repaintManager->markDirty,如果当前正在绘制,则通过事件QUpdateLaterEvent进行重绘。这部分逻辑代码如下:
    在这里插入图片描述

  2. 重绘区域计算(Dirty Region Calculation):

    • Qt有一个优化机制,它会合并多个重绘请求以减少重绘的次数和区域。重绘区域的计算由Qt的QWidgetRepaintManager负责,该系统维护了一个脏区域(dirty regions),这是所有需要重绘的区域的集合。主要逻辑在QWidgetRepaintManager::markDirty。这部分逻辑稍微复杂,但不是重点,感兴趣的读者可以自行翻阅源码,此处不再列出。
  3. 事件处理(Event Processing):

    • 在QWidgetRepaintManager::sendUpdateRequest,会生成一个QEvent::UpdateRequest的事件,即使指定了UpdateNow,也会根据这次更新是否距离上次更新大于60fps而降低这次绘制的优先级。 这部分逻辑代码如下:在这里插入图片描述
  4. 事件循环(Event Loop):
    Qt的事件循环在QCoreApplication::exec()调用后运行,负责处理事件队列中的事件。对于绘制事件,事件循环会传递给QWidget的event()方法。这部分不是本文章重点,不列出详细细节。

  5. 事件处理(Event Handling):
    QWidget的event()方法会检查事件的类型。如果是绘制事件QEvent::UpdateRequest或者QEvent::UpdateLater,会转调到QWidgetPrivate::paintOnScreen函数,接着使用QWidgetRepaintManager类提供的功能,转调到每个Widget::paintEvent函数。 这部分逻辑代码如下:在这里插入图片描述

  6. 绘制逻辑paintAndFlush:这部分是本文的重点,也比较复杂,在后文详细展开。

  7. 绘制(Painting):
    paintEvent()方法中,一般使用QPainter对象,它是Qt中负责绘制的类。QPainter可以绘制各种图形元素,如文本、线条、形状等。

  8. 绘图设备(Paint Device):
    QPainter对象会被绑定到一个绘图设备(QPaintDevice),比如QWidget本身,或者一个QPixmapQImageQPicture等。QWidget通过其paintEngine()方法提供了一个QPaintEngine对象,这是实际进行绘制操作的底层接口。

  9. 绘图引擎(Paint Engine):
    QPaintEngine是一个抽象基类,它定义了绘图操作的接口。Qt提供了多种绘图引擎,比如QRasterPaintEngineQOpenGLPaintEngine等,具体使用哪个引擎取决于QWidget的绘制设备以及平台特性。

在源代码层面,以下是几个关键类和它们在绘制流程中的作用:

  • QWidget: 作为所有UI组件的基类,管理绘制和事件。
  • QPaintEvent: 继承自QEvent,封装了绘制事件的信息。
  • QPainter: 提供了一组API来执行绘制操作。
  • QPaintDevice: 是一个抽象类,QWidget和其他一些类比如QImage、QPixmap都是这个类的子类,用于表示可以被绘制的对象。
  • QPaintEngine: 抽象基类,定义了底层绘图操作的接口。
  • QWidgetRepaintManager:主要绘制流程的管理类。

Qt提供了QWidget::setUpdatesEnabled()方法,允许开发者禁用或启用控件的更新。这可以用来在批量修改控件时暂时禁用更新,以避免不必要的重绘。

例如,QPushButton的paintEvent堆栈如下:在这里插入图片描述

绘制半透明的控件:父子Widget绘制细节

在Qt中,重绘一个子控件默认不会导致父控件重绘。但是,如果子控件是半透明的(具有alpha通道不是完全不透明的颜色),那么会导致父控件重绘内容作为背景来正确地绘制子控件。
这部分的逻辑比较复杂,核心逻辑在QWidgetRepaintManager::paintAndFlush里,这个函数的源码不在此贴出,但是分析这个函数内部的主要逻辑。

QWidgetRepaintManager::paintAndFlush

QWidgetRepaintManager::paintAndFlush 这个函数的逻辑,具体可以分解为以下步骤:

  1. 检查更新是否被禁用
    如果 QWidget 的 updatesEnabled 属性为 false,则不进行任何绘制操作。

  2. 检查并更新脏区域
    如果窗口的大小已更改,并且更新没有被禁用,函数会检查是否有静态内容(不需要重绘的部分)。如果有,它会只将新可见的部分添加到脏区域;否则,它会标记整个窗口为需要重绘。

  3. 调整后台存储的大小
    如果后台存储(store)的大小与窗口大小不一致,它会被调整以匹配窗口的大小。

  4. 绘制和清理脏区域
    函数创建一个包含所有需要重绘的区域的 QRegion 对象。然后它遍历所有标记为脏的控件,并根据是否有透明的重叠兄弟控件,将其分为可直接绘制和需要合成的控件。

  5. 处理特殊的绘制情况
    对于具有 render-to-texture 特性的控件(如 OpenGL 小部件),它们会被特别处理,因为它们的绘制可以直接在纹理上完成,不需要经过常规的后台存储绘制过程。

  6. 发送绘制事件
    遍历所有需要绘制的控件,并为它们发送 QPaintEvent 事件。这些事件触发控件的 paintEvent 方法,从而完成实际的绘制工作。

  7. 绘制不透明的非重叠控件
    直接在后台存储上绘制那些不透明且没有被兄弟控件重叠的控件。

  8. 合成
    如果需要,将所有剩余的控件绘制到后台存储上,并处理任何必要的合成操作,以确保正确的层叠和透明度效果。

  9. 结束绘制
    调用 store->endPaint() 表示绘制操作的结束。

  10. 刷新
    将后台存储的内容刷新到屏幕上。如果启用了双缓冲,这将涉及到将后台缓冲区的内容复制到前台缓冲区,并在适当的时间将其展示到屏幕上。

这个函数体现了 Qt 绘制的一些核心概念,包括脏区域管理、后台存储、控件的绘制事件、以及绘图设备和绘图引擎的使用。所有的绘制操作都是在主线程中进行的,即使是那些涉及 OpenGL 或其他渲染技术的绘制也不例外。

QWidgetPrivate::drawWidget

QWidgetRepaintManager::paintAndFlush在顶层处理主要的绘制流程,除了这个函数,QWidgetPrivate::drawWidget 函数也包含大量绘制流程的实现细节,这个函数作为第二层处理绘制细节。同样地,这个函数的源码不在此贴出,但是总结这个函数内部的主要流程:

QWidgetPrivate::drawWidget 函数是一个内部函数,用于在给定的绘制设备(pdev)上绘制一个控件及其子控件。这个函数处理了许多绘制相关的细节,包括处理图形效果、设置裁剪区域、绘制背景以及发送绘制事件。以下是函数的主要逻辑步骤:

  1. 检查是否有内容需要绘制
    如果传入的区域(rgn)为空,则没有内容需要绘制,函数立即返回。

  2. 记录绘制操作的日志信息
    使用 qCInfo 记录绘制区域、控件、偏移量、目标绘制设备以及标志。

  3. 处理图形效果
    如果控件有启用的图形效果,那么绘制流程会交给图形效果处理器。它可能会修改绘制的方式,例如添加阴影或模糊效果。

  4. 计算需要绘制的区域
    根据控件的属性和标志计算出实际需要绘制的区域(toBePainted)。可能会考虑是否绘制根控件、是否绘制到屏幕上、是否递归绘制子控件以及是否绘制不可见控件。

  5. 预处理绘制设备
    设置或重定向绘制目标,并设置系统裁剪区域。

  6. 绘制背景
    如果需要,绘制控件的背景。这可能涉及到自动填充背景、绘制不透明的绘制事件或处理窗口系统背景。

  7. 处理渲染到纹理的控件
    如果控件渲染到纹理(例如使用 OpenGL),则相应地处理,可能是通过绘制一个透明矩形来为纹理"打孔",或者将纹理复制到屏幕上。

  8. 发送绘制事件
    如果没有跳过绘制事件,发送一个 QPaintEvent 给控件,这将触发控件的 paintEvent 方法。

  9. 标记需要刷新
    如果有 repaintManager,则调用 markNeedsFlush 来标记区域为需要刷新。

  10. 恢复状态
    恢复重定向的绘制设备和系统裁剪区域到原始状态,并清除激活状态的绘制标志。

  11. 递归绘制子控件
    如果设置了递归标志并且控件有子控件,递归地绘制这些子控件。

整个函数的逻辑很大程度上是关于准备好绘制上下文,然后根据需要绘制控件本身或者委托给图形效果和子控件的绘制。这个函数是 Qt 控件绘制流程中的核心部分,它确保了控件及其子控件能够正确地在屏幕上渲染。

总体而言,Qt体系的绘制实现基本可以在这两个函数中体现出来。相关的数据结构和逻辑也逃不出QWidgetRepaintManagerQWidgetPrivate,感兴趣的读者可以深入了解这两个类。

如何判断一个Widget是否半透明?

核心在这个函数里:

void QWidgetPrivate::updateIsOpaque()
{
    // hw: todo: only needed if opacity actually changed
    setDirtyOpaqueRegion();

#if QT_CONFIG(graphicseffect)
    if (graphicsEffect) {
        // ### We should probably add QGraphicsEffect::isOpaque at some point.
        setOpaque(false);
        return;
    }
#endif // QT_CONFIG(graphicseffect)

    Q_Q(QWidget);
    if (q->testAttribute(Qt::WA_OpaquePaintEvent) || q->testAttribute(Qt::WA_PaintOnScreen)) {
        setOpaque(true);
        return;
    }

    const QPalette &pal = q->palette();

    if (q->autoFillBackground()) {
        const QBrush &autoFillBrush = pal.brush(q->backgroundRole());
        if (autoFillBrush.style() != Qt::NoBrush && autoFillBrush.isOpaque()) {
            setOpaque(true);
            return;
        }
    }

    if (q->isWindow() && !q->testAttribute(Qt::WA_NoSystemBackground)) {
        const QBrush &windowBrush = q->palette().brush(QPalette::Window);
        if (windowBrush.style() != Qt::NoBrush && windowBrush.isOpaque()) {
            setOpaque(true);
            return;
        }
    }
    setOpaque(false);
}

QWidgetPrivate::updateIsOpaque 函数的工作流程如下:

  1. 设置脏不透明区域
    调用 setDirtyOpaqueRegion 方法,这通常意味着标记控件的不透明区域需要更新。这个区域是指控件中不需要考虑透明度处理的部分。

  2. 检查是否有图形效果
    如果控件应用了 QGraphicsEffect,函数立即将控件标记为非不透明(因为图形效果可能会引入透明度),然后返回。图形效果可能包括模糊、阴影等,这些都可能改变控件的不透明度。

  3. 检查控件属性
    函数检查控件是否具有 Qt::WA_OpaquePaintEventQt::WA_PaintOnScreen 属性。这些属性通常由开发者设置,用来指示控件的绘制事件是不透明的,或者控件直接在屏幕上绘制。如果有任何一个属性被设置,函数将控件标记为不透明并返回。

  4. 检查自动填充背景
    如果控件的 autoFillBackground 属性为真,表示控件在绘制前会自动用背景色填充。函数会检查用于自动填充的画刷是否不透明。如果是,控件被标记为不透明。

  5. 检查窗口属性
    如果控件是一个窗口,并且没有设置 Qt::WA_NoSystemBackground 属性(这意味着窗口系统不会自动填充背景),函数会检查窗口背景画刷是否不透明。如果是,窗口被标记为不透明。

  6. 设置为非不透明
    如果之前的检查都没有导致函数返回,最后将控件标记为非不透明。

在大型复杂界面中和性能敏感的应用中,我们要避免过多的不透明控件可以减少绘制负担。

绘制逻辑的复用:标准控件绘制与QStyle的细节

在Qt中,QStyle类负责控件的外观和行为。这包括控件的绘制(如按钮、滑块、复选框等),以及控件的尺寸、布局和交互行为(如鼠标悬停、按下状态的视觉反馈)。QStyle提供了一种机制,通过它可以统一控制应用程序中所有控件的外观,而无需在每个控件的绘制逻辑中单独实现这些。

QStyle是一个抽象基类,它定义了一套API,用于绘制标准的GUI组件以及获取与风格相关的属性和尺寸信息。Qt自带了几种风格,如QWindowsStyleQMacStyleQFusionStyle等,它们实现了在不同平台下的本地外观和行为。可以通过继承QStyle来创建自定义风格。

绘制标准控件

当一个标准控件(例如QPushButton)需要被绘制时,它会调用其paintEvent()函数。在paintEvent()中,控件通常不直接进行绘制,而是将绘制任务委托给当前的QStyle对象。这是通过调用style()方法来获取当前应用程序风格,然后使用QStyle的绘制函数来完成的。

例如,一个按钮会这样使用QStyle来进行绘制:
在这里插入图片描述
这里,QStylePainterQPainter的一个特殊版本,专门用于风格绘制。QStyleOptionButton是一个包含按钮状态和属性的结构体。drawControl()函数是QStyle的一个方法,用于绘制控件元素(Control Element),在这个例子中是一个按钮。

风格元素和选项

QStyle类定义了多个枚举,用于指定控件的哪一部分需要绘制,以及如何绘制。这些枚举包括ControlElementPrimitiveElementComplexControl等。

  • ControlElement: 这些是高级UI元素,如整个按钮、工具栏、滚动条等。
  • PrimitiveElement: 这些是构成控件的基本图形元素,如按钮的边框、复选框的勾选标记等。
  • ComplexControl: 这些是由多个交互部分组成的控件,如组合框或滑块。

QStyleOption类及其派生类携带了关于如何绘制控件的信息。QStyleOption包含了状态信息(如是否被按下、是否有焦点等),而派生类则包含了更具体的信息。例如,QStyleOptionButton包含了按钮特有的信息,如是否是默认按钮、是否是复选按钮等。

自定义风格

要创建自定义风格,你可以继承QStyle或者任何已有的风格类,并重写相应的绘制和尺寸计算方法。例如,你可能会重写drawControl()drawPrimitive()sizeFromContents()等方法来自定义控件的绘制和布局。

应用风格

可以通过调用QApplication::setStyle()方法来为整个应用程序设置风格。这个风格会被所有控件使用,除非某个控件显式地设置了不同的风格。QStyle负责定义和实现Qt控件的外观和行为,而具体的控件类则通过委托给QStyle来执行实际的绘制操作。这种设计使得Qt的外观和感觉可以非常灵活地被定制和更换,而不需要修改每个控件的实现代码。

QWidget绘制体系为什么这么设计【重点】

请跳转第二篇《Qt底层原理:深入解析QWidget的绘制技术细节(2)》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/726281.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

从设计到实践:高速公路监控技术架构全剖析

随着高速公路网络的迅速扩展和交通流量的日益增加,高效的监控系统成为保障交通安全、提升管理效率的重要手段。本文将深入探讨高速公路监控技术架构,从设计理念到实际应用,全面解析这一关键技术的各个环节。 ### 一、系统设计理念 #### 1. 高…

岁月长河中的温柔等待

在那个年代,爱情往往像是一条静静流淌的小河,不动声色却又波澜不惊。在一个小村庄里,住着一对中年夫妻,人们叫他们李大叔和赵阿姨。他们的故事,就像是那个时代的缩影,承载着岁月的沧桑与深情的守候。 李大…

PyCharm新手入门

前言 在之前《Python集成开发工具的选择》一文中介绍了python初学者可以使用Jupyter Notebook,Jupyter Notebook简单易用,可以用来练习代码编写,但是实际生产开发环境使用这个工具是远远不够用的,因为实际软件开发中需要软件调试…

大数据数据挖掘系统可视化设计艺术

1.系统背景 在我们实际进行数据挖掘研发过程中,为了验证某些算法在业务中的性能每次都需要去从头写代码,如果我们将我们研发的算法以模块化的思想封装起来,下次再使用的时候直接在系统中进行拖拉一下生成一个工作流,就能完成数据挖…

Hive数据锁问题处理

在测试环境有定时任务会定期将flume采集的数据load到hive表中,在查看yarn application过程中发现load操作没有执行,且后续的任务在上一个任务执行结束后很久才开始。感觉像是阻塞一样,于是手动执行相关脚本,发现也是会卡住&#x…

无引擎游戏开发(3):数据结构设计|功能函数完善

为了简单起见,我们将棋盘的二维数组定义为全局变量。除此之外还要定义一个char类型的全局变量来识别当前的落子类型,我们将其初始化为‘O’。 char Board_data[3][3] {{-, -, -},{-, -, -},{-, -, -}, };char Cur_piece O; 现在回到“读取操作”部分…

Rancher注册已有k8s集群

Rancher安装后注册K8s集群操作 1.Rancher安装 编辑docker—compose文件 version: 3.8services:rancher:image: registry.cn-hangzhou.aliyuncs.com/rancher-images/rancher:v2.8.5container_name: rancherprivileged: truerestart: unless-stoppedports:- "18080:80&qu…

[创业之路-118] :制造业企业的必备管理神器-ERP-主要功能模块说明与系统架构

目录 一、ERP功能的标准化 二、常见的ERP标准化功能 2.1 基础档案 2.2 供应链 2.3 人力资源管理 2.4 资产管理 2.5 生产制造 2.6 财务会计 2.7 管理会计 2.8 CRM客户管理管理 2.9 商业智能分析 三、常见的ERP软件供应商 国内ERP软件供应商 国外ERP软件供应商 四…

西电研究生录取通知书来啦~ (吸吸欧气)

今日桂枝平折得 几年春色并将来,西安电子科技大学 那些深夜点灯的过往 那种日夜备考的迷惘 那个全力奔赴的梦想 都在这封通知书里得到回响! —— Xidian University —— —— Xidian University —— XDU 今年的通知书采用紫色为主色 封面压印年…

阿里云API文档有哪些实用功能?如何使用?

阿里云API安全性如何保障?阿里云API怎么实现自动化? 阿里云作为全球领先的云计算服务提供商,提供了广泛的API接口,以满足各类用户的需求。阿里云API文档不仅详尽,而且易于使用,AokSend将详细介绍阿里云API…

渗透测试基础(四) MS08-067 漏洞攻击

1. 漏洞介绍 漏洞描述 Microsoft Windows Server服务RPC请求缓冲区溢出漏洞Windows的Server服务在处理特质RPC请求时存在缓冲区溢出漏洞,远程攻击者可以通过发送恶意的RPC请求触发这个溢出,导致完全入侵用户系统,以SYSTEM权限执行任意指令。…

iptables(3)规则管理

简介 上一篇文章中,我们已经介绍了怎样使用iptables命令查看规则,那么这篇文章我们就来介绍一下,怎样管理规则,即对iptables进行”增、删、改”操作。 注意:在进行iptables实验时,请务必在个人的测试机上进行,不要再有任何业务的机器上进行测试。 在进行测试前,为保障…

海量数据处理利器 Roaring BitMap 原理介绍

作者:来自 vivo 互联网服务器团队- Zheng Rui 本文结合个人理解梳理了BitMap及Roaring BitMap的原理及使用,分别主要介绍了Roaring BitMap的存储方式及三种container类型及Java中Roaring BitMap相关API使用。 一、引言 在进行大数据开发时,…

Raycaster--当物体放在容器中并做了转换,交点坐标不对的问题。

交点坐标问题 问题解决x关键点 总结 问题 子代放在了一个容器里,容器做了旋转、位移。 递归获得了最近的相交子代获取到的交点坐标并不是想要的交点坐标。 经过可视化观察,很像是没转换之前的坐标点。 解决x 在 Three.js 中,当你使用 Rayc…

详细介绍如何解决vcomp140.dll丢失的步骤,分享几种vcomp140.dll修复方法

当这个vcomp140.dll文件丢失时,可能会导致相关程序运行出错甚至无法运行。很多用户可能会遇到vcomp140.dll丢失的问题,但是这并不是不可解决的困难。接下来就和大家分享几种解决vcomp140.dll丢失的方法,给大家详细的关于如何解决vcomp140.dll…

matplotlib 做饼图

饼图可以很好地帮助用户快速了解整体市场数据的占比分配 import matplotlib.pyplot as pltexplode (0,0.1,0,0) labels Frogs,Hogs,Dogs,Logs sizes [15, 30, 45, 10] fig,ax plt.subplots() # colors 设置图形颜色 ;pctdistance:设置百分比标签与圆心的距离&am…

MacBook Air M3的电脑怎么样 新买MacBook Air提示内存不足 苹果电脑内存不够用怎么办

Apple的MacBook Air系列一直是轻薄便携笔记本电脑的代表,最新推出的MacBook Air M3因其出色的性能和优雅的设计而受到广泛关注。然而,许多用户在购买全新的MacBook Air后反应他们遇到了内存不足的提示。 本文将探讨MacBook Air M3的电脑怎么样&#xff0…

【MySQL】事务二

事务二 1.数据库并发的场景2.读-写2.1 3个记录隐藏字段2.2 undo日志2.3 模拟 MVCC2.4 Read View2.5 RR 与 RC的本质区别 3.读-读4.写-写 点赞👍👍收藏🌟🌟关注💖💖 你的支持是对我最大的鼓励,我…

示例:应用DependencyPropertyDescriptor监视依赖属性值的改变

一、目的:开发过程中,经常碰到使用别人的控件时有些属性改变没有对应的事件抛出,从而无法做处理。比如TextBlock当修改了IsEnabled属性我们可以用IsEnabledChanged事件去做对应的逻辑处理,那么如果有类似Background属性改变我想找…

太湖远大毛利率下滑:研发费用率远低同行,募投项目合理性疑点重重

《港湾商业观察》黄懿 6月20日,浙江太湖远大新材料股份有限公司(以下简称“太湖远大”,873743.NQ)即将迎来过会。 2023年11月30日,太湖远大所提交的上市申请材料正式获北交所受理,保荐机构为招商证券&…