GTK 4 中的布局管理器

自 GTK 设计之初,容器和布局策略一直是其核心组成部分。如果您希望小部件根据特定策略布局其子项,则必须实现 GtkContainer 来处理子小部件的添加、删除和迭代,然后您必须实现 GtkWidget 中的尺寸协商虚拟函数来测量、定位和调整每个子项的大小。

GTK 4 开发周期的主要主题之一是将更多功能委托给辅助对象,而不是将其编码到 GTK 提供的基类中。例如,我们将事件处理从 GtkWidget 描述的信号处理程序移动到事件控制器,并将渲染委托给 GtkSnapshot 对象。该方向上的另一个步骤是将布局机制从 GtkWidget 本身解耦到一个辅助类型 GtkLayoutManager

布局管理器

布局管理器是负责测量和调整小部件及其子项大小的对象。每个 GtkWidget 都拥有一个 GtkLayoutManager,并使用它来代替 measure()allocate() 虚拟函数(这些函数即将被废弃)。更改的要点:您不再需要通过子类化 GtkWidget 来实现其布局策略,而是子类化 GtkLayoutManager,然后将布局管理器分配给小部件。

就像旧的 GtkWidget 代码一样,您需要覆盖一个虚拟函数来测量布局,该函数称为 measure(),它取代了 GTK 3 的 get_preferred_* 系列虚拟函数。

static void
layout_measure (GtkLayoutManager *layout_manager,
                GtkWidget        *widget,
                GtkOrientation    orientation,
                int               for_size,
                int              *minimum,
                int              *natural,
                int              *minimum_baseline,
                int              *natural_baseline)

测量之后,您需要将大小分配给布局;这发生在 allocate() 虚拟函数中,该函数取代了以前 GTK 主要版本的 size_allocate() 虚拟函数。

static void
layout_allocate (GtkLayoutManager *layout_manager,
                 GtkWidget        *widget,
                 int               width,
                 int               height,
                 int               baseline)

在更深奥的一方面,您还可以覆盖 get_request_mode() 虚拟函数,该函数允许您声明布局管理器是否请求固定大小,或者其大小之一是否取决于相反的大小,例如高对宽或宽对高。

static GtkSizeRequestMode
layout_get_request_mode (GtkLayoutManager *layout_manager,
                         GtkWidget        *widget)

您可能会注意到,每个虚拟函数都会传递布局管理器实例以及使用该布局管理器的小部件。

当然,这对 GTK 小部件的工作方式的各个方面产生了更大的影响,最明显的是,布局代码的所有复杂性现在都可以限制在其自己的对象中,通常是不可派生的,而小部件可以保持可派生并变得更简单。

这项工作的另一个特点是,如果您想更改容器的布局策略,则可以在运行时更改布局管理器;您还可以拥有每个小部件的布局策略,而无需增加小部件代码的复杂性。

最后,布局管理器允许我们摆脱 GTK 的特殊情况之一,即:容器子属性。

子属性

GtkContainer 的内部,存在一个基本上是 GObject 属性相关代码的副本,其唯一的工作是为从 GtkContainer 派生的类型实现“子”属性。这些容器/子属性仅在子项作为特定容器类的父项存在时才存在,并且出于各种原因而使用,但通常用于控制布局选项,例如框和类框容器中的填充方向; GtkFixed 内的固定位置;或笔记本选项卡小部件的展开/填充规则。

子属性很难使用,因为它们需要临时的 API 而不是通常的 GObject API,因此在 GtkBuilder、gtk-doc 和语言绑定中需要特殊处理。子属性也附加到容器的实际直接子项,因此如果一个小部件介入了一个子项(例如,GtkScrolledWindowGtkListBox),那么您需要保留对*该*子项的引用,以便更改应用于*您自己的小部件*的布局。

