在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里,这个函数的源码不在此贴出,但是分析这个函数内部的主要逻辑。