GTK+ 如何绘制窗口内容是一个相当复杂的话题;它涉及到从 GtkWidget
到 GdkWindow
,再到 Cairo,最后到当前使用的窗口系统的深入探讨。即使对于那些熟悉从应用程序开发角度使用 GTK+ API 的人来说,这项任务也可能显得有些令人生畏。因此,我决定快速介绍一下 GTK+ 的绘图方式,从部件到窗口,到表面,再到原生窗口资源。
它是如何开始的
GTK+ 始终在收到请求时进行绘制。这个请求可能来自窗口系统 - 例如,因为窗口管理器向用户展示了您的应用程序窗口,或者因为用户调整了它的大小 - 但更常见的情况是,它来自更新其内容的部件。例如,进度条从 50% 变为 60%;或标签,更改其文本;或微调器,进行新的迭代。此请求会使部件的后备 GdkWindow
失效 - 通常是包含该部件的顶层 GtkWindow
的 GdkWindow
。每次失效都会携带窗口的失效区域(“损坏”),以便我们在实际绘图时,知道窗口的哪些部分需要更新,并且我们可以避免在损坏区域之外进行绘制。
与时间赛跑
第一次失效将启动“帧时钟”;这个时钟是一个对象,用于跟踪帧内的每个阶段,例如绘制窗口、布局部件或处理事件队列。这使得 GTK+ 能够与窗口系统合成器等事物同步,并避免执行用户看不到的不必要的工作 - 例如,当您的显示器只能以 60 Hz 的频率运行时,以每秒 1000 帧的速度绘制某些内容。
一旦时钟到达“绘制”阶段,我们就会处理窗口上的所有计划更新;这将导致发出 GDK_EXPOSE
事件。GDK_EXPOSE
事件包含需要更新的 GdkWindow
以及所有失效区域的并集。重要的是要注意,通常情况下,只有顶层窗口才会接收到 GDK_EXPOSE
事件;然而,出于历史原因,某些部件可能会应用特定的事件掩码,这也会导致将 GDK_EXPOSE
事件传递给它们。您不应该编写依赖于此的代码,如果您有从旧版本的 GTK+ 2.x 移植的旧代码,则应认真考虑从事件掩码中删除 GDK_EXPOSURE_MASK
。
渲染
GTK+ 从 GDK_EXPOSE
事件中取出窗口和失效区域,并找出它们属于哪个顶层部件。一旦找到,GTK+ 将开始实际的渲染过程。首先,GTK+ 将要求 GdkWindow
创建一个缓冲区,用于绘制窗口的内容;缓冲区将被裁剪到需要绘制的区域,并将使用窗口的背景颜色清除。GDK 将创建一个“绘图上下文” - 一个临时对象,用于跟踪 OpenGL 和 Cairo 绘图等内容。然后,GTK+ 将要求部件使用 Cairo 上下文绘制自身。对于叶子部件,这意味着在该上下文中绘制自身;对于容器部件,这还意味着递归遍历其所有子部件。在此过程结束时,GTK+ 将通过告知 GDK 获取包含所有渲染部件的缓冲区,并使用它替换窗口的当前内容来结束帧。然后,GDK 将在更合适的时候要求窗口系统向用户呈现窗口。
改变历史
上面概述的过程有各种注意事项,并且在 GDK 内部处理窗口失效和验证的代码相当复杂;它也有很长的历史,这意味着它的 API 布满了过去时代的墓碑。
例如,在 GTK+ 3.0 之前,您应该自己处理“expose”事件,并使用 gdk_cairo_create()
在部件上创建 Cairo 上下文进行绘制;这早已是不必要的,因为 GtkWidget::draw
虚函数已经为我们提供了一个可用于绘制的 Cairo 上下文。但是,gdk_cairo_create()
函数已在 GTK+ 3.22 中被弃用,不应在新编写的代码中使用;如果您需要 Cairo 上下文,则应创建一个类似的 Cairo 表面,在其上调用 cairo_create()
,然后使用该表面作为 GTK+ 在绘制部件时提供给您的 Cairo 上下文的源。另一方面,如果您使用 gdk_cairo_create()
在响应 GDK_EXPOSE
事件时在顶层原生 GdkWindow
上进行绘制,则应改为使用新添加的 gdk_window_begin_draw_frame()
,gdk_window_end_draw_frame()
和 GdkDrawingContext
API。
塑造未来
GTK+ 中绘图代码的内部机制多年来一直在逐步更新,以应对 新的窗口系统 以及 其他渲染 API。可以肯定的是,它们将再次发生变化,尤其是在提高渲染性能方面。许多看似任意的更改实际上是朝着减少每帧在工具包内花费的时间,并为应用程序逻辑留下更多时间的垫脚石。