构建和测试 GTK

… 或者:GTK 开发人员如何检查他们在工具包上的工作.

自从 GNOME 集体迁移到 GitLab 以来,GTK 充分利用了该平台提供的功能,尤其是在其持续集成管道方面。

在过去,检查我们对工具包的更改是否正确的唯一方法是等待持续构建机器人通知我们主开发分支上的任何中断。虽然这比什么都没有好,但它不允许我们在任何事情的开发阶段(从功能到错误修复,从文档改进到添加新测试)期间阻止中断。

现在,GitLab 中提供的 CI 管道在每个分支和合并请求上运行,远早于更改到达其他所有人使用的公共开发分支。

主题分支和合并请求

当针对 GTK 4 主开发分支开发主题分支时,我们运行一个 CI 管道,该管道首先对分支中应用的更改进行简单的代码样式检查。样式检查使用 clang-format,它通常足以满足 GTK 编码风格;编码风格有一些“特殊”注意事项,clang-format 可能会产生误报和漏报。因此,允许样式检查失败,但强烈建议贡献者和审查者在失败时检查日志。

通过样式检查后,我们运行构建阶段,目前包含三个单独的作业

  • 使用 Fedora 容器的 Linux 调试构建
  • Windows 上的 MSYS2 构建
  • Linux 发布构建

Linux 调试构建非常标准。

MSYS2 构建可以捕获 Windows 上 GNU 工具链的任何问题。

发布构建是必要的,以确保我们不依赖于我们在开发过程中使用的调试代码的副作用。

所有这些作业都会运行 GTK 测试套件。

我们同时以 JUnit 文件(利用 GitLab 的支持)和 HTML 报告(存储为管道工件)的形式发布测试报告。这使我们更容易检查哪些失败了,哪些成功了。

理想情况下,我们希望添加更多环境

  • 基于其他主流发行版的 Linux 构建
  • 使用 MSVC 工具链的 Windows 构建
  • 一旦 GDK 后端修复,就进行 macOS 构建

构建和测试作业通过后,我们进入分析阶段。我们在 GTK 的代码库上运行 Clang 静态分析工具 并生成报告。在不久的将来,我们还可以运行像 UBSan 和 ASan 这样的消毒工具;用于我们的解析器(如 GtkBuilder 和 CSS)的模糊测试工具;或者验证我们的 UI 定义是否包含适当的可访问性描述符的工具。

与测试一样,我们将分析报告作为 GitLab 工件发布以供审查。

通过分析阶段后,我们构建 API 参考,并检查结果,以便正确记录新添加的符号。

最后,我们有手动 CI 作业来构建 GTK 演示应用程序、widget 工厂和图标浏览器的 Flatpak 包。这使设计师可以立即测试 Adwaita 中的更改或新添加的 widget,而无需在他们的系统上从头开始构建 GTK。

主线开发分支

一旦主题分支/合并请求的 CI 管道通过,我们就可以将更改合并到主开发分支中,并有一定的信心,即代码是正确的并且符合我们的预期。

主开发分支运行与先前描述的相同的管道,只是 Flatpak 作业不再是手动的了,因此始终可以在本地测试 GTK 的当前前沿版本。此外,文档在在线发布,因此始终是最新的。

The GTK CI pipeline

GTK 3 怎么样?

在 GTK 3 分支中,我们有一个更简单的管道,它运行以下作业

  • 在 Linux 和 Windows/MSYS2 上,对静态和共享库工件进行完整的 Meson 调试构建,在 Fedora 和 Debian 的当前稳定版本上
  • 在 Linux 上进行完整的 Meson 发布构建,该构建还会生成 API 参考
  • 在 Linux 和 Windows/MSYS2 上进行 Autotools 构建
  • 在 Linux 上进行可选的 Autotools distcheck 构建

只要 GTK 3 支持 Autotools,Autotools 作业就会存在。理想情况下,我们希望利用 Meson 构建为 macOS 和 Windows/MSVC 添加其他作业。

The GTK3 CI pipeline

一旦 GTK 4 CI 管道达到一定的功能和稳定性水平,我们将把它反向移植到 GTK 3,这样我们就可以更加确信当前稳定分支不会退步。


有关更多信息,您可以查看 GTK 存储库

约束布局

什么是约束

最基本的,约束是两个值之间的关系。该关系
可以描述为线性方程

target.attribute = source.attribute × multiplier + constant

例如,这个

可以描述为

blue.start = red.end × 1.0 + 8.0

或者

  • 将由约束设置的目标“蓝色”的属性“start”;这是等式的左侧
  • 等式左侧和右侧之间的关系,在本例中为相等;关系也可以是大于或等于
    以及小于或等于
  • 将由约束读取的“红色”的属性“end”;这是等式的右侧
  • 应用于源属性的乘数“1.0”
  • 添加到属性的偏移量常数“8.0”

约束布局是一系列像上面这样的方程,描述了 UI 的各个部分之间的所有关系。

重要的是要注意,关系不是赋值,而是相等(或不等):等式的两边都将以满足约束的方式求解;这意味着约束列表可以重新排列;例如,上面的例子可以改写为

red.end = blue.start × 1.0 - 8.0

一般来说,为了方便和可读性,您应该按照阅读顺序排列约束,从前导边缘到后导边缘,从上到下。您还应该优先考虑整数作为乘数,以及正数作为常数。

求解布局

线性方程组可以有一个解、多个解,甚至根本没有解。此外,出于性能原因,您并不真的希望每次都重新计算所有解。

早在 1998 年,Greg J. Badros 和 Alan Borning 发布了用于求解线性算术约束的 Cassowary 算法,以及其在 C++、Smalltalk 和 Java 中的实现。Cassowary 算法试图通过找到其最佳解来求解线性方程组;此外,它还以增量方式进行,这使得它对于用户界面非常有用。

在过去的十年中,各种平台和工具包开始提供基于约束的布局管理器,其中大多数使用了 Cassowary 算法。第一个是 Apple 的 AutoLayout,于 2011 年发布;2016 年,Google 在 Android SDK 中添加了 ConstraintLayout。