在 GTK 的主分支中,我们摆脱了其中的大部分-要么通过简单地删除它们(当有实际的小部件 API 实现相同的功能时),要么通过创建辅助 GObject 类型并将子属性移动到这些类型。最终目标是在 GTK 4 发布时删除所有子属性以及 GtkContainer 的相关 API。对于与布局相关的属性,GtkLayoutManager 提供了自己的 API,以便在分别将子项添加到使用布局管理器的小部件或从中删除时,自动创建和销毁对象。创建的对象是可自省的,并且在文档或绑定方面不需要特殊处理。

您首先从派生 GtkLayoutChild 类自己的类型开始,并添加属性,就像您为任何其他 GObject 类型所做的那样。然后,您覆盖 GtkLayoutManagercreate_layout_child() 虚拟函数

static GtkLayoutChild *
create_layout_child (GtkLayoutManager *manager,
                     GtkWidget *container,
                     GtkWidget *child)
{
  // The simplest implementation
  return g_object_new (your_layout_child_get_type (),
                       "layout-manager", manager,
                       "child-widget", child,
                       "some-property", some_property_initial_state,
                       NULL);
}

之后,只要小部件仍然是使用布局管理器的容器的子项,您就可以访问布局子对象;如果子项从其父项中删除,或者容器更改了布局管理器,则会自动收集布局子项。

新的布局管理器

当然,仅在 GTK 中拥有 GtkLayoutManager 类对我们没有任何好处。GTK 4 为应用程序和小部件开发人员引入了各种布局管理器

  • GtkBinLayout 实现了 GtkBin 的布局策略,此外还支持多个子项彼此堆叠,类似于 GtkOverlay 的工作方式。您可以使用每个小部件的对齐和展开属性来控制它们在分配区域内的位置,并且 GtkBinLayout 将始终要求尽可能多的空间来分配其最大的子项。
  • GtkBoxLayoutGtkBox 实现的布局策略的直接移植; GtkBox 本身已被移植为在内部使用 GtkBoxLayout
  • GtkFixedLayoutGtkFixedGtkLayout 的固定布局定位策略的移植,此外还增加了允许您定义通用变换的功能,而不是每个子项的纯 2D 平移;GtkFixed 已被修改为使用 GtkFixedLayout 并使用 2D 平移——并且 GtkLayout 已被合并到 GtkFixed 中,因为它唯一的区别特性是 GtkScrollable 接口的实现。
  • GtkCustomLayout 是一个方便的布局管理器,它接受曾经是 GtkWidget 虚拟函数覆盖的函数,并且它主要用作将现有小部件移植到布局管理器未来的桥梁。

我们仍在实现 GtkGridLayout 并使 GtkGrid 在内部使用它,遵循与 GtkBoxLayoutGtkBox 相同的模式。GTK 中的其他小部件也将在此过程中获得自己的布局管理器,但与此同时,它们可以使用 GtkCustomLayout

最后一步是实现一个基于约束的布局管理器,这将使我们能够创建复杂的、响应式的用户界面,而无需将小部件打包到嵌套层次结构中。基于约束的布局值得单独写一篇博文,敬请期待!

GTK 4 中的条目

最近在 GTK 主分支中落地的一个较大的重构是重新设计条目层次结构。这篇文章总结了发生的变化,以及我们为什么认为这种方式更好。

GTK 3 中的条目

让我们首先看看 GTK 3 中的情况。

GtkEntry 是这里的基础类。它实现了 GtkEditable 接口。GtkSpinButtonGtkEntry 的子类。多年来,添加了更多内容。GtkEntry 获得了对条目完成、嵌入图标和显示进度的支持。我们添加了另一个子类 GtkSearchEntry

这种方法的一些问题立即显现。gtkentry.c 超过 11100 行代码。不仅很难向这个庞大的代码库添加更多功能,而且也很难对其进行子类化 – 这是创建您自己的条目的唯一方法,因为所有单行文本编辑功能都在 GtkEntry 中。

GtkEditable 接口非常古老 - 它在 GTK 2 之前就已经存在了。不幸的是,它作为一个接口并没有真正成功 - GtkEntry 是唯一的实现,并且它以一种令人困惑的方式在内部使用接口函数。

GTK 4 中的条目

现在让我们看看 GTK 主分支中的情况。

