GTK+ 中的绘图

GTK+ 如何绘制窗口内容是一个相当复杂的话题;它涉及到从 GtkWidgetGdkWindow,再到 Cairo,最后到当前使用的窗口系统的深入研究。即使对于那些从应用程序开发角度熟悉 GTK+ API 的人来说,这项任务也可能显得有些令人生畏,因此我决定快速介绍一下 GTK+ 如何进行绘制,从窗口小部件到窗口,再到表面,最后到原生窗口资源。

它是如何开始的

GTK+ 总是因为有请求才会进行绘制。此请求可能来自窗口系统——例如,因为窗口管理器向用户展示了您的应用程序窗口,或者因为用户调整了其大小——但更常见的情况是,它来自更新其内容的窗口小部件。例如,进度条从 50% 变为 60%;或者标签更改其文本;或者微调器进行新的迭代。此请求会使窗口小部件的后备 GdkWindow 失效——通常是包含该窗口小部件的顶层 GtkWindowGdkWindow。每次失效都会带有需要失效的窗口区域(“损坏”),这样当我们实际开始绘制时,我们就知道窗口的哪些部分需要更新,并且我们可以避免在损坏区域之外进行绘制。

与时间赛跑

第一次失效将启动“帧时钟”;此时钟是一个对象,用于跟踪帧内的每个阶段,例如绘制窗口、布局窗口小部件或处理事件队列。这允许 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 之类的事情。可以肯定的是,它们会再次更改,尤其是在提高渲染性能方面。许多看起来可能很随意的更改实际上是减少每个帧在工具包中花费的时间,并为应用程序逻辑留出更多时间的垫脚石。

关于“GTK+ 中的绘图”的一个想法

  1. 感谢您写下这篇文章。很高兴看到如此复杂系统的高级概述。一般来说,我从来没有想过要了解 GTK+ 是如何绘制的。

评论已关闭。