2016 年,Endless 在一个名为 Emeus 的库中为 GTK 3 实现了约束布局。从这项工作开始,GTK 4 现在为应用程序和 widget 开发人员提供了一个可用的 GtkConstraintLayout 布局管理器。

实现约束求解器的机制是 GTK 私有的,但公共 API 提供了一个可以分配给 GtkWidget 类的布局管理器,以及一个不可变的 GtkConstraint 对象,它描述了您希望添加到布局中的每个约束,将两个 widget 绑定在一起。

引导约束

约束使用 widget 作为源和目标,但在某些情况下,您希望将 widget 属性绑定到实际上不会在屏幕上绘制任何内容的矩形区域。您可以向布局添加一个虚拟 widget,然后将其不透明度设置为 0 以避免渲染它,但这会给场景增加不必要的开销。相反,GTK 提供了 GtkConstraintGuide,该对象的唯一工作是为布局做出贡献

An example of the guide UI element

在上面的示例中,只有标记为“子项 1”和“子项 2”的 widget 是可见的,而引导将是一个空白空间。

引导具有最小、自然(或首选)和最大尺寸。它们都是约束,这意味着您不仅可以将引导用作对齐的帮助程序,还可以将其用作布局中可以增长和收缩的灵活空间。

描述布局中的约束

约束可以通过编程方式添加,但像 GTK 中的许多其他东西一样,为了方便起见,它们也可以在 GtkBuilder UI 文件中描述。如果您在 UI 文件中添加 GtkConstraintLayout,则可以在特殊的“<constraints>”元素内列出约束和参考线。

  <object class="GtkConstraintLayout">
    <constraints>
      <constraint target="button1" target-attribute="width"
                     relation="eq"
                     source="button2" source-attribute="width" />
      <constraint target="button2" target-attribute="start"
                     relation="eq"
                     source="button1" source-attribute="end"
                     constant="12" />
      <constraint target="button1" target-attribute="start"
                     relation="eq"
                     source="super" source-attribute="start"
                     constant="12" />
      <constraint target="button2" target-attribute="end"
                     relation="eq"
                     source="super" source-attribute="end"
                     constant="-12"/>
    </constraints>
  </object>

您还可以使用 “<guide>” 自定义元素描述参考线。

  <constraints>
    <guide min-width="100" max-width="500" />
  </constraints>

可视化格式语言

除了 XML 之外,约束还可以使用称为“可视化格式语言”的简洁语法来描述。VFL 描述是面向行和列的:您使用一行来描述布局中的每一行和每一列,该行在视觉上类似于您正在实现的布局,例如:

|-[findButton]-[findEntry(<=250)]-[findNext][findPrev]-|

描述了一个水平布局,其中 findButton 部件与布局管理器的前边缘之间隔着一些默认空间,后面跟着相同的默认空间;然后是 findEntry 部件,其宽度最多为 250 像素。在 findEntry 部件之后,我们再次有一些默认空间,然后是两个部件 findNextfindPrev,它们彼此紧贴;最后,这两个部件与布局管理器的后边缘之间隔着默认的空间量。

使用 VFL 表示法,GtkConstraintLayout 将创建所有必需的约束,而不必手动描述它们。

重要的是要注意,VFL 不能描述所有可能的约束;在某些情况下,您需要使用 GtkConstraint 的 API 来创建它们。

约束布局的局限性

约束布局非常灵活,因为它们可以实现任何布局策略。这种灵活性是有代价的:

  • 您的布局可能存在太多解决方案,这会使其模糊且不稳定;如果您的布局非常复杂,这可能会成为问题。
  • 您的布局可能没有任何解决方案。这通常发生在您没有使用足够约束的情况下;一个经验法则是每个目标在每个维度上至少使用两个约束,因为所有部件都应该具有定义的位置和大小。
  • 相同的布局可以通过不同的约束系列来描述;在某些情况下,几乎不可能说哪种方法更好,这意味着您需要进行实验,尤其是在涉及到动态添加或删除 UI 元素或允许用户交互(如拖动 UI 元素)的布局时。

此外,在更大的规模上,本地的、临时的布局管理器可能比基于约束的布局管理器更高效;如果您有一个可以增长到未知行数的列表框,则不应将其替换为约束布局,除非您预先测量了性能影响。

演示

当然,自从我们添加了此新 API 以来,我们还在 GTK 演示应用程序中添加了一些演示。

A constraints demo
作为 GTK 演示应用程序一部分的约束演示窗口。

以及完整的约束编辑器演示

The GTK constraints editor demo
GTK 约束编辑器演示应用程序的屏幕截图,在左侧边栏显示 UI 元素、参考线和约束的列表,在窗口的右侧显示结果。

更多信息

GLib 2.58 的新闻

