一、QWidget
QWidget 既是 QObject 的子类,也是 QPaintDevice 的子类!
而 QPainter 类实例,都要有一个目标 device!
TLW(Top Level Widget)顶层窗口,可以看作是 “没有 parent 的 widget” 或者 “widget.window() == self 的 widget”。当调用 widget.show() 的时候,tlw 作为一个独立窗口显示在屏幕上,non top level widget 则作为一个内部控件显示在某个 tlw 中。
1. QWidget.update(rect)
底层逻辑(源码分析)
QWidget.update(rect)
调用私有类的方法QWidgetPrivate.update(rect)
QWidgetPrivate.update(rect)
首先判断 widget 是否需要绘制。如果不需要,则直接返回。比如 widget 不可见、update 被禁用、参数 rect 与 widget.rect 无交集,这些都不需要绘制。如果已经正在绘制过程中,就向程序全局QCoreApplication
发布一个 UpdateLaterEvent 事件并返回。如果需要绘制,则在顶层窗口(TLW: Top Level Widget),将该 widget 需要绘制的 clipped 区域标记为脏。tlwExtra→repaintManager→markDirty(clipped, q)
QWidgetRepaintManager::markDirty()
中。
首先需要判断该 widget 是否在顶层窗口 tlw 中显示,如果 widget 与当前 tlw 没有联系,那么连标脏都不需要了,直接丢弃。
然后将 widget 以及需要重绘的区域加入 tlw 的 dirtyWidgets 列表( addDirtyWidget(widget, r) )。顶层窗口 tlw 发布重绘请求 sendUpdateRequest(tlw)。
- 在 sendUpdateRequest() 中,实际是向程序全局
QCoreApplication
发布一个QEvent::UpdateRequest
事件。 - 接着就进入了 Qt 的事件响应机制,由
QApplicationPrivate::notify_helper(QObject *receiver, QEvent * e)
负责将QEvent::UpdateRequest
事件交给顶层窗口 tlw (我猜的)的QWidget.event()
分发处理。 QWidget.event()
调用QWidgetPrivate::syncBackingStore()
处理QEvent::UpdateRequest
事件。QWidgetPrivate::syncBackingStore()
>>QWidgetRepaintManager::sync()
>>QWidgetRepaintManager::paintAndFlush()
负责最终的 paint(QWidgetPrivate::drawWidget()
) 与 flush(QWidgetRepaintManager::flush()
)。我理解的是,paint 是指在“Qt 画布”上绘制出图像,flush 是指将内存画布上的图像冲洗到“显卡画布”上(flush 就跟各操作系统的显示驱动有关了)。
- 在
QWidgetPrivate::drawWidget()
方法中,主要会执行三个操作:① 绘制背景paintBackground(painter, region, flags)
;② 调用paintEvent()
(通过发布QEvent::Paint
事件);③ 调用paintSiblingsRecursive()
递归绘制 child widgets。 - 最后,
QWidgetRepaintManager::flush()
调用 QtSrc/plugins/platforms/xxxos/xxxBackingStore::flush() 将内存画布冲印到显卡。
参考:https://www.cnblogs.com/appsucc/p/14528310.html
3、QWidget.paintEvent()
所有 QWidget 绘制的时候,都是调用 paintEvent() 方法进行绘制。不管是 QWidget.upadte()
还是 QWidget.repaint()
,都是要调用 paintEvent()
。
如果继承自某个内建 widget(比如 QLabel, QPushButton 这些),那么重写该方法将会覆盖父类的绘制行为。
此外!!!如果该 widget 还有 child widgets 的话,在执行完自己的 paintEvent() 之后,还会接着自动调用所有 child widgets 的 paintEvent()!
(其实并不是所有,只需要调用与该 event.rect() 有交集的 child widgets 的 paintEvent()
方法。)
4、QWidget.render(painter, targetOffset, sourceRegion, renderFlags)
先在临时画布上绘制 widget,截取出 sourceRegion 区域。然后在 target painter 上的 targetOffset 位置开始绘制。
但是实际上 QWidget.render()
一直有个官方 BUG 未解决!(https://bugreports.qt.io/browse/QTBUG-26694)
它实际绘制时候的 targetOffset,可能会变成是窗口(top-level widget)坐标系,而不是 painter.device() 指向的父 widget 坐标系。
5、先清除,再绘制!
QWidget.update(rect)
会将 rect 区域先清除,再绘制。(paintBackground()
>> paintEvent()
)
Qt normally erases the widget’s area before the paintEvent() call.
二、QPainter
1、QPainter 内部必须关联一个 QPaintDevice 对象(所有的 QWidget 都属于 QPaintDevice)。
2、要使用 QPainter 对象,必须先激活它。QPainter 必须以 begin(paintDevice)
方法激活,或者由带 paintDevice 参数的构造函数自动激活。然后由 end()
方法注销,或者是由析构函数自动注销。
3、我认为,可以将 QPainter 看作内存画布。Qt 对 widgets 的绘制,是先从顶层窗口(TLW,Top Level Widget)进行绘制,然后一级一级往下绘制 child widgets。并且绘制子 widget 的时候,可以看作是先在另一个临时画布上绘制,然后再裁剪出所需要的区域 clipped,贴到父 widget 的画布上。最后,所有 widgets 都绘制完后,才将顶层窗口的画布 flush 到显卡上。
三、QMovie – GIF
1、Qlabel.setMovie()
的时候会绑定 QMovie 的 update 信号到私有方法 Qlabel._q_movieUpdated。 然后在该方法中调用 QWidget.update(rect)
来进行重绘。
2、而我们在上文说过,QWidget.update(rect)
方法不会立即绘制,而是先在顶层窗口 tlw 将该区域标记为脏。然后由 tlw 响应全局 UpdateRequest 事件,再一层一层往下传递绘制。
假设此时的 QLabel 是属于某个 widget 的 child,然后这个 QLabel 最终的父 widget 是 None,无法追溯到 tlw,那么 tlw 就不会响应它的 update(rect) 事件。也就无法重绘 GIF 了!
当然,如果此时你直接 QLabel.show(),那么它自己就会作为一个 tlw 显示在屏幕上,这样就可以触发对 update 的响应了。