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

news2024/10/6 18:33:23

在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)》

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

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

相关文章

线程间通信方式(互斥(互斥锁)与同步(无名信号量、条件变量))

1通信机制:互斥与同步 线程的互斥通过线程的互斥锁完成; 线程的同步通过无名信号量或者条件变量完成。 2 互斥 2.1 何为互斥? 互斥是在多个线程在访问同一个全局变量的时候,先让这个线程争抢锁的资源,那个线程争抢…

理解广角镜头的视野和畸变

为什么广角镜头的视野会比长焦镜头的视野大呢? 我之前用等光程解释了景深,也解释了为什么焦距越远,成像越大,但是从来没有提到过视野范围这个概念。实际上在我之前建立的数学模型中,物曲面S是无限大的,像曲…

python:画圆圈,绘制生命之花图案

参阅:生命之花/平衡轮:一个实用的人生规划的工具 先画一个圆圈,编写 draw_circle1.py 如下 # -*- coding: utf-8 -*- """ 画一个圆圈 """ import numpy as np from matplotlib import pyplot as plt# 用于…

(done) 关于 GNU/Linux API setenv 的实验

写一个下面的代码来验证 #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <sys/types.h>int main() {// 设置环境变量 MY_VAR 的值为 "hello_world"if (setenv("MY_VAR", "hello_world", 1) ! 0…

动手学深度学习(Pytorch版)代码实践 -卷积神经网络-29残差网络ResNet

29残差网络ResNet import torch from torch import nn from torch.nn import functional as F import liliPytorch as lp import matplotlib.pyplot as plt# 定义一个继承自nn.Module的残差块类 class Residual(nn.Module):def __init__(self, input_channels, num_chan…

adb 查看哪些应用是双开的

adb shell pm list users 得到 这 里有 user 0 ,11,999 其中0是系统默认的&#xff0c;11是平行空间的&#xff0c;999是双开用户 pm list packages --user 999 -3 得到了999用户安装第三方应用的包名 pm list packages --user 11 -3 得到了隐私空间用户安装第三方应用的…

Git客户端安装步骤详解

git windows7 百度经验:jingyan.baidu.com 方法/步骤 1 从git官网下一个git安装包。 步骤阅读 2 点击git.exe安装程序&#xff0c;点击【next】 ![git的安装和配置](https://imgsa.baidu.com/exp/w500/sign7565f44ba58b87d65042ab1f37092860/21a4462309f790525e5b0144…

兰州理工大学24计算机考研情况,好多专业都接受调剂,只有计算机专硕不接收调剂,复试线为283分!

兰州理工大学&#xff08;Lanzhou University of Technology&#xff09;&#xff0c;位于甘肃省兰州市&#xff0c;是甘肃省人民政府、教育部、国家国防科技工业局共建高校&#xff0c;甘肃省高水平大学和“一流学科”建设高校&#xff1b;入选国家“中西部高校基础能力建设工…

Android-系统开发_四大组件篇----探讨-Activity-的生命周期

当一个活动不再处于栈顶位置&#xff0c;但仍然可见时&#xff0c;这时活动就进入了暂停状态。你可能会觉得既然活动已经不在栈顶了&#xff0c;还怎么会可见呢&#xff1f; 这是因为并不是每一个活动都会占满整个屏幕&#xff0c;比如对话框形式的活动只会占用屏幕中间的部分…

Retrieval-Augmented Generation for Large Language Models A Survey

Retrieval-Augmented Generation for Large Language Models: A Survey 文献综述 文章目录 Retrieval-Augmented Generation for Large Language Models: A Survey 文献综述 Abstract背景介绍 RAG概述原始RAG先进RAG预检索过程后检索过程 模块化RAGModules部分Patterns部分 RAG…

15天搭建ETF量化交易系统Day9—玩大A必学网格策略

搭建过程 每个交易者都应该形成一套自己的交易系统。 很多交易者也清楚知道&#xff0c;搭建自己交易系统的重要性。现实中&#xff0c;从&#xff10;到&#xff11;往往是最难跨越的一步。 授人鱼不如授人以渔&#xff0c;为了帮助大家跨出搭建量化系统的第一步&#xff0c;我…