今年 9 月,GLib 将达到 2.58 版本。在过去的两个开发周期中,进行了一些更改,最值得注意的是 Meson 构建的改进,这反过来又提高了 GLib 在 Windows、macOS 和 Android 等平台上的可移植性。现在是时候评估 GLib 的当前状态,并强调一些将影响基于 GLib 的代码的更改了。

  • Meson – 感谢 Nirbheek Chauhan 和 Xavier Claessens 的持续工作,Meson 构建一直在不断改进,以至于我们可以开始将其作为默认构建系统。该计划——正如邮件列表中概述的那样——是使用 Meson 发布 GLib 2.58,同时在树中保留 Autotools 构建并在发行存档中可用;然后,我们将在接下来的开发周期中放弃 Autotools 构建,并发布不带 Autotools 支持的 GLib 2.60。非常欢迎 Linux 发行商开始在他们的构建器中测试 Meson 构建;我们一直在运行 Meson 构建作为我们的CI过程已经有一段时间了,但是更多的曝光将揭示我们遗漏的最终回归;此外,如果使用与 GCC/Clang/MSVC 不同的工具链的人们开始尝试 Meson 构建并报告错误,那将是极好的。同时,如果您在 macOS 和 Windows 上使用 GLib,我们已经建议您切换到 Meson 来构建 GLib,因为它比 Autotools 更容易且与这些平台集成得更好。
  • 可靠性和可移植性 – GLib 与 GNOME 的其余部分一起切换到 GitLab,这意味着能够在 GNOME Continuous 构建之外运行持续集成。现在,我们为每次提交和合并请求在多个工具链、多个构建系统和多个平台上运行CI,这大大降低了构建中断的可能性。我们还改进了测试套件中的代码覆盖率。当然,我们总是可以做得更好;例如,我们没有一个CImacOS 和 Solaris 系列操作系统的运行器,并且非常感谢为 *BSD 系列提供更多的运行器。如果您有空闲的机器和一些可以捐赠的带宽,我们已经发布了寻求帮助的呼吁
  • *BSD 上的文件监控 – 关于 *BSD 系列,Martin Pieuchot 和 Ting-Wei Lan 完全修改了 GIO 中文件监控的 kqueue 后端;新代码更简单、更健壮,并通过了所有测试。
  • 使用 posix_spawn() 进行高效的进程启动 – 感谢 Daniel Drake,如果平台的 C 库支持,GLib 现在可以在特定情况下使用 posix_spawn();与手动调用 fork() + exec() 相比,这允许在内核中命中快速路径;当在内存受限的平台上运行时,这些快速路径尤其有利。
  • 引用计数类型和分配 – GLib 在其许多类型中使用引用计数作为内存管理和垃圾回收机制,但缺少公共 API 以允许其他人在其自己的数据结构中实现相同的语义;这会导致大量的复制粘贴和重新实现,并且通常会导致在饱和度和线程安全性方面出现诸如未定义行为之类的问题。GLib 2.58 具有 grefcountgatomicrefcount 类型,以及它们的 API,以减少这种重复。此外,借鉴了 Rust 等其他语言,GLib 提供了一种在内存分配上添加引用计数语义的方法,通过添加一个低级 API,允许您分配没有引用计数字段的结构,并自动向它们添加引用计数语义。
  • 弃用 – 在上一个开发周期中,一些软弃用已成为真正的弃用。
      • g_type_class_add_private() 终于被弃用了,自从我们引入实例私有数据宏后的五年;如果您仍在类初始化中使用该函数,请切换到 G_DEFINE_TYPE_WITH_PRIVATEG_ADD_PRIVATE
      • g_main_context_wait() 已正式弃用,但您应该已经看到了关于其使用的运行时警告。
      • GLib 提供的 GTest 工具 gtester 已被弃用;如果您使用的是 Autotools,则应使用TAPAutomake 附带的工具。

