GTK+ 4.0 中的 Widget 层级结构

今天,我们请到了客座作者 Timm Bäder,Corebird 的维护者和 GTK+ 的贡献者,来谈谈在 GTK+ 4.0 中编写复合 widget 的变化。

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

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

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

GtkWidget 的更改

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

迭代子 widget

在 GTK+3 中,每个容器都必须实现 GtkContainer::forall,并且可以使用 gtk_container_foreach()gtk_container_forall() 轻松地迭代所有子 widget。而在 GTK+4 中,每个 GtkWidget 都可以有子 widget,因此每个 widget 都可能有一个我们需要绘制、测量、尺寸分配等的子项列表。在 widget 层级结构中,可以通过以下方式访问这些子项和同级 widget:
  • gtk_widget_get_first_child()
  • gtk_widget_get_last_child()
  • gtk_widget_get_prev_sibling()
  • gtk_widget_get_next_sibling()
因此,迭代给定 GtkWidget 的子 widget 的最简单方法是(用 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);
  }

向非容器父项添加 widget

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

在 GTK+4 中,gtk_widget_set_parent() 仍然有效,并将 widget 添加到父项的子 widget 列表的末尾。但是,我们显然也想管理子 widget 的顺序,以及我们在列表中添加新子项的位置,因此我们有:

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

Widget CSS 名称

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

转换后的 Widget 示例

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

GtkSwitch

在 GTK+3 中,GtkSwitch 是一个直接的 GtkWidget 子类(耶!),它使用 GdkWindow 进行输入(单击开关将启用/禁用它),使用一个 GtkCssGadget 表示 widget 本身,使用两个 PangoLayout 表示 ON/OFF 文本,以及使用另一个 GtkCssGadget 表示滑块。
在 GTK+ 主分支中,开关仍然有其 widget 级别的 GtkCssGadget,因此它支持 min-width/min-height CSS 属性和 CSS 边距,但是滑块小部件已被 GtkButton 取代,两个 PangoLayout 已被 GtkLabel 取代。这样,我们可以节省大约 300 行 gtkswitch.c 中的代码。理论上,我们还拥有更多功能,例如可以使用 GtkLabel 支持的文本装饰 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 使用构件(gadget)来表示槽和进度节点。它还使用 PangoLayout 来显示百分比或用户给定的字符串。

在主分支(master)中,槽和进度条都是部件,PangoLayout 当然是一个 GtkLabel。不必监听 GtkWidget::style-changed(部件会自动处理),并且不必自己绘制 PangoLayout(现在由 GtkLabel 处理)可以节省大约 200 行代码,这是一个不错的代码大小节省。

 

GtkExpander

GtkExpander 比看起来更复杂。在 GTK+3 中,它由 2 个 GtkBoxGadget 组成(类似于 GtkBox,但不是部件…),一个用于标题部件左侧的箭头,标题部件和实际内容部件的构件。在主分支中,这是通过使用实际的 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+ 4.0 中的部件层级结构”的 5 个想法

  1. @Salamandar:在性能方面应该没有任何区别。对于一个简单的文本标签(没有换行、链接或省略号),GtkLabel 基本上是 PangoLayout 的包装器。

    [WORDPRESS HASHCASH] 发布者发送给我们的是 ‘0,这不是一个哈希现金值。

  2. 如果您的 API 仍在变化,那么为遍历子项创建迭代器对象可能是有意义的。
    由于这可能是一个常见操作,并且使用自定义结构可能会带来显著的性能提升,具体取决于列表的实现。

    但也可能不会,只是一个建议 :-)

    类似这样的东西;
    GtkWidget *widget;
    GtkWidget *child;
    GtkWidgetIterator * iter = gtk_widget_iter_init(widget);
    for (gtk_widget_iter_has_next(iter);
    child = gtk_widget_iter_next (iter))
    {
    /* 使用 @child 执行操作 */
    g_assert (gtk_widget_get_parent (child) == widget);
    }

评论已关闭。