我们所做的第一件事是将 GtkEntry 的核心文本编辑功能移动到一个名为 GtkText 的新小部件中。这基本上是一个没有所有额外内容的条目,例如图标、完成和进度。

我们通过向其添加一些更常见的功能(如 width-chars 和 max-width-chars)使 GtkEditable 接口更有用,并使 GtkText 实现它。我们还添加了辅助 API,以使其易于将 GtkEditable 实现委托给另一个对象。

“复杂”的条目小部件(GtkEntryGtkSpinButtonGtkSearchEntry)现在都是复合小部件,其中包含一个 GtkText 子项,并将其 GtkEditable 实现委托给该子项。

最后,我们添加了一个新的 GtkPasswordEntry 小部件,它接管了 GtkEntry 过去拥有的相应功能,例如显示大写锁定警告

或让用户查看内容。

为什么这样更好?

此重构的主要目标之一是使在 GTK 外部创建自定义条目小部件更容易。

过去,这需要子类化 GtkEntry,并浏览一个复杂的 vfunc 覆盖迷宫。现在,您只需添加一个 GtkText 小部件,将您的 GtkEditable 实现委托给它,就可以轻松获得一个功能齐全的条目小部件。

您可以在 GtkText 组件周围添加许多灵活的花哨功能。例如,我们在 gtk4-demo 中添加了一个带标签的输入框,现在可以很容易地在 GTK 之外实现。

从 GTK 3 移植时,这会影响您吗?

在将代码移植到这种新的输入框样式时,需要注意一些可能存在的问题。

GtkSearchEntry 和 GtkSpinButton 不再派生自 GtkEntry。如果您看到有关将这些类之一强制转换为 GtkEntry 的运行时警告,则很可能需要切换到使用 GtkEditable API。

GtkEntry 和其他复杂的输入框小部件不再可聚焦 – 焦点会转移到包含的 GtkText 上。但 gtk_widget_grab_focus() 仍然有效,并将焦点移动到正确的位置。您不太可能受到此影响。

Caps Lock 警告功能已从 GtkEntry 中删除。如果您使用 visibility==FALSE 的 GtkEntry 作为密码,则只需切换到 GtkPasswordEntry。

如果您使用 GtkEntry 进行基本编辑功能,并且不需要任何额外的输入框功能,则应考虑改用 GtkText。

为 GTK 测试 Discourse

在过去的 20 年左右,GTK 使用 IRC 和邮件列表进行与项目相关的讨论。多年来,使用电子邮件进行通信的情况有所减少,而维护基础设施的开销却增加了;在服务提供商看来,向数百或数千人发送电子邮件越来越难以与垃圾邮件区分开来,GNOME 不得不尝试申请例外——这并不容易获得,而且很容易被撤销。最重要的是,用于管理邮件列表的基础设施非常陈旧且脆弱,并且不必要地分成各种子类别,这使得跟踪讨论变得比必要更困难。

在 GTK 团队、GNOME 基础设施维护人员以及广大 GTK 社区进行讨论后,我们决定开始试运行 Discourse 作为邮件列表的替代方案,首先是作为一种为 GTK 社区提供官方场所讨论 GTK 的开发以及使用 GTK 进行开发的方式,以及其余核心 GNOME 平台:GLib、Pango、GdkPixbuf 等。

您可以在 discourse.gnome.org 上找到 Discourse 实例。在上面,您可以使用 平台核心 类别来讨论核心 GNOME 平台;您可以使用合适的标签 为您的主题添加标签,并订阅您感兴趣的标签。

我们还计划将 wiki 上的一些页面迁移到 Discourse,特别是那些我们希望收到社区反馈的页面。

我们仍在 努力研究如何迁移 与 GTK 相关的各种邮件列表的用户,以便关闭列表并拥有一个单一的场所,而不是分割社区;与此同时,如果您订阅了一个或多个这些列表

  • gtk-devel-list
  • gtk-app-devel-list
  • gtk-list
  • gtk-i18n-list

那么您可能需要查看 Discourse,并加入那里的讨论。