在过去的这个周期中,由于 Philip Withnall 的不懈努力,GLib 中有很多贡献;他在审查补丁、分类错误和实施项目开发过程中的更改方面发挥了重要作用。切换到 GitLab 也改进了贡献过程,更多开发人员打开了合并请求。

  • 2.54.0..c182cd68:来自 143 位开发人员的 968 个变更集,高于 2.53 开发周期中的 412 个变更集和 68 位开发人员。
  • 总共 添加了 31851 行删除了 27976 行(差值:+3875
拥有最多变更集的开发人员
Philip Withnall 303 31.3%
Xavier Claessens 79 8.2%
Emmanuele Bassi 69 7.1%
Christoph Reiter 42 4.3%
Ting-Wei Lan 21 2.2%
Chun-wei Fan 21 2.2%
Nirbheek Chauhan 21 2.2%
Ondrej Holy 20 2.1%
Руслан Ижбулатов 20 2.1%
Mikhail Zabaluev 20 2.1%
Simon McVittie 15 1.5%
Matthias Clasen 14 1.4%
Christian Hergert 13 1.3%
Iñigo Martínez 12 1.2%
Bastien Nocera 10 1.0%
Rafal Luzynski 9 0.9%
Michael Catanzaro 9 0.9%
Will Thompson 8 0.8%
Allison Lortie 8 0.8%
Daniel Boles 8 0.8%

请务必使用 GLib 2.57.2 测试您的代码,这是下一个面向 2.58.0 稳定版本的开发快照。

GTK+ 3.92

昨天,我们发布了 GTK+ 3.92.1,重庆市。由于距离上次 3.91 版本发布已经有一段时间了,这里简要回顾一下主要的变化。

此版本是我们迈向 GTK+ 4 的又一个里程碑。虽然还有很多工作要做,但此版本让我们初窥 GTK+ 4 中我们希望实现的一些功能。

GSK

自上次发布以来,大部分工作都投入到了 GSK 中。 Vulkan 渲染器现在已经接近完成,尽可能避免使用 Cairo 回退。唯一缺失的部分是模糊阴影(诚然,这是一个重要的部分)。

自 3.91.2 版本以来的一个重大进步是,我们不再对所有文本使用 Cairo 回退。相反,文本(在标签和条目中,遗憾的是尚未在文本视图中)被转换为文本节点。每个文本节点包含一个 PangoGlyphString 和一个 PangoFont。 Vulkan 渲染器使用字形缓存来避免为每一帧重新渲染字形。

Vulkan 渲染器的内部逻辑已被重新设计为使用纹理而不是 Cairo 表面来处理中间结果,从而避免更多的 Cairo 回退。

Vulkan 渲染器中获得支持的其他节点类型包括模糊、重复节点、混合模式和交叉淡入淡出。在某些情况下,我们使用的着色器是非常简单的实现。 如果能帮助改进它们,我们将非常欢迎!

作为使用渲染节点的一个初步示例,我们为 GtkOverlay 实现了模糊底层功能。它的工作原理是将覆盖的“主子级”捕获为渲染节点,然后多次重用它,并使用正确的剪切,有时还使用模糊节点。

检查器

为了帮助您探索 GSK,检查器现在会显示 Vulkan 信息,并且记录器会显示有关渲染节点的更多信息。

输入

在输入方面,事件已经获得了访问器,并且我们不再直接访问它们的字段。这是一个中间步骤,清理事件仍在进行中。我们已将事件的传统小部件信号(例如 ::key-press-event)移动到事件控制器,并且 GTK+ 中的大多数小部件已完全停止使用它们。

构建系统

我们已切换为完全使用 Meson 来构建 GTK+,并且 3.92.1 版本是第一个使用 Meson 的 dist 支持完成的版本。为了发布此版本,我们还必须将文档、测试套件和已安装的测试移植到使用 Meson。

仍然存在一些粗糙的地方(我们无法 100% 获得所有依赖项),但总的来说,Meson 对我们来说效果很好。

其余部分

当然,每个人都喜欢 Emoji,并且此版本中也提供了 GTK+ 3.22 中已有的相同颜色 Emoji 支持。除此之外,CSS 中的字体支持通过对 CSS3 font-variant 属性的支持略有改进。

当然,这依赖于具有相应功能的字体。

试用一下

使用 GTK+ 3.92.1,您应该可以轻松地自己尝试其中一些功能。

如果您一直想参与 GTK+ 开发但从未找到合适的机会,那么现在是参与的好时机!

日志记录及更多

不久前,GLib 获得了一个用于“结构化日志记录”的新工具。与此同时,它还获得了将日志写入 systemd 日志的支持。显然,GLib 中的日志记录变得更加复杂,并且可能会有些令人困惑。

本文试图澄清一些问题。

结构化与否

传统的 GLib 日志记录工具是 g_message(), g_debug() 等宏,它们最终调用 g_log() 函数,然后使用通过 g_log_set_handler() 设置的日志处理程序来执行实际写入。您可以将任何信息放入日志中,但它必须全部格式化为单个字符串,即消息。

g_debug ("You have %d eggs", 12 + 2);

g_log (G_LOG_DOMAIN,
       G_LOG_LEVEL_DEBUG,
       "You have %d eggs", 12 + 2);

使用新的结构化日志记录工具,您可以调用 g_log_structured(),然后使用日志写入器函数来执行写入。到目前为止,这与较旧的日志记录工具非常相似。结构化日志的优点是,您可以将多个字段放入日志中,而无需将其全部格式化为字符串。相反,您可以传递一个日志字段数组,这些字段是键值对。

g_log_structured (G_LOG_DOMAIN,
                  G_LOG_LEVEL_DEBUG,
                  "CODE_FILE", "mysource.c",
                  "CODE_LINE", 312,
                  "MESSSAGE_ID", "06d4df59e6c24647bfe69d2c27ef0b4e",
                  "MESSAGE", "You have %d eggs", 12 + 2);

这里的 CODE_FILECODE_LINEMESSAGE_ID 只是“标准”字段的示例。您还可以发明自己的字段。请注意,您仍然可以将 printf 样式格式用于 MESSAGE 字段。

因此,GLib 现在有两个独立的日志记录工具。为了使事情更有趣,我们允许您将 g_message()g_debug() 等包装宏重定向为在底层使用 g_log_structured() 而不是 g_log()。为此,请在包含 glib.h 之前定义 G_LOG_USE_STRUCTURED 宏。

为什么这很有用?首先,它可以省去您替换所有 g_debug() 的麻烦,并且仍然可以让您利用结构化日志记录的一些优势 - 以这种方式使用时,传统的宏会使用单独的字段来表示日志域、代码文件和行以及其他一些字段,这有助于在生成的日志中进行筛选和搜索,尤其是在 systemd 日志中。

另一个优点是,您可以使用单个后端(日志写入器函数)来控制新旧日志调用最终的去向。

我的所有日志都去哪了?

结构化日志记录通常与 systemd 日志相关联。因此,人们期望 g_log_structured() 的输出转到日志也就不足为奇了。对于服务或从桌面图标启动应用程序时,这确实很有用。但是,如果您从终端运行它,您可能会希望看到它的输出在那里。

为了满足这些相互竞争的需求,GLib 默认的日志写入器函数会尝试变得智能。如果它检测到 stderr 被重定向到 journald 套接字,则它会将其结构化输出写入日志。否则,它会格式化消息并将其写入 stderr

GNOME Shell 和 DBus 都会在启动应用或服务时安排将 stderr 重定向到日志。将 stderr 显式重定向到日志的一种方法是在 systemd-cat 下运行您的应用程序。

systemd-cat my-app-that-logs

如果您确定希望您的日志始终转到日志,则可以告诉 GLib 使用一个执行此操作的日志写入器。

g_log_set_writer_func (g_log_writer_journald, NULL, NULL)

超越默认

即使 GLib 默认提供的日志写入器函数应该可以满足许多需求,您可能仍然需要编写自己的函数。在这种情况下,GLib 具有许多有用的函数可以帮助您,例如 g_log_writer_format_fields(), g_log_writer_is_journald()g_log_writer_supports_color()

祝您日志记录愉快!

参考资料

  • Philipps 关于结构化日志记录的演讲
  • GLib 日志记录文档
  • Systemd 日志文档

GTK+ 4.0 中的小部件层次结构

今天,我们将邀请客座作者 Timm Bäder,他是 Corebird 的维护者和 GTK+ 的贡献者,来谈谈在 GTK+ 4.0 中编写复合窗口部件的更改。

(注意:这里的一些信息基于尚未合并到主分支的分支,但我相信它们在不久的将来会合并)

在 GTK+3 中,只有 GtkContainer 的子类才能拥有子窗口部件。这对于我们所知的“公共”容器子部件来说很有意义,例如 GtkBox — 即开发者可以任意添加、删除和重新排序子窗口部件,而容器只负责布局。

然而,GTK+3 中还有一些更复杂的窗口部件不继承自 GtkContainer,例如 GtkSpinButtonGtkSwitch。这些窗口部件从来没有真正的 GtkWidget 子部件。例如,考虑一下 GtkSpinButton 中的两个可点击区域。我在这里不称它们为“按钮”是有原因的,因为在 GTK+3 中,它们不是实际的 GtkButton 实例,因为 GtkSpinButton 不是 GtkContainer。相反,GtkSpinButton 必须绕过这个事实,为向上/向下区域创建两个 GdkWindow,然后在其中渲染两个图标;处理悬停和 CSS 状态;各种按钮向上/向下事件;以及 GdkWindow 的生命周期等。为了绕过 GtkContainer 的要求,我们在 GTK+3 中引入了小工具GtkCssGadget)。在样式方面,小工具对应于一个 CSS 框,因此表示 CSS 树中的一个节点。在窗口部件方面,它们被用于为非容器窗口部件提供“类似窗口部件”、可使用 CSS 设置样式、的子部件。

