Qt:QWidget 的绘制逻辑(源码分析)

一、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) 底层逻辑(源码分析)

  1. QWidget.update(rect) 调用私有类的方法 QWidgetPrivate.update(rect)
  2. QWidgetPrivate.update(rect) 首先判断 widget 是否需要绘制。如果不需要,则直接返回。比如 widget 不可见、update 被禁用、参数 rect 与 widget.rect 无交集,这些都不需要绘制。如果已经正在绘制过程中,就向程序全局 QCoreApplication 发布一个 UpdateLaterEvent 事件并返回。如果需要绘制,则在顶层窗口(TLW: Top Level Widget),将该 widget 需要绘制的 clipped 区域标记为脏。tlwExtra→repaintManager→markDirty(clipped, q)
  3. QWidgetRepaintManager::markDirty() 中。
    首先需要判断该 widget 是否在顶层窗口 tlw 中显示,如果 widget 与当前 tlw 没有联系,那么连标脏都不需要了,直接丢弃。
    然后将 widget 以及需要重绘的区域加入 tlw 的 dirtyWidgets 列表( addDirtyWidget(widget, r) )。顶层窗口 tlw 发布重绘请求 sendUpdateRequest(tlw)。
  4. 在 sendUpdateRequest() 中,实际是向程序全局 QCoreApplication 发布一个 QEvent::UpdateRequest 事件。
  5. 接着就进入了 Qt 的事件响应机制,由 QApplicationPrivate::notify_helper(QObject *receiver, QEvent * e) 负责将 QEvent::UpdateRequest 事件交给顶层窗口 tlw (我猜的)的 QWidget.event() 分发处理。
  6. QWidget.event() 调用 QWidgetPrivate::syncBackingStore() 处理 QEvent::UpdateRequest 事件。
  7. QWidgetPrivate::syncBackingStore() >> QWidgetRepaintManager::sync() >> QWidgetRepaintManager::paintAndFlush() 负责最终的 paint(QWidgetPrivate::drawWidget()) 与 flush(QWidgetRepaintManager::flush())。我理解的是,paint 是指在“Qt 画布”上绘制出图像,flush 是指将内存画布上的图像冲洗到“显卡画布”上(flush 就跟各操作系统的显示驱动有关了)。
  8. QWidgetPrivate::drawWidget() 方法中,主要会执行三个操作:① 绘制背景 paintBackground(painter, region, flags);② 调用 paintEvent() (通过发布 QEvent::Paint 事件);③ 调用 paintSiblingsRecursive() 递归绘制 child widgets。
  9. 最后, 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 的响应了。

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top