绘图模型概述 [src]
本章详细介绍了 GTK 绘图模型。如果您有兴趣了解 GTK 绘制其小组件和窗口所遵循的过程,您应该阅读本章。如果您决定实现自己的小组件,了解此内容将很有用。本章还将阐明在 GTK 中某些操作的实现方式背后的原因。
窗口和事件
使用窗口系统的应用程序通常在屏幕中创建称为表面的矩形区域(GTK 遵循 Wayland 术语,其他窗口系统(如 X11)可能称其为窗口)。传统的窗口系统不会自动保存表面的图形内容,而是要求应用程序在需要时提供新内容。例如,如果堆叠在其他窗口下方的窗口升至顶部,则应用程序必须为其重新绘制,以便显示之前被遮挡的区域。当窗口系统要求某个应用程序重新绘制窗口时,它会为该窗口发送一个帧事件(在 X11 术语中为曝光事件)。
每个 GTK 顶级窗口或对话框都与一个窗口系统表面相关联。按钮或条目等子小组件没有自己的表面,它们使用其顶级窗口的表面。
通常情况下,当 GTK 收到来自底层窗口系统的帧事件时,绘图周期便会开始:如果用户将一个窗口拖动到另一个窗口上方,窗口系统将告诉底层表面它需要重新绘制自身。当小组件本身决定需要更新其显示时,也可以启动绘图周期。例如,当用户在条目小组件中输入一个字符时,该条目会要求 GTK 为自身排队一项重新绘制操作。
窗口系统为表面生成帧事件。GDK 与窗口系统的接口将此类事件转换为对受影响表面的 ::render 信号的发射。 GTK 顶级窗口连接到该信号并做出相应的反应。
以下几部分介绍了 GTK 如何在响应此类事件时确定需要重新绘制哪些小组件,以及小组件如何从窗口系统内部利用它们使用的资源。
帧时钟
所有 GTK 应用程序都是由主循环驱动的,这意味着应用程序大部分时间都处于循环内处于空闲状态,只是等待某些事情发生,并在发生时调用正确的位置。在此之上,GTK 有一个帧时钟,可向应用程序发出“脉冲”。此时钟以稳定的速率跳动,与输出的帧速率相关(通过窗口管理器/合成器将其与显示器同步)。典型的刷新率为每秒 60 帧,因此每隔大约 16 毫秒就会发生一个新的“脉冲”。
此时钟有几个阶段
- 事件
- 更新
- 布局
- 绘制
这些阶段按此顺序发生,并且在返回开头之前所有阶段始终都会执行。
事件阶段是在每次重绘之间的一段时间延伸,在此期间,GTK 会处理来自用户和其他事件(例如网络 I/O)的输入事件。一些事件(如鼠标移动)会进行压缩,这样每个时钟周期只需要处理一次鼠标移动事件即可。
事件阶段结束后,外部事件将暂停并运行重绘循环。首先是更新阶段,在此阶段,所有动画都会运行以根据下一帧将可见的时间估算值(可通过帧时钟获得)计算新状态。这通常涉及到几何形状变化,从而驱动下一个布局阶段。如果窗口小部件尺寸要求发生任何变化,则将为窗口小部件层次结构计算新的布局(即,确定所有窗口小部件的大小和位置)。然后进入绘制阶段,在此阶段,我们将重绘需要重绘的窗口区域。
如果任何内容都不需要更新/布局/绘制阶段,我们将一直停留在事件阶段,因为如果没有任何变化,我们就不想重绘。每个阶段都可以在后续阶段请求进一步处理(例如,更新阶段将导致布局工作,而布局更改将导致重新绘制)。
有多种方法可以驱动时钟,在最低级别,您可以使用 gdk_frame_clock_request_phase()
请求特定的阶段,这将根据需要安排时钟节拍,以便最终到达请求的阶段。然而,在实践中,大多数事情发生在更高的层次。
- 如果您要进行动画,可以使用
gtk_widget_add_tick_callback()
,这将导致在更新阶段定期跳动时钟,并回调,直到您停止滴答声。 - 如果某些状态更改导致窗口小部件的大小发生变化,则调用
gtk_widget_queue_resize()
,这会请求布局阶段并将您的窗口小部件标记为需要重新布局。 - 如果某些状态发生更改,因此您需要重绘窗口小部件,请使用
gtk_widget_queue_draw()
为您的窗口小部件请求绘制阶段。
还有很多隐式的触发器来自 CSS 层(根据需要进行动画、调整大小和重新绘制)。
场景图
“绘制”窗口的第一步是 GTK 为窗口中的所有窗口小部件创建呈现节点。这些呈现节点被组合成一棵树,您可以将其视为描述您的窗口内容的场景图。
呈现节点属于 GSK 层,有各种类型的呈现节点,用于您在转换窗口小部件内容和 CSS 样式时可能需要的各种绘制基元。典型的示例是文本节点、渐变节点、纹理节点或剪辑节点。
过去,GTK 中的所有绘制都是通过 Cairo 完成的。通过使用 Cairo 呈现节点,仍然可以使用 Cairo 绘制您自定义的窗口小部件内容。
GSK 呈现器获取这些呈现节点,将它们转换为目标绘制 API 的渲染命令,并安排将生成的图形与正确的曲面关联。GSK 具有适用于 OpenGL、Vulkan 和 Cairo 的呈现器。
分层绘制
在绘制阶段,GTK 将在顶层的图面上收到一个 ::render 单独信号。信号处理程序将创建快照对象(这是创建场景图表的助手),并调用 GtkWidget snapshot()
虚拟函数,该函数将沿控件层级向下传播。这可以让每个控件在正确的时间和位置快照其内容,正确地处理部分透明度和重叠控件之类的操作。
在每个控件的快照处理过程中,GTK 将根据 CSS 框模型自动处理 CSS 渲染。它将首先快照背景,然后是边框,再然后是控件内容本身,最后是轮廓。
为了在生成场景图表时避免过度工作,GTK 会缓存渲染节点。每个控件都会保留对其渲染节点的引用(该节点又会引用子级、孙级等渲染节点),并在绘制阶段重用该节点。通过调用 gtk_widget_queue_draw() 使控件失效会丢弃缓存的渲染节点,强制控件在下一次需要生成快照时重新生成该节点。