GtkWidget 的更改

当然,在 GTK+4 中需要进行大量更改才能支持这些用例。我不会在这里列出所有更改(特别是那些比较难看的,如焦点处理),但我认为其中大部分对于应用程序开发人员和自定义窗口部件作者来说都非常有趣和重要。总的来说,我们正试图摆脱特殊情况,通过尽可能多地重用窗口部件来走向更通用的方式。因此,与其使用 PangoLayout 来显示文本,不如让窗口部件使用 GtkLabel。如果你有一个具有类似按钮语义的可点击区域,请尝试使用 GtkButton。如果你想在水平或垂直方向上布局窗口部件,请使用 GtkBox。这样,我们就拥有一个输入和渲染都可以操作的窗口部件树。在实践中,这意味着主要是摆脱窗口部件内部使用的所有小工具,以及独立的 GtkCssNode 实例。

迭代子窗口部件

在 GTK+3 中,每个容器都必须实现 GtkContainer::forall,并且可以使用 gtk_container_foreach()gtk_container_forall() 轻松迭代所有子窗口部件,而在 GTK+4 中,每个 GtkWidget 都可以拥有子窗口部件,因此每个窗口部件都可能有一个我们需要绘制、测量、大小分配等的子窗口部件列表。在窗口部件层次结构中,这些子窗口部件和兄弟窗口部件可以通过以下方式访问
  • gtk_widget_get_first_child()
  • gtk_widget_get_last_child()
  • gtk_widget_get_prev_sibling()
  • gtk_widget_get_next_sibling()
因此,迭代给定 GtkWidget 的子窗口部件最简单的方法是(用 C 语言)
GtkWidget *widget;
GtkWidget *child;
for (child = gtk_widget_get_first_child (widget);
     child != NULL;
     child = gtk_widget_get_next_sibling (child))
  {
    /* Do stuff with @child */
    g_assert (gtk_widget_get_parent (child) == widget);
  }

将窗口部件添加到非容器父级

GTK+(3 和 4)中的每个窗口部件都会保存一个指向其父级窗口部件的指针。可以使用 gtk_widget_set_parent() 设置此父级,并且所有 GtkContainer::add 实现最终都必须使用此函数来设置给定子窗口部件的父级。

在 GTK+4 中,gtk_widget_set_parent() 仍然有效,并将窗口部件添加到父级的子窗口部件列表的末尾。但是,我们显然也希望管理子窗口部件的顺序,以及我们在列表中添加新子窗口部件的位置,因此我们有

  • gtk_widget_insert_before()
  • gtk_widget_insert_after()
在父级列表中已有的子窗口部件之前或之后添加新的子窗口部件。这些也可以通过传递已设置给定父级的子窗口部件来重新排序子窗口部件。
由于 GTK+ 中的许多窗口部件目前都使用 XML 形式的复合窗口部件模板,GtkWidget 现在也有其自己的 GtkBuildable::add_child() 实现来支持此用例。例如,GtkFileChooserWidget 就使用它,该窗口部件几乎完全在 XML 中定义。

窗口部件 CSS 名称

由于我们经常需要为任意窗口部件使用任意 CSS 节点名称,GtkWidget 现在有一个仅构造的属性,称为 GtkWidget:css-name,如果指定,它将用作 CSS 节点名称。如果未指定,则将使用传递给窗口部件的 gtk_widget_class_set_css_name() 调用的名称。如果这两种都没有使用,则 CSS 节点将简单地称为“widget”。

已转换窗口部件的示例

当前主分支(和侧分支)中已经有一些窗口部件从使用各种 GtkCssGadgetGdkWindowPangoLayout 转换为使用实际窗口部件。最终目标当然是尽可能多地重用窗口部件,从而减少代码大小和维护负担。

GtkSwitch

在 GTK+3 中,GtkSwitch 是一个直接的 GtkWidget 子类(太棒了!),它使用一个 GdkWindow 用于输入(点击开关将启用/禁用它),一个 GtkCssGadget 用于窗口部件本身,两个 PangoLayout 用于 ON/OFF 文本,以及另一个 GtkCssGadget 用于滑块。
在 GTK+ 主分支中,开关仍然有其窗口部件级别的 GtkCssGadget,因此它支持 min-width/min-height CSS 属性和 CSS 边距,但滑块小工具已替换为 GtkButton,并且两个 PangoLayout 已替换为 GtkLabel。这样,我们可以节省 gtkswitch.c 中大约 300 行的代码。理论上,我们也有更多的功能,例如可以使用 GtkLabel 支持的对 text-decoration CSS 属性的有限支持,但我怀疑这是否非常有用。

GtkSpinButton

如前所述,GtkSpinButton 可以很容易地将实际的 GtkButton 用于向上/向下区域,并且它在 GTK+ 主分支(在某个时候将成为 GTK+4)中也这样做了。这消除了 gtkspinbutton.c 中的另外 300 行代码。通过使用 GtkButton,旧的图标辅助小工具也变成了实际的 GtkImage 实例。不幸的是,我们必须在这里自己实现一些 GtkGesture 的魔力,因为 GtkSpinButton 还支持在其按钮上进行中间和右键点击,而 GtkButton::clicked 仅对单个主鼠标按钮点击做出反应。

GtkLevelBar

