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 changedsetDirtyOpaqueRegion();#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)》

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

001、DM8安装

参照:https://eco.dameng.com/document/dm/zh-cn/pm/install-uninstall.html 1. 准备工作 操作系统查看 [rootora19c ~]# cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core)新建用户 [rootora19c ~]# groupadd dinstall -g 2001 [rootora19c ~]# …

数据分析第十二讲 数据可视化入门(一)

数据可视化入门(一) 在完成了对数据的透视之后,我们可以将数据透视的结果通过可视化的方式呈现出来,简单的说,就是将数据变成漂亮的统计图表,因为人类对颜色和形状会更加敏感,然后再进一步解读…

AXI三板斧之Outstanding、Out-of-order、interleaving

1、AXI三板斧之Outstanding 可以不用等单个命令的响应,直接连续发送N个命令(N>1),假设Slave端的Outstanding能力为N时(N>1),那么Master端可以在Slave不返回读数据的情况下,连…

SARscape——Frost斑点滤波

目录 一、算法原理1、概述2、参考文献 二、软件操作三、结果展示1、原始图像2、滤波结果 一、算法原理 1、概述 2、参考文献 [1] 廉小亲,黄雪,高超,等. 基于Frost滤波和改进CNN的SAR图像TR方法 [J]. 计算机仿真, 2023, 40 (05): 49-55233. [2] SAR图像相干斑滤波算法研究_朱俊…

资深专家教你如何开展新版FMEA培训

新版FMEA的出现,不仅优化了原有的分析流程,更引入了一系列创新的理念和方法,为企业提供了更为全面、细致的风险评估与管理手段。因此,开展新版FMEA培训对于提升企业的质量管理水平、增强产品竞争力具有重要意义。 本文&#xff0…

【昇思25天学习打卡营打卡指南-第一天】基本介绍与快速入门

昇思MindSpore介绍 昇思MindSpore是一个全场景深度学习框架,旨在实现易开发、高效执行、全场景统一部署三大目标。 其中,易开发表现为API友好、调试难度低;高效执行包括计算效率、数据预处理效率和分布式训练效率;全场景则指框架…

【CT】LeetCode手撕—92. 反转链表 II

目录 题目1- 思路2- 实现⭐92. 反转链表 II——题解思路 3- ACM实现 题目 原题连接:92. 反转链表 II 1- 思路 模式识别:翻转 给定 left 和 right 固定区间的链表 ——> ①虚拟头结点 ②三个指针方式实现 2- 实现 ⭐92. 反转链表 II——题解思路 c…

面向对象进阶--抽象(Java 抽象)详解

1.1 抽象类引入 父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了(因为子类对象会调用自己重写的方法)。换句话说,父类可能知道…

流媒体学习之路(WebRTC)——音频NackTracker优化思路(8)

流媒体学习之路(WebRTC)——音频NackTracker优化思路(8) —— 我正在的github给大家开发一个用于做实验的项目 —— github.com/qw225967/Bifrost目标:可以让大家熟悉各类Qos能力、带宽估计能力,提供每个环节关键参数调节接口并实…

数据结构——排序(期末总结)

1. 插入排序 1.1 直接插入排序 思想 假设第一个元素是已经排好序的元素&#xff0c;从第二个元素开始依次插入操作&#xff0c;大的放后面&#xff0c;小的放前面。 代码 void insert(int a[], int n) {int i, j, key;for (i 2; i < n; i){key a[i];j i - 1;while (j…

kotlin集合框架

1、集合框架的接口类型对比 2、不可变和可变List fun main() {// 不可变List - 不能删除或添加元素val intList: List<Int> listOf(1,2,3)intList.forEach{println(it) // 1 2 3}println("")// 可变List - 可以删除或添加元素val mutableList mutableListO…

目标检测——YOLOv10算法解读

论文&#xff1a;YOLOv10: Real-Time End-to-End Object Detection (2024.5.23) 作者&#xff1a;Ao Wang, Hui Chen, Lihao Liu, Kai Chen, Zijia Lin, Jungong Han, Guiguang Ding 链接&#xff1a;https://arxiv.org/abs/2405.14458 代码&#xff1a;https://github.com/THU…

C++ | Leetcode C++题解之第165题比较版本号

题目&#xff1a; 题解&#xff1a; class Solution { public:int compareVersion(string version1, string version2) {int n version1.length(), m version2.length();int i 0, j 0;while (i < n || j < m) {long long x 0;for (; i < n && version1[…

【OpenHarmony开发】自定义系统应用之实践

前言 OpenHarmony系统应用是指预装在OpenHarmony操作系统中的应用程序&#xff0c;也称为系统应用。这些应用程序通常由操作系统开发者开发&#xff0c;包括系统设置、电话、短信、浏览器、相机、音乐、视频等常用应用程序。这些应用程序通常具有更高的权限和更深入的系统集成…

机器学习参数寻优:方法、实例与分析

机器学习参数寻优:方法、实例与分析 机器学习模型的性能很大程度上依赖于其参数的选择。参数寻优(Hyperparameter Tuning)是提升模型表现的关键步骤之一。本文将详细介绍主流的参数寻优方法,包括网格搜索(Grid Search)、随机搜索(Random Search)、贝叶斯优化(Bayesia…

反激开关电源EMI电路选型及计算

EMI &#xff1a;开关电源对电网或者其他电子产品的干扰 EMI &#xff1a;传导与辐射 共模电感的滤波电路&#xff0c;La和Lb就是共模电感线圈。这两个线圈绕在同一铁芯上&#xff0c;匝数和相位都相 同(绕制反向)。 这样&#xff0c;当电路中的正常电流&#xff08;差模&…

三十分钟学会RabbitMQ

1、初识MQ 1.1 MQ是什么&#xff1f; MQ(message queue)&#xff0c;从字面意思上看&#xff0c;本质是个队列&#xff0c;FIFO先入先出&#xff0c;只不过队列中存放的内容是message而已&#xff0c;还是一种跨进程的通信机制&#xff0c;用于上下游传递消息。在互联网架构中…

VMare连接Centos7无法连接网络

VMare连接Centos7无法连接网络 打开ifcfg-ens33文件检查ONBOOT是否为yes&#xff0c;如果是no需要修改成yes vi /ect/syscong ig/network-scripts/ifcfg-ens33 保存后输入ip a命令&#xff0c;能看到自己的ip就是成功了 ip就是成功了

MapReduce笔记

第1章 MapReduce概述 1.1 MapReduce定义 MapReduce是一个分布式运算程序的编程框架&#xff0c;是用户开发“基于Hadoop的数据分析应用”的核心框架。 MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序&#xff0c;并发运行在一个H…

回归算法详解

回归算法详解 回归分析是一类重要的机器学习方法&#xff0c;主要用于预测连续变量。本文将详细讲解几种常见的回归算法&#xff0c;包括线性回归、岭回归、Lasso 回归、弹性网络回归、决策树回归和支持向量回归&#xff08;SVR&#xff09;&#xff0c;并展示它们的特点、应用…