vscode用vue框架2,续写登陆页面逻辑,以及首页框架的搭建

目录 前言&#xff1a; 一、实现登录页信息验证逻辑 1.实现登录数据双向绑定 2.验证用户输入数据是否和默认数据相同 补充知识1&#xff1a; 知识点补充2&#xff1a; 二、首页和登录页之间的逻辑(1) 1. 修改路由&#xff0c;使得程序被访问先访问首页 知识点补充3&am…

[每周一更]-(第102期):认识相机格式Exif

文章目录 EXIF数据包含的信息读取EXIF数据的工具和库EXIF数据读取示例&#xff08;Go语言&#xff09;想法参考 相机拍摄的照片&#xff0c;在照片展示行无水印信息&#xff0c;但是照片属性中会包含比较丰富的信息&#xff0c;相机品牌、型号、镜头信息等&#xff0c;这些我们…

基于NURBS曲线的数据拟合算法matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 4.1NURBS曲线基础 4.2 数据拟合原理 5.完整程序 1.程序功能描述 基于NURBS曲线的数据拟合算法,非均匀有理B样条&#xff08;Non-Uniform Rational B-Splines&#xff0c;简称NURBS&#xf…

电子商务的未来:大数据||论主流电商大数据采集API接口的重要性

每天全球生成2.5万亿字节。金融交易&#xff0c;用户出版物和众多内容&#xff0c;数据对于组织了解消费者的行为和实际需求极为重要。 在一个互联、动态和竞争激烈的多元世界中&#xff0c;能够预测技术和市场变化的组织在竞争中生存下来。这是大数据管理的真正目标&#xff0…

鸿蒙生态伙伴SDK市场正式发布,驱动千行百业鸿蒙原生应用开发

6月21-23日&#xff0c;华为开发者大会&#xff08;HDC 2024&#xff09;在东莞举办。在22日举办的【鸿蒙生态伙伴SDK】论坛中&#xff0c;正式发布了【鸿蒙生态伙伴SDK市场】&#xff08;以下简称&#xff1a;伙伴SDK市场&#xff09;&#xff0c;伙伴SDK市场是为开发者提供各…

PHP转Go系列 | ThinkPHP与Gin的使用姿势

大家好&#xff0c;我是码农先森。 安装 使用 composer 进行项目的创建。 composer create-project topthink/think thinkphp_demo使用 go mod 初始化项目。 go mod init gin_demo目录 thinkphp_demo 项目目录结构。 thinkphp_demo ├── LICENSE.txt ├── README.md …

JVM专题三:Java代码如何运行

通过前面的第一篇文章&#xff0c;对JVM整体脉络有了一个大概了解。第二篇文章我们通过对高级语言低级语言不同特性的探讨引出了Java的编译过程。有了前面的铺垫&#xff0c;咱们今天正式进入Java到底是如何运行起来的探讨。 目前大部分公司都是使用maven作为包管理工具&#x…

Flink-03 Flink Java 3分钟上手 Stream 给 Flink-02 DataStreamSource Socket写一个测试的工具!

代码仓库 会同步代码到 GitHub https://github.com/turbo-duck/flink-demo 当前章节 继续上一节的内容&#xff1a;https://blog.csdn.net/w776341482/article/details/139875037 上一节中&#xff0c;我们需要使用 nc 或者 telnet 等工具来模拟 Socket 流。这节我们写一个 …

基于YOLOv5的火灾检测系统的设计与实现(PyQT页面+YOLOv5模型+数据集)

基于YOLOv5的火灾检测系统的设计与实现 概述系统架构主要组件代码结构功能描述YOLOv5检测器视频处理器主窗口详细代码说明YOLOv5检测器类视频处理类主窗口类使用说明环境配置运行程序操作步骤检测示例图像检测视频检测实时检测数据集介绍数据集获取数据集规模YOLOv5模型介绍YOL…