GtkLevelBar 管理一组块并为其分配不同的样式类。在 GTK+3 中,这些块都是 GtkCssGadget 实例。它们都是“哑”的,也就是说,它们不做任何特殊的事情 — 它们只是 CSS 框而已。这就是为什么将其转换为对所有块使用 GtkWidget 并没有减少太多文件大小的原因。

 

GtkProgressBar

GtkProgressBar 使用小工具来表示槽和进度节点。它还使用 PangoLayout 来显示百分比或用户给定的字符串。

在主分支中,槽和进度都是窗口部件,并且 PangoLayout 当然是 GtkLabel。不必监听 GtkWidget::style-changed(这会自动为窗口部件完成),并且不必自己绘制 PangoLayout(现在由 GtkLabel 处理)也确实节省了大约 200 行代码。

 

GtkExpander

GtkExpander 比看起来要复杂得多。在 GTK+3 中,它由 2 个 GtkBoxGadget 组成(这类似于 GtkBox,但不是一个部件……),一个用于标题部件左侧的箭头的小工具,标题部件和实际的内容部件。在 master 分支中,这是通过使用实际的 GtkBox 和一个用于箭头的 GtkIcon(一个内部部件)完成的。我不确定这是否是表达 GtkExpander 功能的最佳方式,例如,我们也可以使用 GtkButton 来表示箭头+标题部件的组合。
由于 GtkBoxGadget 几乎已经是一个完美的 GtkBox 克隆,因此这里的代码节省意义不大,但是不必再次监听 GtkWidget::direction-changed 大约可以节省 30 行代码。

意外的 GtkBox & GtkButton 子类

