相对窗口定位的未来

随着新兴的 显示 服务器 技术的发展,工具包有时需要调整它们实现其功能的方式。其中一组需要调整的功能是如何让 GTK+ 定位弹出窗口,例如菜单、弹出框和工具提示,以使其放置在显示器的工作区域内。

在过去,当 GTK+ 想要定位菜单时,它会首先查找菜单父窗口的全局位置。然后它会查找所有已连接显示器的工作区域。有了给定的工作区域、父窗口的全局位置以及相对于父窗口的预期菜单位置,GTK+ 将使用一个巧妙的算法来计算菜单的合理位置,使其对用户可见。例如,如果“文件”菜单没有足够的空间在父菜单项下方弹出,则 GTK+ 会将其重新定位到父菜单项上方。

popup-flip

由于 各种 原因,“全局窗口位置”的概念已从这些新的显示服务器技术中的客户端中移除,这意味着我们无法再在 GTK+ 中使用我们巧妙的算法。

但是我们仍然希望我们的菜单、工具提示、弹出框等对期望与之交互的用户完全可见,那么我们如何在不知道窗口位置的任何信息的情况下确保这一点呢?

为了在 GTK+ 中解决这个问题,我们必须解决一些问题。

  • 定位逻辑需要移到 GDK 中,同时仍然允许 GTK+ 在初始预期位置最终超出工作区域时影响菜单定位的行为。
  • 不同的 GDK 后端可能会 以不同的方式 执行操作
  • 某些类型的窗口需要知道它最终所处的位置,以便它们可以调整自己的绘制方式。
  • 有些窗口只是想尽可能地占用空间(例如,一个选项过多的菜单不应该比屏幕高)。

去年,William Hua 和我开始 致力于 将 GTK+ 带入无全局位置菜单窗口的美好未来。在提出了一系列实现此目标的补丁之后,关于这样一个 API 实际应该是什么样子开始了讨论。在 200-300 条评论之后,我们决定应该面对面讨论这个问题。

于是有了在多伦多的 GTK2016

在黑客马拉松中,我们有机会坐在白板前,讨论不同的用例、需要解决的问题、后端如何工作,最终我们提出了一个 API。

william-draws-whiteboard(图片来源:Allison Lortie)

我们提出的 API 如下

从 GDK 方面,我们引入了一个新的函数(目前没有任何 API 稳定性承诺;它目前仅供 GTK+ 使用)gdk_window_move_to_rect(),它接受一组描述应用程序希望如何相对于某些父表面放置其窗口的参数。它接受

  • 一个 transient-for 窗口

要放置的父窗口,相对于父窗口上的一个锚点矩形,弹出窗口或菜单通常希望相对于父窗口上的一个矩形放置,例如,右键单击上下文菜单应该从单击时指针所在像素的某个方向展开,或者文件菜单应该放置在父窗口上的文件菜单项矩形的下方或上方。

  • 一个矩形锚点重力

不同的弹出菜单可能希望在特定方向打开。例如,垂直菜单可能希望向右打开,而水平菜单可能希望向下打开。

  • 一个窗口锚点重力

不同的弹出菜单可能希望以不同的方式与父锚点矩形的锚点矩形对齐。例如,虽然组合框可能希望在某个方向展开,但它希望覆盖它展开的矩形。

  • 一个锚点提示

不同的弹出菜单希望以不同的方式调整其位置;有些希望从父锚点矩形在不同的方向展开,有些希望只是滑动到可见位置,有些希望调整大小,而有些则希望将这三者结合使用。

  • 一个矩形锚点偏移量

偏移量只是一个用于常见用例的微调因子,其中弹出菜单会相对于锚点偏移其位置。

通过让 GTK+ 提供一个关于其菜单希望如何定位的声明性描述,我们允许 GDK 根据显示服务器系统的设计以不同的方式实现实际定位。在 Mir 上,会创建一个 MirSurfaceSpec,而在 Wayland 上会创建一个 xdg_positioner 对象。在 X11、Windows 和 Mac OS X 上,后端可以使用可用的全局位置以及显示器工作区域,并像以前一样计算最佳位置。

但是,应用程序开发人员不应直接使用此 API。通常,需要的是创建一个菜单、一个弹出框、一个组合框,为此,我们引入了一组参数和辅助函数,使这非常方便。该 API 包含一些新的属性

  • GtkMenu:anchor-hints – 定位策略。
  • GtkMenu:rect-anchor-dx – 水平偏移量,用于移动窗口。
  • GtkMenu:rect-anchor-dy – 垂直偏移量,用于移动窗口。
  • GtkMenu:menu-type-hint – 窗口类型 – 这仍然是必需的,以便 X11 后端可以通知窗口管理器正在映射的弹出窗口的类型。

还有一些函数

  • gtk_menu_popup_at_rect() – 给定设置的参数,相对于父窗口上的给定矩形弹出菜单。
  • gtk_menu_popup_at_widget() – 给定设置的参数,相对于父窗口上的给定小部件弹出菜单。
  • gtk_menu_popup_at_pointer() – 给定设置的参数,相对于用户刚刚点击的位置弹出菜单。

通过这些函数,自定义小部件的开发人员现在可以以可移植的方式定位弹出菜单。到目前为止,GTK+ 自己的弹出菜单已经移植为使用这些新函数。Mir 后端已经有一个基本的概念验证,并且正在进行 Wayland 的实现。

前往 bug 查看未来如何放置菜单的所有详细信息。

gnome-sponsored-badge-shadow

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 surface,在其上调用 cairo_create(),然后将该 surface 用作 GTK+ 在绘制窗口部件时为您提供的 Cairo 上下文的源。另一方面,如果您使用 gdk_cairo_create() 来响应 GDK_EXPOSE 事件在顶层的原生 GdkWindow 上绘制,那么您应该使用新添加的 gdk_window_begin_draw_frame()gdk_window_end_draw_frame()GdkDrawingContext API 代替。

塑造未来

GTK+ 中绘图代码的内部结构多年来一直在逐步更新,以应对诸如 新的窗口系统以及 其他渲染 API 之类的事情。可以肯定的是,它们还会再次改变,尤其是在提高渲染性能方面。许多可能看起来是任意的更改实际上是减少每一帧在工具包内部花费的时间,并为应用程序逻辑留出更多时间的踏脚石。