在Qt5中,QWidget的绘制流程比较分散,网上介绍的文章也很少,因此写一篇文章总结记录一下这部分的知识点。
笔者使用的是Qt5.15.2的源码。
基本的绘制流程:从update到合成
-
更新请求(Invalidate):
当一个QWidget需要被重绘时(比如大小改变、数据更新等),会调用update()
方法来标记该widget为需要重绘。update一般会到repaintManager->markDirty,如果当前正在绘制,则通过事件QUpdateLaterEvent进行重绘。这部分逻辑代码如下:
-
重绘区域计算(Dirty Region Calculation):
- Qt有一个优化机制,它会合并多个重绘请求以减少重绘的次数和区域。重绘区域的计算由Qt的QWidgetRepaintManager负责,该系统维护了一个脏区域(dirty regions),这是所有需要重绘的区域的集合。主要逻辑在QWidgetRepaintManager::markDirty。这部分逻辑稍微复杂,但不是重点,感兴趣的读者可以自行翻阅源码,此处不再列出。
-
事件处理(Event Processing):
- 在QWidgetRepaintManager::sendUpdateRequest,会生成一个
QEvent::UpdateRequest
的事件,即使指定了UpdateNow,也会根据这次更新是否距离上次更新大于60fps而降低这次绘制的优先级。 这部分逻辑代码如下:
- 在QWidgetRepaintManager::sendUpdateRequest,会生成一个
-
事件循环(Event Loop):
Qt的事件循环在QCoreApplication::exec()
调用后运行,负责处理事件队列中的事件。对于绘制事件,事件循环会传递给QWidget的event()
方法。这部分不是本文章重点,不列出详细细节。 -
事件处理(Event Handling):
QWidget的event()
方法会检查事件的类型。如果是绘制事件QEvent::UpdateRequest或者QEvent::UpdateLater,会转调到QWidgetPrivate::paintOnScreen函数,接着使用QWidgetRepaintManager类提供的功能,转调到每个Widget::paintEvent函数。 这部分逻辑代码如下: -
绘制逻辑paintAndFlush:这部分是本文的重点,也比较复杂,在后文详细展开。
-
绘制(Painting):
在paintEvent()
方法中,一般使用QPainter
对象,它是Qt中负责绘制的类。QPainter
可以绘制各种图形元素,如文本、线条、形状等。 -
绘图设备(Paint Device):
QPainter
对象会被绑定到一个绘图设备(QPaintDevice
),比如QWidget本身,或者一个QPixmap
、QImage
、QPicture
等。QWidget通过其paintEngine()
方法提供了一个QPaintEngine
对象,这是实际进行绘制操作的底层接口。 -
绘图引擎(Paint Engine):
QPaintEngine
是一个抽象基类,它定义了绘图操作的接口。Qt提供了多种绘图引擎,比如QRasterPaintEngine
、QOpenGLPaintEngine
等,具体使用哪个引擎取决于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
这个函数的逻辑,具体可以分解为以下步骤:
-
检查更新是否被禁用:
如果 QWidget 的updatesEnabled
属性为false
,则不进行任何绘制操作。 -
检查并更新脏区域:
如果窗口的大小已更改,并且更新没有被禁用,函数会检查是否有静态内容(不需要重绘的部分)。如果有,它会只将新可见的部分添加到脏区域;否则,它会标记整个窗口为需要重绘。 -
调整后台存储的大小:
如果后台存储(store
)的大小与窗口大小不一致,它会被调整以匹配窗口的大小。 -
绘制和清理脏区域:
函数创建一个包含所有需要重绘的区域的QRegion
对象。然后它遍历所有标记为脏的控件,并根据是否有透明的重叠兄弟控件,将其分为可直接绘制和需要合成的控件。 -
处理特殊的绘制情况:
对于具有 render-to-texture 特性的控件(如 OpenGL 小部件),它们会被特别处理,因为它们的绘制可以直接在纹理上完成,不需要经过常规的后台存储绘制过程。 -
发送绘制事件:
遍历所有需要绘制的控件,并为它们发送QPaintEvent
事件。这些事件触发控件的paintEvent
方法,从而完成实际的绘制工作。 -
绘制不透明的非重叠控件:
直接在后台存储上绘制那些不透明且没有被兄弟控件重叠的控件。 -
合成:
如果需要,将所有剩余的控件绘制到后台存储上,并处理任何必要的合成操作,以确保正确的层叠和透明度效果。 -
结束绘制:
调用store->endPaint()
表示绘制操作的结束。 -
刷新:
将后台存储的内容刷新到屏幕上。如果启用了双缓冲,这将涉及到将后台缓冲区的内容复制到前台缓冲区,并在适当的时间将其展示到屏幕上。
这个函数体现了 Qt 绘制的一些核心概念,包括脏区域管理、后台存储、控件的绘制事件、以及绘图设备和绘图引擎的使用。所有的绘制操作都是在主线程中进行的,即使是那些涉及 OpenGL 或其他渲染技术的绘制也不例外。
QWidgetPrivate::drawWidget
QWidgetRepaintManager::paintAndFlush在顶层处理主要的绘制流程,除了这个函数,QWidgetPrivate::drawWidget 函数也包含大量绘制流程的实现细节,这个函数作为第二层处理绘制细节。同样地,这个函数的源码不在此贴出,但是总结这个函数内部的主要流程:
QWidgetPrivate::drawWidget
函数是一个内部函数,用于在给定的绘制设备(pdev
)上绘制一个控件及其子控件。这个函数处理了许多绘制相关的细节,包括处理图形效果、设置裁剪区域、绘制背景以及发送绘制事件。以下是函数的主要逻辑步骤:
-
检查是否有内容需要绘制:
如果传入的区域(rgn
)为空,则没有内容需要绘制,函数立即返回。 -
记录绘制操作的日志信息:
使用qCInfo
记录绘制区域、控件、偏移量、目标绘制设备以及标志。 -
处理图形效果:
如果控件有启用的图形效果,那么绘制流程会交给图形效果处理器。它可能会修改绘制的方式,例如添加阴影或模糊效果。 -
计算需要绘制的区域:
根据控件的属性和标志计算出实际需要绘制的区域(toBePainted
)。可能会考虑是否绘制根控件、是否绘制到屏幕上、是否递归绘制子控件以及是否绘制不可见控件。 -
预处理绘制设备:
设置或重定向绘制目标,并设置系统裁剪区域。 -
绘制背景:
如果需要,绘制控件的背景。这可能涉及到自动填充背景、绘制不透明的绘制事件或处理窗口系统背景。 -
处理渲染到纹理的控件:
如果控件渲染到纹理(例如使用 OpenGL),则相应地处理,可能是通过绘制一个透明矩形来为纹理"打孔",或者将纹理复制到屏幕上。 -
发送绘制事件:
如果没有跳过绘制事件,发送一个QPaintEvent
给控件,这将触发控件的paintEvent
方法。 -
标记需要刷新:
如果有repaintManager
,则调用markNeedsFlush
来标记区域为需要刷新。 -
恢复状态:
恢复重定向的绘制设备和系统裁剪区域到原始状态,并清除激活状态的绘制标志。 -
递归绘制子控件:
如果设置了递归标志并且控件有子控件,递归地绘制这些子控件。
整个函数的逻辑很大程度上是关于准备好绘制上下文,然后根据需要绘制控件本身或者委托给图形效果和子控件的绘制。这个函数是 Qt 控件绘制流程中的核心部分,它确保了控件及其子控件能够正确地在屏幕上渲染。
总体而言,Qt体系的绘制实现基本可以在这两个函数中体现出来。相关的数据结构和逻辑也逃不出QWidgetRepaintManager与QWidgetPrivate,感兴趣的读者可以深入了解这两个类。
如何判断一个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
函数的工作流程如下:
-
设置脏不透明区域:
调用setDirtyOpaqueRegion
方法,这通常意味着标记控件的不透明区域需要更新。这个区域是指控件中不需要考虑透明度处理的部分。 -
检查是否有图形效果:
如果控件应用了QGraphicsEffect
,函数立即将控件标记为非不透明(因为图形效果可能会引入透明度),然后返回。图形效果可能包括模糊、阴影等,这些都可能改变控件的不透明度。 -
检查控件属性:
函数检查控件是否具有Qt::WA_OpaquePaintEvent
或Qt::WA_PaintOnScreen
属性。这些属性通常由开发者设置,用来指示控件的绘制事件是不透明的,或者控件直接在屏幕上绘制。如果有任何一个属性被设置,函数将控件标记为不透明并返回。 -
检查自动填充背景:
如果控件的autoFillBackground
属性为真,表示控件在绘制前会自动用背景色填充。函数会检查用于自动填充的画刷是否不透明。如果是,控件被标记为不透明。 -
检查窗口属性:
如果控件是一个窗口,并且没有设置Qt::WA_NoSystemBackground
属性(这意味着窗口系统不会自动填充背景),函数会检查窗口背景画刷是否不透明。如果是,窗口被标记为不透明。 -
设置为非不透明:
如果之前的检查都没有导致函数返回,最后将控件标记为非不透明。
在大型复杂界面中和性能敏感的应用中,我们要避免过多的不透明控件可以减少绘制负担。
绘制逻辑的复用:标准控件绘制与QStyle的细节
在Qt中,QStyle
类负责控件的外观和行为。这包括控件的绘制(如按钮、滑块、复选框等),以及控件的尺寸、布局和交互行为(如鼠标悬停、按下状态的视觉反馈)。QStyle
提供了一种机制,通过它可以统一控制应用程序中所有控件的外观,而无需在每个控件的绘制逻辑中单独实现这些。
QStyle
是一个抽象基类,它定义了一套API,用于绘制标准的GUI组件以及获取与风格相关的属性和尺寸信息。Qt自带了几种风格,如QWindowsStyle
、QMacStyle
、QFusionStyle
等,它们实现了在不同平台下的本地外观和行为。可以通过继承QStyle
来创建自定义风格。
绘制标准控件
当一个标准控件(例如QPushButton
)需要被绘制时,它会调用其paintEvent()
函数。在paintEvent()
中,控件通常不直接进行绘制,而是将绘制任务委托给当前的QStyle
对象。这是通过调用style()
方法来获取当前应用程序风格,然后使用QStyle
的绘制函数来完成的。
例如,一个按钮会这样使用QStyle
来进行绘制:
这里,QStylePainter
是QPainter
的一个特殊版本,专门用于风格绘制。QStyleOptionButton
是一个包含按钮状态和属性的结构体。drawControl()
函数是QStyle
的一个方法,用于绘制控件元素(Control Element),在这个例子中是一个按钮。
风格元素和选项
QStyle
类定义了多个枚举,用于指定控件的哪一部分需要绘制,以及如何绘制。这些枚举包括ControlElement
、PrimitiveElement
、ComplexControl
等。
- ControlElement: 这些是高级UI元素,如整个按钮、工具栏、滚动条等。
- PrimitiveElement: 这些是构成控件的基本图形元素,如按钮的边框、复选框的勾选标记等。
- ComplexControl: 这些是由多个交互部分组成的控件,如组合框或滑块。
QStyleOption
类及其派生类携带了关于如何绘制控件的信息。QStyleOption
包含了状态信息(如是否被按下、是否有焦点等),而派生类则包含了更具体的信息。例如,QStyleOptionButton
包含了按钮特有的信息,如是否是默认按钮、是否是复选按钮等。
自定义风格
要创建自定义风格,你可以继承QStyle
或者任何已有的风格类,并重写相应的绘制和尺寸计算方法。例如,你可能会重写drawControl()
、drawPrimitive()
、sizeFromContents()
等方法来自定义控件的绘制和布局。
应用风格
可以通过调用QApplication::setStyle()
方法来为整个应用程序设置风格。这个风格会被所有控件使用,除非某个控件显式地设置了不同的风格。QStyle
负责定义和实现Qt控件的外观和行为,而具体的控件类则通过委托给QStyle
来执行实际的绘制操作。这种设计使得Qt的外观和感觉可以非常灵活地被定制和更换,而不需要修改每个控件的实现代码。
QWidget绘制体系为什么这么设计【重点】
请跳转第二篇《Qt底层原理:深入解析QWidget的绘制技术细节(2)》