GTK+3 包含相当多的部件,它们继承自另一个部件,仅仅是为了看起来和行为像它们。这里的问题是,这些部件也继承了父类的所有 API,而这通常是不需要的。
对于 GtkBox,GTK+3 中几乎所有子类都是“意外的”,因为实际上将它们用作 GtkBox 没有任何意义,人们通常不会这样做,但是它们必须是 GtkBox 的子类才能满足 GTK+ 的 GtkContainer 要求。这样的部件的一个例子是 GtkFileChooserWidget。这已经是有史以来最复杂的部件之一,但是您有没有考虑过使用 gtk_container_add()gtk_box_pack_{start,end}() 向其中添加部件?这没有多大意义。这是一个具有自身 API 的封闭实体。因此,在 GTK+4 中,它将是直接的 GtkWidget 子类,其中包含一个 GtkBox。或者可能不是。这只是一个您不必关心的实现细节。(顺便说一句:GtkFileChooserButton 在 GTK+3 中是一个 GtkBox
这同样适用于 GtkButton。在 GTK+3 中,GtkButton 有很多子类,它们继承了所有 GtkButton API,但实际上不支持它。如果您从 GtkLinkButton 中删除子部件会发生什么?如果您设置 GtkFontButtonGtkButton:label 属性会发生什么?同样,这些是封闭的实体,它们有自己的 API 来设置和获取各种数据,并根据它们更改行为和/或外观,但这并不意味着它们支持所有 GtkButton/GtkContainer 的恶作剧。

通用重构规则和未来

对于此重构工作,我们尝试保持 CSS 节点结构与 GTK+3 中的结构相同,即,我们尝试不破坏当前在 testsuite/css/nodes.c 中的 CSS 节点测试。
GTK+ 中一些更复杂的部件仍然严重依赖小工具,将它们移植为仅使用实际的部件将需要大量工作。GtkRange 在历史上是 GTK+ 中最复杂的非容器部件之一。它既用于滚动条,又用于刻度尺,因此将其移植到部件可能首先需要另一轮重构。
另一个有趣的案例是 GtkNotebook,它结合了小工具和部件的使用。例如,在这里我们可以使用真正的 GtkStack 来在页面之间切换,并轻松支持页面切换过渡。
当然,另一个令人兴奋的未来展望是 Carlos 的 wip/carlosg/event-delivery 分支,它摆脱了大量的 GdkWindow 实例,并使部件输入比以往任何时候都更容易。

GTK+ 中的版本控制和长期稳定性承诺

本文中提出的计划取代了 2016 年 6 月多伦多 GTK+ 黑客马拉松之后公开的计划。

本月,GTK+ 团队将发布一系列长期稳定版本中的第一个。这将使 GTK+ 更具可预测性和可靠性,同时不会妨碍 GTK+ 未来的改进。

这些计划是在自去年 6 月多伦多 GTK+ 黑客马拉松制定初步计划以来,与各种利益相关者讨论的结果。

背景

自 2002 年发布 2.0 版本以来,GTK+ 遵循了一个相当简单的版本控制方案

  • 主版本指定常规 API 版本
  • 次版本指定开发周期(如果为奇数)和稳定周期(如果为偶数)
  • 微版本指定错误修复更新

任何在 GTK+ 中引入的 API 都保证存在到下一个主版本;在开发周期中引入的 API 保证在稳定周期开始后保留。稳定周期不提供新功能或新 API。

该方案运作良好,但其问题在 3.x 系列中变得越来越明显,尤其是在结合 6 个月基于时间的开发周期时。在 2.x 周期中,GNOME 应用程序中的新功能被迫出现在其他库中,因为工具包过于复杂或移动速度太慢。自 3.0 以来,GTK+ 的开发步伐一直在加快。GTK+ 一直处于最前沿,每六个月推出新的部件和新功能。但是,为了实现这些新的部件和功能,工具包的某些内部结构一直处于不断变化的状态。

从 GNOME 的角度来看,GTK+ 一直是一个相当稳定但不断变化的目标,因为 GNOME 开发人员和 GTK+ 开发人员可以共享反馈和建议,并快速跟上内部变化。另一方面,从社区的角度来看,跟踪 GTK+ 的开发更加痛苦。GNOME 项目之外的应用程序开发人员很难了解工具包中的哪些更改会影响他们的代码库。GTK+ 团队试图改进其沟通渠道,但是关于开发周期中更改的博客对于 GTK+ 用户的更广泛社区中的许多人来说还不够。

长期稳定的 GTK+ 版本

GTK+ 有三个主要的利益相关者:希望功能和 API 稳定的应用程序开发人员;希望访问 GTK+ 开发版本以便快速引入新功能(包括 GNOME 项目的大部分)的桌面开发人员;以及 GTK+ 团队本身,该团队需要在较长的开发周期中迭代库的内部结构。

引入长期稳定的 GTK+ 版本旨在确保 GTK+ 在每个受众之间取得良好的平衡。特别是,应用程序开发人员将可以使用一个稳定的平台,该平台仍然可以访问在 3.x 系列中开发的新 GTK+ 功能,例如 CSS 样式、触摸屏支持、HiDPI 显示器支持、Wayland 支持、新部件、GTK+ 检查器等。

GTK+ 将继续发布主版本、次版本和微版本。新的主版本将在新功能稳定后发布,预计大约每 2-3 年发布一次。当升级到新的主版本时,将删除已弃用的 API。之后,此 API 系列将被视为稳定。新的次版本可能会引入新的部件,或更新 GDK 后端中窗口系统协议的实现,但不允许添加其他功能或主题更改。以前,次版本每六个月发布一次,现在将在必要时发布。我们还将继续进行错误修复和安全问题的微版本发布,至少三年。此后的维护可能会继续,具体取决于可用的志愿者资源量。具有长期支持发布周期(超过三年)的操作系统发行商可能需要联系 GTK 团队以建立向后移植策略。

长期稳定系列中的更新将是 ABI 稳定的。与这些稳定系列一起,GTK+ 开发将在半稳定的开发系列中继续进行。这些开发版本将在次版本之间包含一些 API 更改,尽管更改将尽可能受到限制。这是我们期望 GNOME 应用程序采用的路径,但是如果其他应用程序开发人员想要访问最新功能,则可能会选择此选项,但代价是每个次版本都可能需要进行一些移植工作。

尽管 GTK+ 团队保留在开发系列期间更改 API 的权利,但这并不意味着整个 GTK+ API 将在每个版本中不断破坏;只有特定的且希望很少使用的 API 部分可能会更改,如果更改过于广泛,则很可能会延迟到下一个主要开发周期。我们将确保提前充分沟通这些更改。

新的版本控制方案

新的 GTK+ 版本控制方案是我们目前遵循的“语义版本控制”方案的修改。一旦发布了新的主要稳定版本,开发周期就开始,我们将

  • 将 pkg-config 文件更新为新的主版本,以允许 GNOME 开发人员在开发期间定位新的 API
  • 保持现有主版本的编号不变
  • 将次版本更新为 90,以表示开发版本

例如,在 3.22.0 版本发布后以及新的开发周期开始时,pkg-config 文件将被称为 gtk+-4.0,并且 configure.ac 文件中的版本将设置为 3.90

每六个月会发布一个新的偶数开发版本,例如 x.90x.92x.94,直到 GTK+ 团队确信新的 API 和功能集稳定为止。这些次要版本中的每一个都会提升共享库的 soname,以确保自动化工具可以捕捉到最终的变化并通知发行商和维护者。一旦我们达到 API 和功能集足够稳定,可以供更广泛的社区使用的程度,我们将发布一个新的主要版本 (x + 1).0 并声明 API 稳定。

一旦发布了这个零版本,就会创建一个新的稳定分支,主分支将提升到下一个九十版本并开始新的开发周期。九十版本将与之前的稳定版本并行安装。

gtk-versioning-scheme

3.22 将是 3.x 系列的最后一个次要版本,新的版本控制方案将从 3.90 开始生效。3.22 版本在该方案中是不规则的,因为它是一个长期稳定版本,但不会再接收后续的次要版本,并且没有 .0 版本号。这是一个必要的过渡步骤。

接下来是什么

关于这些计划的更多细节,包括针对库开发人员和发行包维护者的具体信息,将在后续的博客文章中发布。GTK+ 开发博客也将继续提供有关 GTK+ 本身技术变化的更新,以便提供有关每个即将发布的主要版本中将出现的更改的信息。

我们对这些计划感到兴奋,并希望它们能为 GTK+ 开创一个新时代,在这个时代中,应用程序作者可以对我们的平台更有信心,同时仍然允许我们在 3.x 系列中看到的快速开发步伐。

本文中提出的计划取代了 2016 年 6 月多伦多 GTK+ 黑客马拉松之后公开的计划。

GTK+ 中的绘图

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

它是如何开始的

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

与时间赛跑

第一次失效将启动“帧时钟”;此时钟是一个对象,用于跟踪帧内的每个阶段,例如绘制窗口、布局 widget 或处理事件队列。这允许 GTK+ 与窗口系统合成器等事物同步,并避免执行用户看不到的不必要的工作——例如,当您的显示器只能以 60 Hz 的频率运行时,以每秒 1000 帧的速度绘制某些内容。

一旦时钟到达“绘制”阶段,我们就会处理窗口上所有计划的更新;这将导致发出 GDK_EXPOSE 事件。GDK_EXPOSE 事件包含需要更新的 GdkWindow 以及所有失效区域的并集。重要的是要注意,总的来说,只有顶级窗口才会收到 GDK_EXPOSE 事件;但是,由于历史原因,某些 widget 可能会应用特定的事件掩码,这将导致 GDK_EXPOSE 事件也传递给它们。您不应该编写依赖于此的代码,并且如果您有从旧版本的 GTK+ 2.x 移植的旧代码,您应该认真考虑从事件掩码中删除 GDK_EXPOSURE_MASK

渲染

GTK+ 从 GDK_EXPOSE 事件中取出窗口和失效区域,并确定它们属于哪个顶级 widget。一旦找到,GTK+ 将开始实际的渲染过程。首先,GTK+ 将要求 GdkWindow 创建一个缓冲区,用于绘制窗口的内容;该缓冲区将被裁剪到需要绘制的区域,并将使用窗口的背景颜色清除。GDK 将创建一个“绘图上下文”——一个临时对象,用于跟踪 OpenGL 和 Cairo 绘图等内容。然后,GTK+ 将要求 widget 使用 Cairo 上下文绘制自身。对于叶子 widget,这意味着在上下文中绘制它们自己;对于容器 widget,这还意味着递归遍历它们的所有子项。在此过程结束时,GTK+ 将通过告诉 GDK 获取包含所有渲染 widget 的缓冲区并使用它来替换窗口的当前内容来结束帧。然后,GDK 将要求窗口系统在适当的时候将窗口呈现给用户。

改变历史

上面概述的过程有各种各样的注意事项,并且 GDK 内部处理窗口的失效和验证的代码相当复杂;它也有很长的历史,这意味着它的 API 遍布过去的里程碑。

例如,在 GTK+ 3.0 之前,您应该自己处理“expose”事件,并通过使用 gdk_cairo_create() 在 widget 上创建 Cairo 上下文进行绘制;这早已是不必要的,因为 GtkWidget::draw 虚函数已经为我们提供了一个 Cairo 上下文,可以用来绘制。但是,gdk_cairo_create() 函数在 GTK+ 3.22 中已被弃用,不应在新编写的代码中使用;如果您需要 Cairo 上下文,您应该创建一个类似的 Cairo 表面,在其上调用 cairo_create(),然后将该表面用作 GTK+ 在绘制 widget 时为您提供的 Cairo 上下文的源。另一方面,如果您正在使用 gdk_cairo_create() 来响应 GDK_EXPOSE 事件而在顶级的原生 GdkWindow 上进行绘制,那么您应该改用新添加的 gdk_window_begin_draw_frame()gdk_window_end_draw_frame()GdkDrawingContext API。

塑造未来

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

在 GtkScrolledWindow 中控制内容大小

GtkScrolledWindow widget 是 Gtk+ 应用程序开发人员的老朋友;它的目的是通过使用滚动条使大的 widget 适应小的空间。

GtkScrolledWindow Example
正在运行的垂直 GtkScrolledWindow

自 Gtk+ 3.0 以来,GtkScrolledWindow 能够通过 GtkScrolledWindow:min-content-widthGtkScrolledWindow:min-content-height 属性及其相关函数来设置最小内容大小(宽度和高度)。

从下一个稳定版本开始,Gtk+ 还将提供这些属性的最大大小对应项。

它们是做什么的?

顾名思义,最小尺寸属性定义了可滚动区域将具有的最小尺寸(无论是宽度还是高度)——即使其子项没有完全填充可用空间。

scrolledwindow min-content-height
即使子 widget 没有填充可用空间,也会分配滚动窗口。

另一方面,最大内容尺寸定义了可滚动区域在开始滚动其内容之前允许增长多少。

让我们看看它的实际效果

scroll animation
演示最小和最大内容大小的示例。滚动窗口永远不会小于 110 像素,并且永远不会高于 250 像素。
在哪里以及如何使用它们

每当您想要限制可滚动区域的大小时,您都想使用新的属性。例如,GtkPopover 总是将其子 widget 缩小到它们的最小尺寸。以下部分举例说明如何使内容在宽度和高度上最大增长到 300 像素

<template>
  <object class="GtkPopover">
    <child>
      <object class="GtkScrolledWindow">
        <property name="visible">True</property>
        <property name="max-content-width">300</property>
        <property name="max-content-height">300</property>
      </object>
    </child>
  </object>
</template>

或者,如果您想以编程方式实现相同的效果,您可以调用 gtk_scrolled_window_set_max_content_width()gtk_scrolled_window_set_max_content_height()

GTK+ 中的光标

历史

在 Linux 中,光标一直是一个大难题。

X11 光标字体是从远古时代流传下来的,它给我们带来了诸如 gumby () 或 trek () 这样的瑰宝。不幸的是,这种情况通过 GdkCursorType 枚举和 gdk_cursor_new() 函数被冻结在了 GDK API 中。

后来,出现了 Xcursor 库。它发明了自己的图像格式来存储光标,并为我们带来了光标主题,但它并没有回答“我的光标主题应该提供什么光标?”这个问题。

由于没有官方推荐的光标名称列表,光标主题经常提供在野外发现的所有光标名称变体。例如,这里是 oxygen 光标主题中包含的光标列表。如果您想知道,此列表中的十六进制字符串是 Xcursor 的一个巧妙技巧,可以在使用上述光标字体的核心 X11 应用程序下改装主题光标。

CSS 来拯救

大约一年前,我们决定最终改进 GTK+ 光标的故事。值得庆幸的是,CSS3 规范包含了一个合理的 光标名称列表,这些名称可以合理地预期在各个平台上都可用。

标准光标由于 GdkCursorType 枚举包含太多无意义的内容且不易扩展,我们决定将 gdk_cursor_new_from_name() 作为获取光标的推荐 API。此函数的文档现在列出了 CSS 光标名称(请点击上面的链接查看),并且各个 GDK 后端中的光标处理代码会尽力为您提供所有这些名称的有意义的光标。

在某些平台(例如使用随机光标主题的 X11)上,如果某个光标在主题中不存在,我们可能必须回退到默认的箭头光标。作为光标代码的总体改进的一部分,Windows 后端增加了对光标主题的支持。

GTK+ 本身现在完全使用 gdk_cursor_new_from_name() 以及标准光标名称。gtk3-demo 包含一个演示,显示了所有标准光标并让您试用它们。上面的屏幕截图显示了它。

此处描述的更改已进入大约 9 个月前发布的 GTK+ 3.18。

您应该在应用程序中执行的操作

很可能,您无需执行任何操作!GTK+ 控件自行使用合适的光标,您无需任何额外工作即可从中受益。

如果您的应用程序出于某种原因正在创建自己的光标,您应该仔细检查上面显示的其中一个标准光标是否适合您。使用标准光标可确保您无论应用程序在哪个平台上运行,也无论用户选择了哪个光标主题,都能获得合适的光标。

请使用 gdk_cursor_new_from_name() 生成您的主题光标,因为这现在是此任务的首选 API。